From f4f508e659f304dcc93ee10660a82bd07a258026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 31 May 2026 01:30:21 +0200 Subject: [PATCH] fix(opencode): support sap-ai-core anthropic opus 4.7+ adaptive reasoning (#29991) --- packages/opencode/src/provider/transform.ts | 22 ++- .../opencode/test/provider/transform.test.ts | 184 ++++++------------ 2 files changed, 82 insertions(+), 124 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index ea55bb2ce6..c7b65a1098 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -597,10 +597,12 @@ function openaiCompatibleReasoningEfforts(id: string) { } function anthropicOpus47OrLater(apiId: string) { - const version = /opus-(\d+)[.-](\d+)(?:[.@-]|$)/i.exec(apiId) + // 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]) - const minor = Number(version[2]) + const major = Number(version[1] ?? version[3]) + const minor = Number(version[2] ?? version[4]) return major > 4 || (major === 4 && minor >= 7) } @@ -608,7 +610,18 @@ function anthropicAdaptiveEfforts(apiId: string): string[] | null { if (anthropicOpus47OrLater(apiId)) { return ["low", "medium", "high", "xhigh", "max"] } - if (["opus-4-6", "opus-4.6", "sonnet-4-6", "sonnet-4.6"].some((v) => apiId.includes(v))) { + if ( + [ + "opus-4-6", + "opus-4.6", + "4-6-opus", + "4.6-opus", + "sonnet-4-6", + "sonnet-4.6", + "4-6-sonnet", + "4.6-sonnet", + ].some((v) => apiId.includes(v)) + ) { return ["low", "medium", "high", "max"] } return null @@ -999,6 +1012,7 @@ export function variants(model: Provider.Model): Record { }) describe("@jerome-benoit/sap-ai-provider-v2", () => { - test("anthropic models return thinking variants", () => { - const model = createMockModel({ - id: "sap-ai-core/anthropic--claude-sonnet-4", + const sapModel = (apiId: string) => + createMockModel({ + id: `sap-ai-core/${apiId}`, providerID: "sap-ai-core", api: { - id: "anthropic--claude-sonnet-4", + id: apiId, url: "https://api.ai.sap", npm: "@jerome-benoit/sap-ai-provider-v2", }, }) - const result = ProviderTransform.variants(model) + + for (const testCase of [ + { + name: "sonnet 4.6", + apiIds: ["anthropic--claude-sonnet-4-6"], + efforts: ["low", "medium", "high", "max"], + expectedHigh: { thinking: { type: "adaptive" }, effort: "high" }, + }, + { + name: "opus 4.6", + apiIds: ["anthropic--claude-4.6-opus", "anthropic--claude-4-6-opus"], + efforts: ["low", "medium", "high", "max"], + expectedHigh: { thinking: { type: "adaptive" }, effort: "high" }, + }, + { + name: "opus 4.7", + apiIds: ["anthropic--claude-4.7-opus", "anthropic--claude-4-7-opus"], + efforts: ["low", "medium", "high", "xhigh", "max"], + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + }, + { + name: "opus 4.8", + apiIds: ["anthropic--claude-4.8-opus", "anthropic--claude-4-8-opus"], + efforts: ["low", "medium", "high", "xhigh", "max"], + expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" }, + }, + ]) { + for (const apiId of testCase.apiIds) { + test(`${testCase.name} ${apiId} returns adaptive thinking variants`, () => { + const result = ProviderTransform.variants(sapModel(apiId)) + expect(Object.keys(result)).toEqual(testCase.efforts) + expect(result.high).toEqual(testCase.expectedHigh) + if (testCase.efforts.includes("xhigh")) { + expect(result.xhigh).toEqual({ ...testCase.expectedHigh, effort: "xhigh" }) + } + }) + } + } + + test("anthropic sonnet 4 returns budget-tokens variants", () => { + const result = ProviderTransform.variants(sapModel("anthropic--claude-sonnet-4")) expect(Object.keys(result)).toEqual(["high", "max"]) - expect(result.high).toEqual({ - thinking: { - type: "enabled", - budgetTokens: 16000, - }, - }) - expect(result.max).toEqual({ - thinking: { - type: "enabled", - budgetTokens: 31999, - }, - }) + expect(result.high).toEqual({ thinking: { type: "enabled", budgetTokens: 16000 } }) + expect(result.max).toEqual({ thinking: { type: "enabled", budgetTokens: 31999 } }) }) - test("anthropic 4.6 models return adaptive thinking variants", () => { - const model = createMockModel({ - id: "sap-ai-core/anthropic--claude-sonnet-4-6", - providerID: "sap-ai-core", - api: { - id: "anthropic--claude-sonnet-4-6", - url: "https://api.ai.sap", - npm: "@jerome-benoit/sap-ai-provider-v2", - }, - }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"]) - expect(result.low).toEqual({ - thinking: { - type: "adaptive", - }, - effort: "low", - }) - expect(result.max).toEqual({ - thinking: { - type: "adaptive", - }, - effort: "max", - }) - }) - - test("gemini 2.5 models return thinkingConfig variants", () => { - const model = createMockModel({ - id: "sap-ai-core/gcp--gemini-2.5-pro", - providerID: "sap-ai-core", - api: { - id: "gcp--gemini-2.5-pro", - url: "https://api.ai.sap", - npm: "@jerome-benoit/sap-ai-provider-v2", - }, - }) - const result = ProviderTransform.variants(model) + test("gemini 2.5 returns thinkingConfig variants", () => { + const result = ProviderTransform.variants(sapModel("gcp--gemini-2.5-pro")) expect(Object.keys(result)).toEqual(["high", "max"]) - expect(result.high).toEqual({ - thinkingConfig: { - includeThoughts: true, - thinkingBudget: 16000, - }, - }) - expect(result.max).toEqual({ - thinkingConfig: { - includeThoughts: true, - thinkingBudget: 24576, - }, - }) + expect(result.high).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } }) + expect(result.max).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } }) }) - test("gpt models return reasoningEffort variants", () => { - const model = createMockModel({ - id: "sap-ai-core/azure-openai--gpt-4o", - providerID: "sap-ai-core", - api: { - id: "azure-openai--gpt-4o", - url: "https://api.ai.sap", - npm: "@jerome-benoit/sap-ai-provider-v2", - }, + for (const apiId of ["azure-openai--gpt-4o", "azure-openai--o3-mini"]) { + test(`${apiId} returns reasoningEffort variants`, () => { + const result = ProviderTransform.variants(sapModel(apiId)) + expect(Object.keys(result)).toEqual(["low", "medium", "high"]) + expect(result.low).toEqual({ reasoningEffort: "low" }) + expect(result.high).toEqual({ reasoningEffort: "high" }) }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) - expect(result.low).toEqual({ reasoningEffort: "low" }) - expect(result.high).toEqual({ reasoningEffort: "high" }) - }) + } - test("o-series models return reasoningEffort variants", () => { - const model = createMockModel({ - id: "sap-ai-core/azure-openai--o3-mini", - providerID: "sap-ai-core", - api: { - id: "azure-openai--o3-mini", - url: "https://api.ai.sap", - npm: "@jerome-benoit/sap-ai-provider-v2", - }, + for (const apiId of ["perplexity--sonar-pro", "mistral--mistral-large"]) { + test(`${apiId} returns empty object`, () => { + expect(ProviderTransform.variants(sapModel(apiId))).toEqual({}) }) - const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) - expect(result.low).toEqual({ reasoningEffort: "low" }) - expect(result.high).toEqual({ reasoningEffort: "high" }) - }) + } - test("sonar models return empty object", () => { - const model = createMockModel({ - id: "sap-ai-core/perplexity--sonar-pro", - providerID: "sap-ai-core", - api: { - id: "perplexity--sonar-pro", - url: "https://api.ai.sap", - npm: "@jerome-benoit/sap-ai-provider-v2", - }, - }) - const result = ProviderTransform.variants(model) - expect(result).toEqual({}) - }) - - test("mistral models return empty object", () => { - const model = createMockModel({ - id: "sap-ai-core/mistral--mistral-large", - providerID: "sap-ai-core", - api: { - id: "mistral--mistral-large", - url: "https://api.ai.sap", - npm: "@jerome-benoit/sap-ai-provider-v2", - }, - }) - const result = ProviderTransform.variants(model) - expect(result).toEqual({}) + test("non-anthropic models with opus-like substrings do not get adaptive thinking", () => { + expect(ProviderTransform.variants(sapModel("aws--llama-opus-4.7-fake"))).toEqual({}) }) })