mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 06:52:47 +00:00
🛂 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:
parent
7f0fde2d98
commit
294bf7c87d
4 changed files with 140 additions and 40 deletions
27
.env.example
27
.env.example
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue