diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 5bb1c42f..19dd4f22 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Amazon Bedrock requests to use the generic `apiKey` stream option as a Bedrock bearer token. + ## [0.80.6] - 2026-07-09 ### Added diff --git a/packages/ai/src/api/bedrock-converse-stream.ts b/packages/ai/src/api/bedrock-converse-stream.ts index 33494a9e..7a9e776e 100644 --- a/packages/ai/src/api/bedrock-converse-stream.ts +++ b/packages/ai/src/api/bedrock-converse-stream.ts @@ -152,7 +152,10 @@ export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> = // Resolve bearer token for Bedrock API key auth. const skipAuth = getProviderEnvValue("AWS_BEDROCK_SKIP_AUTH", options.env) === "1"; const bearerToken = - options.bearerToken || getProviderEnvValue("AWS_BEARER_TOKEN_BEDROCK", options.env) || undefined; + options.bearerToken || + options.apiKey || + getProviderEnvValue("AWS_BEARER_TOKEN_BEDROCK", options.env) || + undefined; const useBearerToken = bearerToken !== undefined && !skipAuth; // in Node.js/Bun environment only diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index d839ab6a..9d2e5918 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -9,7 +9,11 @@ import { AMAZON_BEDROCK_MODELS } from "./amazon-bedrock.models.ts"; * configured. A stored credential key is surfaced as the bearer token. */ const bedrockAuth: ApiKeyAuth = { - name: "AWS credentials", + name: "Bedrock API key or AWS credentials", + login: async (callbacks) => ({ + type: "api_key", + key: await callbacks.prompt({ type: "secret", message: "Enter Bedrock API key" }), + }), resolve: async ({ ctx, credential }) => { if (credential?.key) return { auth: { apiKey: credential.key }, source: "stored credential" }; if (await ctx.env("AWS_BEARER_TOKEN_BEDROCK")) return { auth: {}, source: "AWS_BEARER_TOKEN_BEDROCK" }; diff --git a/packages/ai/test/bedrock-endpoint-resolution.test.ts b/packages/ai/test/bedrock-endpoint-resolution.test.ts index 168cf4d1..0fef3662 100644 --- a/packages/ai/test/bedrock-endpoint-resolution.test.ts +++ b/packages/ai/test/bedrock-endpoint-resolution.test.ts @@ -181,4 +181,13 @@ describe("bedrock endpoint resolution", () => { expect(config.region).toBe("us-gov-west-1"); }); + + it("uses the generic API key option as a Bedrock bearer token", async () => { + const model = getModel("amazon-bedrock", "us.anthropic.claude-opus-4-8"); + + const config = await captureClientConfig(model, { apiKey: "bedrock-api-key" }); + + expect(config.token).toEqual({ token: "bedrock-api-key" }); + expect(config.authSchemePreference).toEqual(["httpBearerAuth"]); + }); }); diff --git a/packages/ai/test/providers.test.ts b/packages/ai/test/providers.test.ts index 0f6e25bd..a22c8abd 100644 --- a/packages/ai/test/providers.test.ts +++ b/packages/ai/test/providers.test.ts @@ -54,6 +54,19 @@ describe("builtin providers", () => { expect(result?.source).toBe("ANTHROPIC_OAUTH_TOKEN"); }); + it("prompts for and stores a Bedrock API key", async () => { + const provider = amazonBedrockProvider(); + const credential = await provider.auth.apiKey?.login?.({ + prompt: async (prompt) => { + expect(prompt).toEqual({ type: "secret", message: "Enter Bedrock API key" }); + return "bedrock-api-key"; + }, + notify: () => {}, + }); + + expect(credential).toEqual({ type: "api_key", key: "bedrock-api-key" }); + }); + it("reports bedrock as configured from ambient AWS credentials without an api key", async () => { const models = createModels({ authContext: fakeAuthContext({ AWS_PROFILE: "dev" }) }); models.setProvider(amazonBedrockProvider()); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 702f262a..933c6f5c 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `/login amazon-bedrock` to prompt for and save a Bedrock API key instead of only displaying ambient AWS credential setup instructions. + ## [0.80.6] - 2026-07-09 ### New Features diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 46163abf..74d228a0 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -55,6 +55,7 @@ pi | DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` | | NVIDIA NIM | `NVIDIA_API_KEY` | `nvidia` | | Google Gemini | `GEMINI_API_KEY` | `google` | +| Amazon Bedrock | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` | | Mistral | `MISTRAL_API_KEY` | `mistral` | | Groq | `GROQ_API_KEY` | `groq` | | Cerebras | `CEREBRAS_API_KEY` | `cerebras` | @@ -170,6 +171,8 @@ export AZURE_OPENAI_DEPLOYMENT_NAME_MAP=gpt-4=my-gpt4,gpt-4o=my-gpt4o ### Amazon Bedrock +Use `/login amazon-bedrock` to store a Bedrock API key, or configure one of the ambient AWS credential sources below: + ```bash # Option 1: AWS Profile export AWS_PROFILE=your-profile diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index 3fc4b516..44ae7498 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -177,14 +177,22 @@ export class LoginDialogComponent extends Container implements Focusable { } /** - * Show informational text without prompting for input. + * Show informational text before another login step. */ - showInfo(lines: string[]): void { + showDetails(lines: string[]): void { this.contentContainer.clear(); this.contentContainer.addChild(new Spacer(1)); for (const line of lines) { this.contentContainer.addChild(new Text(line, 1, 0)); } + this.tui.requestRender(); + } + + /** + * Show informational text without prompting for input. + */ + showInfo(lines: string[]): void { + this.showDetails(lines); this.contentContainer.addChild(new Spacer(1)); this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to close")})`, 1, 0)); this.tui.requestRender(); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 8f492a18..605bfbf1 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -248,8 +248,6 @@ function hasDefaultModelProvider(providerId: string): providerId is keyof typeof return providerId in defaultModelPerProvider; } -const BEDROCK_PROVIDER_ID = "amazon-bedrock"; - const BUILT_IN_MODEL_PROVIDERS = new Set(getProviders()); export function isApiKeyLoginProvider( @@ -4873,8 +4871,6 @@ export class InteractiveMode { private async startProviderLogin(providerOption: AuthSelectorProvider): Promise { if (providerOption.authType === "oauth") { await this.showLoginDialog(providerOption.id, providerOption.name); - } else if (providerOption.id === BEDROCK_PROVIDER_ID) { - this.showBedrockSetupDialog(providerOption.id, providerOption.name); } else { await this.showApiKeyLoginDialog(providerOption.id, providerOption.name); } @@ -5082,34 +5078,6 @@ export class InteractiveMode { } } - private showBedrockSetupDialog(providerId: string, providerName: string): void { - const restoreEditor = () => { - this.editorContainer.clear(); - this.editorContainer.addChild(this.editor); - this.ui.setFocus(this.editor); - this.ui.requestRender(); - }; - - const dialog = new LoginDialogComponent( - this.ui, - providerId, - () => restoreEditor(), - providerName, - "Amazon Bedrock setup", - ); - dialog.showInfo([ - theme.fg("text", "Amazon Bedrock uses AWS credentials instead of a single API key."), - theme.fg("text", "Configure an AWS profile, IAM keys, bearer token, or role-based credentials."), - theme.fg("muted", "See:"), - theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`), - ]); - - this.editorContainer.clear(); - this.editorContainer.addChild(dialog); - this.ui.setFocus(dialog); - this.ui.requestRender(); - } - private async showApiKeyLoginDialog(providerId: string, providerName: string): Promise { const previousModel = this.session.model; @@ -5122,6 +5090,14 @@ export class InteractiveMode { providerName, ); + if (providerId === "amazon-bedrock") { + dialog.showDetails([ + theme.fg("text", "You can also use an AWS profile, IAM keys, or role-based credentials."), + theme.fg("muted", "See:"), + theme.fg("accent", ` ${path.join(getDocsPath(), "providers.md")}`), + ]); + } + this.editorContainer.clear(); this.editorContainer.addChild(dialog); this.ui.setFocus(dialog); diff --git a/packages/coding-agent/test/suite/regressions/5433-extension-oauth-prompt-input.test.ts b/packages/coding-agent/test/suite/regressions/5433-extension-oauth-prompt-input.test.ts index 06562b3d..54dcdd3b 100644 --- a/packages/coding-agent/test/suite/regressions/5433-extension-oauth-prompt-input.test.ts +++ b/packages/coding-agent/test/suite/regressions/5433-extension-oauth-prompt-input.test.ts @@ -70,6 +70,18 @@ describe("LoginDialogComponent OAuth prompts", () => { expect(output).toContain("First prompt:"); }); + test("preserves setup details when showing a prompt", () => { + const dialog = createDialog(); + + dialog.showDetails(["AWS credential setup:", "providers.md"]); + dialog.showPrompt("Enter API key:"); + + const output = renderDialog(dialog).join("\n"); + expect(output).toContain("AWS credential setup:"); + expect(output).toContain("providers.md"); + expect(output).toContain("Enter API key:"); + }); + test("keeps previous manual input stable when a later prompt is active", async () => { const dialog = createDialog();