fix: support Bedrock API key login
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed Amazon Bedrock requests to use the generic `apiKey` stream option as a Bedrock bearer token.
|
||||||
|
|
||||||
## [0.80.6] - 2026-07-09
|
## [0.80.6] - 2026-07-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -152,7 +152,10 @@ export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> =
|
|||||||
// Resolve bearer token for Bedrock API key auth.
|
// Resolve bearer token for Bedrock API key auth.
|
||||||
const skipAuth = getProviderEnvValue("AWS_BEDROCK_SKIP_AUTH", options.env) === "1";
|
const skipAuth = getProviderEnvValue("AWS_BEDROCK_SKIP_AUTH", options.env) === "1";
|
||||||
const bearerToken =
|
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;
|
const useBearerToken = bearerToken !== undefined && !skipAuth;
|
||||||
|
|
||||||
// in Node.js/Bun environment only
|
// in Node.js/Bun environment only
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import { AMAZON_BEDROCK_MODELS } from "./amazon-bedrock.models.ts";
|
|||||||
* configured. A stored credential key is surfaced as the bearer token.
|
* configured. A stored credential key is surfaced as the bearer token.
|
||||||
*/
|
*/
|
||||||
const bedrockAuth: ApiKeyAuth = {
|
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 }) => {
|
resolve: async ({ ctx, credential }) => {
|
||||||
if (credential?.key) return { auth: { apiKey: credential.key }, source: "stored 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" };
|
if (await ctx.env("AWS_BEARER_TOKEN_BEDROCK")) return { auth: {}, source: "AWS_BEARER_TOKEN_BEDROCK" };
|
||||||
|
|||||||
@@ -181,4 +181,13 @@ describe("bedrock endpoint resolution", () => {
|
|||||||
|
|
||||||
expect(config.region).toBe("us-gov-west-1");
|
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"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,19 @@ describe("builtin providers", () => {
|
|||||||
expect(result?.source).toBe("ANTHROPIC_OAUTH_TOKEN");
|
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 () => {
|
it("reports bedrock as configured from ambient AWS credentials without an api key", async () => {
|
||||||
const models = createModels({ authContext: fakeAuthContext({ AWS_PROFILE: "dev" }) });
|
const models = createModels({ authContext: fakeAuthContext({ AWS_PROFILE: "dev" }) });
|
||||||
models.setProvider(amazonBedrockProvider());
|
models.setProvider(amazonBedrockProvider());
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.80.6] - 2026-07-09
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pi
|
|||||||
| DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` |
|
| DeepSeek | `DEEPSEEK_API_KEY` | `deepseek` |
|
||||||
| NVIDIA NIM | `NVIDIA_API_KEY` | `nvidia` |
|
| NVIDIA NIM | `NVIDIA_API_KEY` | `nvidia` |
|
||||||
| Google Gemini | `GEMINI_API_KEY` | `google` |
|
| Google Gemini | `GEMINI_API_KEY` | `google` |
|
||||||
|
| Amazon Bedrock | `AWS_BEARER_TOKEN_BEDROCK` | `amazon-bedrock` |
|
||||||
| Mistral | `MISTRAL_API_KEY` | `mistral` |
|
| Mistral | `MISTRAL_API_KEY` | `mistral` |
|
||||||
| Groq | `GROQ_API_KEY` | `groq` |
|
| Groq | `GROQ_API_KEY` | `groq` |
|
||||||
| Cerebras | `CEREBRAS_API_KEY` | `cerebras` |
|
| 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
|
### Amazon Bedrock
|
||||||
|
|
||||||
|
Use `/login amazon-bedrock` to store a Bedrock API key, or configure one of the ambient AWS credential sources below:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Option 1: AWS Profile
|
# Option 1: AWS Profile
|
||||||
export AWS_PROFILE=your-profile
|
export AWS_PROFILE=your-profile
|
||||||
|
|||||||
@@ -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.clear();
|
||||||
this.contentContainer.addChild(new Spacer(1));
|
this.contentContainer.addChild(new Spacer(1));
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
this.contentContainer.addChild(new Text(line, 1, 0));
|
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 Spacer(1));
|
||||||
this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to close")})`, 1, 0));
|
this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to close")})`, 1, 0));
|
||||||
this.tui.requestRender();
|
this.tui.requestRender();
|
||||||
|
|||||||
@@ -248,8 +248,6 @@ function hasDefaultModelProvider(providerId: string): providerId is keyof typeof
|
|||||||
return providerId in defaultModelPerProvider;
|
return providerId in defaultModelPerProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BEDROCK_PROVIDER_ID = "amazon-bedrock";
|
|
||||||
|
|
||||||
const BUILT_IN_MODEL_PROVIDERS = new Set<string>(getProviders());
|
const BUILT_IN_MODEL_PROVIDERS = new Set<string>(getProviders());
|
||||||
|
|
||||||
export function isApiKeyLoginProvider(
|
export function isApiKeyLoginProvider(
|
||||||
@@ -4873,8 +4871,6 @@ export class InteractiveMode {
|
|||||||
private async startProviderLogin(providerOption: AuthSelectorProvider): Promise<void> {
|
private async startProviderLogin(providerOption: AuthSelectorProvider): Promise<void> {
|
||||||
if (providerOption.authType === "oauth") {
|
if (providerOption.authType === "oauth") {
|
||||||
await this.showLoginDialog(providerOption.id, providerOption.name);
|
await this.showLoginDialog(providerOption.id, providerOption.name);
|
||||||
} else if (providerOption.id === BEDROCK_PROVIDER_ID) {
|
|
||||||
this.showBedrockSetupDialog(providerOption.id, providerOption.name);
|
|
||||||
} else {
|
} else {
|
||||||
await this.showApiKeyLoginDialog(providerOption.id, providerOption.name);
|
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<void> {
|
private async showApiKeyLoginDialog(providerId: string, providerName: string): Promise<void> {
|
||||||
const previousModel = this.session.model;
|
const previousModel = this.session.model;
|
||||||
|
|
||||||
@@ -5122,6 +5090,14 @@ export class InteractiveMode {
|
|||||||
providerName,
|
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.clear();
|
||||||
this.editorContainer.addChild(dialog);
|
this.editorContainer.addChild(dialog);
|
||||||
this.ui.setFocus(dialog);
|
this.ui.setFocus(dialog);
|
||||||
|
|||||||
+12
@@ -70,6 +70,18 @@ describe("LoginDialogComponent OAuth prompts", () => {
|
|||||||
expect(output).toContain("First prompt:");
|
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 () => {
|
test("keeps previous manual input stable when a later prompt is active", async () => {
|
||||||
const dialog = createDialog();
|
const dialog = createDialog();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user