diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.test.ts b/packages/core/src/core/openaiContentGenerator/provider/default.test.ts index 12c04c115cf..ce57b5965f2 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.test.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.test.ts @@ -425,5 +425,86 @@ describe('DefaultOpenAICompatibleProvider', () => { expect(result.max_tokens).toBe(8000); // GPT-4 has 16K limit, min(16K, 8K) = 8K expect(result).not.toHaveProperty('custom_param'); }); + + it('mirrors reasoning_content into reasoning for Qwen3 assistant history turns without mutating the source request', () => { + const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'Qwen/Qwen3.6-35B-A3B', + messages: [ + { role: 'user', content: 'First turn' }, + { + role: 'assistant', + content: 'Visible answer', + reasoning_content: 'Preserved chain of thought', + } as OpenAI.Chat.ChatCompletionAssistantMessageParam & { + reasoning_content: string; + reasoning?: string; + }, + { role: 'user', content: 'Second turn' }, + ], + }; + + const result = provider.buildRequest(originalRequest, 'prompt-id'); + const assistant = result.messages?.[1] as { + reasoning_content?: string; + reasoning?: string; + }; + + expect(assistant.reasoning_content).toBe('Preserved chain of thought'); + expect(assistant.reasoning).toBe('Preserved chain of thought'); + expect( + (originalRequest.messages[1] as { reasoning?: string }).reasoning, + ).toBeUndefined(); + }); + + it('does not overwrite an explicit reasoning field on Qwen3 assistant history turns', () => { + const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'Qwen3-32B', + messages: [ + { + role: 'assistant', + content: 'Visible answer', + reasoning_content: 'Legacy reasoning field', + reasoning: 'Canonical reasoning field', + } as OpenAI.Chat.ChatCompletionAssistantMessageParam & { + reasoning_content: string; + reasoning: string; + }, + ], + }; + + const result = provider.buildRequest(originalRequest, 'prompt-id'); + const assistant = result.messages?.[0] as { + reasoning_content?: string; + reasoning?: string; + }; + + expect(assistant.reasoning).toBe('Canonical reasoning field'); + expect(assistant.reasoning_content).toBe('Legacy reasoning field'); + }); + + it('does not mirror reasoning_content for non-Qwen3 OpenAI-compatible models', () => { + const originalRequest: OpenAI.Chat.ChatCompletionCreateParams = { + model: 'gpt-4o', + messages: [ + { + role: 'assistant', + content: 'Visible answer', + reasoning_content: 'Preserved chain of thought', + } as OpenAI.Chat.ChatCompletionAssistantMessageParam & { + reasoning_content: string; + reasoning?: string; + }, + ], + }; + + const result = provider.buildRequest(originalRequest, 'prompt-id'); + const assistant = result.messages?.[0] as { + reasoning_content?: string; + reasoning?: string; + }; + + expect(assistant.reasoning_content).toBe('Preserved chain of thought'); + expect(assistant.reasoning).toBeUndefined(); + }); }); }); diff --git a/packages/core/src/core/openaiContentGenerator/provider/default.ts b/packages/core/src/core/openaiContentGenerator/provider/default.ts index 891188dcd18..9ddd8d6b01f 100644 --- a/packages/core/src/core/openaiContentGenerator/provider/default.ts +++ b/packages/core/src/core/openaiContentGenerator/provider/default.ts @@ -11,6 +11,38 @@ import { hasExplicitOutputLimit, } from '../../tokenLimits.js'; +type AssistantMessageWithReasoningFields = + OpenAI.Chat.ChatCompletionAssistantMessageParam & { + reasoning_content?: string | null; + reasoning?: string | null; + }; + +function shouldMirrorReasoningContentForQwen3(model: string): boolean { + return model.toLowerCase().includes('qwen3'); +} + +function mirrorReasoningContentToReasoning( + message: OpenAI.Chat.ChatCompletionMessageParam, +): OpenAI.Chat.ChatCompletionMessageParam { + if (message.role !== 'assistant') { + return message; + } + + const assistant = message as AssistantMessageWithReasoningFields; + if ( + typeof assistant.reasoning_content !== 'string' || + assistant.reasoning_content.length === 0 || + typeof assistant.reasoning === 'string' + ) { + return message; + } + + return { + ...assistant, + reasoning: assistant.reasoning_content, + } as OpenAI.Chat.ChatCompletionMessageParam; +} + /** * Default provider for standard OpenAI-compatible APIs */ @@ -75,9 +107,13 @@ export class DefaultOpenAICompatibleProvider // Apply output token limits to ensure max_tokens is set appropriately // This prevents occupying too much context window with output reservation const requestWithTokenLimits = this.applyOutputTokenLimit(request); + const messages = shouldMirrorReasoningContentForQwen3(request.model) + ? requestWithTokenLimits.messages.map(mirrorReasoningContentToReasoning) + : requestWithTokenLimits.messages; return { ...requestWithTokenLimits, + messages, ...(extraBody ? extraBody : {}), }; }