🛂 feat: Add AWS Profile Support for Bedrock Credentials (#10504)

- Add BEDROCK_AWS_PROFILE environment variable support
  - Implement AWS SDK credential provider chain for automatic refresh
  - Update credential loading logic to support profiles, static env vars, and user-provided credentials
  - Add logging for credential source transparency
  - Update .env.example with profile configuration documentation

  Follows S3 implementation pattern for credential handling.
  Enables users to configure AWS profiles with optional credential_process for automatic token refresh.

Co-authored-by: Maxence - Meca.lu <contact@meca.lu>
Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Maxence Dominici 2026-05-23 16:39:11 +02:00 committed by GitHub
parent 7f0fde2d98
commit 294bf7c87d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 140 additions and 40 deletions

View file

@ -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

View file

@ -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';
@ -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,
@ -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 = (
@ -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', () => {
@ -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;
});
])(
'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];
});
// Delete specified environment variables
deleteEnvVars?.forEach((key) => {
delete process.env[key];
});
const params = createMockParams({
config: {
endpoints: {
[EModelEndpoint.bedrock]: {
guardrailConfig: input,
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', () => {
@ -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', () => {
@ -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();

View file

@ -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,
@ -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,
@ -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 }),
};
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 (!credentials) {
throw new Error('Bedrock credentials not provided. Please provide them again.');
}
let credentials: BedrockCredentials | undefined;
if (
!isUserProvided &&
(credentials.accessKeyId === undefined || credentials.accessKeyId === '') &&
(credentials.secretAccessKey === undefined || credentials.secretAccessKey === '')
) {
credentials = undefined;
}
if (isUserProvided) {
const userKey = await db.getUserKey({
userId: req.user?.id ?? '',
name: EModelEndpoint.bedrock,
});
if (expiresAt && isUserProvided) {
checkUserKeyExpiry(expiresAt, EModelEndpoint.bedrock);
if (!userKey) {
throw new Error('Bedrock credentials not provided. Please provide them again.');
}
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> = {
@ -115,6 +124,7 @@ export async function initializeBedrock({
client?: BedrockRuntimeClient;
credentials?: BedrockCredentials;
endpointHost?: string;
profile?: string;
guardrailConfig?: GuardrailConfiguration;
applicationInferenceProfile?: string;
};
@ -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
@ -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,
@ -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;
}

View file

@ -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 */
@ -57,6 +59,7 @@ export interface BedrockLLMConfigResult {
region?: string;
client?: BedrockRuntimeClient;
credentials?: BedrockCredentials;
profile?: string;
endpointHost?: string;
guardrailConfig?: GuardrailConfiguration;
applicationInferenceProfile?: string;