diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 0fdf26392f6..0f00c3a3036 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -165,29 +165,44 @@ export namespace Provider { } }, "amazon-bedrock": async () => { - const [awsProfile, awsAccessKeyId, awsBearerToken, awsRegion] = await Promise.all([ - Env.get("AWS_PROFILE"), - Env.get("AWS_ACCESS_KEY_ID"), - Env.get("AWS_BEARER_TOKEN_BEDROCK"), - Env.get("AWS_REGION"), - ]) + const auth = await Auth.get("amazon-bedrock") + const awsProfile = Env.get("AWS_PROFILE") + const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID") + const awsRegion = Env.get("AWS_REGION") + + const awsBearerToken = iife(() => { + const envToken = Env.get("AWS_BEARER_TOKEN_BEDROCK") + if (envToken) return envToken + if (auth?.type === "api") { + Env.set("AWS_BEARER_TOKEN_BEDROCK", auth.key) + return auth.key + } + return undefined + }) + if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false } - const region = awsRegion ?? "us-east-1" + const defaultRegion = awsRegion ?? "us-east-1" const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers")) return { autoload: true, options: { - region, + region: defaultRegion, credentialProvider: fromNodeProviderChain(), }, - async getModel(sdk: any, modelID: string, _options?: Record) { + async getModel(sdk: any, modelID: string, options?: Record) { // Skip region prefixing if model already has global prefix if (modelID.startsWith("global.")) { return sdk.languageModel(modelID) } + // Region resolution precedence (highest to lowest): + // 1. options.region from opencode.json provider config + // 2. defaultRegion from AWS_REGION environment variable + // 3. Default "us-east-1" (baked into defaultRegion) + const region = options?.region ?? defaultRegion + let regionPrefix = region.split("-")[0] switch (regionPrefix) { diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts new file mode 100644 index 00000000000..30cd2d0b642 --- /dev/null +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -0,0 +1,236 @@ +import { test, expect } from "bun:test" +import path from "path" +import { tmpdir } from "../fixture/fixture" +import { Instance } from "../../src/project/instance" +import { Provider } from "../../src/provider/provider" +import { Env } from "../../src/env" +import { Auth } from "../../src/auth" +import { Global } from "../../src/global" + +test("Bedrock: config region takes precedence over AWS_REGION env var", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "eu-west-1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_REGION", "us-east-1") + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + // Region from config should be used (not env var) + expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1") + }, + }) +}) + +test("Bedrock: falls back to AWS_REGION env var when no config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_REGION", "eu-west-1") + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1") + }, + }) +}) + +test("Bedrock: without explicit region config, uses AWS_REGION env or defaults", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + // AWS_REGION might be set in the environment, use that or default + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + // Should have some region set (either from env or default) + expect(providers["amazon-bedrock"].options?.region).toBeDefined() + expect(typeof providers["amazon-bedrock"].options?.region).toBe("string") + }, + }) +}) + +test("Bedrock: uses config region in provider options", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "eu-north-1", + }, + }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + }, + fn: async () => { + const providers = await Provider.list() + const bedrockProvider = providers["amazon-bedrock"] + expect(bedrockProvider).toBeDefined() + expect(bedrockProvider.options?.region).toBe("eu-north-1") + }, + }) +}) + +test("Bedrock: respects config region for different instances", async () => { + // First instance with EU config + await using tmp1 = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "eu-west-1", + }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp1.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + Env.set("AWS_REGION", "us-east-1") + }, + fn: async () => { + const providers1 = await Provider.list() + expect(providers1["amazon-bedrock"].options?.region).toBe("eu-west-1") + }, + }) + + // Second instance with US config + await using tmp2 = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "us-west-2", + }, + }, + }, + }), + ) + }, + }) + + await Instance.provide({ + directory: tmp2.path, + init: async () => { + Env.set("AWS_PROFILE", "default") + Env.set("AWS_REGION", "eu-west-1") + }, + fn: async () => { + const providers2 = await Provider.list() + expect(providers2["amazon-bedrock"].options?.region).toBe("us-west-2") + }, + }) +}) + +test("Bedrock: loads when bearer token from auth.json is present", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + provider: { + "amazon-bedrock": { + options: { + region: "eu-west-1", + }, + }, + }, + }), + ) + }, + }) + + // Setup auth.json with bearer token for amazon-bedrock + const authPath = path.join(Global.Path.data, "auth.json") + await Bun.write( + authPath, + JSON.stringify({ + "amazon-bedrock": { + type: "api", + key: "test-bearer-token", + }, + }), + ) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + // Clear env vars so only auth.json should trigger autoload + Env.set("AWS_PROFILE", "") + Env.set("AWS_ACCESS_KEY_ID", "") + Env.set("AWS_BEARER_TOKEN_BEDROCK", "") + }, + fn: async () => { + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-1") + }, + }) +})