refactor resolveHeaders

This commit is contained in:
Gopal Sharma 2025-08-10 03:44:49 +05:30 committed by Gopal Sharma
parent eec10bf745
commit d37db43e29
10 changed files with 78 additions and 63 deletions

View file

@ -652,10 +652,10 @@ class OpenAIClient extends BaseClient {
const { headers } = this.options; const { headers } = this.options;
if (headers && typeof headers === 'object' && !Array.isArray(headers)) { if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
configOptions.baseOptions = { configOptions.baseOptions = {
headers: resolveHeaders({ headers: resolveHeaders({ headers: {
...headers, ...headers,
...configOptions?.baseOptions?.headers, ...configOptions?.baseOptions?.headers,
}), } }),
}; };
} }
@ -749,7 +749,7 @@ class OpenAIClient extends BaseClient {
groupMap, groupMap,
}); });
this.options.headers = resolveHeaders(headers); this.options.headers = resolveHeaders({ headers });
this.options.reverseProxyUrl = baseURL ?? null; this.options.reverseProxyUrl = baseURL ?? null;
this.langchainProxy = extractBaseURL(this.options.reverseProxyUrl); this.langchainProxy = extractBaseURL(this.options.reverseProxyUrl);
this.apiKey = azureOptions.azureOpenAIApiKey; this.apiKey = azureOptions.azureOpenAIApiKey;
@ -1181,7 +1181,7 @@ ${convo}
modelGroupMap, modelGroupMap,
groupMap, groupMap,
}); });
opts.defaultHeaders = resolveHeaders(headers); opts.defaultHeaders = resolveHeaders({ headers });
this.langchainProxy = extractBaseURL(baseURL); this.langchainProxy = extractBaseURL(baseURL);
this.apiKey = azureOptions.azureOpenAIApiKey; this.apiKey = azureOptions.azureOpenAIApiKey;

View file

@ -109,14 +109,14 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
apiKey = azureOptions.azureOpenAIApiKey; apiKey = azureOptions.azureOpenAIApiKey;
opts.defaultQuery = { 'api-version': azureOptions.azureOpenAIApiVersion }; opts.defaultQuery = { 'api-version': azureOptions.azureOpenAIApiVersion };
opts.defaultHeaders = resolveHeaders( opts.defaultHeaders = resolveHeaders({
{ headers: {
...headers, ...headers,
'api-key': apiKey, 'api-key': apiKey,
'OpenAI-Beta': `assistants=${version}`, 'OpenAI-Beta': `assistants=${version}`,
}, },
req.user, user: req.user,
); });
opts.model = azureOptions.azureOpenAIApiDeploymentName; opts.model = azureOptions.azureOpenAIApiDeploymentName;
if (initAppClient) { if (initAppClient) {

View file

@ -28,7 +28,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
const CUSTOM_API_KEY = extractEnvVariable(endpointConfig.apiKey); const CUSTOM_API_KEY = extractEnvVariable(endpointConfig.apiKey);
const CUSTOM_BASE_URL = extractEnvVariable(endpointConfig.baseURL); const CUSTOM_BASE_URL = extractEnvVariable(endpointConfig.baseURL);
let resolvedHeaders = resolveHeaders(endpointConfig.headers, req.user, undefined, req.body); let resolvedHeaders = resolveHeaders({ headers: endpointConfig.headers, user: req.user, body: req.body });
if (CUSTOM_API_KEY.match(envVarRegex)) { if (CUSTOM_API_KEY.match(envVarRegex)) {
throw new Error(`Missing API Key for ${endpoint}.`); throw new Error(`Missing API Key for ${endpoint}.`);

View file

@ -67,12 +67,11 @@ describe('custom/initializeClient', () => {
it('calls resolveHeaders with headers, user, and body for body placeholder support', async () => { it('calls resolveHeaders with headers, user, and body for body placeholder support', async () => {
const { resolveHeaders } = require('@librechat/api'); const { resolveHeaders } = require('@librechat/api');
await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true }); await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true });
expect(resolveHeaders).toHaveBeenCalledWith( expect(resolveHeaders).toHaveBeenCalledWith({
{ 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' }, headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
{ id: 'user-123', email: 'test@example.com' }, user: { id: 'user-123', email: 'test@example.com' },
undefined, // customUserVars body: { endpoint: 'test-endpoint' }, // body - supports {{LIBRECHAT_BODY_*}} placeholders
{ endpoint: 'test-endpoint' }, // body - supports {{LIBRECHAT_BODY_*}} placeholders });
);
}); });
it('throws if endpoint config is missing', async () => { it('throws if endpoint config is missing', async () => {

View file

@ -81,10 +81,10 @@ const initializeClient = async ({
serverless = _serverless; serverless = _serverless;
clientOptions.reverseProxyUrl = baseURL ?? clientOptions.reverseProxyUrl; clientOptions.reverseProxyUrl = baseURL ?? clientOptions.reverseProxyUrl;
clientOptions.headers = resolveHeaders( clientOptions.headers = resolveHeaders({
{ ...headers, ...(clientOptions.headers ?? {}) }, headers: { ...headers, ...(clientOptions.headers ?? {}) },
req.user, user: req.user,
); });
clientOptions.titleConvo = azureConfig.titleConvo; clientOptions.titleConvo = azureConfig.titleConvo;
clientOptions.titleModel = azureConfig.titleModel; clientOptions.titleModel = azureConfig.titleModel;

View file

@ -87,10 +87,10 @@ export const initializeOpenAI = async ({
}); });
clientOptions.reverseProxyUrl = configBaseURL ?? clientOptions.reverseProxyUrl; clientOptions.reverseProxyUrl = configBaseURL ?? clientOptions.reverseProxyUrl;
clientOptions.headers = resolveHeaders( clientOptions.headers = resolveHeaders({
{ ...headers, ...(clientOptions.headers ?? {}) }, headers: { ...headers, ...(clientOptions.headers ?? {}) },
req.user, user: req.user,
); });
const groupName = modelGroupMap[modelName || '']?.group; const groupName = modelGroupMap[modelName || '']?.group;
if (groupName && groupMap[groupName]) { if (groupName && groupMap[groupName]) {

View file

@ -36,12 +36,12 @@ describe('resolveHeaders', () => {
}); });
it('should return empty object when headers is null', () => { it('should return empty object when headers is null', () => {
const result = resolveHeaders(null as unknown as Record<string, string> | undefined); const result = resolveHeaders({ headers: null as unknown as Record<string, string> | null });
expect(result).toEqual({}); expect(result).toEqual({});
}); });
it('should return empty object when headers is empty', () => { it('should return empty object when headers is empty', () => {
const result = resolveHeaders({}); const result = resolveHeaders({ headers: {} });
expect(result).toEqual({}); expect(result).toEqual({});
}); });
@ -52,7 +52,7 @@ describe('resolveHeaders', () => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
const result = resolveHeaders(headers); const result = resolveHeaders({ headers });
expect(result).toEqual({ expect(result).toEqual({
Authorization: 'test-api-key-value', Authorization: 'test-api-key-value',
@ -68,7 +68,7 @@ describe('resolveHeaders', () => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
const result = resolveHeaders(headers, user); const result = resolveHeaders({ headers, user });
expect(result).toEqual({ expect(result).toEqual({
'User-Id': 'test-user-123', 'User-Id': 'test-user-123',
@ -82,7 +82,7 @@ describe('resolveHeaders', () => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
const result = resolveHeaders(headers); const result = resolveHeaders({ headers });
expect(result).toEqual({ expect(result).toEqual({
'User-Id': '{{LIBRECHAT_USER_ID}}', 'User-Id': '{{LIBRECHAT_USER_ID}}',
@ -97,7 +97,7 @@ describe('resolveHeaders', () => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
const result = resolveHeaders(headers, user); const result = resolveHeaders({ headers, user });
expect(result).toEqual({ expect(result).toEqual({
'User-Id': '{{LIBRECHAT_USER_ID}}', 'User-Id': '{{LIBRECHAT_USER_ID}}',
@ -123,7 +123,7 @@ describe('resolveHeaders', () => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
const result = resolveHeaders(headers, user); const result = resolveHeaders({ headers, user });
expect(result).toEqual({ expect(result).toEqual({
'User-Email': 'test@example.com', 'User-Email': 'test@example.com',
@ -148,7 +148,7 @@ describe('resolveHeaders', () => {
'Non-Existent': '{{LIBRECHAT_USER_NONEXISTENT}}', 'Non-Existent': '{{LIBRECHAT_USER_NONEXISTENT}}',
}; };
const result = resolveHeaders(headers, user); const result = resolveHeaders({ headers, user });
expect(result).toEqual({ expect(result).toEqual({
'User-Email': 'test@example.com', 'User-Email': 'test@example.com',
@ -171,7 +171,7 @@ describe('resolveHeaders', () => {
'X-User-Id': '{{LIBRECHAT_USER_ID}}', 'X-User-Id': '{{LIBRECHAT_USER_ID}}',
}; };
const result = resolveHeaders(headers, user, customUserVars); const result = resolveHeaders({ headers, user, customUserVars });
expect(result).toEqual({ expect(result).toEqual({
Authorization: 'Bearer user-specific-token', Authorization: 'Bearer user-specific-token',
@ -194,7 +194,7 @@ describe('resolveHeaders', () => {
'Test-Email': '{{LIBRECHAT_USER_EMAIL}}', 'Test-Email': '{{LIBRECHAT_USER_EMAIL}}',
}; };
const result = resolveHeaders(headers, user, customUserVars); const result = resolveHeaders({ headers, user, customUserVars });
expect(result).toEqual({ expect(result).toEqual({
'Test-Email': 'custom-email@example.com', 'Test-Email': 'custom-email@example.com',
@ -213,7 +213,7 @@ describe('resolveHeaders', () => {
'User-Id': '{{LIBRECHAT_USER_ID}}', 'User-Id': '{{LIBRECHAT_USER_ID}}',
}; };
const result = resolveHeaders(headers, user); const result = resolveHeaders({ headers, user });
expect(result).toEqual({ expect(result).toEqual({
'User-Role': 'admin', 'User-Role': 'admin',
@ -233,7 +233,7 @@ describe('resolveHeaders', () => {
'Backup-Email': '{{LIBRECHAT_USER_EMAIL}}', 'Backup-Email': '{{LIBRECHAT_USER_EMAIL}}',
}; };
const result = resolveHeaders(headers, user); const result = resolveHeaders({ headers, user });
expect(result).toEqual({ expect(result).toEqual({
'Primary-Email': 'test@example.com', 'Primary-Email': 'test@example.com',
@ -259,7 +259,7 @@ describe('resolveHeaders', () => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
const result = resolveHeaders(headers, user, customUserVars); const result = resolveHeaders({ headers, user, customUserVars });
expect(result).toEqual({ expect(result).toEqual({
Authorization: 'Bearer secret-token', Authorization: 'Bearer secret-token',
@ -277,7 +277,7 @@ describe('resolveHeaders', () => {
}; };
const user = { id: 'user-123' }; const user = { id: 'user-123' };
const result = resolveHeaders(originalHeaders, user); const result = resolveHeaders({ headers: originalHeaders, user });
// Verify the result is processed // Verify the result is processed
expect(result).toEqual({ expect(result).toEqual({
@ -306,7 +306,7 @@ describe('resolveHeaders', () => {
'Dot-Header': '{{CUSTOM.VAR}}', 'Dot-Header': '{{CUSTOM.VAR}}',
}; };
const result = resolveHeaders(headers, user, customUserVars); const result = resolveHeaders({ headers, user, customUserVars });
expect(result).toEqual({ expect(result).toEqual({
'Dash-Header': 'dash-value', 'Dash-Header': 'dash-value',
@ -357,7 +357,7 @@ describe('resolveHeaders', () => {
'X-User-TermsAccepted': '{{LIBRECHAT_USER_TERMSACCEPTED}}', 'X-User-TermsAccepted': '{{LIBRECHAT_USER_TERMSACCEPTED}}',
}; };
const result = resolveHeaders(headers, user); const result = resolveHeaders({ headers, user });
expect(result['X-User-ID']).toBe('abc'); expect(result['X-User-ID']).toBe('abc');
expect(result['X-User-Name']).toBe('Test User'); expect(result['X-User-Name']).toBe('Test User');
@ -384,7 +384,7 @@ describe('resolveHeaders', () => {
'X-Multi': 'User: {{LIBRECHAT_USER_ID}}, Env: ${TEST_API_KEY}, Custom: {{MY_CUSTOM}}', 'X-Multi': 'User: {{LIBRECHAT_USER_ID}}, Env: ${TEST_API_KEY}, Custom: {{MY_CUSTOM}}',
}; };
const customVars = { MY_CUSTOM: 'custom-value' }; const customVars = { MY_CUSTOM: 'custom-value' };
const result = resolveHeaders(headers, user, customVars); const result = resolveHeaders({ headers, user, customUserVars: customVars });
expect(result['X-Multi']).toBe('User: abc, Env: test-api-key-value, Custom: custom-value'); expect(result['X-Multi']).toBe('User: abc, Env: test-api-key-value, Custom: custom-value');
}); });
@ -394,7 +394,7 @@ describe('resolveHeaders', () => {
'X-Unknown': '{{SOMETHING_NOT_RECOGNIZED}}', 'X-Unknown': '{{SOMETHING_NOT_RECOGNIZED}}',
'X-Known': '{{LIBRECHAT_USER_ID}}', 'X-Known': '{{LIBRECHAT_USER_ID}}',
}; };
const result = resolveHeaders(headers, user); const result = resolveHeaders({ headers, user });
expect(result['X-Unknown']).toBe('{{SOMETHING_NOT_RECOGNIZED}}'); expect(result['X-Unknown']).toBe('{{SOMETHING_NOT_RECOGNIZED}}');
expect(result['X-Known']).toBe('abc'); expect(result['X-Known']).toBe('abc');
}); });
@ -416,7 +416,7 @@ describe('resolveHeaders', () => {
'X-Boolean': '{{LIBRECHAT_USER_EMAILVERIFIED}}', 'X-Boolean': '{{LIBRECHAT_USER_EMAILVERIFIED}}',
}; };
const customVars = { MY_CUSTOM: 'custom-value' }; const customVars = { MY_CUSTOM: 'custom-value' };
const result = resolveHeaders(headers, user, customVars); const result = resolveHeaders({ headers, user, customUserVars: customVars });
expect(result['X-User']).toBe('abc'); expect(result['X-User']).toBe('abc');
expect(result['X-Env']).toBe('test-api-key-value'); expect(result['X-Env']).toBe('test-api-key-value');
@ -430,7 +430,7 @@ describe('resolveHeaders', () => {
it('should process LIBRECHAT_BODY placeholders', () => { it('should process LIBRECHAT_BODY placeholders', () => {
const body = { conversationId: 'conv-123', parentMessageId: 'parent-456', messageId: 'msg-789' }; const body = { conversationId: 'conv-123', parentMessageId: 'parent-456', messageId: 'msg-789' };
const headers = { 'X-Conversation': '{{LIBRECHAT_BODY_CONVERSATIONID}}' }; const headers = { 'X-Conversation': '{{LIBRECHAT_BODY_CONVERSATIONID}}' };
const result = resolveHeaders(headers, undefined, undefined, body); const result = resolveHeaders({ headers, body });
expect(result['X-Conversation']).toBe('conv-123'); expect(result['X-Conversation']).toBe('conv-123');
}); });
}); });

View file

@ -1,5 +1,5 @@
import { extractEnvVariable } from 'librechat-data-provider'; import { extractEnvVariable } from 'librechat-data-provider';
import type { TUser, MCPOptions } from 'librechat-data-provider'; import type { TUser, MCPOptions, RequestBody } from 'librechat-data-provider';
/** /**
* List of allowed user fields that can be used in MCP environment variables. * List of allowed user fields that can be used in MCP environment variables.
@ -72,12 +72,15 @@ function processUserPlaceholders(value: string, user?: TUser): string {
} }
/** /**
* Processes a string value to replace request body field placeholders * Replaces request body field placeholders within a string.
* Recognized placeholders: `{{LIBRECHAT_BODY_<FIELD>}}` where `<FIELD>` ALLOWED_BODY_FIELDS.
* If a body field is absent or null/undefined, it is replaced with an empty string.
*
* @param value - The string value to process * @param value - The string value to process
* @param body - The request body object * @param body - The request body object
* @returns The processed string with placeholders replaced * @returns The processed string with placeholders replaced
*/ */
function processBodyPlaceholders(value: string, body: Record<string, any>): string { function processBodyPlaceholders(value: string, body: RequestBody): string {
for (const field of ALLOWED_BODY_FIELDS) { for (const field of ALLOWED_BODY_FIELDS) {
const placeholder = `{{LIBRECHAT_BODY_${field.toUpperCase()}}}`; const placeholder = `{{LIBRECHAT_BODY_${field.toUpperCase()}}}`;
@ -110,7 +113,7 @@ function processSingleValue({
originalValue: string; originalValue: string;
customUserVars?: Record<string, string>; customUserVars?: Record<string, string>;
user?: TUser; user?: TUser;
body?: Record<string, any>; body?: RequestBody;
}): string { }): string {
let value = originalValue; let value = originalValue;
@ -191,25 +194,30 @@ export function processMCPEnv(
} }
/** /**
* Resolves header values by replacing user placeholders, custom variables, body variables, and environment variables * Resolves header values by replacing user placeholders, body variables, custom variables, and environment variables.
* @param headers - The headers object to process *
* @param user - Optional user object for replacing user field placeholders (can be partial with just id) * @param options - Optional configuration object.
* @param customUserVars - Optional custom user variables to replace placeholders * @param options.headers - The headers object to process.
* @param body - Optional request body object for replacing body field placeholders * @param options.user - Optional user object for replacing user field placeholders (can be partial with just id).
* @returns - The processed headers with all placeholders replaced * @param options.body - Optional request body object for replacing body field placeholders.
* @param options.customUserVars - Optional custom user variables to replace placeholders.
* @returns The processed headers with all placeholders replaced.
*/ */
export function resolveHeaders( export function resolveHeaders(options?: {
headers: Record<string, string> | undefined, headers: Record<string, string> | undefined;
user?: Partial<TUser> | { id: string }, user?: Partial<TUser> | { id: string };
customUserVars?: Record<string, string>, body?: RequestBody;
body?: Record<string, any>, customUserVars?: Record<string, string>;
) { }) {
const resolvedHeaders = { ...(headers ?? {}) }; const { headers, user, body, customUserVars } = options ?? {};
const inputHeaders = headers ?? {};
if (headers && typeof headers === 'object' && !Array.isArray(headers)) { const resolvedHeaders: Record<string, string> = { ...inputHeaders };
Object.keys(headers).forEach((key) => {
if (inputHeaders && typeof inputHeaders === 'object' && !Array.isArray(inputHeaders)) {
Object.keys(inputHeaders).forEach((key) => {
resolvedHeaders[key] = processSingleValue({ resolvedHeaders[key] = processSingleValue({
originalValue: headers[key], originalValue: inputHeaders[key],
customUserVars, customUserVars,
user: user as TUser, user: user as TUser,
body, body,

View file

@ -27,6 +27,7 @@ export * from './types/mutations';
export * from './types/queries'; export * from './types/queries';
export * from './types/runs'; export * from './types/runs';
export * from './types/web'; export * from './types/web';
export * from './types/http';
/* query/mutation keys */ /* query/mutation keys */
export * from './keys'; export * from './keys';
/* api call helpers */ /* api call helpers */

View file

@ -0,0 +1,7 @@
export interface RequestBody {
parentMessageId: string;
messageId: string;
conversationId?: string;
[key: string]: unknown;
}