Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/ai/src/api/bedrock-converse-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,14 @@ export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> =

const blocks = output.content as Block[];

// A profile explicitly configured through pi's auth flow (the `profile`
// option or scoped `AWS_PROFILE` on the stored credential's env) must win
// over ambient AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY. The SDK default
// chain already prefers a configured profile over env keys, but only when
// `credentials` is not set on the client config. See #6957.
const optionsProfile = options.profile || options.env?.AWS_PROFILE;
const config: BedrockRuntimeClientConfig = {
profile: options.profile || getProviderEnvValue("AWS_PROFILE", options.env),
profile: optionsProfile || getProviderEnvValue("AWS_PROFILE", options.env),
};
const configuredRegion = getConfiguredBedrockRegion(options);
const hasAmbientConfiguredProfile = Boolean(getProviderEnvValue("AWS_PROFILE"));
Expand Down Expand Up @@ -184,7 +190,7 @@ export const stream: StreamFunction<"bedrock-converse-stream", BedrockOptions> =
}

const credentials = getConfiguredBedrockCredentials(options.env);
if (!skipAuth && credentials) {
if (!skipAuth && credentials && !optionsProfile) {
config.credentials = credentials;
}

Expand Down
115 changes: 115 additions & 0 deletions packages/ai/test/bedrock-credentials.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { afterEach, describe, expect, it, vi } from "vitest";

const bedrockMock = vi.hoisted(() => ({
constructorCalls: [] as Array<Record<string, unknown>>,
}));

vi.mock("@aws-sdk/client-bedrock-runtime", () => {
class BedrockRuntimeServiceException extends Error {}

class BedrockRuntimeClient {
constructor(config: Record<string, unknown>) {
bedrockMock.constructorCalls.push(config);
}

send(): Promise<never> {
return Promise.reject(new Error("mock send"));
}
}

class ConverseStreamCommand {
readonly input: unknown;

constructor(input: unknown) {
this.input = input;
}
}

return {
BedrockRuntimeClient,
BedrockRuntimeServiceException,
ConverseStreamCommand,
StopReason: {
END_TURN: "end_turn",
STOP_SEQUENCE: "stop_sequence",
MAX_TOKENS: "max_tokens",
MODEL_CONTEXT_WINDOW_EXCEEDED: "model_context_window_exceeded",
TOOL_USE: "tool_use",
},
CachePointType: { DEFAULT: "default" },
CacheTTL: { ONE_HOUR: "ONE_HOUR" },
ConversationRole: { ASSISTANT: "assistant", USER: "user" },
ImageFormat: { JPEG: "jpeg", PNG: "png", GIF: "gif", WEBP: "webp" },
ToolResultStatus: { ERROR: "error", SUCCESS: "success" },
};
});

import type { BedrockOptions } from "../src/api/bedrock-converse-stream.ts";
import { stream as streamBedrock } from "../src/compat.ts";
import { getBuiltinModel } from "../src/providers/all.ts";
import type { Context, Model } from "../src/types.ts";

const context: Context = {
messages: [{ role: "user", content: "hello", timestamp: Date.now() }],
};

async function captureClientConfig(
model: Model<"bedrock-converse-stream">,
options: BedrockOptions = {},
): Promise<Record<string, unknown>> {
bedrockMock.constructorCalls.length = 0;
await streamBedrock(model, context, { cacheRetention: "none", ...options }).result();
expect(bedrockMock.constructorCalls).toHaveLength(1);
return bedrockMock.constructorCalls[0];
}

describe("bedrock credential priority", () => {
afterEach(() => {
vi.unstubAllEnvs();
});

it("prefers explicit and scoped profiles over ambient AWS access keys", async () => {
vi.stubEnv("AWS_ACCESS_KEY_ID", "AKIAEXAMPLE");
vi.stubEnv("AWS_SECRET_ACCESS_KEY", "secretexample");
const model = getBuiltinModel("amazon-bedrock", "us.anthropic.claude-opus-4-8");

let config = await captureClientConfig(model, { profile: "explicit-profile" });

expect(config.profile).toBe("explicit-profile");
expect(config.credentials).toBeUndefined();

config = await captureClientConfig(model, { env: { AWS_PROFILE: "scoped-profile" } });

expect(config.profile).toBe("scoped-profile");
expect(config.credentials).toBeUndefined();
});

it("uses ambient AWS access keys when no profile is configured", async () => {
vi.stubEnv("AWS_ACCESS_KEY_ID", "AKIAEXAMPLE");
vi.stubEnv("AWS_SECRET_ACCESS_KEY", "secretexample");
const model = getBuiltinModel("amazon-bedrock", "us.anthropic.claude-opus-4-8");

const config = await captureClientConfig(model);

expect(config.profile).toBeUndefined();
expect(config.credentials).toEqual({
accessKeyId: "AKIAEXAMPLE",
secretAccessKey: "secretexample",
});
});

it("uses ambient AWS access keys when only an ambient profile is set", async () => {
vi.stubEnv("AWS_ACCESS_KEY_ID", "AKIAEXAMPLE");
vi.stubEnv("AWS_SECRET_ACCESS_KEY", "secretexample");
vi.stubEnv("AWS_PROFILE", "ambient-profile");
const model = getBuiltinModel("amazon-bedrock", "us.anthropic.claude-opus-4-8");

const config = await captureClientConfig(model);

expect(config.profile).toBe("ambient-profile");
expect(config.credentials).toEqual({
accessKeyId: "AKIAEXAMPLE",
secretAccessKey: "secretexample",
});
});
});