From 9c7f41f20283d3e064c810f6e5e19bc3b7f50c7e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 24 Jul 2026 15:41:22 -0500 Subject: [PATCH 1/3] fix(provider): generalize Claude adaptive thinking --- packages/opencode/src/provider/transform.ts | 27 +++----- .../opencode/test/provider/transform.test.ts | 69 +++++++++++++++++++ 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 81759160bfeb..513824eb8dff 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -635,24 +635,19 @@ function openaiCompatibleReasoningEfforts(id: string) { return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS } -function anthropicOpus47OrLater(apiId: string) { - // Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted). - // Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility. - const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId) - if (!version) return false - const major = Number(version[1] ?? version[3]) - const minor = Number(version[2] ?? version[4]) +function anthropicUsesModernAdaptiveThinking(apiId: string) { + if (!apiId.toLowerCase().includes("claude-")) return false + // Covers family-first IDs such as claude-opus-4.7 and version-first IDs such as claude-4.7-opus. + // Limit minors to two digits so release dates in IDs such as claude-opus-4-20250514 are not versions. + const version = /claude-(?:[a-z]+-)?(\d+)(?:[.-](\d{1,2}))?(?:[.@-]|$)/i.exec(apiId) + if (!version) return true + const major = Number(version[1]) + const minor = Number(version[2] ?? 0) return major > 4 || (major === 4 && minor >= 7) } -function anthropicSonnet5OrLater(apiId: string) { - const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId) - if (!version) return false - return Number(version[1] ?? version[2]) >= 5 -} - function anthropicAdaptiveEfforts(apiId: string): string[] | null { - if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")) { + if (anthropicUsesModernAdaptiveThinking(apiId)) { return ["low", "medium", "high", "xhigh", "max"] } if ( @@ -666,7 +661,7 @@ function anthropicAdaptiveEfforts(apiId: string): string[] | null { } function anthropicOmitsThinking(apiId: string) { - return anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5") + return anthropicUsesModernAdaptiveThinking(apiId) } function googleThinkingLevelEfforts(apiId: string) { @@ -1103,7 +1098,7 @@ export function variants(model: Provider.Model): Record [ diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index ef2b275035e1..ee10c874e855 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3213,6 +3213,7 @@ describe("ProviderTransform.reasoningVariants", () => { { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, "claude-opus-4-7", ], + ["@ai-sdk/anthropic", { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, "claude-opus-5"], ["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }], ["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }], [ @@ -4617,6 +4618,12 @@ describe("ProviderTransform.variants", () => { describe("@ai-sdk/anthropic", () => { for (const testCase of [ + { + name: "opus 4 dated", + apiIds: ["claude-opus-4-20250514"], + efforts: ["high", "max"], + expectedHigh: { thinking: { type: "enabled", budgetTokens: 16000 } }, + }, { name: "opus 4.5", apiIds: ["claude-opus-4-5-20251101", "claude-opus-4.5-20251101"], @@ -4653,6 +4660,18 @@ describe("ProviderTransform.variants", () => { efforts: ["low", "medium", "high", "xhigh", "max"], expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, }, + { + name: "opus 5", + apiIds: ["claude-opus-5", "claude-opus-5-20260724"], + efforts: ["low", "medium", "high", "xhigh", "max"], + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + }, + { + name: "unversioned future model", + apiIds: ["claude-future"], + efforts: ["low", "medium", "high", "xhigh", "max"], + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + }, { name: "fable 5", apiIds: ["claude-fable-5"], @@ -4772,6 +4791,28 @@ describe("ProviderTransform.variants", () => { effort: "high", }) }) + + test("opus 5 uses adaptive reasoning for Vertex model IDs", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "google-vertex-anthropic/claude-opus-5@default", + providerID: "google-vertex-anthropic", + api: { + id: "claude-opus-5@default", + url: "https://us-central1-aiplatform.googleapis.com", + npm: "@ai-sdk/google-vertex/anthropic", + }, + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "adaptive", + display: "summarized", + }, + effort: "high", + }) + }) }) describe("@ai-sdk/amazon-bedrock", () => { @@ -4867,6 +4908,28 @@ describe("ProviderTransform.variants", () => { }) }) + test("anthropic opus 5 returns adaptive reasoning options with xhigh", () => { + const result = ProviderTransform.variants( + createMockModel({ + id: "bedrock/anthropic-claude-opus-5", + providerID: "bedrock", + api: { + id: "us.anthropic.claude-opus-5-v1:0", + url: "https://bedrock.amazonaws.com", + npm: "@ai-sdk/amazon-bedrock", + }, + }), + ) + expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result.high).toEqual({ + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: "high", + display: "summarized", + }, + }) + }) + test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => { const model = createMockModel({ id: "bedrock/llama-4", @@ -5055,6 +5118,12 @@ describe("ProviderTransform.variants", () => { efforts: ["low", "medium", "high", "xhigh", "max"], thinking: { type: "adaptive", display: "summarized" }, }, + { + name: "opus 5", + apiIds: ["anthropic--claude-opus-5", "anthropic--claude-5-opus"], + efforts: ["low", "medium", "high", "xhigh", "max"], + thinking: { type: "adaptive", display: "summarized" }, + }, ]) { for (const apiId of testCase.apiIds) { test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => { From 4cb1ccc74582e2a7f550b6eb96a4239c17324633 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 24 Jul 2026 15:59:42 -0500 Subject: [PATCH 2/3] fix(provider): enable Opus 4.5 thinking with effort --- packages/opencode/src/provider/transform.ts | 17 +++++++++++++++-- .../opencode/test/provider/transform.test.ts | 11 ++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 513824eb8dff..dc2e09cdb607 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -986,7 +986,9 @@ export function variants(model: Provider.Model): Record model.api.id.includes(v))) { - return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }])) + return Object.fromEntries( + WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, anthropicOpus45Effort(model, effort)]), + ) } return { @@ -1746,7 +1748,8 @@ function reasoningEffort(model: Provider.Model, effort: string) { } function anthropicEffort(model: Provider.Model, effort: string) { - if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort } + if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) + return anthropicOpus45Effort(model, effort) // Kimi defaults to omitting adaptive thinking text unless summarized display is requested. if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort } if (!anthropicAdaptiveEfforts(model.api.id)) return @@ -1759,6 +1762,16 @@ function anthropicEffort(model: Provider.Model, effort: string) { } } +function anthropicOpus45Effort(model: Provider.Model, effort: string) { + return { + thinking: { + type: "enabled", + budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)), + }, + effort, + } +} + function reasoningBudget(model: Provider.Model, budget: number) { switch (model.api.npm) { case "@openrouter/ai-sdk-provider": diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index ee10c874e855..9533f88f0933 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3265,13 +3265,18 @@ describe("ProviderTransform.reasoningVariants", () => { ) }) - test("uses bare effort for Claude Opus 4.5", () => { + test("combines effort with extended thinking for Claude Opus 4.5", () => { expect( ProviderTransform.reasoningVariants( model([{ type: "effort", values: ["high"] }]), target("@ai-sdk/anthropic", "claude-opus-4-5"), ), - ).toEqual({ high: { effort: "high" } }) + ).toEqual({ + high: { + thinking: { type: "enabled", budgetTokens: 16_000 }, + effort: "high", + }, + }) }) test("uses explicit effort metadata for Anthropic-compatible models", () => { @@ -4628,7 +4633,7 @@ describe("ProviderTransform.variants", () => { name: "opus 4.5", apiIds: ["claude-opus-4-5-20251101", "claude-opus-4.5-20251101"], efforts: ["low", "medium", "high"], - expectedHigh: { effort: "high" }, + expectedHigh: { thinking: { type: "enabled", budgetTokens: 16000 }, effort: "high" }, }, { name: "sonnet 4.6", From 3f7798085940e8ae595509b0d28d70936ce06084 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 24 Jul 2026 16:16:14 -0500 Subject: [PATCH 3/3] fix(provider): preserve Bedrock Claude variants --- packages/opencode/src/provider/transform.ts | 17 ++++++++-- .../opencode/test/provider/transform.test.ts | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index dc2e09cdb607..705af3d5c1b5 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -646,6 +646,10 @@ function anthropicUsesModernAdaptiveThinking(apiId: string) { return major > 4 || (major === 4 && minor >= 7) } +function anthropicOpus45(apiId: string) { + return ["opus-4-5", "opus-4.5"].some((value) => apiId.includes(value)) +} + function anthropicAdaptiveEfforts(apiId: string): string[] | null { if (anthropicUsesModernAdaptiveThinking(apiId)) { return ["low", "medium", "high", "xhigh", "max"] @@ -985,7 +989,7 @@ export function variants(model: Provider.Model): Record model.api.id.includes(v))) { + if (anthropicOpus45(model.api.id)) { return Object.fromEntries( WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, anthropicOpus45Effort(model, effort)]), ) @@ -1708,6 +1712,14 @@ function reasoningEffort(model: Provider.Model, effort: string) { ...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}), }, } + if (anthropicOpus45(model.api.id)) + return { + reasoningConfig: { + type: "enabled", + budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)), + maxReasoningEffort: effort, + }, + } if (model.api.id.includes("anthropic")) return return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } } case "@ai-sdk/gateway": @@ -1748,8 +1760,7 @@ function reasoningEffort(model: Provider.Model, effort: string) { } function anthropicEffort(model: Provider.Model, effort: string) { - if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) - return anthropicOpus45Effort(model, effort) + if (anthropicOpus45(model.api.id)) return anthropicOpus45Effort(model, effort) // Kimi defaults to omitting adaptive thinking text unless summarized display is requested. if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort } if (!anthropicAdaptiveEfforts(model.api.id)) return diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 9533f88f0933..93e166c4a8c9 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3325,6 +3325,38 @@ describe("ProviderTransform.reasoningVariants", () => { }) }) + test("uses adaptive reasoning config for Claude Opus 5 on Bedrock", () => { + const result = ProviderTransform.reasoningVariants( + model([{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }]), + target("@ai-sdk/amazon-bedrock", "us.anthropic.claude-opus-5"), + ) + expect(Object.keys(result ?? {})).toEqual(["low", "medium", "high", "xhigh", "max"]) + expect(result?.high).toEqual({ + reasoningConfig: { + type: "adaptive", + maxReasoningEffort: "high", + display: "summarized", + }, + }) + }) + + test("combines effort with extended thinking for Claude Opus 4.5 on Bedrock", () => { + expect( + ProviderTransform.reasoningVariants( + model([{ type: "effort", values: ["high"] }]), + target("@ai-sdk/amazon-bedrock", "us.anthropic.claude-opus-4-5-20251101-v1:0"), + ), + ).toEqual({ + high: { + reasoningConfig: { + type: "enabled", + budgetTokens: 16_000, + maxReasoningEffort: "high", + }, + }, + }) + }) + test("does not replace unsupported Anthropic Bedrock effort options with token budgets", () => { expect( ProviderTransform.reasoningVariants(