mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-23 20:00:15 +01:00
refactor resolveHeaders
This commit is contained in:
parent
eec10bf745
commit
d37db43e29
10 changed files with 78 additions and 63 deletions
|
|
@ -652,10 +652,10 @@ class OpenAIClient extends BaseClient {
|
|||
const { headers } = this.options;
|
||||
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
||||
configOptions.baseOptions = {
|
||||
headers: resolveHeaders({
|
||||
headers: resolveHeaders({ headers: {
|
||||
...headers,
|
||||
...configOptions?.baseOptions?.headers,
|
||||
}),
|
||||
} }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -749,7 +749,7 @@ class OpenAIClient extends BaseClient {
|
|||
groupMap,
|
||||
});
|
||||
|
||||
this.options.headers = resolveHeaders(headers);
|
||||
this.options.headers = resolveHeaders({ headers });
|
||||
this.options.reverseProxyUrl = baseURL ?? null;
|
||||
this.langchainProxy = extractBaseURL(this.options.reverseProxyUrl);
|
||||
this.apiKey = azureOptions.azureOpenAIApiKey;
|
||||
|
|
@ -1181,7 +1181,7 @@ ${convo}
|
|||
modelGroupMap,
|
||||
groupMap,
|
||||
});
|
||||
opts.defaultHeaders = resolveHeaders(headers);
|
||||
opts.defaultHeaders = resolveHeaders({ headers });
|
||||
this.langchainProxy = extractBaseURL(baseURL);
|
||||
this.apiKey = azureOptions.azureOpenAIApiKey;
|
||||
|
||||
|
|
|
|||
|
|
@ -109,14 +109,14 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
|
|||
|
||||
apiKey = azureOptions.azureOpenAIApiKey;
|
||||
opts.defaultQuery = { 'api-version': azureOptions.azureOpenAIApiVersion };
|
||||
opts.defaultHeaders = resolveHeaders(
|
||||
{
|
||||
opts.defaultHeaders = resolveHeaders({
|
||||
headers: {
|
||||
...headers,
|
||||
'api-key': apiKey,
|
||||
'OpenAI-Beta': `assistants=${version}`,
|
||||
},
|
||||
req.user,
|
||||
);
|
||||
user: req.user,
|
||||
});
|
||||
opts.model = azureOptions.azureOpenAIApiDeploymentName;
|
||||
|
||||
if (initAppClient) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
|
|||
const CUSTOM_API_KEY = extractEnvVariable(endpointConfig.apiKey);
|
||||
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)) {
|
||||
throw new Error(`Missing API Key for ${endpoint}.`);
|
||||
|
|
|
|||
|
|
@ -67,12 +67,11 @@ describe('custom/initializeClient', () => {
|
|||
it('calls resolveHeaders with headers, user, and body for body placeholder support', async () => {
|
||||
const { resolveHeaders } = require('@librechat/api');
|
||||
await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true });
|
||||
expect(resolveHeaders).toHaveBeenCalledWith(
|
||||
{ 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||
{ id: 'user-123', email: 'test@example.com' },
|
||||
undefined, // customUserVars
|
||||
{ endpoint: 'test-endpoint' }, // body - supports {{LIBRECHAT_BODY_*}} placeholders
|
||||
);
|
||||
expect(resolveHeaders).toHaveBeenCalledWith({
|
||||
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
body: { endpoint: 'test-endpoint' }, // body - supports {{LIBRECHAT_BODY_*}} placeholders
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if endpoint config is missing', async () => {
|
||||
|
|
|
|||
|
|
@ -81,10 +81,10 @@ const initializeClient = async ({
|
|||
serverless = _serverless;
|
||||
|
||||
clientOptions.reverseProxyUrl = baseURL ?? clientOptions.reverseProxyUrl;
|
||||
clientOptions.headers = resolveHeaders(
|
||||
{ ...headers, ...(clientOptions.headers ?? {}) },
|
||||
req.user,
|
||||
);
|
||||
clientOptions.headers = resolveHeaders({
|
||||
headers: { ...headers, ...(clientOptions.headers ?? {}) },
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
clientOptions.titleConvo = azureConfig.titleConvo;
|
||||
clientOptions.titleModel = azureConfig.titleModel;
|
||||
|
|
|
|||
|
|
@ -87,10 +87,10 @@ export const initializeOpenAI = async ({
|
|||
});
|
||||
|
||||
clientOptions.reverseProxyUrl = configBaseURL ?? clientOptions.reverseProxyUrl;
|
||||
clientOptions.headers = resolveHeaders(
|
||||
{ ...headers, ...(clientOptions.headers ?? {}) },
|
||||
req.user,
|
||||
);
|
||||
clientOptions.headers = resolveHeaders({
|
||||
headers: { ...headers, ...(clientOptions.headers ?? {}) },
|
||||
user: req.user,
|
||||
});
|
||||
|
||||
const groupName = modelGroupMap[modelName || '']?.group;
|
||||
if (groupName && groupMap[groupName]) {
|
||||
|
|
|
|||
|
|
@ -36,12 +36,12 @@ describe('resolveHeaders', () => {
|
|||
});
|
||||
|
||||
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({});
|
||||
});
|
||||
|
||||
it('should return empty object when headers is empty', () => {
|
||||
const result = resolveHeaders({});
|
||||
const result = resolveHeaders({ headers: {} });
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ describe('resolveHeaders', () => {
|
|||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers);
|
||||
const result = resolveHeaders({ headers });
|
||||
|
||||
expect(result).toEqual({
|
||||
Authorization: 'test-api-key-value',
|
||||
|
|
@ -68,7 +68,7 @@ describe('resolveHeaders', () => {
|
|||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user);
|
||||
const result = resolveHeaders({ headers, user });
|
||||
|
||||
expect(result).toEqual({
|
||||
'User-Id': 'test-user-123',
|
||||
|
|
@ -82,7 +82,7 @@ describe('resolveHeaders', () => {
|
|||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers);
|
||||
const result = resolveHeaders({ headers });
|
||||
|
||||
expect(result).toEqual({
|
||||
'User-Id': '{{LIBRECHAT_USER_ID}}',
|
||||
|
|
@ -97,7 +97,7 @@ describe('resolveHeaders', () => {
|
|||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user);
|
||||
const result = resolveHeaders({ headers, user });
|
||||
|
||||
expect(result).toEqual({
|
||||
'User-Id': '{{LIBRECHAT_USER_ID}}',
|
||||
|
|
@ -123,7 +123,7 @@ describe('resolveHeaders', () => {
|
|||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user);
|
||||
const result = resolveHeaders({ headers, user });
|
||||
|
||||
expect(result).toEqual({
|
||||
'User-Email': 'test@example.com',
|
||||
|
|
@ -148,7 +148,7 @@ describe('resolveHeaders', () => {
|
|||
'Non-Existent': '{{LIBRECHAT_USER_NONEXISTENT}}',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user);
|
||||
const result = resolveHeaders({ headers, user });
|
||||
|
||||
expect(result).toEqual({
|
||||
'User-Email': 'test@example.com',
|
||||
|
|
@ -171,7 +171,7 @@ describe('resolveHeaders', () => {
|
|||
'X-User-Id': '{{LIBRECHAT_USER_ID}}',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user, customUserVars);
|
||||
const result = resolveHeaders({ headers, user, customUserVars });
|
||||
|
||||
expect(result).toEqual({
|
||||
Authorization: 'Bearer user-specific-token',
|
||||
|
|
@ -194,7 +194,7 @@ describe('resolveHeaders', () => {
|
|||
'Test-Email': '{{LIBRECHAT_USER_EMAIL}}',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user, customUserVars);
|
||||
const result = resolveHeaders({ headers, user, customUserVars });
|
||||
|
||||
expect(result).toEqual({
|
||||
'Test-Email': 'custom-email@example.com',
|
||||
|
|
@ -213,7 +213,7 @@ describe('resolveHeaders', () => {
|
|||
'User-Id': '{{LIBRECHAT_USER_ID}}',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user);
|
||||
const result = resolveHeaders({ headers, user });
|
||||
|
||||
expect(result).toEqual({
|
||||
'User-Role': 'admin',
|
||||
|
|
@ -233,7 +233,7 @@ describe('resolveHeaders', () => {
|
|||
'Backup-Email': '{{LIBRECHAT_USER_EMAIL}}',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user);
|
||||
const result = resolveHeaders({ headers, user });
|
||||
|
||||
expect(result).toEqual({
|
||||
'Primary-Email': 'test@example.com',
|
||||
|
|
@ -259,7 +259,7 @@ describe('resolveHeaders', () => {
|
|||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user, customUserVars);
|
||||
const result = resolveHeaders({ headers, user, customUserVars });
|
||||
|
||||
expect(result).toEqual({
|
||||
Authorization: 'Bearer secret-token',
|
||||
|
|
@ -277,7 +277,7 @@ describe('resolveHeaders', () => {
|
|||
};
|
||||
const user = { id: 'user-123' };
|
||||
|
||||
const result = resolveHeaders(originalHeaders, user);
|
||||
const result = resolveHeaders({ headers: originalHeaders, user });
|
||||
|
||||
// Verify the result is processed
|
||||
expect(result).toEqual({
|
||||
|
|
@ -306,7 +306,7 @@ describe('resolveHeaders', () => {
|
|||
'Dot-Header': '{{CUSTOM.VAR}}',
|
||||
};
|
||||
|
||||
const result = resolveHeaders(headers, user, customUserVars);
|
||||
const result = resolveHeaders({ headers, user, customUserVars });
|
||||
|
||||
expect(result).toEqual({
|
||||
'Dash-Header': 'dash-value',
|
||||
|
|
@ -357,7 +357,7 @@ describe('resolveHeaders', () => {
|
|||
'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-Name']).toBe('Test User');
|
||||
|
|
@ -384,7 +384,7 @@ describe('resolveHeaders', () => {
|
|||
'X-Multi': 'User: {{LIBRECHAT_USER_ID}}, Env: ${TEST_API_KEY}, Custom: {{MY_CUSTOM}}',
|
||||
};
|
||||
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');
|
||||
});
|
||||
|
||||
|
|
@ -394,7 +394,7 @@ describe('resolveHeaders', () => {
|
|||
'X-Unknown': '{{SOMETHING_NOT_RECOGNIZED}}',
|
||||
'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-Known']).toBe('abc');
|
||||
});
|
||||
|
|
@ -416,7 +416,7 @@ describe('resolveHeaders', () => {
|
|||
'X-Boolean': '{{LIBRECHAT_USER_EMAILVERIFIED}}',
|
||||
};
|
||||
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-Env']).toBe('test-api-key-value');
|
||||
|
|
@ -430,7 +430,7 @@ describe('resolveHeaders', () => {
|
|||
it('should process LIBRECHAT_BODY placeholders', () => {
|
||||
const body = { conversationId: 'conv-123', parentMessageId: 'parent-456', messageId: 'msg-789' };
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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.
|
||||
|
|
@ -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 body - The request body object
|
||||
* @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) {
|
||||
const placeholder = `{{LIBRECHAT_BODY_${field.toUpperCase()}}}`;
|
||||
|
|
@ -110,7 +113,7 @@ function processSingleValue({
|
|||
originalValue: string;
|
||||
customUserVars?: Record<string, string>;
|
||||
user?: TUser;
|
||||
body?: Record<string, any>;
|
||||
body?: RequestBody;
|
||||
}): string {
|
||||
let value = originalValue;
|
||||
|
||||
|
|
@ -191,25 +194,30 @@ export function processMCPEnv(
|
|||
}
|
||||
|
||||
/**
|
||||
* Resolves header values by replacing user placeholders, custom variables, body 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 customUserVars - Optional custom user variables to replace placeholders
|
||||
* @param body - Optional request body object for replacing body field placeholders
|
||||
* @returns - The processed headers with all placeholders replaced
|
||||
* Resolves header values by replacing user placeholders, body variables, custom variables, and environment variables.
|
||||
*
|
||||
* @param options - Optional configuration object.
|
||||
* @param options.headers - The headers object to process.
|
||||
* @param options.user - Optional user object for replacing user field placeholders (can be partial with just id).
|
||||
* @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(
|
||||
headers: Record<string, string> | undefined,
|
||||
user?: Partial<TUser> | { id: string },
|
||||
customUserVars?: Record<string, string>,
|
||||
body?: Record<string, any>,
|
||||
) {
|
||||
const resolvedHeaders = { ...(headers ?? {}) };
|
||||
export function resolveHeaders(options?: {
|
||||
headers: Record<string, string> | undefined;
|
||||
user?: Partial<TUser> | { id: string };
|
||||
body?: RequestBody;
|
||||
customUserVars?: Record<string, string>;
|
||||
}) {
|
||||
const { headers, user, body, customUserVars } = options ?? {};
|
||||
const inputHeaders = headers ?? {};
|
||||
|
||||
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
||||
Object.keys(headers).forEach((key) => {
|
||||
const resolvedHeaders: Record<string, string> = { ...inputHeaders };
|
||||
|
||||
if (inputHeaders && typeof inputHeaders === 'object' && !Array.isArray(inputHeaders)) {
|
||||
Object.keys(inputHeaders).forEach((key) => {
|
||||
resolvedHeaders[key] = processSingleValue({
|
||||
originalValue: headers[key],
|
||||
originalValue: inputHeaders[key],
|
||||
customUserVars,
|
||||
user: user as TUser,
|
||||
body,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export * from './types/mutations';
|
|||
export * from './types/queries';
|
||||
export * from './types/runs';
|
||||
export * from './types/web';
|
||||
export * from './types/http';
|
||||
/* query/mutation keys */
|
||||
export * from './keys';
|
||||
/* api call helpers */
|
||||
|
|
|
|||
7
packages/data-provider/src/types/http.ts
Normal file
7
packages/data-provider/src/types/http.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface RequestBody {
|
||||
parentMessageId: string;
|
||||
messageId: string;
|
||||
conversationId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue