feat(bedrock): config options and authentication precedence (#6377)

This commit is contained in:
Grégoire Morpain
2026-01-05 19:51:43 +01:00
committed by GitHub
parent 6b207b09d6
commit e3b4d4ad49
6 changed files with 258 additions and 157 deletions

View File

@@ -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

View File

@@ -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<string, any>) {
// Skip region prefixing if model already has global prefix
if (modelID.startsWith("global.")) {

View File

@@ -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"]

View File

@@ -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,129 +93,6 @@ 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 () => {
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) => {
@@ -207,7 +112,6 @@ 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,
@@ -222,7 +126,6 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => {
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", "")
@@ -234,3 +137,69 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => {
},
})
})
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"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"amazon-bedrock": {
options: {
profile: "my-custom-profile",
region: "us-east-1",
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("AWS_PROFILE", "default")
Env.set("AWS_ACCESS_KEY_ID", "test-key-id")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["amazon-bedrock"]).toBeDefined()
expect(providers["amazon-bedrock"].options?.region).toBe("us-east-1")
},
})
})
test("Bedrock: includes custom endpoint in options when specified", 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: {
endpoint: "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
init: async () => {
Env.set("AWS_PROFILE", "default")
},
fn: async () => {
const providers = await Provider.list()
expect(providers["amazon-bedrock"]).toBeDefined()
expect(providers["amazon-bedrock"].options?.endpoint).toBe(
"https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
)
},
})
})