mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-04 20:39:53 +00:00
fix(core): route Bedrock packages natively (#40165)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
parent
bf7f0cb521
commit
dd014120cd
4 changed files with 164 additions and 21 deletions
|
|
@ -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":
|
||||
|
|
@ -47,6 +52,21 @@ 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
|
||||
|
|
@ -54,20 +74,69 @@ function mapBedrockMantle(input: MapInput, baseSettings: Readonly<Record<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 }),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue