From 0b9d66e8341336acbdcc1d24e175e15953c61eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 30 May 2026 15:55:35 +0200 Subject: [PATCH 1/3] fix(opencode): support sap-ai-core anthropic opus 4.7+ adaptive reasoning SAP AI Core uses inverted naming `anthropic--claude-{N}.{M}-opus` (family at the end), which never matched the existing `opus-(\d+)[.-](\d+)` regex or the `anthropicAdaptiveEfforts` substring list, and the SAP branch in `variants()` did not spread `display: "summarized"` on opus 4.7+. - Extend `anthropicOpus47OrLater` regex with a second alternation arm matching `claude-{N}.{M}-opus` (and dash form). Single shared major/minor comparison preserved. - Extend `anthropicAdaptiveEfforts` substring list with the SAP-style `4-6-opus`, `4.6-opus`, `4-6-sonnet`, `4.6-sonnet` markers. - Add `...(adaptiveOpus ? { display: "summarized" } : {})` spread to the `@jerome-benoit/sap-ai-provider-v2` anthropic adaptive branch, mirroring `@ai-sdk/anthropic` and `@ai-sdk/amazon-bedrock`. Closes #29990 --- packages/opencode/src/provider/transform.ts | 22 +++- .../opencode/test/provider/transform.test.ts | 103 ++++++++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 688e7dc8c985..4ae7aa898390 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 @@ -995,6 +1008,7 @@ export function variants(model: Provider.Model): Record { const result = ProviderTransform.variants(model) expect(result).toEqual({}) }) + + test("anthropic opus 4.6 dot-format models return adaptive thinking variants without display", () => { + const model = createMockModel({ + id: "sap-ai-core/anthropic--claude-4.6-opus", + providerID: "sap-ai-core", + api: { + id: "anthropic--claude-4.6-opus", + 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.high).toEqual({ thinking: { type: "adaptive" }, effort: "high" }) + expect(result.max).toEqual({ thinking: { type: "adaptive" }, effort: "max" }) + }) + + test("anthropic opus 4.6 dash-format models return adaptive thinking variants without display", () => { + const model = createMockModel({ + id: "sap-ai-core/anthropic--claude-4-6-opus", + providerID: "sap-ai-core", + api: { + id: "anthropic--claude-4-6-opus", + 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.high).toEqual({ thinking: { type: "adaptive" }, effort: "high" }) + }) + + test("anthropic opus 4.7 dot-format models return adaptive thinking variants with xhigh and display summarized", () => { + const model = createMockModel({ + id: "sap-ai-core/anthropic--claude-4.7-opus", + providerID: "sap-ai-core", + api: { + id: "anthropic--claude-4.7-opus", + 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", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "high", + }) + expect(result.xhigh).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "xhigh", + }) + }) + + test("anthropic opus 4.7 dash-format models return adaptive thinking variants with xhigh and display summarized", () => { + const model = createMockModel({ + id: "sap-ai-core/anthropic--claude-4-7-opus", + providerID: "sap-ai-core", + api: { + id: "anthropic--claude-4-7-opus", + 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", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "high", + }) + }) + + test("anthropic opus 4.8 dot-format models return adaptive thinking variants with xhigh and display summarized", () => { + const model = createMockModel({ + id: "sap-ai-core/anthropic--claude-4.8-opus", + providerID: "sap-ai-core", + api: { + id: "anthropic--claude-4.8-opus", + 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", "xhigh", "max"]) + expect(result.high).toEqual({ + thinking: { type: "adaptive", display: "summarized" }, + effort: "high", + }) + }) + + test("non-anthropic models with opus-like substrings do not get adaptive thinking", () => { + const model = createMockModel({ + id: "sap-ai-core/aws--llama-opus-4.7-fake", + providerID: "sap-ai-core", + api: { + id: "aws--llama-opus-4.7-fake", + url: "https://api.ai.sap", + npm: "@jerome-benoit/sap-ai-provider-v2", + }, + }) + const result = ProviderTransform.variants(model) + expect(result).toEqual({}) + }) }) describe("ai-gateway-provider (cloudflare-ai-gateway)", () => { From e55a7b83e9fcf9bc11919bc817961c550ba79f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 30 May 2026 16:14:11 +0200 Subject: [PATCH 2/3] test(opencode): parameterize sap-ai-core variants tests Refactor the @jerome-benoit/sap-ai-provider-v2 describe block to use a local sapModel() helper and parameterized loops, mirroring the cloudflare-ai-gateway pattern in the same file. Reduces ~159 lines of createMockModel boilerplate while preserving coverage. - Adaptive cases (sonnet 4.6, opus 4.6/4.7/4.8, dot/dash formats) collapsed into a single parameterized loop with xhigh assertion gated on testCase.efforts. - gpt / o-series collapsed into a small loop. - sonar / mistral collapsed into a small loop. - Non-anthropic opus-substring guard kept standalone to preserve its intent as a regression guard for the id.includes("anthropic") gate. - sonnet 4 (budget tokens) and gemini 2.5 (thinkingConfig) kept standalone since their assertion shapes are unique. No production code changes. --- .../opencode/test/provider/transform.test.ts | 283 ++++-------------- 1 file changed, 62 insertions(+), 221 deletions(-) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index d0029fa41b72..ad9254272f1a 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3538,245 +3538,86 @@ describe("ProviderTransform.variants", () => { }) 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) - 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, - }, - }) - }) - 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", - }) - }) + 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"], + 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("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("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({ - thinkingConfig: { - includeThoughts: true, - thinkingBudget: 16000, - }, - }) - expect(result.max).toEqual({ - thinkingConfig: { - includeThoughts: true, - thinkingBudget: 24576, - }, - }) + expect(result.high).toEqual({ thinking: { type: "enabled", budgetTokens: 16000 } }) + expect(result.max).toEqual({ thinking: { type: "enabled", budgetTokens: 31999 } }) }) - 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", - }, - }) - 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", - }, - }) - 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("anthropic opus 4.6 dot-format models return adaptive thinking variants without display", () => { - const model = createMockModel({ - id: "sap-ai-core/anthropic--claude-4.6-opus", - providerID: "sap-ai-core", - api: { - id: "anthropic--claude-4.6-opus", - 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.high).toEqual({ thinking: { type: "adaptive" }, effort: "high" }) - expect(result.max).toEqual({ thinking: { type: "adaptive" }, effort: "max" }) - }) - - test("anthropic opus 4.6 dash-format models return adaptive thinking variants without display", () => { - const model = createMockModel({ - id: "sap-ai-core/anthropic--claude-4-6-opus", - providerID: "sap-ai-core", - api: { - id: "anthropic--claude-4-6-opus", - 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.high).toEqual({ thinking: { type: "adaptive" }, effort: "high" }) - }) - - test("anthropic opus 4.7 dot-format models return adaptive thinking variants with xhigh and display summarized", () => { - const model = createMockModel({ - id: "sap-ai-core/anthropic--claude-4.7-opus", - providerID: "sap-ai-core", - api: { - id: "anthropic--claude-4.7-opus", - 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", "xhigh", "max"]) - expect(result.high).toEqual({ - thinking: { type: "adaptive", display: "summarized" }, - effort: "high", - }) - expect(result.xhigh).toEqual({ - thinking: { type: "adaptive", display: "summarized" }, - effort: "xhigh", - }) + 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 } }) }) - test("anthropic opus 4.7 dash-format models return adaptive thinking variants with xhigh and display summarized", () => { - const model = createMockModel({ - id: "sap-ai-core/anthropic--claude-4-7-opus", - providerID: "sap-ai-core", - api: { - id: "anthropic--claude-4-7-opus", - 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", "xhigh", "max"]) - expect(result.high).toEqual({ - thinking: { type: "adaptive", display: "summarized" }, - effort: "high", + 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" }) }) - }) + } - test("anthropic opus 4.8 dot-format models return adaptive thinking variants with xhigh and display summarized", () => { - const model = createMockModel({ - id: "sap-ai-core/anthropic--claude-4.8-opus", - providerID: "sap-ai-core", - api: { - id: "anthropic--claude-4.8-opus", - 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", "xhigh", "max"]) - expect(result.high).toEqual({ - thinking: { type: "adaptive", display: "summarized" }, - effort: "high", - }) - }) + } test("non-anthropic models with opus-like substrings do not get adaptive thinking", () => { - const model = createMockModel({ - id: "sap-ai-core/aws--llama-opus-4.7-fake", - providerID: "sap-ai-core", - api: { - id: "aws--llama-opus-4.7-fake", - url: "https://api.ai.sap", - npm: "@jerome-benoit/sap-ai-provider-v2", - }, - }) - const result = ProviderTransform.variants(model) - expect(result).toEqual({}) + expect(ProviderTransform.variants(sapModel("aws--llama-opus-4.7-fake"))).toEqual({}) }) }) From a11a8863d60628d67fd8b48fcdf03e3bf7e22a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sat, 30 May 2026 16:18:47 +0200 Subject: [PATCH 3/3] test(opencode): cover sap-ai-core opus 4.8 dash format Mirror the dot+dash symmetry already in place for opus 4.6 and 4.7. Addresses asymmetry flagged in PR #29991 review. --- packages/opencode/test/provider/transform.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index ad9254272f1a..2fed4e511ee9 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3570,7 +3570,7 @@ describe("ProviderTransform.variants", () => { }, { name: "opus 4.8", - apiIds: ["anthropic--claude-4.8-opus"], + 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" }, },