From 8f4d77d186c686e40757524d0c199538763c3baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E5=B2=B3=E5=B3=B0?= <132282304+syf2211@users.noreply.github.com> Date: Sat, 27 Jun 2026 06:59:14 +0000 Subject: [PATCH 1/3] fix(anthropic): honor adaptive thinking config for newer Claude models - Parse thinking and output_config in completion options schema - Forward adaptive thinking/output_config to Anthropic API requests - Skip legacy reasoning toggle when model config specifies thinking - Add regression tests for adaptive thinking request bodies - test: add required model field to completionOptions in vitest Fixes #12908 --- core/index.d.ts | 7 ++ core/llm/llms/Anthropic.ts | 44 +++++++-- core/llm/llms/Anthropic.vitest.ts | 101 +++++++++++++++++++++ gui/src/redux/thunks/streamNormalInput.ts | 4 + packages/config-yaml/src/schemas/models.ts | 13 +++ 5 files changed, 161 insertions(+), 8 deletions(-) diff --git a/core/index.d.ts b/core/index.d.ts index bec3e0e0ff8..e92fc47bb38 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1214,6 +1214,13 @@ export interface BaseCompletionOptions { toolChoice?: ToolChoice; reasoning?: boolean; reasoningBudgetTokens?: number; + thinking?: { + type?: "enabled" | "adaptive" | "disabled"; + budget_tokens?: number; + }; + output_config?: { + effort?: "low" | "medium" | "high"; + }; promptCaching?: boolean; } diff --git a/core/llm/llms/Anthropic.ts b/core/llm/llms/Anthropic.ts index e33f92f203f..3772375420d 100644 --- a/core/llm/llms/Anthropic.ts +++ b/core/llm/llms/Anthropic.ts @@ -55,9 +55,43 @@ class Anthropic extends BaseLLM { } // Public for use within VertexAI + private buildThinkingParams( + options: CompletionOptions, + ): Record { + if (options.thinking?.type) { + const thinking: Record = { + type: options.thinking.type, + }; + if (options.thinking.type === "enabled") { + thinking.budget_tokens = + options.thinking.budget_tokens ?? + options.reasoningBudgetTokens ?? + DEFAULT_REASONING_TOKENS; + } + const params: Record = { thinking }; + if (options.output_config) { + params.output_config = options.output_config; + } + return params; + } + + if (options.reasoning) { + return { + thinking: { + type: "enabled", + budget_tokens: + options.reasoningBudgetTokens ?? DEFAULT_REASONING_TOKENS, + }, + }; + } + + return {}; + } + public convertArgs( options: CompletionOptions, ): Omit { + const thinkingParams = this.buildThinkingParams(options); const finalOptions = { top_k: options.topK, top_p: options.topP, @@ -67,13 +101,7 @@ class Anthropic extends BaseLLM { stop_sequences: options.stop?.filter((x) => x.trim() !== ""), stream: options.stream ?? true, tools: options.tools?.map(this.convertToolToAnthropicTool), - thinking: options.reasoning - ? { - type: "enabled" as const, - budget_tokens: - options.reasoningBudgetTokens ?? DEFAULT_REASONING_TOKENS, - } - : undefined, + ...thinkingParams, tool_choice: options.toolChoice ? { type: "tool" as const, @@ -82,7 +110,7 @@ class Anthropic extends BaseLLM { : undefined, }; - return finalOptions; + return finalOptions as Omit; } private convertMessageContentToBlocks( diff --git a/core/llm/llms/Anthropic.vitest.ts b/core/llm/llms/Anthropic.vitest.ts index d01d361040f..2033bc76e3a 100644 --- a/core/llm/llms/Anthropic.vitest.ts +++ b/core/llm/llms/Anthropic.vitest.ts @@ -421,6 +421,107 @@ describe("Anthropic", () => { }); }); + test("should forward adaptive thinking and output_config from completion options", async () => { + const anthropic = new Anthropic({ + apiKey: "test-api-key", + model: "claude-opus-4-8", + apiBase: "https://api.anthropic.com/v1/", + }); + + await runLlmTest({ + llm: anthropic, + methodToTest: "streamChat", + params: [ + [{ role: "user", content: "hello" }], + new AbortController().signal, + { + model: "claude-opus-4-8", + thinking: { type: "adaptive" }, + output_config: { effort: "high" }, + }, + ], + expectedRequest: { + url: "https://api.anthropic.com/v1/messages", + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "anthropic-version": "2023-06-01", + "x-api-key": "test-api-key", + }, + body: { + model: "claude-opus-4-8", + max_tokens: 8192, + stream: true, + messages: [ + { + role: "user", + content: [{ type: "text", text: "hello" }], + }, + ], + thinking: { type: "adaptive" }, + output_config: { effort: "high" }, + system: "", + }, + }, + mockStream: [ + '{"type": "content_block_delta", "delta": {"type": "text_delta", "text": "Hello!"}}', + '{"type": "content_block_stop"}', + ], + }); + }); + + test("should prefer configured thinking over legacy reasoning toggle", async () => { + const anthropic = new Anthropic({ + apiKey: "test-api-key", + model: "claude-opus-4-8", + apiBase: "https://api.anthropic.com/v1/", + completionOptions: { + model: "claude-opus-4-8", + thinking: { type: "adaptive" }, + output_config: { effort: "high" }, + }, + }); + + await runLlmTest({ + llm: anthropic, + methodToTest: "streamChat", + params: [ + [{ role: "user", content: "hello" }], + new AbortController().signal, + { reasoning: true }, + ], + expectedRequest: { + url: "https://api.anthropic.com/v1/messages", + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "anthropic-version": "2023-06-01", + "x-api-key": "test-api-key", + }, + body: { + model: "claude-opus-4-8", + max_tokens: 8192, + stream: true, + messages: [ + { + role: "user", + content: [{ type: "text", text: "hello" }], + }, + ], + thinking: { type: "adaptive" }, + output_config: { effort: "high" }, + system: "", + }, + }, + mockStream: [ + '{"type": "content_block_delta", "delta": {"type": "text_delta", "text": "Hello!"}}', + '{"type": "content_block_stop"}', + ], + }); + }); + test("should handle custom max tokens", async () => { const anthropic = new Anthropic({ apiKey: "test-api-key", diff --git a/gui/src/redux/thunks/streamNormalInput.ts b/gui/src/redux/thunks/streamNormalInput.ts index 23429852d7a..c66e55a58be 100644 --- a/gui/src/redux/thunks/streamNormalInput.ts +++ b/gui/src/redux/thunks/streamNormalInput.ts @@ -54,6 +54,10 @@ function buildReasoningCompletionOptions( return baseOptions; } + if (model.completionOptions?.thinking?.type) { + return baseOptions; + } + const reasoningOptions: LLMFullCompletionOptions = { ...baseOptions, reasoning: !!hasReasoningEnabled, diff --git a/packages/config-yaml/src/schemas/models.ts b/packages/config-yaml/src/schemas/models.ts index 7c048e8d548..c81173358b1 100644 --- a/packages/config-yaml/src/schemas/models.ts +++ b/packages/config-yaml/src/schemas/models.ts @@ -44,6 +44,17 @@ export const modelCapabilitySchema = z.union([ // not ideal but lose type suggestions if use z.infer because of the string fallback export type ModelCapability = "tool_use" | "image_input" | "next_edit"; +export const thinkingConfigSchema = z.object({ + type: z.enum(["enabled", "adaptive", "disabled"]).optional(), + budget_tokens: z.number().optional(), +}); +export type ThinkingConfig = z.infer; + +export const outputConfigSchema = z.object({ + effort: z.enum(["low", "medium", "high"]).optional(), +}); +export type OutputConfig = z.infer; + export const completionOptionsSchema = z.object({ contextLength: z.number().optional(), maxTokens: z.number().optional(), @@ -57,6 +68,8 @@ export const completionOptionsSchema = z.object({ n: z.number().optional(), reasoning: z.boolean().optional(), reasoningBudgetTokens: z.number().optional(), + thinking: thinkingConfigSchema.optional(), + output_config: outputConfigSchema.optional(), promptCaching: z.boolean().optional(), stream: z.boolean().optional(), keepAlive: z.number().optional(), From 9d1784e9abd82fdcb98cbd7e2bf07010381f46cc Mon Sep 17 00:00:00 2001 From: syf2211 Date: Sat, 27 Jun 2026 15:33:59 +0000 Subject: [PATCH 2/3] fix(anthropic): forward output_config without explicit thinking.type Address cubic P2 review: output_config was only included when thinking.type was set, so configs with output_config alone (or on the legacy reasoning path) were dropped from API requests. Add regression test for output_config-only completion options. --- core/llm/llms/Anthropic.ts | 26 ++++++++--------- core/llm/llms/Anthropic.vitest.ts | 48 +++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/core/llm/llms/Anthropic.ts b/core/llm/llms/Anthropic.ts index 3772375420d..38961c4052c 100644 --- a/core/llm/llms/Anthropic.ts +++ b/core/llm/llms/Anthropic.ts @@ -58,6 +58,8 @@ class Anthropic extends BaseLLM { private buildThinkingParams( options: CompletionOptions, ): Record { + const params: Record = {}; + if (options.thinking?.type) { const thinking: Record = { type: options.thinking.type, @@ -68,24 +70,20 @@ class Anthropic extends BaseLLM { options.reasoningBudgetTokens ?? DEFAULT_REASONING_TOKENS; } - const params: Record = { thinking }; - if (options.output_config) { - params.output_config = options.output_config; - } - return params; + params.thinking = thinking; + } else if (options.reasoning) { + params.thinking = { + type: "enabled", + budget_tokens: + options.reasoningBudgetTokens ?? DEFAULT_REASONING_TOKENS, + }; } - if (options.reasoning) { - return { - thinking: { - type: "enabled", - budget_tokens: - options.reasoningBudgetTokens ?? DEFAULT_REASONING_TOKENS, - }, - }; + if (options.output_config) { + params.output_config = options.output_config; } - return {}; + return params; } public convertArgs( diff --git a/core/llm/llms/Anthropic.vitest.ts b/core/llm/llms/Anthropic.vitest.ts index 2033bc76e3a..05830f3d63b 100644 --- a/core/llm/llms/Anthropic.vitest.ts +++ b/core/llm/llms/Anthropic.vitest.ts @@ -471,6 +471,54 @@ describe("Anthropic", () => { }); }); + test("should forward output_config without explicit thinking.type", async () => { + const anthropic = new Anthropic({ + apiKey: "test-api-key", + model: "claude-opus-4-8", + apiBase: "https://api.anthropic.com/v1/", + }); + + await runLlmTest({ + llm: anthropic, + methodToTest: "streamChat", + params: [ + [{ role: "user", content: "hello" }], + new AbortController().signal, + { + model: "claude-opus-4-8", + output_config: { effort: "high" }, + }, + ], + expectedRequest: { + url: "https://api.anthropic.com/v1/messages", + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "anthropic-version": "2023-06-01", + "x-api-key": "test-api-key", + }, + body: { + model: "claude-opus-4-8", + max_tokens: 8192, + stream: true, + messages: [ + { + role: "user", + content: [{ type: "text", text: "hello" }], + }, + ], + output_config: { effort: "high" }, + system: "", + }, + }, + mockStream: [ + '{"type": "content_block_delta", "delta": {"type": "text_delta", "text": "Hello!"}}', + '{"type": "content_block_stop"}', + ], + }); + }); + test("should prefer configured thinking over legacy reasoning toggle", async () => { const anthropic = new Anthropic({ apiKey: "test-api-key", From 69f7f0f73ffcc7f4d894cfacf70ba541c2264651 Mon Sep 17 00:00:00 2001 From: syf2211 Date: Sat, 27 Jun 2026 15:37:36 +0000 Subject: [PATCH 3/3] test(anthropic): cover output_config on legacy reasoning path --- core/llm/llms/Anthropic.vitest.ts | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/core/llm/llms/Anthropic.vitest.ts b/core/llm/llms/Anthropic.vitest.ts index 05830f3d63b..e91107a67a2 100644 --- a/core/llm/llms/Anthropic.vitest.ts +++ b/core/llm/llms/Anthropic.vitest.ts @@ -519,6 +519,56 @@ describe("Anthropic", () => { }); }); + test("should forward output_config on legacy reasoning path", async () => { + const anthropic = new Anthropic({ + apiKey: "test-api-key", + model: "claude-opus-4-8", + apiBase: "https://api.anthropic.com/v1/", + }); + + await runLlmTest({ + llm: anthropic, + methodToTest: "streamChat", + params: [ + [{ role: "user", content: "hello" }], + new AbortController().signal, + { + model: "claude-opus-4-8", + reasoning: true, + output_config: { effort: "high" }, + }, + ], + expectedRequest: { + url: "https://api.anthropic.com/v1/messages", + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "anthropic-version": "2023-06-01", + "x-api-key": "test-api-key", + }, + body: { + model: "claude-opus-4-8", + max_tokens: 8192, + stream: true, + messages: [ + { + role: "user", + content: [{ type: "text", text: "hello" }], + }, + ], + thinking: { type: "enabled", budget_tokens: 2048 }, + output_config: { effort: "high" }, + system: "", + }, + }, + mockStream: [ + '{"type": "content_block_delta", "delta": {"type": "text_delta", "text": "Hello!"}}', + '{"type": "content_block_stop"}', + ], + }); + }); + test("should prefer configured thinking over legacy reasoning toggle", async () => { const anthropic = new Anthropic({ apiKey: "test-api-key",