Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 83 additions & 14 deletions packages/core/src/aisdk-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -47,27 +52,91 @@ 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
: 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),
},
...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 }),
}
}

Expand Down
1 change: 0 additions & 1 deletion packages/core/src/model-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 65 additions & 6 deletions packages/core/test/aisdk-native.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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",
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 16 additions & 0 deletions packages/core/test/model-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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")
}),
)

Expand Down
Loading