Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/core/src/core/contentGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ export type ContentGeneratorConfig = {
customHeaders?: Record<string, string>;
// Extra body parameters to be merged into the request body
extra_body?: Record<string, unknown>;
// 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;
Expand Down
80 changes: 80 additions & 0 deletions packages/core/src/core/openaiContentGenerator/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,86 @@ 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.
// 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,
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];
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.
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/core/openaiContentGenerator/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,12 @@ export class ContentGenerationPipeline {
this.contentGeneratorConfig,
)
) {
typed['enable_thinking'] = false;
// 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 {
// Non-DashScope OpenAI-compatible servers (vLLM, SGLang, ...) render
// the model's chat template server-side and read the thinking switch
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/models/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const MODEL_GENERATION_CONFIG_FIELDS = [
'contextWindowSize',
'customHeaders',
'extra_body',
'thinkingMandatory',
'modalities',
'splitToolMedia',
'toolResultContentFormat',
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/models/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type ModelGenerationConfig = Pick<
| 'reasoning'
| 'customHeaders'
| 'extra_body'
| 'thinkingMandatory'
| 'contextWindowSize'
| 'modalities'
| 'splitToolMedia'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/providers/provider-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,21 @@ export function resolveOwnsModel(
}

function buildGenerationConfig(
spec: Pick<ModelSpec, 'enableThinking' | 'contextWindowSize' | 'modalities'>,
spec: Pick<
ModelSpec,
'enableThinking' | 'thinkingMandatory' | 'contextWindowSize' | 'modalities'
>,
): ProviderModelConfig['generationConfig'] | undefined {
const parts: ProviderModelConfig['generationConfig'] = {};
let hasAny = false;
if (spec.enableThinking) {
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;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/providers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ModelSpec {
id: string;
contextWindowSize?: number;
enableThinking?: boolean;
thinkingMandatory?: boolean;
modalities?: InputModalities;
description?: string;
}
Expand Down
Loading