Skip to content

Commit 2b2aacc

Browse files
authored
fix(provider): generalize Claude adaptive thinking (anomalyco#38757)
1 parent 7840562 commit 2b2aacc

2 files changed

Lines changed: 145 additions & 20 deletions

File tree

packages/opencode/src/provider/transform.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -635,24 +635,23 @@ function openaiCompatibleReasoningEfforts(id: string) {
635635
return gpt5CodexReasoningEfforts(apiId) ?? versionedGpt5ReasoningEfforts(apiId) ?? OPENAI_EFFORTS
636636
}
637637

638-
function anthropicOpus47OrLater(apiId: string) {
639-
// Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted).
640-
// Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility.
641-
const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId)
642-
if (!version) return false
643-
const major = Number(version[1] ?? version[3])
644-
const minor = Number(version[2] ?? version[4])
638+
function anthropicUsesModernAdaptiveThinking(apiId: string) {
639+
if (!apiId.toLowerCase().includes("claude-")) return false
640+
// Covers family-first IDs such as claude-opus-4.7 and version-first IDs such as claude-4.7-opus.
641+
// Limit minors to two digits so release dates in IDs such as claude-opus-4-20250514 are not versions.
642+
const version = /claude-(?:[a-z]+-)?(\d+)(?:[.-](\d{1,2}))?(?:[.@-]|$)/i.exec(apiId)
643+
if (!version) return true
644+
const major = Number(version[1])
645+
const minor = Number(version[2] ?? 0)
645646
return major > 4 || (major === 4 && minor >= 7)
646647
}
647648

648-
function anthropicSonnet5OrLater(apiId: string) {
649-
const version = /sonnet-(\d+)(?:[.@-]|$)|claude-(\d+)-sonnet(?:[.@-]|$)/i.exec(apiId)
650-
if (!version) return false
651-
return Number(version[1] ?? version[2]) >= 5
649+
function anthropicOpus45(apiId: string) {
650+
return ["opus-4-5", "opus-4.5"].some((value) => apiId.includes(value))
652651
}
653652

654653
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
655-
if (anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")) {
654+
if (anthropicUsesModernAdaptiveThinking(apiId)) {
656655
return ["low", "medium", "high", "xhigh", "max"]
657656
}
658657
if (
@@ -666,7 +665,7 @@ function anthropicAdaptiveEfforts(apiId: string): string[] | null {
666665
}
667666

668667
function anthropicOmitsThinking(apiId: string) {
669-
return anthropicOpus47OrLater(apiId) || anthropicSonnet5OrLater(apiId) || apiId.includes("fable-5")
668+
return anthropicUsesModernAdaptiveThinking(apiId)
670669
}
671670

672671
function googleThinkingLevelEfforts(apiId: string) {
@@ -990,8 +989,10 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
990989
)
991990
}
992991

993-
if (["opus-4-5", "opus-4.5"].some((v) => model.api.id.includes(v))) {
994-
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { effort }]))
992+
if (anthropicOpus45(model.api.id)) {
993+
return Object.fromEntries(
994+
WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, anthropicOpus45Effort(model, effort)]),
995+
)
995996
}
996997

997998
return {
@@ -1103,7 +1104,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
11031104
if (id.includes("anthropic")) {
11041105
if (adaptiveEfforts) {
11051106
// Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
1106-
// native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
1107+
// native which inlines it). Claude 4.7+ defaults `display` to "omitted".
11071108
return wrapInSapModelParams(
11081109
Object.fromEntries(
11091110
adaptiveEfforts.map((effort) => [
@@ -1711,6 +1712,14 @@ function reasoningEffort(model: Provider.Model, effort: string) {
17111712
...(anthropicOmitsThinking(model.api.id) ? { display: "summarized" } : {}),
17121713
},
17131714
}
1715+
if (anthropicOpus45(model.api.id))
1716+
return {
1717+
reasoningConfig: {
1718+
type: "enabled",
1719+
budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
1720+
maxReasoningEffort: effort,
1721+
},
1722+
}
17141723
if (model.api.id.includes("anthropic")) return
17151724
return { reasoningConfig: { type: "enabled", maxReasoningEffort: effort } }
17161725
case "@ai-sdk/gateway":
@@ -1751,7 +1760,7 @@ function reasoningEffort(model: Provider.Model, effort: string) {
17511760
}
17521761

17531762
function anthropicEffort(model: Provider.Model, effort: string) {
1754-
if (["opus-4-5", "opus-4.5"].some((value) => model.api.id.includes(value))) return { effort }
1763+
if (anthropicOpus45(model.api.id)) return anthropicOpus45Effort(model, effort)
17551764
// Kimi defaults to omitting adaptive thinking text unless summarized display is requested.
17561765
if (isKimiFamily(model)) return { thinking: { type: "adaptive", display: "summarized" }, effort }
17571766
if (!anthropicAdaptiveEfforts(model.api.id)) return
@@ -1764,6 +1773,16 @@ function anthropicEffort(model: Provider.Model, effort: string) {
17641773
}
17651774
}
17661775

1776+
function anthropicOpus45Effort(model: Provider.Model, effort: string) {
1777+
return {
1778+
thinking: {
1779+
type: "enabled",
1780+
budgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),
1781+
},
1782+
effort,
1783+
}
1784+
}
1785+
17671786
function reasoningBudget(model: Provider.Model, budget: number) {
17681787
switch (model.api.npm) {
17691788
case "@openrouter/ai-sdk-provider":

packages/opencode/test/provider/transform.test.ts

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3213,6 +3213,7 @@ describe("ProviderTransform.reasoningVariants", () => {
32133213
{ thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
32143214
"claude-opus-4-7",
32153215
],
3216+
["@ai-sdk/anthropic", { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, "claude-opus-5"],
32163217
["@ai-sdk/google", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
32173218
["@ai-sdk/google-vertex", { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } }],
32183219
[
@@ -3264,13 +3265,18 @@ describe("ProviderTransform.reasoningVariants", () => {
32643265
)
32653266
})
32663267

3267-
test("uses bare effort for Claude Opus 4.5", () => {
3268+
test("combines effort with extended thinking for Claude Opus 4.5", () => {
32683269
expect(
32693270
ProviderTransform.reasoningVariants(
32703271
model([{ type: "effort", values: ["high"] }]),
32713272
target("@ai-sdk/anthropic", "claude-opus-4-5"),
32723273
),
3273-
).toEqual({ high: { effort: "high" } })
3274+
).toEqual({
3275+
high: {
3276+
thinking: { type: "enabled", budgetTokens: 16_000 },
3277+
effort: "high",
3278+
},
3279+
})
32743280
})
32753281

32763282
test("uses explicit effort metadata for Anthropic-compatible models", () => {
@@ -3319,6 +3325,38 @@ describe("ProviderTransform.reasoningVariants", () => {
33193325
})
33203326
})
33213327

3328+
test("uses adaptive reasoning config for Claude Opus 5 on Bedrock", () => {
3329+
const result = ProviderTransform.reasoningVariants(
3330+
model([{ type: "effort", values: ["low", "medium", "high", "xhigh", "max"] }]),
3331+
target("@ai-sdk/amazon-bedrock", "us.anthropic.claude-opus-5"),
3332+
)
3333+
expect(Object.keys(result ?? {})).toEqual(["low", "medium", "high", "xhigh", "max"])
3334+
expect(result?.high).toEqual({
3335+
reasoningConfig: {
3336+
type: "adaptive",
3337+
maxReasoningEffort: "high",
3338+
display: "summarized",
3339+
},
3340+
})
3341+
})
3342+
3343+
test("combines effort with extended thinking for Claude Opus 4.5 on Bedrock", () => {
3344+
expect(
3345+
ProviderTransform.reasoningVariants(
3346+
model([{ type: "effort", values: ["high"] }]),
3347+
target("@ai-sdk/amazon-bedrock", "us.anthropic.claude-opus-4-5-20251101-v1:0"),
3348+
),
3349+
).toEqual({
3350+
high: {
3351+
reasoningConfig: {
3352+
type: "enabled",
3353+
budgetTokens: 16_000,
3354+
maxReasoningEffort: "high",
3355+
},
3356+
},
3357+
})
3358+
})
3359+
33223360
test("does not replace unsupported Anthropic Bedrock effort options with token budgets", () => {
33233361
expect(
33243362
ProviderTransform.reasoningVariants(
@@ -4617,11 +4655,17 @@ describe("ProviderTransform.variants", () => {
46174655

46184656
describe("@ai-sdk/anthropic", () => {
46194657
for (const testCase of [
4658+
{
4659+
name: "opus 4 dated",
4660+
apiIds: ["claude-opus-4-20250514"],
4661+
efforts: ["high", "max"],
4662+
expectedHigh: { thinking: { type: "enabled", budgetTokens: 16000 } },
4663+
},
46204664
{
46214665
name: "opus 4.5",
46224666
apiIds: ["claude-opus-4-5-20251101", "claude-opus-4.5-20251101"],
46234667
efforts: ["low", "medium", "high"],
4624-
expectedHigh: { effort: "high" },
4668+
expectedHigh: { thinking: { type: "enabled", budgetTokens: 16000 }, effort: "high" },
46254669
},
46264670
{
46274671
name: "sonnet 4.6",
@@ -4653,6 +4697,18 @@ describe("ProviderTransform.variants", () => {
46534697
efforts: ["low", "medium", "high", "xhigh", "max"],
46544698
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
46554699
},
4700+
{
4701+
name: "opus 5",
4702+
apiIds: ["claude-opus-5", "claude-opus-5-20260724"],
4703+
efforts: ["low", "medium", "high", "xhigh", "max"],
4704+
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
4705+
},
4706+
{
4707+
name: "unversioned future model",
4708+
apiIds: ["claude-future"],
4709+
efforts: ["low", "medium", "high", "xhigh", "max"],
4710+
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
4711+
},
46564712
{
46574713
name: "fable 5",
46584714
apiIds: ["claude-fable-5"],
@@ -4772,6 +4828,28 @@ describe("ProviderTransform.variants", () => {
47724828
effort: "high",
47734829
})
47744830
})
4831+
4832+
test("opus 5 uses adaptive reasoning for Vertex model IDs", () => {
4833+
const result = ProviderTransform.variants(
4834+
createMockModel({
4835+
id: "google-vertex-anthropic/claude-opus-5@default",
4836+
providerID: "google-vertex-anthropic",
4837+
api: {
4838+
id: "claude-opus-5@default",
4839+
url: "https://us-central1-aiplatform.googleapis.com",
4840+
npm: "@ai-sdk/google-vertex/anthropic",
4841+
},
4842+
}),
4843+
)
4844+
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
4845+
expect(result.high).toEqual({
4846+
thinking: {
4847+
type: "adaptive",
4848+
display: "summarized",
4849+
},
4850+
effort: "high",
4851+
})
4852+
})
47754853
})
47764854

47774855
describe("@ai-sdk/amazon-bedrock", () => {
@@ -4867,6 +4945,28 @@ describe("ProviderTransform.variants", () => {
48674945
})
48684946
})
48694947

4948+
test("anthropic opus 5 returns adaptive reasoning options with xhigh", () => {
4949+
const result = ProviderTransform.variants(
4950+
createMockModel({
4951+
id: "bedrock/anthropic-claude-opus-5",
4952+
providerID: "bedrock",
4953+
api: {
4954+
id: "us.anthropic.claude-opus-5-v1:0",
4955+
url: "https://bedrock.amazonaws.com",
4956+
npm: "@ai-sdk/amazon-bedrock",
4957+
},
4958+
}),
4959+
)
4960+
expect(Object.keys(result)).toEqual(["low", "medium", "high", "xhigh", "max"])
4961+
expect(result.high).toEqual({
4962+
reasoningConfig: {
4963+
type: "adaptive",
4964+
maxReasoningEffort: "high",
4965+
display: "summarized",
4966+
},
4967+
})
4968+
})
4969+
48704970
test("returns WIDELY_SUPPORTED_EFFORTS with reasoningConfig", () => {
48714971
const model = createMockModel({
48724972
id: "bedrock/llama-4",
@@ -5055,6 +5155,12 @@ describe("ProviderTransform.variants", () => {
50555155
efforts: ["low", "medium", "high", "xhigh", "max"],
50565156
thinking: { type: "adaptive", display: "summarized" },
50575157
},
5158+
{
5159+
name: "opus 5",
5160+
apiIds: ["anthropic--claude-opus-5", "anthropic--claude-5-opus"],
5161+
efforts: ["low", "medium", "high", "xhigh", "max"],
5162+
thinking: { type: "adaptive", display: "summarized" },
5163+
},
50585164
]) {
50595165
for (const apiId of testCase.apiIds) {
50605166
test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => {

0 commit comments

Comments
 (0)