diff --git a/packages/core/src/aisdk-native.ts b/packages/core/src/aisdk-native.ts index 9e8a587591c..61502d8bc1e 100644 --- a/packages/core/src/aisdk-native.ts +++ b/packages/core/src/aisdk-native.ts @@ -14,12 +14,17 @@ export interface MapInput { readonly packageName: string | undefined readonly settings: Readonly> 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>): 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>, + baseSettings: Readonly>, +) { const apiKey = typeof settings.apiKey === "string" ? settings.apiKey @@ -54,20 +74,69 @@ function mapBedrockMantle(input: MapInput, baseSettings: Readonly { + 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 }), } } diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index a9a5b6ef6c5..fbf345a02af 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -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 diff --git a/packages/core/test/aisdk-native.test.ts b/packages/core/test/aisdk-native.test.ts index 841bc061d94..6a39c82dfcb 100644 --- a/packages/core/test/aisdk-native.test.ts +++ b/packages/core/test/aisdk-native.test.ts @@ -5,11 +5,75 @@ const map = (packageName: string, settings: Readonly>, 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( diff --git a/packages/core/test/model-resolver.test.ts b/packages/core/test/model-resolver.test.ts index 98a5b646792..b74a9b799c2 100644 --- a/packages/core/test/model-resolver.test.ts +++ b/packages/core/test/model-resolver.test.ts @@ -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") }), )