From 294bf7c87db064e06242cb696ab55031fe96f2d8 Mon Sep 17 00:00:00 2001 From: Maxence Dominici <38140638+iElsha@users.noreply.github.com> Date: Sat, 23 May 2026 16:39:11 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=82=20feat:=20Add=20AWS=20Profile=20Su?= =?UTF-8?q?pport=20for=20Bedrock=20Credentials=20(#10504)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Co-authored-by: Danny Avila --- .env.example | 27 ++++++ .../src/endpoints/bedrock/initialize.spec.ts | 89 +++++++++++++++---- .../api/src/endpoints/bedrock/initialize.ts | 61 ++++++++----- packages/api/src/types/bedrock.ts | 3 + 4 files changed, 140 insertions(+), 40 deletions(-) diff --git a/.env.example b/.env.example index 8129498982..2020b38242 100644 --- a/.env.example +++ b/.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 diff --git a/packages/api/src/endpoints/bedrock/initialize.spec.ts b/packages/api/src/endpoints/bedrock/initialize.spec.ts index f69da3d496..666291e7b7 100644 --- a/packages/api/src/endpoints/bedrock/initialize.spec.ts +++ b/packages/api/src/endpoints/bedrock/initialize.spec.ts @@ -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; + const credentials = client.credentials as { config?: Record }; + 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(); diff --git a/packages/api/src/endpoints/bedrock/initialize.ts b/packages/api/src/endpoints/bedrock/initialize.ts index 8d61f77330..42b57fffd7 100644 --- a/packages/api/src/endpoints/bedrock/initialize.ts +++ b/packages/api/src/endpoints/bedrock/initialize.ts @@ -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 = { @@ -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; } diff --git a/packages/api/src/types/bedrock.ts b/packages/api/src/types/bedrock.ts index 6608c52fcf..e092e6bc04 100644 --- a/packages/api/src/types/bedrock.ts +++ b/packages/api/src/types/bedrock.ts @@ -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;