feat(ai): add native Bedrock Mantle support (#40119)

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

View file

@ -10,32 +10,103 @@ export interface Mapping {
readonly body?: Readonly<Record<string, unknown>>
}
export function map(packageName: string | undefined, settings: Readonly<Record<string, unknown>>): Mapping | undefined {
const baseSettings = mapBaseSettings(settings)
switch (packageName) {
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/mantle":
return mapBedrockMantle(input, baseSettings)
case "@ai-sdk/google":
return {
package: "@opencode-ai/ai/providers/google",
settings: {
...baseSettings,
...mapAPIKey(settings),
...mapGoogleOptions(settings),
...mapAPIKey(input.settings),
...mapGoogleOptions(input.settings),
},
}
case "@openrouter/ai-sdk-provider":
return mapOpenRouter(settings, baseSettings)
return mapOpenRouter(input.settings, baseSettings)
case "@ai-sdk/xai":
return {
package: "@opencode-ai/ai/providers/xai",
settings: {
...baseSettings,
...mapAPIKey(settings),
...mapXAIOptions(settings),
...mapAPIKey(input.settings),
...mapXAIOptions(input.settings),
},
}
}
}
function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<string, unknown>>): Mapping | undefined {
const settings = input.settings
const apiKey =
typeof settings.apiKey === "string"
? settings.apiKey
: typeof settings.bearerToken === "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),
},
}
}
function mapBedrockCredentials(settings: Readonly<Record<string, unknown>>) {
const credentials = isRecord(settings.credentials) ? settings.credentials : settings
const region =
typeof settings.region === "string"
? settings.region
: typeof credentials.region === "string"
? credentials.region
: undefined
if (
region === undefined ||
typeof credentials.accessKeyId !== "string" ||
typeof credentials.secretAccessKey !== "string"
)
return undefined
return {
region,
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
...(typeof credentials.sessionToken === "string" ? { sessionToken: credentials.sessionToken } : {}),
}
}
function mapOpenAIOptions(settings: Readonly<Record<string, unknown>>) {
const options = {
...(typeof settings.reasoningEffort === "string" ? { reasoningEffort: settings.reasoningEffort } : {}),
...(typeof settings.reasoningSummary === "string" ? { reasoningSummary: settings.reasoningSummary } : {}),
...(Array.isArray(settings.include) ? { include: settings.include } : {}),
...(typeof settings.store === "boolean" ? { store: settings.store } : {}),
...(typeof settings.promptCacheKey === "string" ? { promptCacheKey: settings.promptCacheKey } : {}),
...(typeof settings.textVerbosity === "string" ? { textVerbosity: settings.textVerbosity } : {}),
...(typeof settings.serviceTier === "string" ? { serviceTier: settings.serviceTier } : {}),
}
if (Object.keys(options).length === 0) return {}
return { providerOptions: { openai: options } }
}
function mapBaseSettings(settings: Readonly<Record<string, unknown>>) {
return {
...(typeof settings.baseURL === "string" ? { baseURL: settings.baseURL } : {}),

View file

@ -176,14 +176,20 @@ export const fromCatalogModel = (
)
}
const configured = { ...resolved.settings, ...credential?.metadata }
const mapping = Provider.isAISDK(resolved.package) ? AISDKNative.map(packageName, configured) : undefined
const mapping = Provider.isAISDK(resolved.package)
? AISDKNative.map({
packageName,
settings: configured,
modelID: resolved.modelID ?? resolved.id,
hasCredential: key !== undefined,
})
: undefined
const native = mapping?.package ?? resolved.package
if (Provider.isAISDK(resolved.package) && !mapping) {
if (!dependencies?.loadAISDK) return Effect.fail(unsupported(resolved))
const runtime = produce(resolved, (draft) => {
draft.settings = Provider.mergeOverlay(draft.settings, {
...(credential?.type === "key" ? { apiKey: credential.key } : {}),
...(credential?.type === "oauth" ? { apiKey: credential.access } : {}),
...nativeCredentialSettings(resolved.package ?? "", credential),
...credential?.metadata,
})
})

View file

@ -34,6 +34,15 @@ export type ProviderPackage = ProviderPackageDefinition
const packages = new Map<string, Promise<unknown>>()
const builtins = new Map<string, () => Promise<unknown>>([
["@opencode-ai/ai/providers/amazon-bedrock", () => import("@opencode-ai/ai/providers/amazon-bedrock")],
["@opencode-ai/ai/providers/amazon-bedrock/mantle", () => import("@opencode-ai/ai/providers/amazon-bedrock/mantle")],
[
"@opencode-ai/ai/providers/amazon-bedrock/mantle/chat",
() => import("@opencode-ai/ai/providers/amazon-bedrock/mantle/chat"),
],
[
"@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
() => import("@opencode-ai/ai/providers/amazon-bedrock/mantle/responses"),
],
["@opencode-ai/ai/providers/anthropic", () => import("@opencode-ai/ai/providers/anthropic")],
["@opencode-ai/ai/providers/azure", () => import("@opencode-ai/ai/providers/azure")],
["@opencode-ai/ai/providers/azure/chat", () => import("@opencode-ai/ai/providers/azure/chat")],

View file

@ -1,10 +1,92 @@
import { describe, expect, test } from "bun:test"
import { AISDKNative } from "@opencode-ai/core/aisdk-native"
const map = (packageName: string, settings: Readonly<Record<string, unknown>>, modelID = "test-model") =>
AISDKNative.map({ packageName, settings, modelID })
describe("AISDKNative", () => {
test("maps Bedrock Mantle models to their supported native APIs", () => {
const settings = {
bearerToken: "token",
region: "us-west-2",
baseURL: "https://mantle.test/v1",
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
}
expect(map("@ai-sdk/amazon-bedrock/mantle", settings, "openai.gpt-oss-120b")).toEqual({
package: "@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
settings: {
apiKey: "token",
baseURL: "https://mantle.test/v1",
region: "us-west-2",
providerOptions: {
openai: {
reasoningEffort: "high",
reasoningSummary: "auto",
include: ["reasoning.encrypted_content"],
},
},
},
})
expect(map("@ai-sdk/amazon-bedrock/mantle", settings, "openai.gpt-oss-safeguard-20b")?.package).toBe(
"@opencode-ai/ai/providers/amazon-bedrock/mantle/chat",
)
})
test("maps static Bedrock Mantle credentials without leaking connection options", () => {
expect(
map(
"@ai-sdk/amazon-bedrock/mantle",
{
credentials: {
accessKeyId: "key",
secretAccessKey: "secret",
sessionToken: "session",
},
region: "eu-west-1",
profile: "ignored",
credentialProvider: "ignored",
fetch: "ignored",
store: false,
},
"openai.gpt-oss-120b",
),
).toEqual({
package: "@opencode-ai/ai/providers/amazon-bedrock/mantle/responses",
settings: {
credentials: {
accessKeyId: "key",
secretAccessKey: "secret",
sessionToken: "session",
region: "eu-west-1",
},
region: "eu-west-1",
providerOptions: { openai: { store: false } },
},
})
})
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(
"@ai-sdk/amazon-bedrock/mantle",
{ bearerToken: "token", endpoint: "https://mantle.private/v1", region: "us-east-1" },
"openai.gpt-oss-120b",
),
).toMatchObject({ settings: { baseURL: "https://mantle.private/v1" } })
})
test("maps OpenRouter settings to native destinations", () => {
expect(
AISDKNative.map("@openrouter/ai-sdk-provider", {
map("@openrouter/ai-sdk-provider", {
appName: "OpenCode",
appUrl: "https://opencode.ai",
headers: { "x-openrouter-title": "Configured", "x-provider-api-keys": "Configured BYOK" },
@ -40,7 +122,7 @@ describe("AISDKNative", () => {
test("maps every Google thinking setting", () => {
expect(
AISDKNative.map("@ai-sdk/google", {
map("@ai-sdk/google", {
cachedContent: "cachedContents/example",
safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
serviceTier: "flex",
@ -72,7 +154,7 @@ describe("AISDKNative", () => {
test("maps Google thinking settings independently", () => {
for (const thinkingConfig of [{ thinkingBudget: -1 }, { includeThoughts: true }, { thinkingLevel: "medium" }]) {
expect(AISDKNative.map("@ai-sdk/google", { thinkingConfig })).toMatchObject({
expect(map("@ai-sdk/google", { thinkingConfig })).toMatchObject({
settings: { providerOptions: { gemini: { thinkingConfig } } },
})
}
@ -80,7 +162,7 @@ describe("AISDKNative", () => {
test("maps Google request options without thinking settings", () => {
expect(
AISDKNative.map("@ai-sdk/google", {
map("@ai-sdk/google", {
cachedContent: "cachedContents/example",
safetySettings: [{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" }],
serviceTier: "future-tier",
@ -100,7 +182,7 @@ describe("AISDKNative", () => {
test("maps supported xAI settings", () => {
expect(
AISDKNative.map("@ai-sdk/xai", {
map("@ai-sdk/xai", {
apiKey: "secret",
baseURL: "https://xai.example/v1",
reasoningEffort: "custom",
@ -125,7 +207,7 @@ describe("AISDKNative", () => {
test("omits invalid and unsupported xAI settings", () => {
expect(
AISDKNative.map("@ai-sdk/xai", {
map("@ai-sdk/xai", {
reasoningEffort: 10,
store: "yes",
include: ["unknown"],

View file

@ -42,6 +42,35 @@ const model = (packageName: string | undefined, options: ModelOptions = {}) =>
})
describe("ModelResolver", () => {
it.effect("maps Bedrock Mantle models to native Responses and safeguards to Chat", () =>
Effect.gen(function* () {
const credential = Credential.Key.make({ type: "key", key: "secret" })
const responses = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), {
modelID: "openai.gpt-oss-120b",
settings: { region: "us-east-2" },
}),
credential,
)
const chat = yield* ModelResolver.fromCatalogModel(
model(Provider.aisdk("@ai-sdk/amazon-bedrock/mantle"), {
modelID: "openai.gpt-oss-safeguard-20b",
settings: { region: "us-east-2" },
}),
credential,
)
expect(responses.route).toMatchObject({
id: "bedrock-mantle-responses",
endpoint: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/v1" },
})
expect(chat.route).toMatchObject({
id: "bedrock-mantle-chat",
endpoint: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/v1" },
})
}),
)
it.effect("uses the API modelID instead of the catalog ID for native OpenAI routes", () =>
Effect.gen(function* () {
const catalog = model(Provider.aisdk("@ai-sdk/openai"), {