fix(core): route Bedrock packages natively (#40165)

Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot] 2026-08-02 11:47:35 -05:00 committed by GitHub
parent bf7f0cb521
commit dd014120cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 164 additions and 21 deletions

View file

@ -14,12 +14,17 @@ export interface MapInput {
readonly packageName: string | undefined
readonly settings: Readonly<Record<string, unknown>>
readonly modelID: string
readonly hasCredential?: boolean
}
export function map(input: MapInput): Mapping | undefined {
const baseSettings = mapBaseSettings(input.settings)
switch (input.packageName) {
case "@ai-sdk/amazon-bedrock":
return {
package: "@opencode-ai/ai/providers/amazon-bedrock",
settings: mapBedrockSettings(input.settings, baseSettings),
...mapBedrockRequest(input),
}
case "@ai-sdk/amazon-bedrock/mantle":
return mapBedrockMantle(input, baseSettings)
case "@ai-sdk/google":
@ -47,6 +52,21 @@ export function map(input: MapInput): Mapping | undefined {
function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
const settings = input.settings
const chat = input.modelID === "openai.gpt-oss-safeguard-20b" || input.modelID === "openai.gpt-oss-safeguard-120b"
return {
package: `@opencode-ai/ai/providers/amazon-bedrock/mantle/${chat ? "chat" : "responses"}`,
settings: {
...mapBedrockSettings(settings, baseSettings),
...mapOpenAIOptions(settings),
},
...(isStringRecord(settings.headers) ? { headers: settings.headers } : {}),
}
}
function mapBedrockSettings(
settings: Readonly<Record<string, unknown>>,
baseSettings: Readonly<Record<string, unknown>>,
) {
const apiKey =
typeof settings.apiKey === "string"
? settings.apiKey
@ -54,20 +74,69 @@ function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string,
? settings.bearerToken
: undefined
const credentials = mapBedrockCredentials(settings)
if (!input.hasCredential && apiKey === undefined && credentials === undefined) return undefined
const chat = input.modelID === "openai.gpt-oss-safeguard-20b" || input.modelID === "openai.gpt-oss-safeguard-120b"
return {
package: `@opencode-ai/ai/providers/amazon-bedrock/mantle/${chat ? "chat" : "responses"}`,
settings: {
...baseSettings,
...(typeof settings.baseURL !== "string" && typeof settings.endpoint === "string"
? { baseURL: settings.endpoint }
: {}),
...(apiKey === undefined ? {} : { apiKey }),
...(credentials === undefined ? {} : { credentials }),
...(typeof settings.region === "string" ? { region: settings.region } : {}),
...mapOpenAIOptions(settings),
},
...baseSettings,
...(typeof settings.baseURL !== "string" && typeof settings.endpoint === "string"
? { baseURL: settings.endpoint }
: {}),
...(apiKey === undefined ? {} : { apiKey }),
...(credentials === undefined ? {} : { credentials }),
...(typeof settings.region === "string" ? { region: settings.region } : {}),
...(typeof settings.topP === "number" ? { topP: settings.topP } : {}),
}
}
function mapBedrockRequest(input: MapInput): Pick<Mapping, "headers" | "body"> {
const settings = input.settings
const headers = isStringRecord(settings.headers) ? settings.headers : undefined
const additional = isRecord(settings.additionalModelRequestFields) ? settings.additionalModelRequestFields : {}
const reasoning = isRecord(settings.reasoningConfig) ? settings.reasoningConfig : undefined
const anthropic = input.modelID.includes("anthropic")
const openai = input.modelID.startsWith("openai.")
const effort = typeof reasoning?.maxReasoningEffort === "string" ? reasoning.maxReasoningEffort : undefined
const type = typeof reasoning?.type === "string" ? reasoning.type : undefined
const budget = typeof reasoning?.budgetTokens === "number" ? reasoning.budgetTokens : undefined
const display = typeof reasoning?.display === "string" ? reasoning.display : undefined
const betas = Array.isArray(settings.anthropicBeta)
? settings.anthropicBeta.filter((item): item is string => typeof item === "string")
: []
const existingBetas = Array.isArray(additional.anthropic_beta)
? additional.anthropic_beta.filter((item): item is string => typeof item === "string")
: []
const fields = Provider.mergeOverlay(additional, {
...(betas.length > 0 ? { anthropic_beta: [...existingBetas, ...betas] } : {}),
...(anthropic && type === "enabled" && budget !== undefined
? { thinking: { type: "enabled", budget_tokens: budget } }
: {}),
...(anthropic && type === "adaptive"
? { thinking: { type: "adaptive", ...(display === undefined ? {} : { display }) } }
: {}),
...(anthropic && effort !== undefined
? {
output_config: {
...(isRecord(additional.output_config) ? additional.output_config : {}),
effort,
},
}
: {}),
...(!anthropic && openai && effort !== undefined ? { reasoning_effort: effort } : {}),
...(!anthropic && !openai && effort !== undefined
? {
reasoningConfig: {
...(type === undefined || type === "adaptive" ? {} : { type }),
...(budget === undefined ? {} : { budgetTokens: budget }),
maxReasoningEffort: effort,
},
}
: {}),
})
const body = {
...(fields && Object.keys(fields).length > 0 ? { additionalModelRequestFields: fields } : {}),
...(typeof settings.serviceTier === "string" ? { serviceTier: { type: settings.serviceTier } } : {}),
}
return {
...(headers === undefined ? {} : { headers }),
...(Object.keys(body).length === 0 ? {} : { body }),
}
}

View file

@ -181,7 +181,6 @@ export const fromCatalogModel = (
packageName,
settings: configured,
modelID: resolved.modelID ?? resolved.id,
hasCredential: key !== undefined,
})
: undefined
const native = mapping?.package ?? resolved.package

View file

@ -5,11 +5,75 @@ const map = (packageName: string, settings: Readonly<Record<string, unknown>>, m
AISDKNative.map({ packageName, settings, modelID })
describe("AISDKNative", () => {
test("maps both models.dev Bedrock packages to native providers", () => {
expect(map("@ai-sdk/amazon-bedrock", { region: "us-east-1" })).toEqual({
package: "@opencode-ai/ai/providers/amazon-bedrock",
settings: { region: "us-east-1" },
})
expect(map("@ai-sdk/amazon-bedrock/mantle", { region: "us-east-1" }, "openai.gpt-oss-120b")).toEqual({
package: "@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
settings: { region: "us-east-1" },
})
})
test("maps Bedrock provider and request options", () => {
expect(
map(
"@ai-sdk/amazon-bedrock",
{
region: "us-east-1",
topP: 0.8,
headers: { "x-test": "value" },
additionalModelRequestFields: {
existing: true,
anthropic_beta: ["existing-beta"],
output_config: { format: "text" },
},
reasoningConfig: { type: "adaptive", display: "summarized", maxReasoningEffort: "high" },
anthropicBeta: ["context-1m-2025-08-07"],
serviceTier: "priority",
},
"anthropic.claude-sonnet-4-6-v1",
),
).toEqual({
package: "@opencode-ai/ai/providers/amazon-bedrock",
settings: { region: "us-east-1", topP: 0.8 },
headers: { "x-test": "value" },
body: {
additionalModelRequestFields: {
existing: true,
anthropic_beta: ["existing-beta", "context-1m-2025-08-07"],
thinking: { type: "adaptive", display: "summarized" },
output_config: { format: "text", effort: "high" },
},
serviceTier: { type: "priority" },
},
})
expect(
map(
"@ai-sdk/amazon-bedrock",
{ reasoningConfig: { type: "enabled", maxReasoningEffort: "max" } },
"amazon.nova-2-lite-v1:0",
)?.body,
).toEqual({
additionalModelRequestFields: {
reasoningConfig: { type: "enabled", maxReasoningEffort: "max" },
},
})
expect(
map("@ai-sdk/amazon-bedrock", { reasoningConfig: { maxReasoningEffort: "high" } }, "openai.gpt-oss-120b-1:0")
?.body,
).toEqual({ additionalModelRequestFields: { reasoning_effort: "high" } })
})
test("maps Bedrock Mantle models to their supported native APIs", () => {
const settings = {
bearerToken: "token",
region: "us-west-2",
baseURL: "https://mantle.test/v1",
headers: { "x-test": "value" },
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
@ -29,6 +93,7 @@ describe("AISDKNative", () => {
},
},
},
headers: { "x-test": "value" },
})
expect(map("@ai-sdk/amazon-bedrock/mantle", settings, "openai.gpt-oss-safeguard-20b")?.package).toBe(
"@opencode-ai/ai/providers/amazon-bedrock/mantle/chat",
@ -68,12 +133,6 @@ describe("AISDKNative", () => {
})
})
test("keeps Bedrock Mantle on the AI SDK when native static auth is unavailable", () => {
expect(
map("@ai-sdk/amazon-bedrock/mantle", { region: "us-east-1", profile: "production" }, "openai.gpt-oss-120b"),
).toBeUndefined()
})
test("maps the legacy Bedrock endpoint override", () => {
expect(
map(

View file

@ -620,6 +620,18 @@ describe("ModelResolver", () => {
const xai = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk("@ai-sdk/xai"), { settings: { reasoningEffort: "high" } }),
)
const bedrock = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk("@ai-sdk/amazon-bedrock"), {
settings: { region: "us-east-1", topP: 0.8, serviceTier: "priority" },
body: {},
}),
)
const mantle = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), {
modelID: "openai.gpt-oss-120b",
settings: { region: "us-east-1" },
}),
)
expect(google.route.id).toBe("gemini")
expect(google.route.defaults.providerOptions).toEqual({
@ -631,6 +643,10 @@ describe("ModelResolver", () => {
expect(xai.route.defaults.providerOptions).toEqual({
xai: { reasoningEffort: "high", store: false },
})
expect(bedrock.route.id).toBe("bedrock-converse")
expect(bedrock.route.defaults.generation).toEqual({ topP: 0.8 })
expect(bedrock.route.defaults.http?.body).toEqual({ serviceTier: { type: "priority" } })
expect(mantle.route.id).toBe("bedrock-mantle-responses")
}),
)