diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index f200ec4fe06..adbd3f45a5e 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -335,7 +335,11 @@ export const AuthLoginCommand = cmd({ if (provider === "amazon-bedrock") { prompts.log.info( - "Amazon bedrock can be configured with standard AWS environment variables like AWS_BEARER_TOKEN_BEDROCK, AWS_PROFILE or AWS_ACCESS_KEY_ID", + "Amazon Bedrock authentication priority:\n" + + " 1. Bearer token (AWS_BEARER_TOKEN_BEDROCK or /connect)\n" + + " 2. AWS credential chain (profile, access keys, IAM roles)\n\n" + + "Configure via opencode.json options (profile, region, endpoint) or\n" + + "AWS environment variables (AWS_PROFILE, AWS_REGION, AWS_ACCESS_KEY_ID).", ) prompts.outro("Done") return diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9967edec5dd..606714f08b4 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -15,7 +15,7 @@ import { Flag } from "../flag/flag" import { iife } from "@/util/iife" // Direct imports for bundled providers -import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock" +import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock" import { createAnthropic } from "@ai-sdk/anthropic" import { createAzure } from "@ai-sdk/azure" import { createGoogleGenerativeAI } from "@ai-sdk/google" @@ -168,10 +168,22 @@ export namespace Provider { } }, "amazon-bedrock": async () => { + const config = await Config.get() + const providerConfig = config.provider?.["amazon-bedrock"] + const auth = await Auth.get("amazon-bedrock") - const awsProfile = Env.get("AWS_PROFILE") + + // Region precedence: 1) config file, 2) env var, 3) default + const configRegion = providerConfig?.options?.region + const envRegion = Env.get("AWS_REGION") + const defaultRegion = configRegion ?? envRegion ?? "us-east-1" + + // Profile: config file takes precedence over env var + const configProfile = providerConfig?.options?.profile + const envProfile = Env.get("AWS_PROFILE") + const profile = configProfile ?? envProfile + 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") @@ -183,17 +195,27 @@ export namespace Provider { return undefined }) - if (!awsProfile && !awsAccessKeyId && !awsBearerToken) return { autoload: false } - - const defaultRegion = awsRegion ?? "us-east-1" + if (!profile && !awsAccessKeyId && !awsBearerToken) return { autoload: false } const { fromNodeProviderChain } = await import(await BunProc.install("@aws-sdk/credential-providers")) + + // Build credential provider options (only pass profile if specified) + const credentialProviderOptions = profile ? { profile } : {} + + const providerOptions: AmazonBedrockProviderSettings = { + region: defaultRegion, + credentialProvider: fromNodeProviderChain(credentialProviderOptions), + } + + // Add custom endpoint if specified (endpoint takes precedence over baseURL) + const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL + if (endpoint) { + providerOptions.baseURL = endpoint + } + return { autoload: true, - options: { - region: defaultRegion, - credentialProvider: fromNodeProviderChain(), - }, + options: providerOptions, async getModel(sdk: any, modelID: string, options?: Record) { // Skip region prefixing if model already has global prefix if (modelID.startsWith("global.")) { diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index 76d1329f40c..35b0b6c7642 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -42,6 +42,8 @@ delete process.env["GOOGLE_GENERATIVE_AI_API_KEY"] delete process.env["AZURE_OPENAI_API_KEY"] delete process.env["AWS_ACCESS_KEY_ID"] delete process.env["AWS_PROFILE"] +delete process.env["AWS_REGION"] +delete process.env["AWS_BEARER_TOKEN_BEDROCK"] delete process.env["OPENROUTER_API_KEY"] delete process.env["GROQ_API_KEY"] delete process.env["MISTRAL_API_KEY"] diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 30cd2d0b642..d10e851391e 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -1,11 +1,40 @@ -import { test, expect } from "bun:test" +import { test, expect, mock } 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" + +// === Mocks === +// These mocks are required because Provider.list() triggers: +// 1. BunProc.install("@aws-sdk/credential-providers") - in bedrock custom loader +// 2. Plugin.list() which calls BunProc.install() for default plugins +// Without mocks, these would attempt real package installations that timeout in tests. + +mock.module("../../src/bun/index", () => ({ + BunProc: { + install: async (pkg: string) => pkg, + run: async () => { + throw new Error("BunProc.run should not be called in tests") + }, + which: () => process.execPath, + InstallFailedError: class extends Error {}, + }, +})) + +mock.module("@aws-sdk/credential-providers", () => ({ + fromNodeProviderChain: () => async () => ({ + accessKeyId: "mock-access-key-id", + secretAccessKey: "mock-secret-access-key", + }), +})) + +const mockPlugin = () => ({}) +mock.module("opencode-copilot-auth", () => ({ default: mockPlugin })) +mock.module("opencode-anthropic-auth", () => ({ default: mockPlugin })) + +// Import after mocks are set up +const { tmpdir } = await import("../fixture/fixture") +const { Instance } = await import("../../src/project/instance") +const { Provider } = await import("../../src/provider/provider") +const { Env } = await import("../../src/env") +const { Global } = await import("../../src/global") test("Bedrock: config region takes precedence over AWS_REGION env var", async () => { await using tmp = await tmpdir({ @@ -34,13 +63,12 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async () 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 () => { +test("Bedrock: falls back to AWS_REGION env var when no config region", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( @@ -65,34 +93,7 @@ test("Bedrock: falls back to AWS_REGION env var when no config", async () => { }) }) -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 () => { +test("Bedrock: loads when bearer token from auth.json is present", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( @@ -102,7 +103,7 @@ test("Bedrock: uses config region in provider options", async () => { provider: { "amazon-bedrock": { options: { - region: "eu-north-1", + region: "eu-west-1", }, }, }, @@ -110,54 +111,35 @@ test("Bedrock: uses config region in provider options", async () => { ) }, }) + + 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 () => { - Env.set("AWS_PROFILE", "default") + Env.set("AWS_PROFILE", "") + Env.set("AWS_ACCESS_KEY_ID", "") + Env.set("AWS_BEARER_TOKEN_BEDROCK", "") }, fn: async () => { const providers = await Provider.list() - const bedrockProvider = providers["amazon-bedrock"] - expect(bedrockProvider).toBeDefined() - expect(bedrockProvider.options?.region).toBe("eu-north-1") + expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers["amazon-bedrock"].options?.region).toBe("eu-west-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({ +test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async () => { + await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( path.join(dir, "opencode.json"), @@ -166,7 +148,8 @@ test("Bedrock: respects config region for different instances", async () => { provider: { "amazon-bedrock": { options: { - region: "us-west-2", + profile: "my-custom-profile", + region: "us-east-1", }, }, }, @@ -174,21 +157,21 @@ test("Bedrock: respects config region for different instances", async () => { ) }, }) - await Instance.provide({ - directory: tmp2.path, + directory: tmp.path, init: async () => { Env.set("AWS_PROFILE", "default") - Env.set("AWS_REGION", "eu-west-1") + Env.set("AWS_ACCESS_KEY_ID", "test-key-id") }, fn: async () => { - const providers2 = await Provider.list() - expect(providers2["amazon-bedrock"].options?.region).toBe("us-west-2") + const providers = await Provider.list() + expect(providers["amazon-bedrock"]).toBeDefined() + expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1") }, }) }) -test("Bedrock: loads when bearer token from auth.json is present", async () => { +test("Bedrock: includes custom endpoint in options when specified", async () => { await using tmp = await tmpdir({ init: async (dir) => { await Bun.write( @@ -198,7 +181,7 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => { provider: { "amazon-bedrock": { options: { - region: "eu-west-1", + endpoint: "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com", }, }, }, @@ -206,31 +189,17 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => { ) }, }) - - // 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", "") + 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") + expect(providers["amazon-bedrock"].options?.endpoint).toBe( + "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com", + ) }, }) }) diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index 24b822cc423..d9076e13a36 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -205,6 +205,41 @@ You can also configure [local models](/docs/models#local). [Learn more](/docs/mo --- +#### Provider-Specific Options + +Some providers support additional configuration options beyond the generic `timeout` and `apiKey` settings. + +##### Amazon Bedrock + +Amazon Bedrock supports AWS-specific configuration: + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "amazon-bedrock": { + "options": { + "region": "us-east-1", + "profile": "my-aws-profile", + "endpoint": "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com" + } + } + } +} +``` + +- `region` - AWS region for Bedrock (defaults to `AWS_REGION` env var or `us-east-1`) +- `profile` - AWS named profile from `~/.aws/credentials` (defaults to `AWS_PROFILE` env var) +- `endpoint` - Custom endpoint URL for VPC endpoints. This is an alias for the generic `baseURL` option using AWS-specific terminology. If both are specified, `endpoint` takes precedence. + +:::note +Bearer tokens (`AWS_BEARER_TOKEN_BEDROCK` or `/connect`) take precedence over profile-based authentication. See [authentication precedence](/docs/providers#authentication-precedence) for details. +::: + +[Learn more about Amazon Bedrock configuration](/docs/providers#amazon-bedrock). + +--- + ### Themes You can configure the theme you want to use in your OpenCode config through the `theme` option. diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index e59fbc81921..0e4539e122d 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -107,27 +107,96 @@ To use Amazon Bedrock with OpenCode: You need to have access to the model you want in Amazon Bedrock. ::: -1. You'll need either to set one of the following environment variables: - - `AWS_ACCESS_KEY_ID`: You can get this by creating an IAM user and generating - an access key for it. - - `AWS_PROFILE`: First login through AWS IAM Identity Center (or AWS SSO) using - `aws sso login`. Then get the name of the profile you want to use. - - `AWS_BEARER_TOKEN_BEDROCK`: You can generate a long-term API key from the - Amazon Bedrock console. +2. **Configure authentication** using one of the following methods: - Once you have one of the above, set it while running opencode. + #### Environment Variables (Quick Start) + + Set one of these environment variables while running opencode: ```bash - AWS_ACCESS_KEY_ID=XXX opencode + # Option 1: Using AWS access keys + AWS_ACCESS_KEY_ID=XXX AWS_SECRET_ACCESS_KEY=YYY opencode + + # Option 2: Using named AWS profile + AWS_PROFILE=my-profile opencode + + # Option 3: Using Bedrock bearer token + AWS_BEARER_TOKEN_BEDROCK=XXX opencode ``` - Or add it to your bash profile. + Or add them to your bash profile: ```bash title="~/.bash_profile" - export AWS_ACCESS_KEY_ID=XXX + export AWS_PROFILE=my-dev-profile + export AWS_REGION=us-east-1 + ``` + + #### Configuration File (Recommended) + + For project-specific or persistent configuration, use `opencode.json`: + + ```json title="opencode.json" + { + "$schema": "https://opencode.ai/config.json", + "provider": { + "amazon-bedrock": { + "options": { + "region": "us-east-1", + "profile": "my-aws-profile" + } + } + } + } ``` -1. Run the `/models` command to select the model you want. + **Available options:** + - `region` - AWS region (e.g., `us-east-1`, `eu-west-1`) + - `profile` - AWS named profile from `~/.aws/credentials` + - `endpoint` - Custom endpoint URL for VPC endpoints (alias for generic `baseURL` option) + + :::tip + Configuration file options take precedence over environment variables. + ::: + + #### Advanced: VPC Endpoints + + If you're using VPC endpoints for Bedrock: + + ```json title="opencode.json" + { + "$schema": "https://opencode.ai/config.json", + "provider": { + "amazon-bedrock": { + "options": { + "region": "us-east-1", + "profile": "production", + "endpoint": "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com" + } + } + } + } + ``` + + :::note + The `endpoint` option is an alias for the generic `baseURL` option, using AWS-specific terminology. If both `endpoint` and `baseURL` are specified, `endpoint` takes precedence. + ::: + + #### Authentication Methods + - **`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`**: Create an IAM user and generate access keys in the AWS Console + - **`AWS_PROFILE`**: Use named profiles from `~/.aws/credentials`. First configure with `aws configure --profile my-profile` or `aws sso login` + - **`AWS_BEARER_TOKEN_BEDROCK`**: Generate long-term API keys from the Amazon Bedrock console + + #### Authentication Precedence + + Amazon Bedrock uses the following authentication priority: + 1. **Bearer Token** - `AWS_BEARER_TOKEN_BEDROCK` environment variable or token from `/connect` command + 2. **AWS Credential Chain** - Profile, access keys, shared credentials, IAM roles, instance metadata + + :::note + When a bearer token is set (via `/connect` or `AWS_BEARER_TOKEN_BEDROCK`), it takes precedence over all AWS credential methods including configured profiles. + ::: + +3. Run the `/models` command to select the model you want. ```txt /models