From 873df0ffbee564d8df12304e316871ce7380b246 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 7 Oct 2026 11:09:06 -0500 Subject: [PATCH] feat(core): add AWS profile setup for Bedrock --- .../ai/src/protocols/utils/bedrock-auth.ts | 3 +- packages/core/src/model-resolver.ts | 5 +- .../src/plugin/provider/amazon-bedrock.ts | 57 +++++++++++++++++++ .../plugin/provider-amazon-bedrock.test.ts | 3 +- 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/ai/src/protocols/utils/bedrock-auth.ts b/packages/ai/src/protocols/utils/bedrock-auth.ts index 7be6c260063b..54cd33939cb0 100644 --- a/packages/ai/src/protocols/utils/bedrock-auth.ts +++ b/packages/ai/src/protocols/utils/bedrock-auth.ts @@ -37,7 +37,8 @@ export const defaultChain = (options: DefaultChainOptions): Effect.Effect { const { fromNodeProviderChain } = await import("@aws-sdk/credential-providers") - const identity = await fromNodeProviderChain(options.profile === undefined ? {} : { profile: options.profile })() + // ignoreCache re-reads shared config and SSO token files, so `aws sso login` takes effect without a restart. + const identity = await fromNodeProviderChain({ ignoreCache: true, profile: options.profile })() return { region: options.region, accessKeyId: identity.accessKeyId, diff --git a/packages/core/src/model-resolver.ts b/packages/core/src/model-resolver.ts index 715bbdd45f82..ea8665a49bbf 100644 --- a/packages/core/src/model-resolver.ts +++ b/packages/core/src/model-resolver.ts @@ -300,7 +300,10 @@ function unresolvedProviderVariables(model: RuntimeInfo, baseURL: string) { } const nativeCredentialSettings = (specifier: string, credential: Credential.Value | undefined) => { - if (!credential || credential.type === "external") return {} + if (!credential) return {} + // The saved profile reaches the package through metadata; SigV4 keeps an ambient bearer token from taking over. + if (credential.type === "external") + return specifier.startsWith("@opencode/ai/providers/amazon-bedrock") ? { auth: "sigv4" } : {} if (credential.type === "key") return { apiKey: credential.key } if (specifier === "@opencode/ai/providers/anthropic" || specifier === "@opencode/ai/providers/anthropic-compatible") return { authToken: credential.access } diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index a8cb75e5c200..e7838b4402d9 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -1,5 +1,8 @@ import { Effect } from "effect" +import path from "node:path" import { define } from "@opencode/plugin/effect/plugin" +import { FSUtil } from "@opencode/util/fs-util" +import { Global } from "@opencode/util/global" import { Provider } from "../../provider.js" // Ambient inputs the AWS default credential chain can turn into credentials @@ -19,6 +22,32 @@ const isBedrock = (item: { readonly package: string }) => export const AmazonBedrockPlugin = define({ id: "opencode.provider.amazon.bedrock", effect: Effect.fn(function* (ctx) { + const fs = yield* FSUtil.Service + const paths = [ + process.env.AWS_CONFIG_FILE ?? path.join(Global.Path.home, ".aws", "config"), + process.env.AWS_SHARED_CREDENTIALS_FILE ?? path.join(Global.Path.home, ".aws", "credentials"), + ] + const files = yield* Effect.all( + paths.map((file) => fs.readFileStringSafe(file).pipe(Effect.orElseSucceed(() => undefined))), + ) + // Discover names only. Resolving every profile here could run credential helpers or contact AWS. + const profiles = Array.from( + new Set( + files.flatMap((content, index) => + Array.from((content ?? "").matchAll(/^\s*\[([^\]\r\n]+)\]/gm)).flatMap((match) => { + const section = match[1].trim() + // The credentials file uses bare names; the config file prefixes all but "default" with "profile ". + if (index === 1 || section === "default") return [section] + return section.startsWith("profile ") ? [section.slice(8).trim()] : [] + }), + ), + ), + ) + .filter(Boolean) + .toSorted() + const sources = paths + .map((file) => (file.startsWith(Global.Path.home + path.sep) ? `~${file.slice(Global.Path.home.length)}` : file)) + .join(" and ") 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; @@ -27,6 +56,34 @@ export const AmazonBedrockPlugin = define({ integrationID: Provider.ID.amazonBedrock, method: { type: "env", names: ["AWS_BEARER_TOKEN_BEDROCK"] }, }) + editor.method.update({ + integrationID: Provider.ID.amazonBedrock, + method: { type: "key", label: "Bedrock API key" }, + }) + editor.method.update({ + integrationID: Provider.ID.amazonBedrock, + method: { + id: "aws-profile", + type: "external", + label: "AWS profile (SSO or named profile)", + form: [ + { + key: "profile", + type: "string", + title: "AWS profile", + description: profiles.length + ? `Found ${profiles.length} profile${profiles.length === 1 ? "" : "s"} in ${sources} on the server.` + : `No AWS profiles found in ${sources} on the server.`, + required: true, + minLength: 1, + pattern: "\\S", + placeholder: "Profile name", + custom: true, + options: profiles.map((profile) => ({ value: profile, label: profile })), + }, + ], + }, + }) }) yield* ctx.provider.transform((evt) => { for (const item of evt.list()) { diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index f3624e8a674d..3575a131dc1c 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -104,8 +104,7 @@ describe("AmazonBedrockPlugin", () => { }) }) yield* addPlugin() - expect((yield* integrations.get(integrationID))?.methods).toEqual([ - { type: "key" }, + expect((yield* integrations.get(integrationID))?.methods.filter((method) => method.type === "env")).toEqual([ { type: "env", names: ["AWS_BEARER_TOKEN_BEDROCK"] }, ]) }),