From 695422f6bd34e9e325c49f84fac84116011b2b1f Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Mon, 20 Jul 2026 19:25:35 +0800 Subject: [PATCH 1/2] fix(core): skip enable_thinking=false for thinking-only models (#7332) Internal operations (context compaction, goal judge, permission classifier) set includeThoughts=false, which causes the pipeline to send enable_thinking=false to the model API. Thinking-only models like qwen3.8-max-preview reject this with a 400 error. Added a guard: when the model's preset sets extra_body.enable_thinking to true (indicating the model requires thinking), the pipeline no longer overrides it with false. The model uses its default thinking behavior for internal operations instead of erroring. Added regression test verifying enable_thinking is not set to false when extra_body.enable_thinking is true. --- .../openaiContentGenerator/pipeline.test.ts | 41 +++++++++++++++++++ .../core/openaiContentGenerator/pipeline.ts | 10 ++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts index a41c28a726e..49b43039030 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts @@ -952,6 +952,47 @@ describe('ContentGenerationPipeline', () => { expect(apiCall.enable_thinking).toBe(false); }); + it('skips enable_thinking:false for thinking-only models (#7332)', async () => { + // qwen3.8-max-preview rejects enable_thinking=false with a 400 error. + // When the model's preset sets extra_body.enable_thinking=true, the + // pipeline must NOT override it with false. + mockContentGeneratorConfig = { + ...mockContentGeneratorConfig, + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + model: 'qwen3.8-max-preview', + extra_body: { enable_thinking: true }, + } as ContentGeneratorConfig; + mockConfig = { + ...mockConfig, + contentGeneratorConfig: mockContentGeneratorConfig, + }; + pipeline = new ContentGenerationPipeline(mockConfig); + + const request: GenerateContentParameters = { + model: 'qwen3.8-max-preview', + contents: [{ parts: [{ text: 'Summarize' }], role: 'user' }], + config: { thinkingConfig: { includeThoughts: false } }, + }; + + (mockConverter.convertGeminiRequestToOpenAI as Mock).mockReturnValue([ + { role: 'user', content: 'Summarize' }, + ]); + (mockConverter.convertOpenAIResponseToGemini as Mock).mockReturnValue( + new GenerateContentResponse(), + ); + (mockClient.chat.completions.create as Mock).mockResolvedValue({ + id: 'r', + choices: [{ message: { content: 'ok' }, finish_reason: 'stop' }], + } as OpenAI.Chat.ChatCompletion); + + await pipeline.execute(request, 'forked_query'); + + const apiCall = (mockClient.chat.completions.create as Mock).mock + .calls[0][0]; + // Must NOT set enable_thinking to false for thinking-only models + expect(apiCall.enable_thinking).not.toBe(false); + }); + it('emits enable_thinking:false on DashScope hostname when reasoning is configured to false', async () => { // Config-level opt-out (`reasoning: false`) should also disable // qwen3 thinking, mirroring the DeepSeek pair above. diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.ts b/packages/core/src/core/openaiContentGenerator/pipeline.ts index 6f65b978ee0..57ea9f224bb 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.ts @@ -847,7 +847,15 @@ export class ContentGenerationPipeline { this.contentGeneratorConfig, ) ) { - typed['enable_thinking'] = false; + // Skip disabling thinking for models whose preset explicitly + // requires it (e.g. qwen3.8-max-preview). These models reject + // enable_thinking=false with a 400 error (#7332). The preset + // signals this via extra_body.enable_thinking=true. + const presetThinking = + this.contentGeneratorConfig.extra_body?.['enable_thinking']; + if (presetThinking !== true) { + typed['enable_thinking'] = false; + } } else { // Non-DashScope OpenAI-compatible servers (vLLM, SGLang, ...) render // the model's chat template server-side and read the thinking switch From 14cd7b528a1854fc0da14ac06c0d0a0483620a9a Mon Sep 17 00:00:00 2001 From: Qwen Code Bot Date: Mon, 20 Jul 2026 13:19:14 +0000 Subject: [PATCH 2/2] fix(core): use thinkingMandatory flag instead of extra_body.enable_thinking (#7332) --- packages/core/src/core/contentGenerator.ts | 3 ++ .../openaiContentGenerator/pipeline.test.ts | 45 +++++++++++++++++-- .../core/openaiContentGenerator/pipeline.ts | 11 ++--- packages/core/src/models/constants.ts | 1 + packages/core/src/models/types.ts | 1 + .../providers/presets/alibaba-token-plan.ts | 1 + .../core/src/providers/provider-config.ts | 9 +++- packages/core/src/providers/types.ts | 1 + 8 files changed, 61 insertions(+), 11 deletions(-) diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index ebfb79d4b3c..a5e358b6ef6 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -137,6 +137,9 @@ export type ContentGeneratorConfig = { customHeaders?: Record; // Extra body parameters to be merged into the request body extra_body?: Record; + // When true, the model requires thinking to be enabled and rejects + // enable_thinking=false with a 400 error (e.g. qwen3.8-max-preview). + thinkingMandatory?: boolean; // Supported input modalities. Unsupported media types are replaced with text // placeholders. Leave undefined to use automatic detection from model name. modalities?: InputModalities; diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts index 49b43039030..5f1947031d7 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.test.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.test.ts @@ -954,13 +954,13 @@ describe('ContentGenerationPipeline', () => { it('skips enable_thinking:false for thinking-only models (#7332)', async () => { // qwen3.8-max-preview rejects enable_thinking=false with a 400 error. - // When the model's preset sets extra_body.enable_thinking=true, the - // pipeline must NOT override it with false. + // The preset signals this via thinkingMandatory=true. mockContentGeneratorConfig = { ...mockContentGeneratorConfig, baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen3.8-max-preview', extra_body: { enable_thinking: true }, + thinkingMandatory: true, } as ContentGeneratorConfig; mockConfig = { ...mockConfig, @@ -989,10 +989,49 @@ describe('ContentGenerationPipeline', () => { const apiCall = (mockClient.chat.completions.create as Mock).mock .calls[0][0]; - // Must NOT set enable_thinking to false for thinking-only models expect(apiCall.enable_thinking).not.toBe(false); }); + it('emits enable_thinking:false for hybrid models with extra_body.enable_thinking (#7332)', async () => { + // Hybrid models (e.g. qwen3.7-max) have extra_body.enable_thinking=true + // from their preset but do NOT have thinkingMandatory. The pipeline must + // still emit enable_thinking=false when reasoning is disabled. + mockContentGeneratorConfig = { + ...mockContentGeneratorConfig, + baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + model: 'qwen3.7-max', + extra_body: { enable_thinking: true }, + } as ContentGeneratorConfig; + mockConfig = { + ...mockConfig, + contentGeneratorConfig: mockContentGeneratorConfig, + }; + pipeline = new ContentGenerationPipeline(mockConfig); + + const request: GenerateContentParameters = { + model: 'qwen3.7-max', + contents: [{ parts: [{ text: 'Summarize' }], role: 'user' }], + config: { thinkingConfig: { includeThoughts: false } }, + }; + + (mockConverter.convertGeminiRequestToOpenAI as Mock).mockReturnValue([ + { role: 'user', content: 'Summarize' }, + ]); + (mockConverter.convertOpenAIResponseToGemini as Mock).mockReturnValue( + new GenerateContentResponse(), + ); + (mockClient.chat.completions.create as Mock).mockResolvedValue({ + id: 'r', + choices: [{ message: { content: 'ok' }, finish_reason: 'stop' }], + } as OpenAI.Chat.ChatCompletion); + + await pipeline.execute(request, 'forked_query'); + + const apiCall = (mockClient.chat.completions.create as Mock).mock + .calls[0][0]; + expect(apiCall.enable_thinking).toBe(false); + }); + it('emits enable_thinking:false on DashScope hostname when reasoning is configured to false', async () => { // Config-level opt-out (`reasoning: false`) should also disable // qwen3 thinking, mirroring the DeepSeek pair above. diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.ts b/packages/core/src/core/openaiContentGenerator/pipeline.ts index 57ea9f224bb..1da00fbacff 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.ts @@ -847,13 +847,10 @@ export class ContentGenerationPipeline { this.contentGeneratorConfig, ) ) { - // Skip disabling thinking for models whose preset explicitly - // requires it (e.g. qwen3.8-max-preview). These models reject - // enable_thinking=false with a 400 error (#7332). The preset - // signals this via extra_body.enable_thinking=true. - const presetThinking = - this.contentGeneratorConfig.extra_body?.['enable_thinking']; - if (presetThinking !== true) { + // Skip disabling thinking for thinking-only models (e.g. + // qwen3.8-max-preview) that reject enable_thinking=false with a + // 400 error (#7332). The preset signals this via thinkingMandatory. + if (!this.contentGeneratorConfig.thinkingMandatory) { typed['enable_thinking'] = false; } } else { diff --git a/packages/core/src/models/constants.ts b/packages/core/src/models/constants.ts index 761c34851ed..d08a19cc328 100644 --- a/packages/core/src/models/constants.ts +++ b/packages/core/src/models/constants.ts @@ -30,6 +30,7 @@ export const MODEL_GENERATION_CONFIG_FIELDS = [ 'contextWindowSize', 'customHeaders', 'extra_body', + 'thinkingMandatory', 'modalities', 'splitToolMedia', 'toolResultContentFormat', diff --git a/packages/core/src/models/types.ts b/packages/core/src/models/types.ts index 4295a5892b8..668eebe6ccf 100644 --- a/packages/core/src/models/types.ts +++ b/packages/core/src/models/types.ts @@ -39,6 +39,7 @@ export type ModelGenerationConfig = Pick< | 'reasoning' | 'customHeaders' | 'extra_body' + | 'thinkingMandatory' | 'contextWindowSize' | 'modalities' | 'splitToolMedia' diff --git a/packages/core/src/providers/presets/alibaba-token-plan.ts b/packages/core/src/providers/presets/alibaba-token-plan.ts index 3431a57084e..68d842a9f4d 100644 --- a/packages/core/src/providers/presets/alibaba-token-plan.ts +++ b/packages/core/src/providers/presets/alibaba-token-plan.ts @@ -32,6 +32,7 @@ const TOKEN_PLAN_MODELS: ModelSpec[] = [ { id: 'qwen3.8-max-preview', contextWindowSize: 1000000, + thinkingMandatory: true, enableThinking: true, modalities: { image: true, video: true }, }, diff --git a/packages/core/src/providers/provider-config.ts b/packages/core/src/providers/provider-config.ts index 23cb0581cab..5d5603155c8 100644 --- a/packages/core/src/providers/provider-config.ts +++ b/packages/core/src/providers/provider-config.ts @@ -59,7 +59,10 @@ export function resolveOwnsModel( } function buildGenerationConfig( - spec: Pick, + spec: Pick< + ModelSpec, + 'enableThinking' | 'thinkingMandatory' | 'contextWindowSize' | 'modalities' + >, ): ProviderModelConfig['generationConfig'] | undefined { const parts: ProviderModelConfig['generationConfig'] = {}; let hasAny = false; @@ -67,6 +70,10 @@ function buildGenerationConfig( parts.extra_body = { enable_thinking: true }; hasAny = true; } + if (spec.thinkingMandatory) { + parts.thinkingMandatory = true; + hasAny = true; + } if (spec.contextWindowSize) { parts.contextWindowSize = spec.contextWindowSize; hasAny = true; diff --git a/packages/core/src/providers/types.ts b/packages/core/src/providers/types.ts index abe356ce55d..b871283a524 100644 --- a/packages/core/src/providers/types.ts +++ b/packages/core/src/providers/types.ts @@ -20,6 +20,7 @@ export interface ModelSpec { id: string; contextWindowSize?: number; enableThinking?: boolean; + thinkingMandatory?: boolean; modalities?: InputModalities; description?: string; }