Skip to content

Commit f4f508e

Browse files
fix(opencode): support sap-ai-core anthropic opus 4.7+ adaptive reasoning (#29991)
1 parent 30f9780 commit f4f508e

2 files changed

Lines changed: 81 additions & 123 deletions

File tree

packages/opencode/src/provider/transform.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -597,18 +597,31 @@ function openaiCompatibleReasoningEfforts(id: string) {
597597
}
598598

599599
function anthropicOpus47OrLater(apiId: string) {
600-
const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)/i.exec(apiId)
600+
// Matches "opus-4.7" (Anthropic/Bedrock/Vertex) and "claude-4.7-opus" (SAP AI Core inverted).
601+
// Greedy \d+ correctly extends to multi-digit majors (e.g. "claude-10.0-opus") for forward compatibility.
602+
const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)|claude-(\d+)[.-](\d+)-opus(?:[.@-]|$)/i.exec(apiId)
601603
if (!version) return false
602-
const major = Number(version[1])
603-
const minor = Number(version[2])
604+
const major = Number(version[1] ?? version[3])
605+
const minor = Number(version[2] ?? version[4])
604606
return major > 4 || (major === 4 && minor >= 7)
605607
}
606608

607609
function anthropicAdaptiveEfforts(apiId: string): string[] | null {
608610
if (anthropicOpus47OrLater(apiId)) {
609611
return ["low", "medium", "high", "xhigh", "max"]
610612
}
611-
if (["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => apiId.includes(v))) {
613+
if (
614+
[
615+
"opus-4-6",
616+
"opus-4.6",
617+
"4-6-opus",
618+
"4.6-opus",
619+
"sonnet-4-6",
620+
"sonnet-4.6",
621+
"4-6-sonnet",
622+
"4.6-sonnet",
623+
].some((v) => apiId.includes(v))
624+
) {
612625
return ["low", "medium", "high", "max"]
613626
}
614627
return null
@@ -999,6 +1012,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
9991012
{
10001013
thinking: {
10011014
type: "adaptive",
1015+
...(adaptiveOpus ? { display: "summarized" } : {}),
10021016
},
10031017
effort,
10041018
},

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

Lines changed: 63 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -3581,142 +3581,86 @@ describe("ProviderTransform.variants", () => {
35813581
})
35823582

35833583
describe("@jerome-benoit/sap-ai-provider-v2", () => {
3584-
test("anthropic models return thinking variants", () => {
3585-
const model = createMockModel({
3586-
id: "sap-ai-core/anthropic--claude-sonnet-4",
3584+
const sapModel = (apiId: string) =>
3585+
createMockModel({
3586+
id: `sap-ai-core/${apiId}`,
35873587
providerID: "sap-ai-core",
35883588
api: {
3589-
id: "anthropic--claude-sonnet-4",
3589+
id: apiId,
35903590
url: "https://api.ai.sap",
35913591
npm: "@jerome-benoit/sap-ai-provider-v2",
35923592
},
35933593
})
3594-
const result = ProviderTransform.variants(model)
3595-
expect(Object.keys(result)).toEqual(["high", "max"])
3596-
expect(result.high).toEqual({
3597-
thinking: {
3598-
type: "enabled",
3599-
budgetTokens: 16000,
3600-
},
3601-
})
3602-
expect(result.max).toEqual({
3603-
thinking: {
3604-
type: "enabled",
3605-
budgetTokens: 31999,
3606-
},
3607-
})
3608-
})
36093594

3610-
test("anthropic 4.6 models return adaptive thinking variants", () => {
3611-
const model = createMockModel({
3612-
id: "sap-ai-core/anthropic--claude-sonnet-4-6",
3613-
providerID: "sap-ai-core",
3614-
api: {
3615-
id: "anthropic--claude-sonnet-4-6",
3616-
url: "https://api.ai.sap",
3617-
npm: "@jerome-benoit/sap-ai-provider-v2",
3618-
},
3619-
})
3620-
const result = ProviderTransform.variants(model)
3621-
expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
3622-
expect(result.low).toEqual({
3623-
thinking: {
3624-
type: "adaptive",
3625-
},
3626-
effort: "low",
3627-
})
3628-
expect(result.max).toEqual({
3629-
thinking: {
3630-
type: "adaptive",
3631-
},
3632-
effort: "max",
3633-
})
3634-
})
3595+
for (const testCase of [
3596+
{
3597+
name: "sonnet 4.6",
3598+
apiIds: ["anthropic--claude-sonnet-4-6"],
3599+
efforts: ["low", "medium", "high", "max"],
3600+
expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
3601+
},
3602+
{
3603+
name: "opus 4.6",
3604+
apiIds: ["anthropic--claude-4.6-opus", "anthropic--claude-4-6-opus"],
3605+
efforts: ["low", "medium", "high", "max"],
3606+
expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
3607+
},
3608+
{
3609+
name: "opus 4.7",
3610+
apiIds: ["anthropic--claude-4.7-opus", "anthropic--claude-4-7-opus"],
3611+
efforts: ["low", "medium", "high", "xhigh", "max"],
3612+
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
3613+
},
3614+
{
3615+
name: "opus 4.8",
3616+
apiIds: ["anthropic--claude-4.8-opus", "anthropic--claude-4-8-opus"],
3617+
efforts: ["low", "medium", "high", "xhigh", "max"],
3618+
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
3619+
},
3620+
]) {
3621+
for (const apiId of testCase.apiIds) {
3622+
test(`${testCase.name} ${apiId} returns adaptive thinking variants`, () => {
3623+
const result = ProviderTransform.variants(sapModel(apiId))
3624+
expect(Object.keys(result)).toEqual(testCase.efforts)
3625+
expect(result.high).toEqual(testCase.expectedHigh)
3626+
if (testCase.efforts.includes("xhigh")) {
3627+
expect(result.xhigh).toEqual({ ...testCase.expectedHigh, effort: "xhigh" })
3628+
}
3629+
})
3630+
}
3631+
}
36353632

3636-
test("gemini 2.5 models return thinkingConfig variants", () => {
3637-
const model = createMockModel({
3638-
id: "sap-ai-core/gcp--gemini-2.5-pro",
3639-
providerID: "sap-ai-core",
3640-
api: {
3641-
id: "gcp--gemini-2.5-pro",
3642-
url: "https://api.ai.sap",
3643-
npm: "@jerome-benoit/sap-ai-provider-v2",
3644-
},
3645-
})
3646-
const result = ProviderTransform.variants(model)
3633+
test("anthropic sonnet 4 returns budget-tokens variants", () => {
3634+
const result = ProviderTransform.variants(sapModel("anthropic--claude-sonnet-4"))
36473635
expect(Object.keys(result)).toEqual(["high", "max"])
3648-
expect(result.high).toEqual({
3649-
thinkingConfig: {
3650-
includeThoughts: true,
3651-
thinkingBudget: 16000,
3652-
},
3653-
})
3654-
expect(result.max).toEqual({
3655-
thinkingConfig: {
3656-
includeThoughts: true,
3657-
thinkingBudget: 24576,
3658-
},
3659-
})
3636+
expect(result.high).toEqual({ thinking: { type: "enabled", budgetTokens: 16000 } })
3637+
expect(result.max).toEqual({ thinking: { type: "enabled", budgetTokens: 31999 } })
36603638
})
36613639

3662-
test("gpt models return reasoningEffort variants", () => {
3663-
const model = createMockModel({
3664-
id: "sap-ai-core/azure-openai--gpt-4o",
3665-
providerID: "sap-ai-core",
3666-
api: {
3667-
id: "azure-openai--gpt-4o",
3668-
url: "https://api.ai.sap",
3669-
npm: "@jerome-benoit/sap-ai-provider-v2",
3670-
},
3671-
})
3672-
const result = ProviderTransform.variants(model)
3673-
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
3674-
expect(result.low).toEqual({ reasoningEffort: "low" })
3675-
expect(result.high).toEqual({ reasoningEffort: "high" })
3640+
test("gemini 2.5 returns thinkingConfig variants", () => {
3641+
const result = ProviderTransform.variants(sapModel("gcp--gemini-2.5-pro"))
3642+
expect(Object.keys(result)).toEqual(["high", "max"])
3643+
expect(result.high).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } })
3644+
expect(result.max).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } })
36763645
})
36773646

3678-
test("o-series models return reasoningEffort variants", () => {
3679-
const model = createMockModel({
3680-
id: "sap-ai-core/azure-openai--o3-mini",
3681-
providerID: "sap-ai-core",
3682-
api: {
3683-
id: "azure-openai--o3-mini",
3684-
url: "https://api.ai.sap",
3685-
npm: "@jerome-benoit/sap-ai-provider-v2",
3686-
},
3647+
for (const apiId of ["azure-openai--gpt-4o", "azure-openai--o3-mini"]) {
3648+
test(`${apiId} returns reasoningEffort variants`, () => {
3649+
const result = ProviderTransform.variants(sapModel(apiId))
3650+
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
3651+
expect(result.low).toEqual({ reasoningEffort: "low" })
3652+
expect(result.high).toEqual({ reasoningEffort: "high" })
36873653
})
3688-
const result = ProviderTransform.variants(model)
3689-
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
3690-
expect(result.low).toEqual({ reasoningEffort: "low" })
3691-
expect(result.high).toEqual({ reasoningEffort: "high" })
3692-
})
3654+
}
36933655

3694-
test("sonar models return empty object", () => {
3695-
const model = createMockModel({
3696-
id: "sap-ai-core/perplexity--sonar-pro",
3697-
providerID: "sap-ai-core",
3698-
api: {
3699-
id: "perplexity--sonar-pro",
3700-
url: "https://api.ai.sap",
3701-
npm: "@jerome-benoit/sap-ai-provider-v2",
3702-
},
3656+
for (const apiId of ["perplexity--sonar-pro", "mistral--mistral-large"]) {
3657+
test(`${apiId} returns empty object`, () => {
3658+
expect(ProviderTransform.variants(sapModel(apiId))).toEqual({})
37033659
})
3704-
const result = ProviderTransform.variants(model)
3705-
expect(result).toEqual({})
3706-
})
3660+
}
37073661

3708-
test("mistral models return empty object", () => {
3709-
const model = createMockModel({
3710-
id: "sap-ai-core/mistral--mistral-large",
3711-
providerID: "sap-ai-core",
3712-
api: {
3713-
id: "mistral--mistral-large",
3714-
url: "https://api.ai.sap",
3715-
npm: "@jerome-benoit/sap-ai-provider-v2",
3716-
},
3717-
})
3718-
const result = ProviderTransform.variants(model)
3719-
expect(result).toEqual({})
3662+
test("non-anthropic models with opus-like substrings do not get adaptive thinking", () => {
3663+
expect(ProviderTransform.variants(sapModel("aws--llama-opus-4.7-fake"))).toEqual({})
37203664
})
37213665
})
37223666

0 commit comments

Comments
 (0)