From a66e6cd55e56d798cedbdb748837ec4a2f557572 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:20:48 -0500 Subject: [PATCH] feat(core): discover Bedrock credentials in the provider plugin (#47548) --- bun.lock | 18 +- packages/core/package.json | 2 - packages/core/src/aisdk-native.ts | 2 + .../src/plugin/provider/amazon-bedrock.ts | 144 ++-- packages/core/test/aisdk-native.test.ts | 14 +- .../plugin/provider-amazon-bedrock.test.ts | 662 +++++------------- 6 files changed, 217 insertions(+), 625 deletions(-) diff --git a/bun.lock b/bun.lock index 9b726797938..9c9eeebf714 100644 --- a/bun.lock +++ b/bun.lock @@ -339,7 +339,6 @@ "version": "1.18.4", "dependencies": { "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.112", "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cohere": "3.0.27", @@ -351,7 +350,6 @@ "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/vercel": "2.0.39", - "@aws-sdk/credential-providers": "3.1057.0", "@ff-labs/fff-bun": "0.10.5", "@ff-labs/fff-node": "0.10.5", "@lydell/node-pty": "catalog:", @@ -1190,7 +1188,7 @@ "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83eXY6p0lUFhSuMvNDmTKDuMciK5XDAWDlNh5c0L80tKjmtCFRItA1MZHp4IKe1r7eK8Rb5nN7qtxqMLUFRIRw=="], - "@ai-sdk/openai": ["@ai-sdk/openai@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oAiGC9eWG7IgtdsdS74bOCnAAHarAfTJhWN9x5INwnWPekL802AvF+0I5DvLzIF1MIRmNw4N8mPSL/GUVbX9Mw=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -5868,6 +5866,8 @@ "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/openai": ["@ai-sdk/openai@3.0.67", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oAiGC9eWG7IgtdsdS74bOCnAAHarAfTJhWN9x5INwnWPekL802AvF+0I5DvLzIF1MIRmNw4N8mPSL/GUVbX9Mw=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], @@ -5876,8 +5876,6 @@ "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], - "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], - "@ai-sdk/azure/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], @@ -5924,9 +5922,9 @@ "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="], - "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], - "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -6386,8 +6384,6 @@ "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.27", "", { "dependencies": { "@ai-sdk/provider": "4.0.7", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EzAn4pdgG5g0xXtH6lE2zyNmfjDQIDjATkfqzuidEI35g++hh4+07vnjzkT/RmGmIClPZiRj/Q2GMPV2V7mkHw=="], - "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], - "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], @@ -7248,10 +7244,6 @@ "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], - - "ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="], - "ai/@ai-sdk/provider-utils/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/packages/core/package.json b/packages/core/package.json index 5ccb87ee814..e0926bb09cb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -100,7 +100,6 @@ }, "dependencies": { "@ai-sdk/alibaba": "1.0.17", - "@ai-sdk/amazon-bedrock": "4.0.112", "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/azure": "3.0.88", "@ai-sdk/cohere": "3.0.27", @@ -112,7 +111,6 @@ "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/vercel": "2.0.39", - "@aws-sdk/credential-providers": "3.1057.0", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", "@ff-labs/fff-bun": "0.10.5", diff --git a/packages/core/src/aisdk-native.ts b/packages/core/src/aisdk-native.ts index c147d00e129..9bb7873c8ab 100644 --- a/packages/core/src/aisdk-native.ts +++ b/packages/core/src/aisdk-native.ts @@ -195,7 +195,9 @@ function mapBedrockSettings( ? { baseURL: settings.endpoint } : {}), ...(apiKey === undefined ? {} : { apiKey }), + ...(settings.auth === "bearer" || settings.auth === "sigv4" ? { auth: settings.auth } : {}), ...(credentials === undefined ? {} : { credentials }), + ...(typeof settings.profile === "string" ? { profile: settings.profile } : {}), ...(typeof settings.region === "string" ? { region: settings.region } : {}), ...(typeof settings.topP === "number" ? { topP: settings.topP } : {}), } diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index f7ceedd2f1a..6ff156471b5 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -1,123 +1,59 @@ import { Effect } from "effect" -import type { LanguageModelV3 } from "@ai-sdk/provider" import { define } from "@opencode-ai/plugin/effect/plugin" import { Provider } from "../../provider.js" -type MantleSDK = { - chat: (modelID: string) => LanguageModelV3 - responses: (modelID: string) => LanguageModelV3 -} +// Ambient inputs the AWS default credential chain can turn into credentials +// without any key stored in opencode. Mirrors the presence checks the AWS CLI +// and SDK use before consulting shared config. +const CHAIN_ENV = [ + "AWS_PROFILE", + "AWS_ACCESS_KEY_ID", + "AWS_WEB_IDENTITY_TOKEN_FILE", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", + "AWS_CONTAINER_CREDENTIALS_FULL_URI", +] -// 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. -function resolveModelID(modelID: string, region: string | undefined) { - const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] - if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) return modelID - - const resolvedRegion = region ?? "us-east-1" - const regionPrefix = resolvedRegion.split("-")[0] - if (regionPrefix === "us") { - const requiresPrefix = ["nova-micro", "nova-lite", "nova-pro", "nova-premier", "nova-2", "claude", "deepseek"].some( - (item) => modelID.includes(item), - ) - if (requiresPrefix && !resolvedRegion.startsWith("us-gov")) return `${regionPrefix}.${modelID}` - return modelID - } - if (regionPrefix === "eu") { - const regionRequiresPrefix = [ - "eu-west-1", - "eu-west-2", - "eu-west-3", - "eu-north-1", - "eu-central-1", - "eu-south-1", - "eu-south-2", - ].some((item) => resolvedRegion.includes(item)) - const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((item) => - modelID.includes(item), - ) - return regionRequiresPrefix && modelRequiresPrefix ? `${regionPrefix}.${modelID}` : modelID - } - if (regionPrefix !== "ap") return modelID - - const australia = ["ap-southeast-2", "ap-southeast-4"].includes(resolvedRegion) - if (australia && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((item) => modelID.includes(item))) { - return `au.${modelID}` - } - - const prefix = resolvedRegion === "ap-northeast-1" ? "jp" : "apac" - return ["claude", "nova-lite", "nova-micro", "nova-pro"].some((item) => modelID.includes(item)) - ? `${prefix}.${modelID}` - : 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) +const isBedrock = (item: { readonly package: string }) => { + const name = Provider.packageName(item.package) + return name.startsWith("@ai-sdk/amazon-bedrock") || name.startsWith("@opencode-ai/ai/providers/amazon-bedrock") } export const AmazonBedrockPlugin = define({ id: "opencode.provider.amazon.bedrock", effect: Effect.fn(function* (ctx) { + yield* ctx.integration.transform((editor) => { + // models.dev advertises AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and + // AWS_REGION alongside the bearer token. Only the bearer token is a key; + // the rest feed the SigV4 credential chain and must not become one. + editor.method.update({ + integrationID: Provider.ID.amazonBedrock, + method: { type: "env", names: ["AWS_BEARER_TOKEN_BEDROCK"] }, + }) + }) yield* ctx.catalog.transform((evt) => { for (const item of evt.provider.list()) { - if (!Provider.isAISDK(item.provider.package)) continue - if (Provider.packageName(item.provider.package) !== "@ai-sdk/amazon-bedrock") continue + if (!isBedrock(item.provider)) continue evt.provider.update(item.provider.id, (provider) => { - if (typeof provider.settings?.endpoint !== "string") return - // The AI SDK expects a base URL, but users configure Bedrock private/VPC - // endpoints as `endpoint`; move it into the catalog endpoint URL once. - provider.settings.baseURL = provider.settings.endpoint + const settings = provider.settings ?? {} + const chain = typeof settings.profile === "string" || CHAIN_ENV.some((name) => process.env[name]) + // SigV4 authenticates through the AWS default chain rather than a key + // credential, so ambient AWS configuration is what makes Bedrock usable. + if (chain && provider.activation === "auto") provider.activation = "enabled" + // Same default the native package uses, made explicit here so catalog + // `${AWS_REGION}` URLs resolve without any region configured. + const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1" + provider.settings = { + ...settings, + ...(typeof settings.region !== "string" ? { region } : {}), + // Users configure Bedrock private/VPC endpoints as `endpoint`; move it + // into the catalog base URL once. + ...(typeof settings.baseURL !== "string" && typeof settings.endpoint === "string" + ? { baseURL: settings.endpoint } + : {}), + } delete provider.settings.endpoint }) } }) - yield* ctx.aisdk.hook( - "sdk", - Effect.fn(function* (evt) { - 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") - const bearerToken = - process.env.AWS_BEARER_TOKEN_BEDROCK ?? - (typeof options.bearerToken === "string" ? options.bearerToken : undefined) - if (bearerToken && !process.env.AWS_BEARER_TOKEN_BEDROCK) process.env.AWS_BEARER_TOKEN_BEDROCK = bearerToken - options.region = region - if (typeof options.endpoint === "string") options.baseURL = options.endpoint - if (!bearerToken && options.credentialProvider === undefined) { - // Do not gate SDK creation on explicit AWS env vars. The default chain - // also handles ~/.aws/credentials, SSO, process creds, and instance roles. - const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers")) - 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) - }), - ) - yield* ctx.aisdk.hook( - "language", - Effect.fn(function* (evt) { - if (evt.model.providerID !== Provider.ID.amazonBedrock) return - if ( - Provider.isAISDK(evt.model.package) && - Provider.packageName(evt.model.package) === "@ai-sdk/amazon-bedrock/mantle" - ) { - evt.language = selectMantleModel(evt.sdk, evt.model.modelID ?? evt.model.id) - return - } - const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION - evt.language = evt.sdk.languageModel(resolveModelID(evt.model.modelID ?? evt.model.id, region)) - }), - ) }), }) diff --git a/packages/core/test/aisdk-native.test.ts b/packages/core/test/aisdk-native.test.ts index becf22b00c7..146d87f8aeb 100644 --- a/packages/core/test/aisdk-native.test.ts +++ b/packages/core/test/aisdk-native.test.ts @@ -311,7 +311,6 @@ describe("AISDKNative", () => { region: "eu-west-1", }, baseURL: "https://bedrock-mantle.${AWS_REGION}.api.aws/v1", - profile: "ignored", credentialProvider: "ignored", fetch: "ignored", store: false, @@ -333,6 +332,19 @@ describe("AISDKNative", () => { }) }) + test("forwards Bedrock profile and auth mode for the default credential chain", () => { + expect( + map("@ai-sdk/amazon-bedrock", { profile: "work", auth: "sigv4", region: "eu-west-1" }, "anthropic.claude"), + ).toEqual({ + package: "@opencode-ai/ai/providers/amazon-bedrock", + settings: { profile: "work", auth: "sigv4", region: "eu-west-1" }, + }) + expect(map("@ai-sdk/amazon-bedrock", { auth: "bogus" }, "anthropic.claude")).toEqual({ + package: "@opencode-ai/ai/providers/amazon-bedrock", + settings: {}, + }) + }) + test("maps the legacy Bedrock endpoint override", () => { expect( map( diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index 6b9c80f8385..c486b7c21f7 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -1,13 +1,11 @@ -import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { Model } from "@opencode-ai/core/model" +import { Integration } from "@opencode-ai/core/integration" import { Plugin } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock" import { Provider } from "@opencode-ai/core/provider" -import { fakeSelectorSdk } from "../fixture/selector" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "./fixture" @@ -45,529 +43,183 @@ function withEnv(vars: Record, fx: () => Ef ) } -function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") { - const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID) - return (language as { config: { baseUrl: () => string } }).config.baseUrl() +const noAmbientAWS = { + AWS_PROFILE: undefined, + AWS_ACCESS_KEY_ID: undefined, + AWS_WEB_IDENTITY_TOKEN_FILE: undefined, + AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: undefined, + AWS_CONTAINER_CREDENTIALS_FULL_URI: undefined, + AWS_REGION: undefined, + AWS_DEFAULT_REGION: undefined, } -function bedrockFetch(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") { - const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID) - return ( - language as { config: { fetch: (input: Parameters[0], init?: RequestInit) => Promise } } - ).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, +const seedBedrock = Effect.fn(function* (settings?: Record) { + const catalog = yield* Catalog.Service + yield* catalog.transform((catalog) => { + catalog.provider.update(Provider.ID.amazonBedrock, (item) => { + item.package = Provider.aisdk("@ai-sdk/amazon-bedrock") + if (settings) item.settings = settings + }) }) -} + return catalog +}) describe("AmazonBedrockPlugin", () => { it.effect("moves endpoint setting to baseURL", () => + withEnv(noAmbientAWS, () => + Effect.gen(function* () { + const catalog = yield* seedBedrock({ endpoint: "https://bedrock.example" }) + yield* addPlugin() + const result = required(yield* catalog.provider.get(Provider.ID.amazonBedrock)) + expect(result.package).toBe(Provider.aisdk("@ai-sdk/amazon-bedrock")) + expect(result.settings).toEqual({ baseURL: "https://bedrock.example", region: "us-east-1" }) + }), + ), + ) + + it.effect("keeps an explicit baseURL over endpoint", () => + withEnv(noAmbientAWS, () => + Effect.gen(function* () { + const catalog = yield* seedBedrock({ baseURL: "https://base.example", endpoint: "https://endpoint.example" }) + yield* addPlugin() + expect(required(yield* catalog.provider.get(Provider.ID.amazonBedrock)).settings).toEqual({ + baseURL: "https://base.example", + region: "us-east-1", + }) + }), + ), + ) + + it.effect("only treats the bearer token env var as a key credential", () => Effect.gen(function* () { - const catalog = yield* Catalog.Service - yield* catalog.transform((catalog) => { - catalog.provider.update(Provider.ID.amazonBedrock, (item) => { - item.package = Provider.aisdk("@ai-sdk/amazon-bedrock") - item.settings = { endpoint: "https://bedrock.example" } + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make(Provider.ID.amazonBedrock) + yield* integrations.transform((editor) => { + editor.method.update({ integrationID, method: { type: "key" } }) + editor.method.update({ + integrationID, + method: { + type: "env", + names: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION", "AWS_BEARER_TOKEN_BEDROCK"], + }, }) }) yield* addPlugin() - const result = required(yield* catalog.provider.get(Provider.ID.amazonBedrock)) - expect(result.package).toBe(Provider.aisdk("@ai-sdk/amazon-bedrock")) - expect(result.settings).toEqual({ baseURL: "https://bedrock.example" }) - }), - ) - - it.effect("prefers endpoint over baseURL for SDK base URL", () => - withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - bearerToken: "token", - baseURL: "https://base.example", - endpoint: "https://endpoint.example", - region: "us-east-1", - }, - }) - expect(bedrockBaseURL(result.sdk)).toBe("https://endpoint.example") - }), - ), - ) - - it.effect("uses baseURL as SDK base URL", () => - withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - bearerToken: "token", - baseURL: "https://base.example", - region: "us-east-1", - }, - }) - expect(bedrockBaseURL(result.sdk)).toBe("https://base.example") - }), - ), - ) - - it.effect("creates SDK without explicit credential env so the default AWS chain can resolve credentials", () => - withEnv( - { - AWS_ACCESS_KEY_ID: undefined, - AWS_BEARER_TOKEN_BEDROCK: undefined, - AWS_CONTAINER_CREDENTIALS_FULL_URI: undefined, - AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: undefined, - AWS_PROFILE: undefined, - AWS_REGION: undefined, - AWS_WEB_IDENTITY_TOKEN_FILE: undefined, - }, - () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { name: "amazon-bedrock" }, - }) - expect(result.sdk).toBeDefined() - expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com") - }), - ), - ) - - it.effect("uses config region over AWS_REGION for SDK base URL", () => - withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { name: "amazon-bedrock", region: "eu-west-1" }, - }) - expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com") - }), - ), - ) - - it.effect("uses AWS_REGION for SDK base URL when config region is absent", () => - withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { name: "amazon-bedrock" }, - }) - expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com") - }), - ), - ) - - it.effect("defaults SDK region to us-east-1", () => - withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { name: "amazon-bedrock" }, - }) - expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com") - }), - ), - ) - - it.effect("loads bearer token option into env and uses bearer auth", () => - withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - const headers: Array = [] - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - bearerToken: "option-token", - fetch: async (_input: Parameters[0], init?: RequestInit) => { - headers.push(new Headers(init?.headers).get("Authorization")) - return new Response("{}") - }, - }, - }) - yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" })) - expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token") - expect(headers).toEqual(["Bearer option-token"]) - }), - ), - ) - - it.effect("prefers bearer token env over bearer token option", () => - withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - const headers: Array = [] - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - bearerToken: "option-token", - fetch: async (_input: Parameters[0], init?: RequestInit) => { - headers.push(new Headers(init?.headers).get("Authorization")) - return new Response("{}") - }, - }, - }) - yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" })) - expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("env-token") - expect(headers).toEqual(["Bearer env-token"]) - }), - ), - ) - - 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 aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-5.5")), - modelID: Model.ID.make("openai.gpt-5.5"), - package: Provider.aisdk("@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 aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-5.5")), - modelID: Model.ID.make("openai.gpt-5.5"), - package: Provider.aisdk("@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* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("openai.gpt-oss-safeguard-120b")), - modelID: Model.ID.make("openai.gpt-oss-safeguard-120b"), - package: Provider.aisdk("@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 aisdk = yield* AISDK.Service - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("@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( - { - AWS_ACCESS_KEY_ID: "test-access-key", - AWS_BEARER_TOKEN_BEDROCK: undefined, - AWS_REGION: "us-east-1", - AWS_SECRET_ACCESS_KEY: "test-secret-key", - AWS_SESSION_TOKEN: "test-session-token", - }, - () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - const headers: Array = [] - yield* addPlugin() - const result = yield* aisdk.runSDK({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - fetch: async (_input: Parameters[0], init?: RequestInit) => { - headers.push(new Headers(init?.headers).get("Authorization")) - return new Response("{}") - }, - }, - }) - yield* Effect.promise(() => - bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", { - body: "{}", - method: "POST", - }), - ) - expect(headers[0]?.startsWith("AWS4-HMAC-SHA256 ")).toBe(true) - }), - ), - ) - - it.effect("applies legacy cross-region inference prefixes", () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: {}, - }) - yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: { region: "eu-west-1" }, - }) - yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("global.anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("global.anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: { region: "eu-west-1" }, - }) - yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: { region: "ap-northeast-1" }, - }) - yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: { region: "ap-southeast-2" }, - }) - expect(calls).toEqual([ - "languageModel:us.anthropic.claude-sonnet-4-5", - "languageModel:eu.anthropic.claude-sonnet-4-5", - "languageModel:global.anthropic.claude-sonnet-4-5", - "languageModel:jp.anthropic.claude-sonnet-4-5", - "languageModel:au.anthropic.claude-sonnet-4-5", + expect((yield* integrations.get(integrationID))?.methods).toEqual([ + { type: "key" }, + { type: "env", names: ["AWS_BEARER_TOKEN_BEDROCK"] }, ]) }), ) - it.effect("uses AWS_REGION for language prefixes when region option is absent", () => - withEnv({ AWS_REGION: "eu-west-1" }, () => + it.effect("leaves activation on auto without ambient AWS configuration", () => + withEnv(noAmbientAWS, () => Effect.gen(function* () { - const aisdk = yield* AISDK.Service - const calls: string[] = [] + const catalog = yield* seedBedrock() yield* addPlugin() - yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: {}, - }) - expect(calls).toEqual(["languageModel:eu.anthropic.claude-sonnet-4-5"]) + expect(required(yield* catalog.provider.get(Provider.ID.amazonBedrock)).activation).toBe("auto") }), ), ) - it.effect("applies the full legacy cross-region prefix matrix", () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - const calls: string[] = [] - const cases = [ - { region: "us-east-1", modelID: "amazon.nova-micro-v1:0", expected: "us.amazon.nova-micro-v1:0" }, - { region: "us-east-1", modelID: "amazon.nova-lite-v1:0", expected: "us.amazon.nova-lite-v1:0" }, - { region: "us-east-1", modelID: "amazon.nova-pro-v1:0", expected: "us.amazon.nova-pro-v1:0" }, - { region: "us-east-1", modelID: "amazon.nova-premier-v1:0", expected: "us.amazon.nova-premier-v1:0" }, - { region: "us-east-1", modelID: "amazon.nova-2-lite-v1:0", expected: "us.amazon.nova-2-lite-v1:0" }, - { region: "us-east-1", modelID: "anthropic.claude-sonnet-4-5", expected: "us.anthropic.claude-sonnet-4-5" }, - { region: "us-east-1", modelID: "deepseek.r1-v1:0", expected: "us.deepseek.r1-v1:0" }, - { region: "us-gov-west-1", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" }, - { region: "us-east-1", modelID: "cohere.command-r-plus-v1:0", expected: "cohere.command-r-plus-v1:0" }, - { region: "eu-west-1", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" }, - { region: "eu-west-2", modelID: "amazon.nova-lite-v1:0", expected: "eu.amazon.nova-lite-v1:0" }, - { region: "eu-west-3", modelID: "amazon.nova-micro-v1:0", expected: "eu.amazon.nova-micro-v1:0" }, - { - region: "eu-north-1", - modelID: "meta.llama3-70b-instruct-v1:0", - expected: "eu.meta.llama3-70b-instruct-v1:0", - }, - { region: "eu-central-1", modelID: "mistral.pixtral-large-v1:0", expected: "eu.mistral.pixtral-large-v1:0" }, - { region: "eu-south-1", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" }, - { region: "eu-south-2", modelID: "anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" }, - { region: "eu-central-2", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" }, - { region: "eu-west-1", modelID: "cohere.command-r-plus-v1:0", expected: "cohere.command-r-plus-v1:0" }, - { - region: "ap-southeast-2", - modelID: "anthropic.claude-sonnet-4-5", - expected: "au.anthropic.claude-sonnet-4-5", - }, - { - region: "ap-southeast-4", - modelID: "anthropic.claude-haiku-v1:0", - expected: "au.anthropic.claude-haiku-v1:0", - }, - { region: "ap-southeast-2", modelID: "anthropic.claude-opus-4", expected: "apac.anthropic.claude-opus-4" }, - { - region: "ap-northeast-1", - modelID: "anthropic.claude-sonnet-4-5", - expected: "jp.anthropic.claude-sonnet-4-5", - }, - { region: "ap-northeast-1", modelID: "amazon.nova-pro-v1:0", expected: "jp.amazon.nova-pro-v1:0" }, - { region: "ap-south-1", modelID: "anthropic.claude-sonnet-4-5", expected: "apac.anthropic.claude-sonnet-4-5" }, - { region: "ap-south-1", modelID: "amazon.nova-lite-v1:0", expected: "apac.amazon.nova-lite-v1:0" }, - { region: "ca-central-1", modelID: "anthropic.claude-sonnet-4-5", expected: "anthropic.claude-sonnet-4-5" }, - { - region: "us-east-1", - modelID: "global.anthropic.claude-sonnet-4-5", - expected: "global.anthropic.claude-sonnet-4-5", - }, - { region: "us-east-1", modelID: "us.anthropic.claude-sonnet-4-5", expected: "us.anthropic.claude-sonnet-4-5" }, - { region: "eu-west-1", modelID: "eu.anthropic.claude-sonnet-4-5", expected: "eu.anthropic.claude-sonnet-4-5" }, - { - region: "ap-northeast-1", - modelID: "jp.anthropic.claude-sonnet-4-5", - expected: "jp.anthropic.claude-sonnet-4-5", - }, - { - region: "ap-south-1", - modelID: "apac.anthropic.claude-sonnet-4-5", - expected: "apac.anthropic.claude-sonnet-4-5", - }, - { - region: "ap-southeast-2", - modelID: "au.anthropic.claude-sonnet-4-5", - expected: "au.anthropic.claude-sonnet-4-5", - }, - ] - yield* addPlugin() - for (const item of cases) { - yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.amazonBedrock, Model.ID.make(item.modelID)), - modelID: Model.ID.make(item.modelID), - package: Provider.aisdk("test-provider"), - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: { region: item.region }, - }) - } - expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`)) - }), + for (const name of Object.keys(noAmbientAWS).filter((name) => !name.includes("REGION"))) { + it.effect(`enables the provider when ${name} is set`, () => + withEnv({ ...noAmbientAWS, [name]: "value" }, () => + Effect.gen(function* () { + const catalog = yield* seedBedrock() + yield* addPlugin() + expect(required(yield* catalog.provider.get(Provider.ID.amazonBedrock)).activation).toBe("enabled") + }), + ), + ) + } + + it.effect("enables the provider when a profile is configured", () => + withEnv(noAmbientAWS, () => + Effect.gen(function* () { + const catalog = yield* seedBedrock({ profile: "work" }) + yield* addPlugin() + const result = required(yield* catalog.provider.get(Provider.ID.amazonBedrock)) + expect(result.activation).toBe("enabled") + expect(result.settings).toEqual({ profile: "work", region: "us-east-1" }) + }), + ), ) - it.effect("ignores non-Bedrock providers for language selection", () => - Effect.gen(function* () { - const aisdk = yield* AISDK.Service - const calls: string[] = [] - yield* addPlugin() - const result = yield* aisdk.runLanguage({ - model: Model.Info.make({ - ...Model.Info.default(Provider.ID.openai, Model.ID.make("anthropic.claude-sonnet-4-5")), - modelID: Model.ID.make("anthropic.claude-sonnet-4-5"), - package: Provider.aisdk("test-provider"), - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: { region: "eu-west-1" }, - }) - expect(calls).toEqual([]) - expect(result.language).toBeUndefined() - }), + it.effect("does not override a disabled provider", () => + withEnv({ ...noAmbientAWS, AWS_PROFILE: "work" }, () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((catalog) => { + catalog.provider.update(Provider.ID.amazonBedrock, (item) => { + item.package = Provider.aisdk("@ai-sdk/amazon-bedrock") + item.activation = "disabled" + }) + }) + yield* addPlugin() + expect(required(yield* catalog.provider.get(Provider.ID.amazonBedrock)).activation).toBe("disabled") + }), + ), + ) + + it.effect("fills region from AWS_REGION then AWS_DEFAULT_REGION without overriding config", () => + withEnv({ ...noAmbientAWS, AWS_REGION: "eu-west-1", AWS_DEFAULT_REGION: "us-west-2" }, () => + Effect.gen(function* () { + const catalog = yield* seedBedrock() + yield* addPlugin() + expect(required(yield* catalog.provider.get(Provider.ID.amazonBedrock)).settings).toEqual({ + region: "eu-west-1", + }) + + yield* catalog.transform((catalog) => { + catalog.provider.update(Provider.ID.amazonBedrock, (item) => { + item.settings = { region: "ap-southeast-2" } + }) + }) + expect(required(yield* catalog.provider.get(Provider.ID.amazonBedrock)).settings).toEqual({ + region: "ap-southeast-2", + }) + }), + ), + ) + + it.effect("falls back to AWS_DEFAULT_REGION then us-east-1", () => + withEnv({ ...noAmbientAWS, AWS_DEFAULT_REGION: "us-west-2" }, () => + Effect.gen(function* () { + const catalog = yield* seedBedrock() + yield* addPlugin() + expect(required(yield* catalog.provider.get(Provider.ID.amazonBedrock)).settings).toEqual({ + region: "us-west-2", + }) + const fallback = yield* Effect.gen(function* () { + yield* catalog.reload() + return required(yield* catalog.provider.get(Provider.ID.amazonBedrock)).settings + }).pipe((fx) => withEnv({ AWS_DEFAULT_REGION: undefined }, () => fx)) + expect(fallback).toEqual({ region: "us-east-1" }) + }), + ), + ) + + it.effect("applies to Mantle and native Bedrock packages", () => + withEnv({ ...noAmbientAWS, AWS_PROFILE: "work" }, () => + Effect.gen(function* () { + const catalog = yield* Catalog.Service + yield* catalog.transform((catalog) => { + catalog.provider.update(Provider.ID.make("mantle"), (item) => { + item.package = Provider.aisdk("@ai-sdk/amazon-bedrock/mantle") + }) + catalog.provider.update(Provider.ID.make("native"), (item) => { + item.package = "@opencode-ai/ai/providers/amazon-bedrock" + }) + catalog.provider.update(Provider.ID.make("other"), (item) => { + item.package = Provider.aisdk("@ai-sdk/anthropic") + }) + }) + yield* addPlugin() + expect(required(yield* catalog.provider.get(Provider.ID.make("mantle"))).activation).toBe("enabled") + expect(required(yield* catalog.provider.get(Provider.ID.make("native"))).activation).toBe("enabled") + expect(required(yield* catalog.provider.get(Provider.ID.make("other"))).activation).toBe("auto") + }), + ), ) })