fix(core): mirror Qwen reasoning history field#4289
Conversation
wenshao
left a comment
There was a problem hiding this comment.
Cross-file note: mirrorQwenReasoningContent adds reasoning field; MistralOpenAICompatibleProvider.buildRequest() calls super.buildRequest() (now running the mirror), then runs stripReasoningContent which removes reasoning_content but not reasoning. Low probability (requires Qwen model on Mistral endpoint) but if triggered, Mistral API may receive an orphaned reasoning field. Consider also stripping reasoning in stripReasoningContent.
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| max_tokens: effectiveMaxTokens, | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
[Suggestion] Missing JSDoc on mirrorQwenReasoningContent. The method silently transforms messages by copying reasoning_content to reasoning, but there is no comment explaining why this is needed. Other methods like applyOutputTokenLimit and DeepSeek's equivalent have JSDoc blocks explaining the motivation and linked issues.
| /** | |
| * Mirrors `reasoning_content` to `reasoning` for Qwen model history. | |
| * Qwen's self-hosted API requires the `reasoning` field (not just | |
| * `reasoning_content`) when replaying assistant messages in multi-turn | |
| * conversations. This copies the content without removing the original | |
| * field to preserve backward compatibility. | |
| * | |
| * Fixes #4285 | |
| */ | |
| private mirrorQwenReasoningContent( |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| }); | ||
| }); | ||
|
|
||
| it('should mirror reasoning_content to reasoning for Qwen model history', () => { |
There was a problem hiding this comment.
[Suggestion] The single test covers only the happy path (one assistant message with reasoning_content). Four critical branches are untested:
- Non-Qwen model no-op —
model: 'gpt-4'withreasoning_contentshould NOT injectreasoning - Empty
reasoning_content—reasoning_content: ''should skip mirroring (explicitlength === 0guard) - Existing
reasoningfield — assistant message with bothreasoningandreasoning_contentshould NOT overwritereasoning - Non-assistant messages — user/system messages with
reasoning_contentshould be ignored (role !== 'assistant'guard)
These guards are the most frequently hit code paths and regressions here would silently break API behavior.
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| return message; | ||
| } | ||
|
|
||
| const extended = message as unknown as Record<string, unknown>; |
There was a problem hiding this comment.
[Suggestion] The double-cast as unknown as Record<string, unknown> to access reasoning_content can be replaced with the project's existing typed interface. ExtendedChatCompletionAssistantMessageParam (exported from converter.ts) already declares reasoning_content?: string | null. DeepSeek's equivalent function uses it directly:
| const extended = message as unknown as Record<string, unknown>; | |
| const extended = message as ExtendedChatCompletionAssistantMessageParam; | |
| const reasoningContent = extended.reasoning_content; |
This catches typos, provides IDE support, and is consistent with how ensureReasoningContentOnToolCalls in deepseek.ts accesses the same field. The write side (reasoning) still needs a cast since that field isn't in the type.
— qwen-latest-series-invite-beta-v28 via Qwen Code /review
Summary
reasoning_contenttoreasoningfor self-hosted Qwen history messages.Testing
Fixes #4285