From 01660997f6dca25196cf7e9e19312171a51fdca9 Mon Sep 17 00:00:00 2001 From: "liyongjie.103" Date: Mon, 24 Aug 2026 10:55:39 +0800 Subject: [PATCH 1/2] fix: apply anthropic default reasoning effort --- src/adapters/anthropic.ts | 17 ++++++++++++----- tests/anthropic-reasoning.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/adapters/anthropic.ts b/src/adapters/anthropic.ts index 626a45f2865..45360f57372 100644 --- a/src/adapters/anthropic.ts +++ b/src/adapters/anthropic.ts @@ -27,6 +27,7 @@ import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint"; import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge"; import { decodeServerSentEvents } from "../lib/sse-decoder"; import { isTranslatorBudgetExceededError, retainTranslatedEventBatch, type TranslatorBudget } from "../lib/translator-budget"; +import { modelRecordValue } from "../reasoning-effort"; /** Map a user content part to an Anthropic content block (text or image source). */ function toAnthropicContentPart(p: OcxContentPart): unknown { @@ -513,6 +514,11 @@ function supportsExplicitThinkingDisable(modelId: string): boolean { return meetsFamilyMinimum(modelId, EXPLICIT_THINKING_DISABLE_FAMILY_MINIMUMS); } +function defaultReasoningEffort(provider: OcxProviderConfig, modelId: string): string | undefined { + const value = modelRecordValue(provider.modelDefaultReasoningEfforts, modelId); + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + /** `output_config.effort` accepts low|medium|high|xhigh|max — "minimal" is rejected with a 400. */ function adaptiveEffort(effort: string): string { return effort === "minimal" ? "low" : effort; @@ -929,18 +935,19 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti // anyway, and thinking shares the caller's `max_tokens` — which truncates a small-budget // request before it can emit its stop sequence (#545). Say "disabled" out loud where the // model both defaults to thinking and accepts being told not to. - if (parsed.options.reasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) { + const effectiveReasoning = parsed.options.reasoning ?? defaultReasoningEffort(provider, parsed.modelId); + if (effectiveReasoning === "none" && supportsExplicitThinkingDisable(parsed.modelId)) { body.thinking = { type: "disabled" }; - } else if (typeof parsed.options.reasoning === "string" && parsed.options.reasoning !== "none") { + } else if (typeof effectiveReasoning === "string" && effectiveReasoning !== "none") { if (usesAdaptiveThinking(parsed.modelId)) { // Adaptive-thinking models replace the token budget with an effort knob and reject // `thinking.type: "enabled"` outright. `max_tokens` still caps thinking plus visible // output, so high effort needs the same total-token headroom as budget thinking or a // default 8192-token request can spend everything on thought and return empty text. body.thinking = { type: "adaptive" }; - body.output_config = { effort: adaptiveEffort(parsed.options.reasoning) }; + body.output_config = { effort: adaptiveEffort(effectiveReasoning) }; const explicitMaxOut = parsed.options.maxOutputTokens; - const wantBudget = reasoningBudget(parsed.options.reasoning); + const wantBudget = reasoningBudget(effectiveReasoning); const floor = wantBudget + OUTPUT_HEADROOM; // Preserve explicit caller limits as-is; for omitted limits use the adaptive ceiling // so effort=max (budget=32k) still leaves OUTPUT_HEADROOM tokens for visible output. @@ -953,7 +960,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti // 400s ("max_tokens must be greater than thinking.budget_tokens"). Size them so max_tokens // always exceeds the budget within a model-safe ceiling, reserving room for visible output. const maxOut = parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS; - const wantBudget = reasoningBudget(parsed.options.reasoning); + const wantBudget = reasoningBudget(effectiveReasoning); const maxTokens = Math.min(REASONING_MAX_TOKENS_CEILING, Math.max(maxOut, wantBudget + OUTPUT_HEADROOM)); const budget = Math.max(MIN_THINKING_BUDGET, Math.min(wantBudget, maxTokens - OUTPUT_FLOOR)); body.max_tokens = maxTokens; diff --git a/tests/anthropic-reasoning.test.ts b/tests/anthropic-reasoning.test.ts index ab91ee8f053..722112d52bb 100644 --- a/tests/anthropic-reasoning.test.ts +++ b/tests/anthropic-reasoning.test.ts @@ -40,6 +40,28 @@ describe("anthropic extended-thinking gate", () => { expect(b.top_p).toBe(0.8); }); + test("modelDefaultReasoningEfforts supplies reasoning when caller omits it", async () => { + const b = await bodyOf(parsed(undefined, { temperature: 0.5, topP: 0.8 }, "always-thinking-model"), { + ...provider, + modelDefaultReasoningEfforts: { "always-thinking-model": "high" }, + }); + const thinking = b.thinking as { type: string; budget_tokens: number } | undefined; + expect(thinking?.type).toBe("enabled"); + expect(typeof thinking?.budget_tokens).toBe("number"); + expect(b.temperature).toBeUndefined(); + expect(b.top_p).toBeUndefined(); + }); + + test("explicit reasoning overrides modelDefaultReasoningEfforts", async () => { + const b = await bodyOf(parsed("low", {}, "always-thinking-model"), { + ...provider, + modelDefaultReasoningEfforts: { "always-thinking-model": "high" }, + }); + const thinking = b.thinking as { type: string; budget_tokens: number } | undefined; + expect(thinking?.type).toBe("enabled"); + expect(thinking?.budget_tokens).toBe(4096); + }); + test("reasoning 'high' enables thinking and drops sampling (extended-thinking rule)", async () => { const b = await bodyOf(parsed("high", { temperature: 0.3, topP: 0.9 })); const thinking = b.thinking as { type: string; budget_tokens: number } | undefined; From ffb2cbc06875706513b79c6f9582fb85bcc3eac1 Mon Sep 17 00:00:00 2001 From: "liyongjie.103" Date: Mon, 24 Aug 2026 19:07:06 +0800 Subject: [PATCH 2/2] fix(anthropic): classify capitalized/dotted Claude ids as adaptive thinking claudeFamilyVersion only matched lowercase `claude-\-\-\` ids. Vendor ids such as `Claude-Opus-4.8-joybuilder` failed both the case-sensitive prefix match and the dotted minor parse (4.8 -> minor 0), so usesAdaptiveThinking() returned false and the adapter sent the legacy `thinking: {type: "enabled", budget_tokens}` wire shape to models that reject it (Bedrock 400: "thinking.type.enabled is not supported for this model. Use thinking.type.adaptive and output_config.effort"). Make the parser case-insensitive, accept `.` as a minor separator, and lowercase the captured family before table lookup. Date-pinned ids (claude-opus-4-20250514) and legacy families (opus <= 4.6) keep their previous classification. --- src/adapters/anthropic.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapters/anthropic.ts b/src/adapters/anthropic.ts index 45360f57372..74bafc4da81 100644 --- a/src/adapters/anthropic.ts +++ b/src/adapters/anthropic.ts @@ -470,10 +470,10 @@ function claudeFamilyVersion(modelId: string): { family: string; major: number; // Find the segment that actually starts with `claude-`, rather than assuming it is either // the first (breaks `anthropic/claude-sonnet-5`) or the last (breaks `claude-sonnet-5/variant`, // where the slash carries a vendor suffix rather than a routing prefix). - const match = /(?:^|\/)claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?!\d)/.exec(modelId); + const match = /(?:^|\/)claude-([a-z]+)-(\d+)(?:[.-](\d{1,2}))?(?![\d.])/i.exec(modelId); if (!match) return undefined; return { - family: match[1]!, + family: match[1]!.toLowerCase(), major: Number(match[2]), minor: match[3] === undefined ? 0 : Number(match[3]), };