fix(core): parse tagged Qwen3 thinking in default provider#6751
Conversation
Qwen3 can return `<think>` blocks in `content` instead of `reasoning_content`, but the default OpenAI-compatible provider never enabled the existing tagged thinking parser on the response path. This wires that parser back in only for Qwen3 model names, keeping the safer GLM revert intact while restoring the intended fallback for Qwen3 responses. Constraint: Must not reintroduce the DashScope GLM buffering regression reverted in QwenLM#6248 Rejected: Enable tagged-thinking parsing for every default provider response | broader blast radius without maintainer direction Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep response-side tagged-thinking gating aligned with the Qwen3 request-side reasoning mirroring check Tested: `npm run typecheck --workspace=packages/core`; `npm run test --workspace=packages/core -- src/core/openaiContentGenerator/provider/default.test.ts`; `npm run test --workspace=packages/core -- src/core/openaiContentGenerator/converter.test.ts -t 'OpenAI -> Gemini tagged thinking content'` Not-tested: End-to-end live DashScope/Qwen API streaming against a real provider Related: QwenLM#6666
|
Thanks for the PR, @barry166! Template looks good ✓ — all required sections present. Problem: Real, observed bug — issue #6666 documents Qwen3 returning Direction: Aligned. The converter already has Size: 3 files, +34 / -1. Production logic: 11 lines (9 in Approach: Scope is exactly right — minimal, focused, one thing. Reuses the existing 中文说明感谢贡献 @barry166! 模板完整 ✓ —— 所有必需章节齐全。 问题: 真实已观测的 bug —— issue #6666 记录了 Qwen3 在 方向: 对齐。converter 已有 规模: 3 个文件,+34 / -1。生产逻辑 11 行( 方案: 范围恰到好处 —— 最小化、聚焦、只做一件事。复用已有的 — Qwen Code · qwen3.7-max |
Code ReviewI read the diff against the existing provider/converter/pipeline infrastructure. The implementation is clean and correct. Independent proposal (before reading diff): I would have added a Comparison: The PR does exactly this — no more, no less. The approach matches my independent proposal 1:1. Findings: No critical blockers. No AGENTS.md violations. Specifically:
Test ResultsPR unit tests (CI green): Maintainer local verification (@wenshao — 212/212 related tests): Load-bearing confirmed: maintainer overlaid Real-Scenario TestingAttempted tmux before/after with both installed The DashScope endpoint rejects both builds with an unrelated 中文说明代码审查将 diff 与现有的 provider/converter/pipeline 基础设施进行了对照阅读。实现干净且正确。 独立方案(阅读 diff 之前): 我会在 对比: PR 恰好做了这些 —— 不多不少,与我的独立方案完全一致。 发现: 无阻塞性问题,无 AGENTS.md 违规。 测试结果CI 单元测试全绿。维护者 @wenshao 本地验证 212/212 项相关测试全部通过,并确认修复是必要的(去掉即失败)。 真实场景测试tmux before/after 无法得出结论 —— DashScope 在 CI 环境中因 — Qwen Code · qwen3.7-max |
|
This is a clean, minimal fix that does exactly what it needs to — opts Qwen3 into the existing The maintainer @wenshao has since done thorough local verification — 212/212 related tests, confirmed the fix is load-bearing (removing the PR's source reproduces the No reservations. The problem is real, the fix is minimal and correct, and the verification is comprehensive. 中文说明这是一个干净、最小化的修复 —— 准确地将 Qwen3 接入 MiniMax 已在使用的 维护者 @wenshao 已进行了充分的本地验证 —— 212/212 项相关测试通过,确认修复是必要的(移除 PR 源码即可复现 没有顾虑。问题真实存在,修复最小且正确,验证全面。 — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
| getResponseParsingOptions(): OpenAIResponseParsingOptions { | ||
| return { | ||
| taggedThinkingTags: shouldMirrorReasoningContentForQwen3( | ||
| this.contentGeneratorConfig.model ?? '', |
There was a problem hiding this comment.
[Suggestion] getResponseParsingOptions() checks this.contentGeneratorConfig.model (the config model), but buildRequest() on the same class checks request.model (the wire model, which may be a per-request override). The pipeline already computes effectiveModel = request.model || config.model (pipeline.ts ~L983) and has explicit tests for this pattern (pipeline.test.ts lines 1288, 1328: "gates on the wire model, not config").
If a caller passes a per-request model override that crosses the Qwen3/non-Qwen3 boundary relative to the config model, request-side message mirroring and response-side tagged thinking parsing would disagree — one enabled, the other not.
This is constrained by the getResponseParsingOptions() interface taking no parameters, so a clean fix would require widening the interface to accept an optional effectiveModel parameter, or computing the flag in the pipeline where the wire model is already known.
— qwen3.7-max via Qwen Code /review
|
|
||
| getResponseParsingOptions(): OpenAIResponseParsingOptions { | ||
| return { | ||
| taggedThinkingTags: shouldMirrorReasoningContentForQwen3( |
There was a problem hiding this comment.
[Suggestion] shouldMirrorReasoningContentForQwen3 was named for its original purpose (request-side message mirroring in buildRequest), but this new call site uses it for a different purpose: deciding whether to enable tagged thinking tag parsing on responses. Consider renaming to something like isQwen3Model(model) to accurately describe both call sites and make it easier to decouple them if a future Qwen3 variant needs one behavior but not the other.
— qwen3.7-max via Qwen Code /review
| }); | ||
|
|
||
| it('keeps tagged thinking parsing disabled for non-Qwen3 models', () => { | ||
| mockContentGeneratorConfig.model = 'gpt-4o'; |
There was a problem hiding this comment.
[Suggestion] The negative test uses 'gpt-4o' which doesn't contain "qwen" at all. A near-miss model name like 'Qwen/Qwen2.5-72B' (contains "qwen" but not "qwen3") would better exercise the boundary of the .includes('qwen3') substring check and catch a plausible regression where someone loosens the match to .includes('qwen').
| mockContentGeneratorConfig.model = 'gpt-4o'; | |
| mockContentGeneratorConfig.model = 'Qwen/Qwen2.5-72B'; |
— qwen3.7-max via Qwen Code /review
✅ Local verification (maintainer) — merge referenceI built and ran the real test suite locally for this PR. Summary: all green, the fix is load-bearing, and behavior is correctly scoped to Qwen3. Environment: macOS · Node What I ran
Before / after — the change is load-bearingTo confirm the fix actually matters, I overlaid Independent end-to-end checkThe PR's unit tests verify the two halves separately (the provider gate in
(Happy to contribute this test to the PR if useful — it is not part of the current diff.) Correctness notes for merge
Verdict: ✅ LGTM from local verification. Tests pass, typecheck/lint clean, the fix is minimal, correctly scoped, and demonstrably resolves #6666. CI on this PR is also already green ( 🇨🇳 中文版本(点击展开)✅ 本地验证(维护者)— 合并参考我在本地构建并运行了这个 PR 的真实测试套件。结论:全部通过,该修复是必要的(去掉就会失败),且行为被正确地限定在 Qwen3 范围内。 环境: macOS · Node 执行的检查
(截图见上方英文部分。) Before / After —证明该修复是必要的为确认修复确实起作用,我把 独立的端到端检查PR 自带的单测分别验证了两个环节(
(如果有用,我很乐意把这个测试贡献到 PR 里 — 它目前不在改动中。) 合并前的正确性说明
结论: ✅ 从本地验证来看 LGTM。测试通过,类型/Lint 干净,修复最小化、限定范围正确,并可证明修复了 #6666。该 PR 的 CI 也已是绿色( |
|
@qwen-code /triage |
|
Thanks for the contribution here. After checking the real failing traces more closely, I think this fix is at the wrong layer. The issue is not that Qwen3 model names should opt into tagged-thinking parsing by default. In the observed failure, the response had already produced structured reasoning / thought content, but raw I am going to revert this PR and handle the issue with response-shape based retry logic instead: when structured reasoning is already present but visible content contains raw thinking tags, we should drop that malformed attempt and retry, not enable parsing or cleanup based on a Qwen3 model-name match. |
…wenLM#6751)" (QwenLM#6767) This reverts commit 220937e.


What this PR does
Fixes #6666.
reasoning_contentmirroring aligned with the same Qwen3 model gateWhy it's needed
Qwen3 can sometimes return
<think>...</think>blocks insidecontentinstead ofreasoning_content. The converter already knows how to extract those tags safely, but the default provider never opted into that response parsing path for Qwen3, so the raw tags leaked into visible output.This keeps the fix scoped to Qwen3 and avoids reopening the broader default-provider behavior that was reverted for DashScope GLM in #6248.
Reviewer Test Plan
DefaultOpenAICompatibleProvidernow returnstaggedThinkingTags: truefor Qwen3 models andfalsefor non-Qwen3 models.Commands run locally:
npm run typecheck --workspace=packages/corenpm run test --workspace=packages/core -- src/core/openaiContentGenerator/provider/default.test.tsnpm run test --workspace=packages/core -- src/core/openaiContentGenerator/converter.test.ts -t 'OpenAI -> Gemini tagged thinking content'Risk & Scope
qwen3.