Skip to content

Commit 0b796c5

Browse files
fix(opencode): route SAP AI Core reasoning variants through modelParams (anomalyco#30482)
1 parent fa6ea8b commit 0b796c5

3 files changed

Lines changed: 126 additions & 107 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ ts-dist
1515
.turbo
1616
**/.serena
1717
.serena/
18+
**/.omo
19+
.omo/
1820
/result
1921
refs
2022
Session.vim

packages/opencode/src/provider/transform.ts

Lines changed: 58 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,32 @@ function googleThinkingBudgetMax(apiId: string) {
611611
return 24_576
612612
}
613613

614+
// SAP's Zod schema drops unknown top-level keys; reasoning controls survive
615+
// only via `modelParams` (catchall), forwarded verbatim by the SAP SDKs.
616+
function wrapInSapModelParams(
617+
variants: Record<string, Record<string, any>>,
618+
): Record<string, Record<string, any>> {
619+
return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }]))
620+
}
621+
622+
function googleThinkingVariants(model: Provider.Model): Record<string, Record<string, any>> {
623+
const id = model.api.id.toLowerCase()
624+
if (id.includes("2.5")) {
625+
return {
626+
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
627+
max: {
628+
thinkingConfig: { includeThoughts: true, thinkingBudget: googleThinkingBudgetMax(id) },
629+
},
630+
}
631+
}
632+
return Object.fromEntries(
633+
googleThinkingLevelEfforts(id).map((effort) => [
634+
effort,
635+
{ thinkingConfig: { includeThoughts: true, thinkingLevel: effort } },
636+
]),
637+
)
638+
}
639+
614640
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
615641
if (!model.capabilities.reasoning) return {}
616642

@@ -716,7 +742,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
716742
max: {
717743
thinkingConfig: {
718744
includeThoughts: true,
719-
thinkingBudget: 24576,
745+
thinkingBudget: googleThinkingBudgetMax(id),
720746
},
721747
},
722748
}
@@ -903,34 +929,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
903929
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
904930
case "@ai-sdk/google":
905931
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
906-
if (id.includes("2.5")) {
907-
return {
908-
high: {
909-
thinkingConfig: {
910-
includeThoughts: true,
911-
thinkingBudget: 16000,
912-
},
913-
},
914-
max: {
915-
thinkingConfig: {
916-
includeThoughts: true,
917-
thinkingBudget: googleThinkingBudgetMax(id),
918-
},
919-
},
920-
}
921-
}
922-
923-
return Object.fromEntries(
924-
googleThinkingLevelEfforts(id).map((effort) => [
925-
effort,
926-
{
927-
thinkingConfig: {
928-
includeThoughts: true,
929-
thinkingLevel: effort,
930-
},
931-
},
932-
]),
933-
)
932+
return googleThinkingVariants(model)
934933

935934
case "@ai-sdk/mistral":
936935
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
@@ -969,57 +968,43 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
969968
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
970969
return {}
971970

972-
case "@jerome-benoit/sap-ai-provider-v2":
973-
if (model.api.id.includes("anthropic")) {
971+
case "@jerome-benoit/sap-ai-provider-v2": {
972+
if (id.includes("anthropic")) {
974973
if (adaptiveEfforts) {
975-
return Object.fromEntries(
976-
adaptiveEfforts.map((effort) => [
977-
effort,
978-
{
979-
thinking: {
980-
type: "adaptive",
981-
...(adaptiveOpus ? { display: "summarized" } : {}),
982-
},
974+
// Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
975+
// native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
976+
return wrapInSapModelParams(
977+
Object.fromEntries(
978+
adaptiveEfforts.map((effort) => [
983979
effort,
984-
},
985-
]),
980+
{
981+
thinking: { type: "adaptive", ...(adaptiveOpus ? { display: "summarized" } : {}) },
982+
output_config: { effort },
983+
},
984+
]),
985+
),
986986
)
987987
}
988-
return {
989-
high: {
990-
thinking: {
991-
type: "enabled",
992-
budgetTokens: 16000,
993-
},
994-
},
995-
max: {
996-
thinking: {
997-
type: "enabled",
998-
budgetTokens: 31999,
999-
},
1000-
},
1001-
}
988+
return wrapInSapModelParams({
989+
high: { thinking: { type: "enabled", budget_tokens: 16000 } },
990+
max: { thinking: { type: "enabled", budget_tokens: 31999 } },
991+
})
1002992
}
1003-
if (model.api.id.includes("gemini") && id.includes("2.5")) {
1004-
return {
1005-
high: {
1006-
thinkingConfig: {
1007-
includeThoughts: true,
1008-
thinkingBudget: 16000,
1009-
},
1010-
},
1011-
max: {
1012-
thinkingConfig: {
1013-
includeThoughts: true,
1014-
thinkingBudget: 24576,
1015-
},
1016-
},
1017-
}
993+
if (id.includes("gemini") && id.includes("2.5")) {
994+
return wrapInSapModelParams(googleThinkingVariants(model))
1018995
}
1019-
if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
1020-
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
996+
if (id.includes("gpt") || /\bo[1-9]/.test(id)) {
997+
const efforts = openaiReasoningEfforts(id, model.release_date)
998+
return wrapInSapModelParams(
999+
Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])),
1000+
)
10211001
}
1022-
return {}
1002+
return wrapInSapModelParams(
1003+
Object.fromEntries(
1004+
["low", "medium", "high"].map((effort) => [effort, { reasoning_effort: effort }]),
1005+
),
1006+
)
1007+
}
10231008
}
10241009
return {}
10251010
}

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

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3504,7 +3504,7 @@ describe("ProviderTransform.variants", () => {
35043504
})
35053505

35063506
describe("@jerome-benoit/sap-ai-provider-v2", () => {
3507-
const sapModel = (apiId: string) =>
3507+
const sapModel = (apiId: string, releaseDate = "2024-01-01") =>
35083508
createMockModel({
35093509
id: `sap-ai-core/${apiId}`,
35103510
providerID: "sap-ai-core",
@@ -3513,78 +3513,110 @@ describe("ProviderTransform.variants", () => {
35133513
url: "https://api.ai.sap",
35143514
npm: "@jerome-benoit/sap-ai-provider-v2",
35153515
},
3516+
release_date: releaseDate,
35163517
})
35173518

35183519
for (const testCase of [
35193520
{
35203521
name: "sonnet 4.6",
35213522
apiIds: ["anthropic--claude-sonnet-4-6"],
35223523
efforts: ["low", "medium", "high", "max"],
3523-
expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
3524+
thinking: { type: "adaptive" },
35243525
},
35253526
{
35263527
name: "opus 4.6",
35273528
apiIds: ["anthropic--claude-4.6-opus", "anthropic--claude-4-6-opus"],
35283529
efforts: ["low", "medium", "high", "max"],
3529-
expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
3530+
thinking: { type: "adaptive" },
35303531
},
35313532
{
35323533
name: "opus 4.7",
35333534
apiIds: ["anthropic--claude-4.7-opus", "anthropic--claude-4-7-opus"],
35343535
efforts: ["low", "medium", "high", "xhigh", "max"],
3535-
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
3536+
thinking: { type: "adaptive", display: "summarized" },
35363537
},
35373538
{
35383539
name: "opus 4.8",
35393540
apiIds: ["anthropic--claude-4.8-opus", "anthropic--claude-4-8-opus"],
35403541
efforts: ["low", "medium", "high", "xhigh", "max"],
3541-
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
3542+
thinking: { type: "adaptive", display: "summarized" },
35423543
},
35433544
]) {
35443545
for (const apiId of testCase.apiIds) {
3545-
test(`${testCase.name} ${apiId} returns adaptive thinking variants`, () => {
3546+
test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => {
35463547
const result = ProviderTransform.variants(sapModel(apiId))
35473548
expect(Object.keys(result)).toEqual(testCase.efforts)
3548-
expect(result.high).toEqual(testCase.expectedHigh)
3549-
if (testCase.efforts.includes("xhigh")) {
3550-
expect(result.xhigh).toEqual({ ...testCase.expectedHigh, effort: "xhigh" })
3549+
for (const effort of testCase.efforts) {
3550+
expect(result[effort]).toEqual({
3551+
modelParams: {
3552+
thinking: testCase.thinking,
3553+
output_config: { effort },
3554+
},
3555+
})
35513556
}
35523557
})
35533558
}
35543559
}
35553560

3556-
test("anthropic sonnet 4 returns budget-tokens variants", () => {
3557-
const result = ProviderTransform.variants(sapModel("anthropic--claude-sonnet-4"))
3558-
expect(Object.keys(result)).toEqual(["high", "max"])
3559-
expect(result.high).toEqual({ thinking: { type: "enabled", budgetTokens: 16000 } })
3560-
expect(result.max).toEqual({ thinking: { type: "enabled", budgetTokens: 31999 } })
3561-
})
3562-
3563-
test("gemini 2.5 returns thinkingConfig variants", () => {
3564-
const result = ProviderTransform.variants(sapModel("gcp--gemini-2.5-pro"))
3565-
expect(Object.keys(result)).toEqual(["high", "max"])
3566-
expect(result.high).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } })
3567-
expect(result.max).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } })
3568-
})
3569-
3570-
for (const apiId of ["azure-openai--gpt-4o", "azure-openai--o3-mini"]) {
3571-
test(`${apiId} returns reasoningEffort variants`, () => {
3561+
for (const apiId of ["anthropic--claude-sonnet-4", "anthropic--claude-4.5-opus"]) {
3562+
test(`${apiId} returns budget_tokens variants under modelParams`, () => {
35723563
const result = ProviderTransform.variants(sapModel(apiId))
3573-
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
3574-
expect(result.low).toEqual({ reasoningEffort: "low" })
3575-
expect(result.high).toEqual({ reasoningEffort: "high" })
3564+
expect(Object.keys(result)).toEqual(["high", "max"])
3565+
expect(result.high).toEqual({
3566+
modelParams: { thinking: { type: "enabled", budget_tokens: 16000 } },
3567+
})
3568+
expect(result.max).toEqual({
3569+
modelParams: { thinking: { type: "enabled", budget_tokens: 31999 } },
3570+
})
35763571
})
35773572
}
35783573

3579-
for (const apiId of ["perplexity--sonar-pro", "mistral--mistral-large"]) {
3580-
test(`${apiId} returns empty object`, () => {
3581-
expect(ProviderTransform.variants(sapModel(apiId))).toEqual({})
3574+
for (const testCase of [
3575+
{ apiId: "gemini-2.5-pro", maxBudget: 32768 },
3576+
{ apiId: "gemini-2.5-flash", maxBudget: 24576 },
3577+
]) {
3578+
test(`${testCase.apiId} returns thinkingConfig variants under modelParams`, () => {
3579+
const result = ProviderTransform.variants(sapModel(testCase.apiId))
3580+
expect(Object.keys(result)).toEqual(["high", "max"])
3581+
expect(result.high).toEqual({
3582+
modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
3583+
})
3584+
expect(result.max).toEqual({
3585+
modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: testCase.maxBudget } },
3586+
})
35823587
})
35833588
}
35843589

3585-
test("non-anthropic models with opus-like substrings do not get adaptive thinking", () => {
3586-
expect(ProviderTransform.variants(sapModel("aws--llama-opus-4.7-fake"))).toEqual({})
3587-
})
3590+
for (const testCase of [
3591+
{ apiId: "gpt-5", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] },
3592+
{ apiId: "gpt-5-mini", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] },
3593+
{ apiId: "gpt-5-nano", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] },
3594+
{ apiId: "gpt-5.4", releaseDate: "2026-01-15", efforts: ["none", "low", "medium", "high", "xhigh"] },
3595+
{ apiId: "azure-openai--o3-mini", releaseDate: "2024-01-01", efforts: ["low", "medium", "high"] },
3596+
]) {
3597+
test(`${testCase.apiId} returns reasoning_effort variants under modelParams`, () => {
3598+
const result = ProviderTransform.variants(sapModel(testCase.apiId, testCase.releaseDate))
3599+
expect(Object.keys(result)).toEqual(testCase.efforts)
3600+
for (const effort of testCase.efforts) {
3601+
expect(result[effort]).toEqual({ modelParams: { reasoning_effort: effort } })
3602+
}
3603+
})
3604+
}
3605+
3606+
for (const apiId of [
3607+
"gemini-3.1-flash-lite",
3608+
"cohere--command-a-reasoning",
3609+
"sonar-deep-research",
3610+
"aws--llama-opus-4.7-fake",
3611+
]) {
3612+
test(`${apiId} falls through to harmonized reasoning_effort fallback`, () => {
3613+
const result = ProviderTransform.variants(sapModel(apiId))
3614+
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
3615+
for (const effort of ["low", "medium", "high"]) {
3616+
expect(result[effort]).toEqual({ modelParams: { reasoning_effort: effort } })
3617+
}
3618+
})
3619+
}
35883620
})
35893621

35903622
describe("ai-gateway-provider (cloudflare-ai-gateway)", () => {

0 commit comments

Comments
 (0)