fix(opencode): route SAP AI Core reasoning variants through modelParams (#30482)
This commit is contained in:
parent
fa6ea8bd25
commit
0b796c5f3d
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,6 +15,8 @@ ts-dist
|
|||||||
.turbo
|
.turbo
|
||||||
**/.serena
|
**/.serena
|
||||||
.serena/
|
.serena/
|
||||||
|
**/.omo
|
||||||
|
.omo/
|
||||||
/result
|
/result
|
||||||
refs
|
refs
|
||||||
Session.vim
|
Session.vim
|
||||||
|
|||||||
@ -611,6 +611,32 @@ function googleThinkingBudgetMax(apiId: string) {
|
|||||||
return 24_576
|
return 24_576
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SAP's Zod schema drops unknown top-level keys; reasoning controls survive
|
||||||
|
// only via `modelParams` (catchall), forwarded verbatim by the SAP SDKs.
|
||||||
|
function wrapInSapModelParams(
|
||||||
|
variants: Record<string, Record<string, any>>,
|
||||||
|
): Record<string, Record<string, any>> {
|
||||||
|
return Object.fromEntries(Object.entries(variants).map(([k, v]) => [k, { modelParams: v }]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function googleThinkingVariants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||||
|
const id = model.api.id.toLowerCase()
|
||||||
|
if (id.includes("2.5")) {
|
||||||
|
return {
|
||||||
|
high: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||||
|
max: {
|
||||||
|
thinkingConfig: { includeThoughts: true, thinkingBudget: googleThinkingBudgetMax(id) },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.fromEntries(
|
||||||
|
googleThinkingLevelEfforts(id).map((effort) => [
|
||||||
|
effort,
|
||||||
|
{ thinkingConfig: { includeThoughts: true, thinkingLevel: effort } },
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
export function variants(model: Provider.Model): Record<string, Record<string, any>> {
|
||||||
if (!model.capabilities.reasoning) return {}
|
if (!model.capabilities.reasoning) return {}
|
||||||
|
|
||||||
@ -716,7 +742,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
max: {
|
max: {
|
||||||
thinkingConfig: {
|
thinkingConfig: {
|
||||||
includeThoughts: true,
|
includeThoughts: true,
|
||||||
thinkingBudget: 24576,
|
thinkingBudget: googleThinkingBudgetMax(id),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -903,34 +929,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
|
|||||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
|
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
|
||||||
case "@ai-sdk/google":
|
case "@ai-sdk/google":
|
||||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
|
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
|
||||||
if (id.includes("2.5")) {
|
return googleThinkingVariants(model)
|
||||||
return {
|
|
||||||
high: {
|
|
||||||
thinkingConfig: {
|
|
||||||
includeThoughts: true,
|
|
||||||
thinkingBudget: 16000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
thinkingConfig: {
|
|
||||||
includeThoughts: true,
|
|
||||||
thinkingBudget: googleThinkingBudgetMax(id),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
|
||||||
googleThinkingLevelEfforts(id).map((effort) => [
|
|
||||||
effort,
|
|
||||||
{
|
|
||||||
thinkingConfig: {
|
|
||||||
includeThoughts: true,
|
|
||||||
thinkingLevel: effort,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
|
|
||||||
case "@ai-sdk/mistral":
|
case "@ai-sdk/mistral":
|
||||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
|
// 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
|
|||||||
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
|
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
case "@jerome-benoit/sap-ai-provider-v2":
|
case "@jerome-benoit/sap-ai-provider-v2": {
|
||||||
if (model.api.id.includes("anthropic")) {
|
if (id.includes("anthropic")) {
|
||||||
if (adaptiveEfforts) {
|
if (adaptiveEfforts) {
|
||||||
return Object.fromEntries(
|
// Bedrock adaptive splits `effort` out into `output_config` (vs Anthropic
|
||||||
adaptiveEfforts.map((effort) => [
|
// native which inlines it). Opus 4.7+ flipped `display` default to "omitted".
|
||||||
effort,
|
return wrapInSapModelParams(
|
||||||
{
|
Object.fromEntries(
|
||||||
thinking: {
|
adaptiveEfforts.map((effort) => [
|
||||||
type: "adaptive",
|
|
||||||
...(adaptiveOpus ? { display: "summarized" } : {}),
|
|
||||||
},
|
|
||||||
effort,
|
effort,
|
||||||
},
|
{
|
||||||
]),
|
thinking: { type: "adaptive", ...(adaptiveOpus ? { display: "summarized" } : {}) },
|
||||||
|
output_config: { effort },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return {
|
return wrapInSapModelParams({
|
||||||
high: {
|
high: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
||||||
thinking: {
|
max: { thinking: { type: "enabled", budget_tokens: 31999 } },
|
||||||
type: "enabled",
|
})
|
||||||
budgetTokens: 16000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
thinking: {
|
|
||||||
type: "enabled",
|
|
||||||
budgetTokens: 31999,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (model.api.id.includes("gemini") && id.includes("2.5")) {
|
if (id.includes("gemini") && id.includes("2.5")) {
|
||||||
return {
|
return wrapInSapModelParams(googleThinkingVariants(model))
|
||||||
high: {
|
|
||||||
thinkingConfig: {
|
|
||||||
includeThoughts: true,
|
|
||||||
thinkingBudget: 16000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
max: {
|
|
||||||
thinkingConfig: {
|
|
||||||
includeThoughts: true,
|
|
||||||
thinkingBudget: 24576,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (model.api.id.includes("gpt") || /\bo[1-9]/.test(model.api.id)) {
|
if (id.includes("gpt") || /\bo[1-9]/.test(id)) {
|
||||||
return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
|
const efforts = openaiReasoningEfforts(id, model.release_date)
|
||||||
|
return wrapInSapModelParams(
|
||||||
|
Object.fromEntries(efforts.map((effort) => [effort, { reasoning_effort: effort }])),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return {}
|
return wrapInSapModelParams(
|
||||||
|
Object.fromEntries(
|
||||||
|
["low", "medium", "high"].map((effort) => [effort, { reasoning_effort: effort }]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3504,7 +3504,7 @@ describe("ProviderTransform.variants", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("@jerome-benoit/sap-ai-provider-v2", () => {
|
describe("@jerome-benoit/sap-ai-provider-v2", () => {
|
||||||
const sapModel = (apiId: string) =>
|
const sapModel = (apiId: string, releaseDate = "2024-01-01") =>
|
||||||
createMockModel({
|
createMockModel({
|
||||||
id: `sap-ai-core/${apiId}`,
|
id: `sap-ai-core/${apiId}`,
|
||||||
providerID: "sap-ai-core",
|
providerID: "sap-ai-core",
|
||||||
@ -3513,6 +3513,7 @@ describe("ProviderTransform.variants", () => {
|
|||||||
url: "https://api.ai.sap",
|
url: "https://api.ai.sap",
|
||||||
npm: "@jerome-benoit/sap-ai-provider-v2",
|
npm: "@jerome-benoit/sap-ai-provider-v2",
|
||||||
},
|
},
|
||||||
|
release_date: releaseDate,
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const testCase of [
|
for (const testCase of [
|
||||||
@ -3520,71 +3521,102 @@ describe("ProviderTransform.variants", () => {
|
|||||||
name: "sonnet 4.6",
|
name: "sonnet 4.6",
|
||||||
apiIds: ["anthropic--claude-sonnet-4-6"],
|
apiIds: ["anthropic--claude-sonnet-4-6"],
|
||||||
efforts: ["low", "medium", "high", "max"],
|
efforts: ["low", "medium", "high", "max"],
|
||||||
expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
|
thinking: { type: "adaptive" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "opus 4.6",
|
name: "opus 4.6",
|
||||||
apiIds: ["anthropic--claude-4.6-opus", "anthropic--claude-4-6-opus"],
|
apiIds: ["anthropic--claude-4.6-opus", "anthropic--claude-4-6-opus"],
|
||||||
efforts: ["low", "medium", "high", "max"],
|
efforts: ["low", "medium", "high", "max"],
|
||||||
expectedHigh: { thinking: { type: "adaptive" }, effort: "high" },
|
thinking: { type: "adaptive" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "opus 4.7",
|
name: "opus 4.7",
|
||||||
apiIds: ["anthropic--claude-4.7-opus", "anthropic--claude-4-7-opus"],
|
apiIds: ["anthropic--claude-4.7-opus", "anthropic--claude-4-7-opus"],
|
||||||
efforts: ["low", "medium", "high", "xhigh", "max"],
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||||
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
thinking: { type: "adaptive", display: "summarized" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "opus 4.8",
|
name: "opus 4.8",
|
||||||
apiIds: ["anthropic--claude-4.8-opus", "anthropic--claude-4-8-opus"],
|
apiIds: ["anthropic--claude-4.8-opus", "anthropic--claude-4-8-opus"],
|
||||||
efforts: ["low", "medium", "high", "xhigh", "max"],
|
efforts: ["low", "medium", "high", "xhigh", "max"],
|
||||||
expectedHigh: { thinking: { type: "adaptive", display: "summarized" }, effort: "high" },
|
thinking: { type: "adaptive", display: "summarized" },
|
||||||
},
|
},
|
||||||
]) {
|
]) {
|
||||||
for (const apiId of testCase.apiIds) {
|
for (const apiId of testCase.apiIds) {
|
||||||
test(`${testCase.name} ${apiId} returns adaptive thinking variants`, () => {
|
test(`${testCase.name} ${apiId} returns adaptive thinking variants under modelParams`, () => {
|
||||||
const result = ProviderTransform.variants(sapModel(apiId))
|
const result = ProviderTransform.variants(sapModel(apiId))
|
||||||
expect(Object.keys(result)).toEqual(testCase.efforts)
|
expect(Object.keys(result)).toEqual(testCase.efforts)
|
||||||
expect(result.high).toEqual(testCase.expectedHigh)
|
for (const effort of testCase.efforts) {
|
||||||
if (testCase.efforts.includes("xhigh")) {
|
expect(result[effort]).toEqual({
|
||||||
expect(result.xhigh).toEqual({ ...testCase.expectedHigh, effort: "xhigh" })
|
modelParams: {
|
||||||
|
thinking: testCase.thinking,
|
||||||
|
output_config: { effort },
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("anthropic sonnet 4 returns budget-tokens variants", () => {
|
for (const apiId of ["anthropic--claude-sonnet-4", "anthropic--claude-4.5-opus"]) {
|
||||||
const result = ProviderTransform.variants(sapModel("anthropic--claude-sonnet-4"))
|
test(`${apiId} returns budget_tokens variants under modelParams`, () => {
|
||||||
expect(Object.keys(result)).toEqual(["high", "max"])
|
const result = ProviderTransform.variants(sapModel(apiId))
|
||||||
expect(result.high).toEqual({ thinking: { type: "enabled", budgetTokens: 16000 } })
|
expect(Object.keys(result)).toEqual(["high", "max"])
|
||||||
expect(result.max).toEqual({ thinking: { type: "enabled", budgetTokens: 31999 } })
|
expect(result.high).toEqual({
|
||||||
})
|
modelParams: { thinking: { type: "enabled", budget_tokens: 16000 } },
|
||||||
|
})
|
||||||
|
expect(result.max).toEqual({
|
||||||
|
modelParams: { thinking: { type: "enabled", budget_tokens: 31999 } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
test("gemini 2.5 returns thinkingConfig variants", () => {
|
for (const testCase of [
|
||||||
const result = ProviderTransform.variants(sapModel("gcp--gemini-2.5-pro"))
|
{ apiId: "gemini-2.5-pro", maxBudget: 32768 },
|
||||||
expect(Object.keys(result)).toEqual(["high", "max"])
|
{ apiId: "gemini-2.5-flash", maxBudget: 24576 },
|
||||||
expect(result.high).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } })
|
]) {
|
||||||
expect(result.max).toEqual({ thinkingConfig: { includeThoughts: true, thinkingBudget: 24576 } })
|
test(`${testCase.apiId} returns thinkingConfig variants under modelParams`, () => {
|
||||||
})
|
const result = ProviderTransform.variants(sapModel(testCase.apiId))
|
||||||
|
expect(Object.keys(result)).toEqual(["high", "max"])
|
||||||
|
expect(result.high).toEqual({
|
||||||
|
modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: 16000 } },
|
||||||
|
})
|
||||||
|
expect(result.max).toEqual({
|
||||||
|
modelParams: { thinkingConfig: { includeThoughts: true, thinkingBudget: testCase.maxBudget } },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const apiId of ["azure-openai--gpt-4o", "azure-openai--o3-mini"]) {
|
for (const testCase of [
|
||||||
test(`${apiId} returns reasoningEffort variants`, () => {
|
{ apiId: "gpt-5", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] },
|
||||||
|
{ apiId: "gpt-5-mini", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] },
|
||||||
|
{ apiId: "gpt-5-nano", releaseDate: "2025-08-07", efforts: ["minimal", "low", "medium", "high"] },
|
||||||
|
{ apiId: "gpt-5.4", releaseDate: "2026-01-15", efforts: ["none", "low", "medium", "high", "xhigh"] },
|
||||||
|
{ apiId: "azure-openai--o3-mini", releaseDate: "2024-01-01", efforts: ["low", "medium", "high"] },
|
||||||
|
]) {
|
||||||
|
test(`${testCase.apiId} returns reasoning_effort variants under modelParams`, () => {
|
||||||
|
const result = ProviderTransform.variants(sapModel(testCase.apiId, testCase.releaseDate))
|
||||||
|
expect(Object.keys(result)).toEqual(testCase.efforts)
|
||||||
|
for (const effort of testCase.efforts) {
|
||||||
|
expect(result[effort]).toEqual({ modelParams: { reasoning_effort: effort } })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const apiId of [
|
||||||
|
"gemini-3.1-flash-lite",
|
||||||
|
"cohere--command-a-reasoning",
|
||||||
|
"sonar-deep-research",
|
||||||
|
"aws--llama-opus-4.7-fake",
|
||||||
|
]) {
|
||||||
|
test(`${apiId} falls through to harmonized reasoning_effort fallback`, () => {
|
||||||
const result = ProviderTransform.variants(sapModel(apiId))
|
const result = ProviderTransform.variants(sapModel(apiId))
|
||||||
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
expect(Object.keys(result)).toEqual(["low", "medium", "high"])
|
||||||
expect(result.low).toEqual({ reasoningEffort: "low" })
|
for (const effort of ["low", "medium", "high"]) {
|
||||||
expect(result.high).toEqual({ reasoningEffort: "high" })
|
expect(result[effort]).toEqual({ modelParams: { reasoning_effort: effort } })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const apiId of ["perplexity--sonar-pro", "mistral--mistral-large"]) {
|
|
||||||
test(`${apiId} returns empty object`, () => {
|
|
||||||
expect(ProviderTransform.variants(sapModel(apiId))).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({})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("ai-gateway-provider (cloudflare-ai-gateway)", () => {
|
describe("ai-gateway-provider (cloudflare-ai-gateway)", () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user