Skip to content
Merged
100 changes: 24 additions & 76 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
exact = true
# Only install newly resolved package versions published at least 3 days ago.
minimumReleaseAge = 259200
minimumReleaseAgeExcludes = ["@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider"]
minimumReleaseAgeExcludes = ["@ai-sdk/amazon-bedrock", "@opentui/core", "@opentui/core-darwin-arm64", "@opentui/core-darwin-x64", "@opentui/core-linux-arm64", "@opentui/core-linux-x64", "@opentui/core-win32-arm64", "@opentui/core-win32-x64", "@opentui/keymap", "@opentui/solid", "gitlab-ai-provider"]

[test]
root = "./do-not-run-tests-from-root"
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
},
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/amazon-bedrock": "4.0.112",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41",
Expand All @@ -74,7 +74,7 @@
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.82",
"@aws-sdk/credential-providers": "3.993.0",
"@aws-sdk/credential-providers": "3.1057.0",
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/aisdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
if (abortSignals.length === 1) opts.signal = abortSignals[0]
if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals)

if ((pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure") && opts.body && opts.method === "POST") {
if (
(pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure" || pkg === "@ai-sdk/amazon-bedrock/mantle") &&
opts.body &&
opts.method === "POST"
) {
const body = JSON.parse(opts.body as string)
if (body.store !== true && Array.isArray(body.input)) {
for (const item of body.input) {
Expand Down
25 changes: 24 additions & 1 deletion packages/core/src/plugin/provider/amazon-bedrock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Effect } from "effect"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { PluginV2 } from "../../plugin"
import { ProviderV2 } from "../../provider"

type MantleSDK = {
languageModel: (modelID: string) => LanguageModelV3
chat: (modelID: string) => LanguageModelV3
responses: (modelID: string) => LanguageModelV3
}

// Bedrock cross-region inference profiles require regional prefixes only for
// specific model/region combinations. Keep the mapping narrow and avoid
// double-prefixing model IDs that models.dev already marks as global/us/eu/etc.
Expand Down Expand Up @@ -46,6 +53,12 @@ function resolveModelID(modelID: string, region: string | undefined) {
: modelID
}

function selectMantleModel(sdk: MantleSDK, modelID: string) {
if (modelID === "openai.gpt-oss-safeguard-20b" || modelID === "openai.gpt-oss-safeguard-120b")
return sdk.chat(modelID)
return sdk.responses(modelID)
}

export const AmazonBedrockPlugin = PluginV2.define({
id: PluginV2.ID.make("amazon-bedrock"),
effect: Effect.gen(function* () {
Expand All @@ -65,7 +78,7 @@ export const AmazonBedrockPlugin = PluginV2.define({
}
}),
"aisdk.sdk": Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/amazon-bedrock") return
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
const options = { ...evt.options }
const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
const region = typeof options.region === "string" ? options.region : (process.env.AWS_REGION ?? "us-east-1")
Expand All @@ -86,11 +99,21 @@ export const AmazonBedrockPlugin = PluginV2.define({
options.credentialProvider = fromNodeProviderChain(profile ? { profile } : {})
}

if (evt.package === "@ai-sdk/amazon-bedrock/mantle") {
const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock/mantle"))
evt.sdk = mod.createBedrockMantle(options)
return
}

const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
evt.sdk = mod.createAmazonBedrock(options)
}),
"aisdk.language": Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
return
}
const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
}),
Expand Down
86 changes: 86 additions & 0 deletions packages/core/test/plugin/provider-amazon-bedrock.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ function bedrockFetch(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
).config.fetch
}

function openAIUrl(language: unknown, path: string, modelId: string) {
return (language as { config: { url: (input: { path: string; modelId: string }) => string } }).config.url({
path,
modelId,
})
}

describe("AmazonBedrockPlugin", () => {
it.effect("moves endpoint option to api URL", () =>
Effect.gen(function* () {
Expand Down Expand Up @@ -242,6 +249,85 @@ describe("AmazonBedrockPlugin", () => {
),
)

it.effect("creates Mantle SDK with GPT-5 OpenAI base path", () =>
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "openai.gpt-5.5", {
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
}),
package: "@ai-sdk/amazon-bedrock/mantle",
options: {
name: "amazon-bedrock",
bearerToken: "token",
baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
region: "us-east-2",
},
},
{},
)
const language = result.sdk.responses("openai.gpt-5.5")
expect(openAIUrl(language, "/responses", "openai.gpt-5.5")).toBe(
"https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses",
)
}),
),
)

it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin)
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", "openai.gpt-5.5", {
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
}),
sdk: fakeSelectorSdk(calls),
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: model("amazon-bedrock", "openai.gpt-oss-safeguard-120b", {
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
}),
sdk: fakeSelectorSdk(calls),
options: { region: "us-east-1" },
},
{},
)
expect(calls).toEqual(["responses:openai.gpt-5.5", "chat:openai.gpt-oss-safeguard-120b"])
}),
)

it.effect("ignores other Bedrock provider subpaths", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5", {
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/anthropic" },
}),
package: "@ai-sdk/amazon-bedrock/anthropic",
options: { name: "amazon-bedrock" },
},
{},
)
expect(result.sdk).toBeUndefined()
}),
)

it.effect("uses SigV4 credential env when bearer token is absent", () =>
withEnv(
{
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.21.0",
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.107",
"@ai-sdk/amazon-bedrock": "4.0.112",
"@ai-sdk/anthropic": "3.0.71",
"@ai-sdk/azure": "3.0.49",
"@ai-sdk/cerebras": "2.0.41",
Expand All @@ -75,7 +75,7 @@
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.82",
"@aws-sdk/credential-providers": "3.993.0",
"@aws-sdk/credential-providers": "3.1057.0",
"@clack/prompts": "1.0.0-alpha.1",
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
Expand Down
32 changes: 25 additions & 7 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,13 @@ function googleVertexAnthropicBaseURL(project: string | undefined, location: str

type BundledSDK = {
languageModel(modelId: string): LanguageModelV3
chat?: (modelId: string) => LanguageModelV3
responses?: (modelId: string) => LanguageModelV3
}

const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>> = {
"@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock),
"@ai-sdk/amazon-bedrock/mantle": () => import("@ai-sdk/amazon-bedrock/mantle").then((m) => m.createBedrockMantle),
"@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic),
"@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure),
"@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI),
Expand All @@ -130,7 +133,7 @@ const BUNDLED_PROVIDERS: Record<string, () => Promise<(opts: any) => BundledSDK>
"venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice),
}

type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>, model?: Model) => Promise<any>
type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
type CustomDiscoverModels = () => Promise<Record<string, Model>>
type CustomLoader = (provider: Info) => Effect.Effect<{
Expand All @@ -156,6 +159,12 @@ function selectAzureLanguageModel(sdk: any, modelID: string, useChat: boolean) {
return sdk.languageModel(modelID)
}

function selectBedrockMantleLanguageModel(sdk: BundledSDK, modelID: string) {
if (modelID === "openai.gpt-oss-safeguard-20b" || modelID === "openai.gpt-oss-safeguard-120b")
return sdk.chat?.(modelID) ?? sdk.languageModel(modelID)
return sdk.responses?.(modelID) ?? sdk.languageModel(modelID)
}

function custom(dep: CustomDep): Record<string, CustomLoader> {
return {
anthropic: () =>
Expand Down Expand Up @@ -331,7 +340,9 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
return {
autoload: true,
options: providerOptions,
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
async getModel(sdk: any, modelID: string, options?: Record<string, any>, model?: Model) {
if (model?.api.npm === "@ai-sdk/amazon-bedrock/mantle") return selectBedrockMantleLanguageModel(sdk, modelID)

// Skip region prefixing if model already has a cross-region inference profile prefix
// Models from models.dev may already include prefixes like us., eu., global., etc.
const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."]
Expand Down Expand Up @@ -1618,7 +1629,9 @@ export const layer = Layer.effect(

// Strip openai itemId metadata following what codex does
if (
(model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") &&
(model.api.npm === "@ai-sdk/openai" ||
model.api.npm === "@ai-sdk/azure" ||
model.api.npm === "@ai-sdk/amazon-bedrock/mantle") &&
opts.body &&
opts.method === "POST"
) {
Expand Down Expand Up @@ -1725,10 +1738,15 @@ export const layer = Layer.effect(
async () => {
const sdk = await resolveSDK(model, s, envs)
const language = s.modelLoaders[model.providerID]
? await s.modelLoaders[model.providerID](sdk, model.api.id, {
...provider.options,
...model.options,
})
? await s.modelLoaders[model.providerID](
sdk,
model.api.id,
{
...provider.options,
...model.options,
},
model,
)
: sdk.languageModel(model.api.id)
s.models.set(key, language)
return language
Expand Down
8 changes: 6 additions & 2 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ function sdkKey(npm: string): string | undefined {
return "azure"
case "@ai-sdk/openai":
return "openai"
case "@ai-sdk/amazon-bedrock/mantle":
return "openai"
case "@ai-sdk/amazon-bedrock":
return "bedrock"
case "@ai-sdk/anthropic":
Expand Down Expand Up @@ -815,6 +817,7 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
},
]),
)
case "@ai-sdk/amazon-bedrock/mantle":
case "@ai-sdk/openai": {
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
const efforts = openaiReasoningEfforts(model.api.id, model.release_date)
Expand Down Expand Up @@ -1021,7 +1024,8 @@ export function options(input: {
if (
input.model.providerID === "openai" ||
input.model.api.npm === "@ai-sdk/openai" ||
input.model.api.npm === "@ai-sdk/github-copilot"
input.model.api.npm === "@ai-sdk/github-copilot" ||
input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle"
) {
result["store"] = false
}
Expand Down Expand Up @@ -1107,7 +1111,7 @@ export function options(input: {
if (!input.model.api.id.includes("gpt-5-pro")) {
result["reasoningEffort"] = "medium"
result["reasoningSummary"] = "auto"
if (input.model.api.npm === "@ai-sdk/openai") {
if (input.model.api.npm === "@ai-sdk/openai" || input.model.api.npm === "@ai-sdk/amazon-bedrock/mantle") {
result["include"] = INCLUDE_ENCRYPTED_REASONING
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
const supportsMediaInToolResult = (attachment: { mime: string }) => {
if (model.api.npm === "@ai-sdk/anthropic") return true
if (model.api.npm === "@ai-sdk/openai") return true
if (model.api.npm === "@ai-sdk/amazon-bedrock/mantle") return true
if (model.api.npm === "@ai-sdk/amazon-bedrock") return attachment.mime.startsWith("image/")
if (model.api.npm === "@ai-sdk/xai") return attachment.mime.startsWith("image/")
if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true
Expand Down
Loading
Loading