feat(ai): add native Bedrock Mantle support (#40119)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
parent
adbd22833c
commit
bf7f0cb521
15 changed files with 506 additions and 44 deletions
|
|
@ -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 } : {}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")],
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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"), {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue