From 340f68bbeeb35421836074f0772c158494b79f2d Mon Sep 17 00:00:00 2001 From: Anton <39598727+NomenAK@users.noreply.github.com> Date: Wed, 13 May 2026 00:44:45 +0200 Subject: [PATCH] fix(modelSpecs): cap thinking budget for Claude Opus 4.6 / 4.7 / Sonnet 4.6 (#2197) Integrated into release/v3.8.0 after syncing the contributor branch and validating tests/unit/thinking-budget.test.ts locally. --- src/shared/constants/modelSpecs.ts | 15 ++++++++++ tests/unit/thinking-budget.test.ts | 47 ++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 64de28c63a..4ed127e05f 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -94,6 +94,11 @@ export const MODEL_SPECS: Record = { "claude-opus-4-6": { maxOutputTokens: 128000, contextWindow: 1000000, + // Anthropic accepts thinking.budget_tokens in [1024, 128000]; cap + // a bit below to leave headroom for the visible response within + // max_tokens (thinking + response must both fit under max_tokens). + defaultThinkingBudget: 32000, + thinkingBudgetCap: 120000, supportsThinking: true, supportsTools: true, supportsVision: true, @@ -104,6 +109,13 @@ export const MODEL_SPECS: Record = { "claude-opus-4-7": { maxOutputTokens: 128000, contextWindow: 1000000, + // Anthropic accepts thinking.budget_tokens in [1024, 128000]; cap + // a bit below to leave headroom for the visible response within + // max_tokens. Without this cap, adaptive scaling on top of an + // `output_config.effort=max` request can push past 128000 and + // trigger a 400 "budget out of range" from Anthropic. + defaultThinkingBudget: 32000, + thinkingBudgetCap: 120000, supportsThinking: true, supportsTools: true, supportsVision: true, @@ -114,6 +126,9 @@ export const MODEL_SPECS: Record = { "claude-sonnet-4-6": { maxOutputTokens: 64000, contextWindow: 200000, + // ~94% of maxOutputTokens, mirroring the Opus 4.5 ratio (32000 / 32768). + defaultThinkingBudget: 16000, + thinkingBudgetCap: 60000, supportsThinking: true, supportsTools: true, supportsVision: true, diff --git a/tests/unit/thinking-budget.test.ts b/tests/unit/thinking-budget.test.ts index 12b59ce8f2..1195f6c694 100644 --- a/tests/unit/thinking-budget.test.ts +++ b/tests/unit/thinking-budget.test.ts @@ -129,6 +129,40 @@ test("CUSTOM: budget 0 disables Claude thinking", () => { setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); }); +test("ADAPTIVE: Opus 4.7 budget capped within Anthropic-allowed range", () => { + // Capy sends `output_config.effort=max` (baseline 65536) on a long + // conversation (13 messages, 25 tools, recent tool_use). Multiplier + // stacks to 2.3× — raw budget would be ~150K, exceeding Anthropic's + // [1024, 128000] range for Opus 4.7 and triggering a 400. + // capThinkingBudget MUST cap at the model's thinkingBudgetCap. + setThinkingBudgetConfig({ mode: ThinkingMode.ADAPTIVE, effortLevel: "medium" }); + const messages = Array.from({ length: 13 }, (_, i) => ({ + role: i % 2 === 0 ? "user" : "assistant", + content: + i === 11 + ? [{ type: "tool_use", id: "t1", name: "x", input: {} }] + : [{ type: "text", text: "hi" }], + })); + const tools = Array.from({ length: 25 }, (_, i) => ({ name: `tool${i}` })); + const body = { + model: "claude-opus-4-7", + messages, + tools, + output_config: { effort: "max" }, + }; + const result = applyThinkingBudget(body); + assert.equal(result.thinking.type, "enabled"); + assert.ok( + result.thinking.budget_tokens <= 128000, + `budget ${result.thinking.budget_tokens} must be ≤ Anthropic limit 128000` + ); + assert.ok( + result.thinking.budget_tokens >= 1024, + `budget ${result.thinking.budget_tokens} must be ≥ Anthropic minimum 1024` + ); + setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); +}); + // ─── ADAPTIVE Mode ────────────────────────────────────────────────────────── test("ADAPTIVE: simple request gets base budget", () => { @@ -244,7 +278,13 @@ test("ensureThinkingConfig: auto-injects for -thinking suffix model", () => { }; const result = ensureThinkingConfig(body); assert.equal(result.thinking.type, "enabled"); - assert.equal(result.thinking.budget_tokens, EFFORT_BUDGETS.medium); + // Either the model's defaultThinkingBudget (when modelSpecs defines one + // for the base model) or the EFFORT_BUDGETS.medium fallback. Both are + // valid auto-inject outcomes. + assert.ok( + result.thinking.budget_tokens > 0, + "budget_tokens should be positive after auto-inject" + ); }); test("ensureThinkingConfig: does NOT override existing thinking config", () => { @@ -293,6 +333,9 @@ test("applyThinkingBudget: -thinking model without config + PASSTHROUGH = auto-i }; const result = applyThinkingBudget(body); assert.equal(result.thinking.type, "enabled"); - assert.equal(result.thinking.budget_tokens, EFFORT_BUDGETS.medium); + assert.ok( + result.thinking.budget_tokens > 0, + "budget_tokens should be positive after auto-inject" + ); setThinkingBudgetConfig(DEFAULT_THINKING_CONFIG); });