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
27 changes: 27 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,35 @@ ANTHROPIC_API_KEY=user_provided
#=================#
# AWS Bedrock #
#=================#
# AWS Bedrock credentials
#
# Preferred for local development: configure an AWS profile in ~/.aws/config or
# ~/.aws/credentials, then set BEDROCK_AWS_PROFILE. LibreChat passes this profile
# to the AWS SDK for JavaScript credential provider chain.
#
# In deployed environments, prefer IAM roles or other short-term credentials
# discoverable by the AWS SDK default credential provider chain. If neither
# BEDROCK_AWS_PROFILE nor Bedrock-specific static credentials are set, the SDK
# uses its default provider chain. AWS-standard environment variables still
# follow AWS SDK precedence.
#
# Profiles can use IAM Identity Center, assume-role settings, or credential_process.
# If you use credential_process, secure the config file and helper command, and do
# not write secret material to stderr.
#
# AWS SDK credential chain:
# https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html
# Shared config/profile settings:
# https://docs.aws.amazon.com/sdkref/latest/guide/settings-reference.html
# credential_process security notes:
# https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html

# BEDROCK_AWS_DEFAULT_REGION=us-east-1 # A default region must be provided

# AWS Profile
# BEDROCK_AWS_PROFILE=your-profile-name

# Static credentials (use only if profiles or IAM roles are not suitable)
# BEDROCK_AWS_ACCESS_KEY_ID=someAccessKey
# BEDROCK_AWS_SECRET_ACCESS_KEY=someSecretAccessKey
# BEDROCK_AWS_SESSION_TOKEN=someSessionToken
Expand Down
93 changes: 72 additions & 21 deletions packages/api/src/endpoints/bedrock/initialize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
BEDROCK_OUTPUT_128K_BETA,
BEDROCK_FINE_GRAINED_TOOL_STREAMING_BETA,
} from 'librechat-data-provider';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { initializeBedrock } from './initialize';
import type { BaseInitializeParams, BedrockLLMConfigResult } from '~/types';
import { checkUserKeyExpiry } from '~/utils';
Expand All @@ -16,6 +17,13 @@ jest.mock('@smithy/node-http-handler', () => ({
NodeHttpHandler: jest.fn().mockImplementation((options) => ({ ...options })),
}));

jest.mock('@aws-sdk/credential-providers', () => ({
fromNodeProviderChain: jest.fn().mockImplementation((config) => {
const provider = jest.fn();
return Object.assign(provider, { config });
}),
}));

jest.mock('@aws-sdk/client-bedrock-runtime', () => ({
BedrockRuntimeClient: jest.fn().mockImplementation((config) => ({
...config,
Expand All @@ -28,6 +36,7 @@ jest.mock('~/utils', () => ({
}));

const mockedCheckUserKeyExpiry = jest.mocked(checkUserKeyExpiry);
const mockedFromNodeProviderChain = jest.mocked(fromNodeProviderChain);
const BEDROCK_CLAUDE_4_BETAS = [BEDROCK_OUTPUT_128K_BETA, BEDROCK_FINE_GRAINED_TOOL_STREAMING_BETA];

const createMockParams = (
Expand Down Expand Up @@ -121,6 +130,19 @@ describe('initializeBedrock', () => {
sessionToken: 'test-session-token',
});
});

it('should pass AWS profile to ChatBedrockConverse when static credentials are unset', async () => {
delete process.env.BEDROCK_AWS_ACCESS_KEY_ID;
delete process.env.BEDROCK_AWS_SECRET_ACCESS_KEY;
process.env.BEDROCK_AWS_PROFILE = 'dev-profile';
const params = createMockParams();
const result = await initializeBedrock(params);

expect(result.llmConfig).toHaveProperty('profile', 'dev-profile');
expect(result.llmConfig).not.toHaveProperty('credentials');
expect(result.llmConfig).not.toHaveProperty('client');
expect(mockedFromNodeProviderChain).not.toHaveBeenCalled();
});
});

describe('GuardrailConfig', () => {
Expand Down Expand Up @@ -301,31 +323,34 @@ describe('initializeBedrock', () => {
trace: 'enabled_full',
},
},
])('should resolve environment variables: $description', async ({ envVars, deleteEnvVars, input, expected }) => {
// Set up environment variables
Object.entries(envVars).forEach(([key, value]) => {
process.env[key] = value;
});

// Delete specified environment variables
deleteEnvVars?.forEach((key) => {
delete process.env[key];
});

const params = createMockParams({
config: {
endpoints: {
[EModelEndpoint.bedrock]: {
guardrailConfig: input,
])(
'should resolve environment variables: $description',
async ({ envVars, deleteEnvVars, input, expected }) => {
// Set up environment variables
Object.entries(envVars).forEach(([key, value]) => {
process.env[key] = value;
});

// Delete specified environment variables
deleteEnvVars?.forEach((key) => {
delete process.env[key];
});

const params = createMockParams({
config: {
endpoints: {
[EModelEndpoint.bedrock]: {
guardrailConfig: input,
},
},
},
},
});
});

const result = (await initializeBedrock(params)) as BedrockLLMConfigResult;
const result = (await initializeBedrock(params)) as BedrockLLMConfigResult;

expect(result.llmConfig.guardrailConfig).toEqual(expected);
});
expect(result.llmConfig.guardrailConfig).toEqual(expected);
},
);
});

describe('Proxy Configuration', () => {
Expand All @@ -351,6 +376,23 @@ describe('initializeBedrock', () => {
'https://custom-bedrock-endpoint.com',
);
});

it('should use AWS profile provider when PROXY is set and static credentials are unset', async () => {
delete process.env.BEDROCK_AWS_ACCESS_KEY_ID;
delete process.env.BEDROCK_AWS_SECRET_ACCESS_KEY;
process.env.BEDROCK_AWS_PROFILE = 'dev-profile';
process.env.PROXY = 'http://proxy:8080';
const params = createMockParams();
const result = (await initializeBedrock(params)) as BedrockLLMConfigResult;

expect(mockedFromNodeProviderChain).toHaveBeenCalledWith({ profile: 'dev-profile' });
expect(result.llmConfig).toHaveProperty('client');
expect(result.llmConfig).not.toHaveProperty('credentials');

const client = result.llmConfig.client as unknown as Record<string, unknown>;
const credentials = client.credentials as { config?: Record<string, string> };
expect(credentials.config).toEqual({ profile: 'dev-profile' });
});
});

describe('Reverse Proxy Configuration', () => {
Expand Down Expand Up @@ -415,6 +457,15 @@ describe('initializeBedrock', () => {
expect(result.llmConfig.credentials).toBeUndefined();
});

it('should throw when only one static credential value is set', async () => {
delete process.env.BEDROCK_AWS_SECRET_ACCESS_KEY;
const params = createMockParams();

await expect(initializeBedrock(params)).rejects.toThrow(
'Both BEDROCK_AWS_ACCESS_KEY_ID and BEDROCK_AWS_SECRET_ACCESS_KEY must be provided together.',
);
});

it('should throw error when user-provided credentials are not found', async () => {
process.env.BEDROCK_AWS_SECRET_ACCESS_KEY = AuthType.USER_PROVIDED;
const params = createMockParams();
Expand Down
63 changes: 41 additions & 22 deletions packages/api/src/endpoints/bedrock/initialize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { HttpsProxyAgent } from 'https-proxy-agent';
import { NodeHttpHandler } from '@smithy/node-http-handler';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime';
import {
AuthType,
Expand Down Expand Up @@ -61,6 +62,7 @@ export async function initializeBedrock({
BEDROCK_AWS_SECRET_ACCESS_KEY,
BEDROCK_AWS_ACCESS_KEY_ID,
BEDROCK_AWS_SESSION_TOKEN,
BEDROCK_AWS_PROFILE,
BEDROCK_REVERSE_PROXY,
BEDROCK_AWS_DEFAULT_REGION,
PROXY,
Expand All @@ -69,30 +71,37 @@ export async function initializeBedrock({
const { key: expiresAt } = req.body;
const isUserProvided = BEDROCK_AWS_SECRET_ACCESS_KEY === AuthType.USER_PROVIDED;

let credentials: BedrockCredentials | undefined = isUserProvided
? await db
.getUserKey({ userId: req.user?.id ?? '', name: EModelEndpoint.bedrock })
.then((key) => JSON.parse(key) as BedrockCredentials)
: {
accessKeyId: BEDROCK_AWS_ACCESS_KEY_ID,
secretAccessKey: BEDROCK_AWS_SECRET_ACCESS_KEY,
...(BEDROCK_AWS_SESSION_TOKEN && { sessionToken: BEDROCK_AWS_SESSION_TOKEN }),
};

if (!credentials) {
throw new Error('Bedrock credentials not provided. Please provide them again.');
}
const hasAccessKey = BEDROCK_AWS_ACCESS_KEY_ID != null && BEDROCK_AWS_ACCESS_KEY_ID !== '';
const hasSecretKey =
BEDROCK_AWS_SECRET_ACCESS_KEY != null && BEDROCK_AWS_SECRET_ACCESS_KEY !== '';

if (
!isUserProvided &&
(credentials.accessKeyId === undefined || credentials.accessKeyId === '') &&
(credentials.secretAccessKey === undefined || credentials.secretAccessKey === '')
) {
credentials = undefined;
}
let credentials: BedrockCredentials | undefined;

if (isUserProvided) {
const userKey = await db.getUserKey({
userId: req.user?.id ?? '',
name: EModelEndpoint.bedrock,
});

if (!userKey) {
throw new Error('Bedrock credentials not provided. Please provide them again.');
}

if (expiresAt && isUserProvided) {
checkUserKeyExpiry(expiresAt, EModelEndpoint.bedrock);
credentials = JSON.parse(userKey) as BedrockCredentials;

if (expiresAt) {
checkUserKeyExpiry(expiresAt, EModelEndpoint.bedrock);
}
} else if (hasAccessKey !== hasSecretKey) {
throw new Error(
'Both BEDROCK_AWS_ACCESS_KEY_ID and BEDROCK_AWS_SECRET_ACCESS_KEY must be provided together.',
);
} else if (hasAccessKey && hasSecretKey) {
credentials = {
accessKeyId: BEDROCK_AWS_ACCESS_KEY_ID,
secretAccessKey: BEDROCK_AWS_SECRET_ACCESS_KEY,
...(BEDROCK_AWS_SESSION_TOKEN && { sessionToken: BEDROCK_AWS_SESSION_TOKEN }),
};
}

const requestOptions: Record<string, unknown> = {
Expand All @@ -115,6 +124,7 @@ export async function initializeBedrock({
client?: BedrockRuntimeClient;
credentials?: BedrockCredentials;
endpointHost?: string;
profile?: string;
guardrailConfig?: GuardrailConfiguration;
applicationInferenceProfile?: string;
};
Expand Down Expand Up @@ -143,6 +153,10 @@ export async function initializeBedrock({

if (PROXY) {
const proxyAgent = new HttpsProxyAgent(PROXY);
const credentialProvider =
!hasCompleteCredentials && BEDROCK_AWS_PROFILE
? fromNodeProviderChain({ profile: BEDROCK_AWS_PROFILE })
: undefined;

// Create a custom BedrockRuntimeClient with proxy-enabled request handler.
// ChatBedrockConverse will use this pre-configured client directly instead of
Expand All @@ -154,6 +168,7 @@ export async function initializeBedrock({
...(hasCompleteCredentials && {
credentials: credentials as { accessKeyId: string; secretAccessKey: string },
}),
...(!hasCompleteCredentials && credentialProvider && { credentials: credentialProvider }),
requestHandler: new NodeHttpHandler({
httpAgent: proxyAgent,
httpsAgent: proxyAgent,
Expand All @@ -171,6 +186,10 @@ export async function initializeBedrock({
llmConfig.credentials = credentials;
}

if (!credentials && BEDROCK_AWS_PROFILE) {
llmConfig.profile = BEDROCK_AWS_PROFILE;
}

if (BEDROCK_REVERSE_PROXY) {
llmConfig.endpointHost = BEDROCK_REVERSE_PROXY;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/types/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export interface BedrockConfigOptions {
client?: BedrockRuntimeClient;
/** AWS credentials */
credentials?: BedrockCredentials;
/** AWS shared config profile for the SDK credential provider chain */
profile?: string;
/** Custom endpoint host for reverse proxy */
endpointHost?: string;
/** Guardrail configuration for content filtering */
Expand All @@ -57,6 +59,7 @@ export interface BedrockLLMConfigResult {
region?: string;
client?: BedrockRuntimeClient;
credentials?: BedrockCredentials;
profile?: string;
endpointHost?: string;
guardrailConfig?: GuardrailConfiguration;
applicationInferenceProfile?: string;
Expand Down
Loading