Compare commits

...

1 Commits

Author SHA1 Message Date
Aiden Cline d16e7ce3c3 feat(ai): add native Bedrock Mantle support 2026-08-02 03:02:03 +00:00
15 changed files with 502 additions and 32 deletions
+23 -23
View File
@@ -60,7 +60,7 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently
| `@ai-sdk/google-vertex/xai` | Vertex Chat / Responses | Partial / usable | Decide Chat/Responses selection for catalog models, add runner mapping and recorded coverage, and review xAI-specific request options. |
| `@ai-sdk/azure` | Azure OpenAI Chat/Responses facade | Partial | Map runner/catalog metadata to native Azure, handle resourceName/baseURL/apiVersion variants, add AAD/token auth story, and verify Chat vs Responses deployment selection. |
| `@ai-sdk/amazon-bedrock` | Bedrock Converse | Partial | Add default AWS credential chain/profile support, region/inference-profile model ID handling, provider option parity via `additionalModelRequestFields`, guardrails/performance config, and runner/catalog mapping. |
| `@ai-sdk/amazon-bedrock/mantle` | Bedrock Mantle OpenAI-compatible Chat/Responses namespace | Missing | Decide native Mantle shape, likely separate from Converse because it uses OpenAI-compatible Chat/Responses semantics over Bedrock. Add package mapping and tests. |
| `@ai-sdk/amazon-bedrock/mantle` | Bedrock Mantle OpenAI-compatible Chat/Responses namespace | Partial / usable | Add default AWS credential chain/profile support; native catalog mapping currently requires bearer auth or explicit static credentials. |
## Highest-Risk Gaps
@@ -71,30 +71,31 @@ Other `aisdk:` packages, including Google Vertex, Azure, and Bedrock, currently
5. Azure is only a provider facade, not a full runtime replacement. Native Azure exists, but the catalog runner does not select it, and token auth/resource variants need review.
6. Provider option typing is uneven. OpenAI, Anthropic, Gemini, Bedrock, and OpenRouter each expose a small typed subset plus raw HTTP overlays; this is useful but not equivalent to AI SDK provider option coverage.
7. Structured output is not provider-native yet. `LLM.generateObject` still uses a synthetic tool strategy, while the future design expects native structured output where reliable and tool fallback where needed.
8. Package/namespace boundaries for the current native loading set are explicit in docs and exports. Other exported provider facades are not catalog package entrypoints until they implement the contract. Vertex xAI still needs catalog API selection; the missing native boundary is Bedrock Mantle.
9. Recorded coverage is uneven. OpenAI, Anthropic, Gemini, Bedrock Converse, Cloudflare, OpenRouter, and several OpenAI-compatible Chat providers have cassettes. Azure, Vertex, and Mantle need first-class recorded scenarios before switching defaults.
8. Package/namespace boundaries for the current native loading set are explicit in docs and exports. Other exported provider facades are not catalog package entrypoints until they implement the contract. Vertex xAI still needs catalog API selection.
9. Recorded coverage is uneven. OpenAI, Anthropic, Gemini, Bedrock Converse, Bedrock Mantle, Cloudflare, OpenRouter, and several OpenAI-compatible Chat providers have cassettes. Azure and Vertex still need first-class recorded scenarios before switching defaults.
## Native Namespace Shape
These are implementation/API slices, not separate npm packages.
| API slice | Package-like entrypoint | Purpose |
| ----------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------- |
| OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. |
| OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. |
| OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. |
| Open Responses-compatible | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic provider-neutral `/responses`. |
| Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. |
| Anthropic Messages | `@opencode-ai/ai/providers/anthropic` | Anthropic Messages API. |
| Gemini Developer API | `@opencode-ai/ai/providers/google` | Google AI Studio Gemini API. |
| Vertex Gemini | `@opencode-ai/ai/providers/google-vertex/gemini` | Vertex Gemini API; `providers/google-vertex` is the default alias. |
| Vertex Chat | `@opencode-ai/ai/providers/google-vertex/chat` | Vertex OpenAI-compatible Chat Completions for MaaS models. |
| Vertex Responses | `@opencode-ai/ai/providers/google-vertex/responses` | Vertex Open Responses for Grok models. |
| Vertex Messages | `@opencode-ai/ai/providers/google-vertex/messages` | Vertex-hosted Anthropic Messages API. |
| Bedrock Converse | `@opencode-ai/ai/providers/amazon-bedrock` | AWS Bedrock Converse API. |
| Bedrock Mantle | Missing | AWS Bedrock Mantle OpenAI-compatible APIs. |
| Azure OpenAI Chat | `@opencode-ai/ai/providers/azure/chat` | Azure specialization of OpenAI Chat. |
| Azure OpenAI Responses | `@opencode-ai/ai/providers/azure/responses` | Azure specialization of OpenAI Responses. |
| API slice | Package-like entrypoint | Purpose |
| ----------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
| OpenAI Chat | `@opencode-ai/ai/providers/openai/chat` | OpenAI `/chat/completions` semantics. |
| OpenAI Responses | `@opencode-ai/ai/providers/openai/responses` | OpenAI `/responses` semantics with HTTP/WebSocket selected through settings. |
| OpenAI-compatible Chat | `@opencode-ai/ai/providers/openai-compatible` | Generic OpenAI-compatible `/chat/completions`. |
| Open Responses-compatible | `@opencode-ai/ai/providers/openai-compatible/responses` | Generic provider-neutral `/responses`. |
| Anthropic-compatible Messages | `@opencode-ai/ai/providers/anthropic-compatible` | Generic Anthropic-compatible `/messages`. |
| Anthropic Messages | `@opencode-ai/ai/providers/anthropic` | Anthropic Messages API. |
| Gemini Developer API | `@opencode-ai/ai/providers/google` | Google AI Studio Gemini API. |
| Vertex Gemini | `@opencode-ai/ai/providers/google-vertex/gemini` | Vertex Gemini API; `providers/google-vertex` is the default alias. |
| Vertex Chat | `@opencode-ai/ai/providers/google-vertex/chat` | Vertex OpenAI-compatible Chat Completions for MaaS models. |
| Vertex Responses | `@opencode-ai/ai/providers/google-vertex/responses` | Vertex Open Responses for Grok models. |
| Vertex Messages | `@opencode-ai/ai/providers/google-vertex/messages` | Vertex-hosted Anthropic Messages API. |
| Bedrock Converse | `@opencode-ai/ai/providers/amazon-bedrock` | AWS Bedrock Converse API. |
| Bedrock Mantle Chat | `@opencode-ai/ai/providers/amazon-bedrock/mantle/chat` | AWS Bedrock Mantle OpenAI-compatible Chat API. |
| Bedrock Mantle Responses | `@opencode-ai/ai/providers/amazon-bedrock/mantle/responses` | AWS Bedrock Mantle OpenAI-compatible Responses API. |
| Azure OpenAI Chat | `@opencode-ai/ai/providers/azure/chat` | Azure specialization of OpenAI Chat. |
| Azure OpenAI Responses | `@opencode-ai/ai/providers/azure/responses` | Azure specialization of OpenAI Responses. |
## Suggested Next Work Slices
@@ -103,6 +104,5 @@ These are implementation/API slices, not separate npm packages.
3. Bring Bedrock native auth/config to AI SDK parity: region, profile, default AWS credential chain, bearer token env, endpoint override, and cross-region inference profile handling.
4. Add runner/catalog mappings and recorded scenarios for the native Vertex Gemini, Chat, Responses, and Messages entrypoints.
5. Decide Chat/Responses selection for `@ai-sdk/google-vertex/xai` catalog models.
6. Add Bedrock Mantle as a separate OpenAI-compatible Bedrock namespace after deciding whether it uses Chat, Responses, or both by model.
7. Expand typed provider options from the existing V1 lowerer knowledge in `packages/core/src/v1/config/provider-options.ts` before adding more raw overlay examples.
8. Add recorded provider tests for Azure, Vertex Gemini, Vertex Chat, Vertex Responses, Vertex Messages, Bedrock credential-chain behavior, and Mantle before making native runtime the default for those packages.
6. Expand typed provider options from the existing V1 lowerer knowledge in `packages/core/src/v1/config/provider-options.ts` before adding more raw overlay examples.
7. Add recorded provider tests for Azure, Vertex Gemini, Vertex Chat, Vertex Responses, Vertex Messages, and Bedrock credential-chain behavior before making native runtime the default for those packages.
@@ -22,6 +22,8 @@ const signRequest = (input: {
readonly body: string
readonly headers: Headers.Headers
readonly credentials: Credentials
readonly service: string
readonly name: string
}) =>
Effect.tryPromise({
try: async () => {
@@ -34,23 +36,26 @@ const signRequest = (input: {
accessKeyId: input.credentials.accessKeyId,
secretAccessKey: input.credentials.secretAccessKey,
sessionToken: input.credentials.sessionToken,
service: "bedrock",
service: input.service,
}).sign()
return Object.fromEntries(signed.headers.entries())
},
catch: (error) =>
ProviderShared.invalidRequest(
`Bedrock Converse SigV4 signing failed: ${error instanceof Error ? error.message : String(error)}`,
`${input.name} SigV4 signing failed: ${error instanceof Error ? error.message : String(error)}`,
),
})
/** Sign the exact JSON bytes with SigV4 using credentials configured on the route. */
export const sigV4 = (credentials: Credentials | undefined) =>
export const sigV4 = (
credentials: Credentials | undefined,
options: { readonly service?: string; readonly name?: string } = {},
) =>
Auth.custom((input: AuthInput) => {
return Effect.gen(function* () {
if (!credentials) {
return yield* ProviderShared.invalidRequest(
"Bedrock Converse requires either route bearer auth or AWS credentials configured on the route",
`${options.name ?? "Bedrock Converse"} requires either route bearer auth or AWS credentials configured on the route`,
)
}
const headersForSigning = Headers.set(input.headers, "content-type", "application/json")
@@ -59,6 +64,8 @@ export const sigV4 = (credentials: Credentials | undefined) =>
body: input.body,
headers: headersForSigning,
credentials,
service: options.service ?? "bedrock",
name: options.name ?? "Bedrock Converse",
})
return Headers.setAll(headersForSigning, signed)
})
@@ -0,0 +1,107 @@
import { Auth } from "../route/auth"
import type { Route as RouteDef, RouteDefaultsInput } from "../route/client"
import type { ProviderPackage } from "../provider-package"
import { OpenAIChat } from "../protocols/openai-chat"
import { OpenAIResponses } from "../protocols/openai-responses"
import { BedrockAuth, type Credentials } from "../protocols/utils/bedrock-auth"
import { ProviderID, type ModelID } from "../schema"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("amazon-bedrock")
export type Config = RouteDefaultsInput & {
readonly apiKey?: string
readonly baseURL?: string
readonly credentials?: Credentials
readonly region?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly auth?: "bearer" | "sigv4"
readonly baseURL?: string
readonly credentials?: Credentials
readonly region?: string
readonly providerOptions?: OpenAIProviderOptionsInput
}
const responsesRoute = OpenAIResponses.route.with({
id: "bedrock-mantle-responses",
provider: id,
})
const chatRoute = OpenAIChat.route.with({
id: "bedrock-mantle-chat",
provider: id,
})
export const routes = [responsesRoute, chatRoute]
const configuredRoute = <Body, Prepared>(route: RouteDef<Body, Prepared>, input: Config) => {
const region = input.region ?? input.credentials?.region ?? "us-east-1"
const credentials = input.credentials === undefined ? undefined : { ...input.credentials, region }
return route.with({
endpoint: { baseURL: input.baseURL ?? `https://bedrock-mantle.${region}.api.aws/v1` },
auth:
input.apiKey === undefined
? BedrockAuth.sigV4(credentials, { service: "bedrock-mantle", name: "Bedrock Mantle" })
: Auth.bearer(input.apiKey),
})
}
const defaults = (input: Config) => {
const { apiKey: _, baseURL: _baseURL, credentials: _credentials, region: _region, ...rest } = input
return rest
}
export const configure = (input: Config = {}) => {
const configuredResponsesRoute = configuredRoute(responsesRoute, input)
const configuredChatRoute = configuredRoute(chatRoute, input)
const modelDefaults = defaults(input)
const responses = (modelID: string | ModelID) =>
configuredResponsesRoute
.with(withOpenAIOptions(modelID, modelDefaults))
.model<OpenAIProviderOptionsInput>({ id: modelID })
const chat = (modelID: string | ModelID) =>
configuredChatRoute
.with(withOpenAIOptions(modelID, modelDefaults))
.model<OpenAIProviderOptionsInput>({ id: modelID })
return {
id,
model: chat,
chat,
responses,
configure,
}
}
export const provider = configure()
const config = (settings: Settings): Config => {
if (settings.auth === "bearer" && settings.apiKey === undefined)
throw new Error("Amazon Bedrock Mantle bearer auth requires apiKey")
if (settings.auth === "sigv4" && settings.apiKey !== undefined)
throw new Error("Amazon Bedrock Mantle SigV4 auth does not accept apiKey")
return {
apiKey: settings.auth === "sigv4" ? undefined : settings.apiKey,
baseURL: settings.baseURL,
credentials: settings.credentials,
headers: settings.headers === undefined ? undefined : { ...settings.headers },
http: settings.body === undefined ? undefined : { body: { ...settings.body } },
limits: settings.limits,
providerOptions: settings.providerOptions,
region: settings.region,
}
}
export const chatModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
settings,
) => configure(config(settings)).chat(modelID)
export const responsesModel: ProviderPackage.Definition<Settings, OpenAIProviderOptionsInput>["model"] = (
modelID,
settings,
) => configure(config(settings)).responses(modelID)
export const model = chatModel
@@ -0,0 +1,2 @@
export { chatModel as model } from "../amazon-bedrock-mantle"
export type { Settings } from "../amazon-bedrock-mantle"
@@ -0,0 +1,2 @@
export { chatModel as model } from "../../amazon-bedrock-mantle"
export type { Settings } from "../../amazon-bedrock-mantle"
@@ -0,0 +1,2 @@
export { responsesModel as model } from "../../amazon-bedrock-mantle"
export type { Settings } from "../../amazon-bedrock-mantle"
+1
View File
@@ -1,6 +1,7 @@
export * as Anthropic from "./anthropic"
export * as AnthropicCompatible from "./anthropic-compatible"
export * as AmazonBedrock from "./amazon-bedrock"
export * as AmazonBedrockMantle from "./amazon-bedrock-mantle"
export * as Azure from "./azure"
export * as Cloudflare from "./cloudflare"
export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
File diff suppressed because one or more lines are too long
@@ -23,12 +23,16 @@ describe("provider package entrypoints", () => {
import("@opencode-ai/ai/providers/google-vertex/messages"),
import("@opencode-ai/ai/providers/openrouter"),
import("@opencode-ai/ai/providers/xai"),
import("@opencode-ai/ai/providers/amazon-bedrock/mantle"),
import("@opencode-ai/ai/providers/amazon-bedrock/mantle/chat"),
import("@opencode-ai/ai/providers/amazon-bedrock/mantle/responses"),
])
for (const module of modules) expect(module.model).toBeFunction()
expect(modules[0].model).toBe(modules[1].model)
expect(modules[8].model).toBe(modules[9].model)
expect(modules[12].model).toBe(modules[13].model)
expect(modules[19].model).toBe(modules[20].model)
})
test("maps OpenRouter and xAI package settings onto executable models", async () => {
@@ -0,0 +1,111 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { HttpClientRequest } from "effect/unstable/http"
import { LLM } from "../../src"
import { AmazonBedrockMantle } from "../../src/providers"
import { compileRequest, LLMClient } from "../../src/route/client"
import { it } from "../lib/effect"
import { dynamicResponse } from "../lib/http"
import { recordedTests } from "../recorded-test"
const credentials = {
region: "us-east-2",
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
secretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
}
describe("Amazon Bedrock Mantle provider", () => {
it.effect("uses Chat by default and exposes Responses", () =>
Effect.gen(function* () {
const provider = AmazonBedrockMantle.configure({ credentials })
const chat = yield* compileRequest(LLM.request({ model: provider.model("openai.gpt-oss-120b"), prompt: "Hi" }))
const responses = yield* compileRequest(
LLM.request({ model: provider.responses("openai.gpt-oss-120b"), prompt: "Hi" }),
)
expect(chat).toMatchObject({
route: "bedrock-mantle-chat",
protocol: "openai-chat",
body: { model: "openai.gpt-oss-120b" },
})
expect(responses).toMatchObject({
route: "bedrock-mantle-responses",
protocol: "openai-responses",
body: { model: "openai.gpt-oss-120b", store: false },
})
}),
)
it.effect("uses the Mantle endpoint and signing service", () =>
Effect.gen(function* () {
const seen: Array<{ readonly url: string; readonly authorization: string | undefined }> = []
const model = AmazonBedrockMantle.configure({ credentials, region: "us-west-1" }).responses("openai.gpt-oss-120b")
yield* LLMClient.generate(LLM.request({ model, prompt: "Hi" })).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request)
seen.push({ url: request.url, authorization: request.headers.get("authorization") ?? undefined })
return input.respond("", { headers: { "content-type": "text/event-stream" } })
}),
),
),
Effect.flip,
)
expect(seen[0]?.url).toBe("https://bedrock-mantle.us-west-1.api.aws/v1/responses")
expect(seen[0]?.authorization).toContain("/us-west-1/bedrock-mantle/aws4_request")
}),
)
it.effect("supports bearer authentication and custom base URLs", () =>
Effect.gen(function* () {
const seen: Array<{ readonly url: string; readonly authorization: string | undefined }> = []
const model = AmazonBedrockMantle.configure({
apiKey: "test-key",
baseURL: "https://mantle.test/v1",
}).chat("openai.gpt-oss-safeguard-20b")
yield* LLMClient.generate(LLM.request({ model, prompt: "Hi" })).pipe(
Effect.provide(
dynamicResponse((input) =>
Effect.gen(function* () {
const request = yield* HttpClientRequest.toWeb(input.request)
seen.push({ url: request.url, authorization: request.headers.get("authorization") ?? undefined })
return input.respond("", { headers: { "content-type": "text/event-stream" } })
}),
),
),
Effect.flip,
)
expect(seen).toEqual([{ url: "https://mantle.test/v1/chat/completions", authorization: "Bearer test-key" }])
}),
)
})
const recorded = recordedTests({
prefix: "bedrock-mantle",
provider: "amazon-bedrock",
protocol: "openai-responses",
requires: ["AWS_BEARER_TOKEN_BEDROCK"],
metadata: { model: "openai.gpt-oss-120b" },
})
describe("Amazon Bedrock Mantle recorded", () => {
recorded.effect("streams text", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLM.request({
model: AmazonBedrockMantle.configure({
apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "fixture",
region: "us-east-1",
}).responses("openai.gpt-oss-120b"),
prompt: "Reply with exactly: hello",
generation: { maxTokens: 256, temperature: 0 },
}),
)
expect(response.text.trim().toLowerCase()).toBe("hello")
}),
)
})
+80 -1
View File
@@ -10,9 +10,15 @@ export interface Mapping {
readonly body?: Readonly<Record<string, unknown>>
}
export function map(packageName: string | undefined, settings: Readonly<Record<string, unknown>>): Mapping | undefined {
export function map(
packageName: string | undefined,
settings: Readonly<Record<string, unknown>>,
modelID?: string,
): Mapping | undefined {
const baseSettings = mapBaseSettings(settings)
switch (packageName) {
case "@ai-sdk/amazon-bedrock/mantle":
return mapBedrockMantle(settings, baseSettings, modelID)
case "@ai-sdk/google":
return {
package: "@opencode-ai/ai/providers/google",
@@ -36,6 +42,79 @@ export function map(packageName: string | undefined, settings: Readonly<Record<s
}
}
function mapBedrockMantle(
settings: Readonly<Record<string, unknown>>,
baseSettings: Readonly<Record<string, unknown>>,
modelID: string | undefined,
): Mapping | undefined {
const apiKey =
typeof settings.apiKey === "string"
? settings.apiKey
: typeof settings.bearerToken === "string"
? settings.bearerToken
: undefined
const credentials = mapBedrockCredentials(settings)
if (apiKey === undefined && credentials === undefined) return undefined
const safeguard = modelID === "openai.gpt-oss-safeguard-20b" || modelID === "openai.gpt-oss-safeguard-120b"
return {
package: `@opencode-ai/ai/providers/amazon-bedrock/mantle/${safeguard ? "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>>) {
if (
isRecord(settings.credentials) &&
typeof settings.credentials.region === "string" &&
typeof settings.credentials.accessKeyId === "string" &&
typeof settings.credentials.secretAccessKey === "string"
) {
return {
region: settings.credentials.region,
accessKeyId: settings.credentials.accessKeyId,
secretAccessKey: settings.credentials.secretAccessKey,
...(typeof settings.credentials.sessionToken === "string"
? { sessionToken: settings.credentials.sessionToken }
: {}),
}
}
if (
typeof settings.region !== "string" ||
typeof settings.accessKeyId !== "string" ||
typeof settings.secretAccessKey !== "string"
)
return undefined
return {
region: settings.region,
accessKeyId: settings.accessKeyId,
secretAccessKey: settings.secretAccessKey,
...(typeof settings.sessionToken === "string" ? { sessionToken: settings.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 } : {}),
+11 -4
View File
@@ -175,15 +175,22 @@ export const fromCatalogModel = (
.model({ id: resolved.modelID ?? resolved.id, compatibility: resolved.compatibility }),
)
}
const configured = { ...resolved.settings, ...credential?.metadata }
const mapping = Provider.isAISDK(resolved.package) ? AISDKNative.map(packageName, configured) : undefined
const configured = {
...resolved.settings,
...credential?.metadata,
...(packageName === "@ai-sdk/amazon-bedrock/mantle"
? nativeCredentialSettings("@opencode-ai/ai/providers/amazon-bedrock/mantle", credential)
: {}),
}
const mapping = Provider.isAISDK(resolved.package)
? AISDKNative.map(packageName, configured, resolved.modelID ?? resolved.id)
: 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,
})
})
+9
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")],
+81
View File
@@ -2,6 +2,87 @@ import { describe, expect, test } from "bun:test"
import { AISDKNative } from "@opencode-ai/core/aisdk-native"
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(AISDKNative.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(AISDKNative.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(
AISDKNative.map(
"@ai-sdk/amazon-bedrock/mantle",
{
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(
AISDKNative.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(
AISDKNative.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", {
+29
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"), {