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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ ANTHROPIC_API_KEY=user_provided
# BEDROCK_AWS_SECRET_ACCESS_KEY=someSecretAccessKey
# BEDROCK_AWS_SESSION_TOKEN=someSessionToken

# Bedrock API key
# BEDROCK_AWS_BEARER_TOKEN=yourBedrockApiKey

# Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you.
# BEDROCK_AWS_MODELS=anthropic.claude-opus-4-7,anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0
# Cross-region inference model IDs: us.anthropic.claude-opus-4-7,us.anthropic.claude-sonnet-4-6,us.anthropic.claude-opus-4-6-v1,global.anthropic.claude-opus-4-6-v1
Expand Down
15 changes: 14 additions & 1 deletion api/server/services/Config/EndpointService.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ const {

const userProvidedOpenAI = isUserProvided(openAIApiKey);
const anthropicUsesVertex = isEnabled(process.env.ANTHROPIC_USE_VERTEX);
const firstNonEmpty = (...values) => values.find((value) => value != null && value !== '');
const bedrockUserProvidedCredential = [
process.env.BEDROCK_AWS_BEARER_TOKEN,
process.env.BEDROCK_AWS_ACCESS_KEY_ID,
process.env.BEDROCK_AWS_SECRET_ACCESS_KEY,
process.env.BEDROCK_AWS_SESSION_TOKEN,
].find(isUserProvided);

module.exports = {
config: {
Expand All @@ -38,7 +45,13 @@ module.exports = {
EModelEndpoint.azureAssistants,
),
[EModelEndpoint.bedrock]: generateConfig(
process.env.BEDROCK_AWS_SECRET_ACCESS_KEY ?? process.env.BEDROCK_AWS_DEFAULT_REGION,
bedrockUserProvidedCredential ??
firstNonEmpty(
process.env.BEDROCK_AWS_BEARER_TOKEN,
process.env.BEDROCK_AWS_SECRET_ACCESS_KEY,
process.env.BEDROCK_AWS_PROFILE,
process.env.BEDROCK_AWS_DEFAULT_REGION,
),
),
/* key will be part of separate config */
[EModelEndpoint.agents]: generateConfig('true', undefined, EModelEndpoint.agents),
Expand Down
36 changes: 36 additions & 0 deletions api/server/services/Config/__tests__/EndpointService.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ describe('EndpointService', () => {
process.env = { ...originalEnv };
delete process.env.ANTHROPIC_API_KEY;
delete process.env.ANTHROPIC_USE_VERTEX;
delete process.env.BEDROCK_AWS_ACCESS_KEY_ID;
delete process.env.BEDROCK_AWS_SECRET_ACCESS_KEY;
delete process.env.BEDROCK_AWS_SESSION_TOKEN;
delete process.env.BEDROCK_AWS_BEARER_TOKEN;
delete process.env.BEDROCK_AWS_PROFILE;
delete process.env.BEDROCK_AWS_DEFAULT_REGION;
Object.assign(process.env, env);
return require('../EndpointService').config;
}
Expand All @@ -80,4 +86,34 @@ describe('EndpointService', () => {

expect(config[EModelEndpoint.anthropic]).toEqual({ userProvide: true });
});

it('requires a user Bedrock key when bearer token user_provided is set with a legacy static secret', () => {
const config = loadConfig({
BEDROCK_AWS_SECRET_ACCESS_KEY: 'legacy-secret',
BEDROCK_AWS_BEARER_TOKEN: 'user_provided',
BEDROCK_AWS_DEFAULT_REGION: 'us-east-1',
});

expect(config[EModelEndpoint.bedrock]).toEqual({ userProvide: true });
});

it('enables Bedrock with static bearer token before static secret credentials', () => {
const config = loadConfig({
BEDROCK_AWS_SECRET_ACCESS_KEY: 'legacy-secret',
BEDROCK_AWS_BEARER_TOKEN: 'bedrock-api-key',
BEDROCK_AWS_DEFAULT_REGION: 'us-east-1',
});

expect(config[EModelEndpoint.bedrock]).toEqual({ userProvide: false });
});

it('skips blank optional Bedrock env vars before falling back to region', () => {
const config = loadConfig({
BEDROCK_AWS_BEARER_TOKEN: '',
BEDROCK_AWS_PROFILE: '',
BEDROCK_AWS_DEFAULT_REGION: 'us-east-1',
});

expect(config[EModelEndpoint.bedrock]).toEqual({ userProvide: false });
});
});
20 changes: 20 additions & 0 deletions client/src/components/Chat/Menus/Endpoints/DialogManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ const DialogManager = ({
endpointType={getEndpointField(endpointsConfig, keyDialogEndpoint, 'type')}
onOpenChange={onOpenChange}
userProvideURL={getEndpointField(endpointsConfig, keyDialogEndpoint, 'userProvideURL')}
userProvideAccessKeyId={getEndpointField(
endpointsConfig,
keyDialogEndpoint,
'userProvideAccessKeyId',
)}
userProvideSecretAccessKey={getEndpointField(
endpointsConfig,
keyDialogEndpoint,
'userProvideSecretAccessKey',
)}
userProvideSessionToken={getEndpointField(
endpointsConfig,
keyDialogEndpoint,
'userProvideSessionToken',
)}
userProvideBearerToken={getEndpointField(
endpointsConfig,
keyDialogEndpoint,
'userProvideBearerToken',
)}
/>
)}
</>
Expand Down
111 changes: 111 additions & 0 deletions client/src/components/Input/SetKeyDialog/BedrockConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import { useLocalize } from '~/hooks';
import InputWithLabel from './InputWithLabel';

const BedrockConfig = ({
userProvideAccessKeyId,
userProvideSecretAccessKey,
userProvideSessionToken,
userProvideBearerToken,
}: {
endpoint: EModelEndpoint | string;
userProvideURL?: boolean | null;
userProvideAccessKeyId?: boolean;
userProvideSecretAccessKey?: boolean;
userProvideSessionToken?: boolean;
userProvideBearerToken?: boolean;
}) => {
const { control } = useFormContext();
const localize = useLocalize();

const renderFields = () => {
const fields: React.ReactNode[] = [];

if (userProvideAccessKeyId) {
fields.push(
<Controller
key="bedrockAccessKeyId"
name="bedrockAccessKeyId"
control={control}
render={({ field }) => (
<InputWithLabel
id="bedrockAccessKeyId"
{...field}
label={localize('com_endpoint_config_bedrock_access_key_id')}
labelClassName="mb-1"
inputClassName="mb-2"
/>
)}
/>,
);
}

if (userProvideSecretAccessKey) {
if (fields.length > 0) fields.push(<div key="spacer1" className="mt-3" />);
fields.push(
<Controller
key="bedrockSecretAccessKey"
name="bedrockSecretAccessKey"
control={control}
render={({ field }) => (
<InputWithLabel
id="bedrockSecretAccessKey"
{...field}
label={localize('com_endpoint_config_bedrock_secret_access_key')}
labelClassName="mb-1"
inputClassName="mb-2"
/>
)}
/>,
);
}

if (userProvideSessionToken) {
if (fields.length > 0) fields.push(<div key="spacer2" className="mt-3" />);
fields.push(
<Controller
key="bedrockSessionToken"
name="bedrockSessionToken"
control={control}
render={({ field }) => (
<InputWithLabel
id="bedrockSessionToken"
{...field}
label={localize('com_endpoint_config_bedrock_session_token')}
labelClassName="mb-1"
inputClassName="mb-2"
/>
)}
/>,
);
}

if (userProvideBearerToken) {
if (fields.length > 0) fields.push(<div key="spacer3" className="mt-3" />);
fields.push(
<Controller
key="bedrockBearerToken"
name="bedrockBearerToken"
control={control}
render={({ field }) => (
<InputWithLabel
id="bedrockBearerToken"
{...field}
label={localize('com_endpoint_config_bedrock_bearer_token')}
labelClassName="mb-1"
inputClassName="mb-2"
/>
)}
/>,
);
}

return <>{fields}</>;
};

return <form className="flex-wrap">{renderFields()}</form>;
};

export default BedrockConfig;
Loading
Loading