From d37db43e29a28dc039a81fe3a5f8036ab2370f66 Mon Sep 17 00:00:00 2001 From: Gopal Sharma Date: Sun, 10 Aug 2025 03:44:49 +0530 Subject: [PATCH] refactor resolveHeaders --- api/app/clients/OpenAIClient.js | 8 ++-- .../Endpoints/azureAssistants/initialize.js | 8 ++-- .../services/Endpoints/custom/initialize.js | 2 +- .../Endpoints/custom/initialize.spec.js | 11 ++--- .../services/Endpoints/openAI/initialize.js | 8 ++-- .../api/src/endpoints/openai/initialize.ts | 8 ++-- packages/api/src/utils/env.spec.ts | 40 ++++++++-------- packages/api/src/utils/env.ts | 48 +++++++++++-------- packages/data-provider/src/index.ts | 1 + packages/data-provider/src/types/http.ts | 7 +++ 10 files changed, 78 insertions(+), 63 deletions(-) create mode 100644 packages/data-provider/src/types/http.ts diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 2eda322640..c81d0c574c 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -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; diff --git a/api/server/services/Endpoints/azureAssistants/initialize.js b/api/server/services/Endpoints/azureAssistants/initialize.js index 51d52b8ac2..e8aaf89e01 100644 --- a/api/server/services/Endpoints/azureAssistants/initialize.js +++ b/api/server/services/Endpoints/azureAssistants/initialize.js @@ -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) { diff --git a/api/server/services/Endpoints/custom/initialize.js b/api/server/services/Endpoints/custom/initialize.js index 1f9026fc0a..7e61dd17da 100644 --- a/api/server/services/Endpoints/custom/initialize.js +++ b/api/server/services/Endpoints/custom/initialize.js @@ -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}.`); diff --git a/api/server/services/Endpoints/custom/initialize.spec.js b/api/server/services/Endpoints/custom/initialize.spec.js index e71489477e..373620d42a 100644 --- a/api/server/services/Endpoints/custom/initialize.spec.js +++ b/api/server/services/Endpoints/custom/initialize.spec.js @@ -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 () => { diff --git a/api/server/services/Endpoints/openAI/initialize.js b/api/server/services/Endpoints/openAI/initialize.js index bfa228f378..30673ecb54 100644 --- a/api/server/services/Endpoints/openAI/initialize.js +++ b/api/server/services/Endpoints/openAI/initialize.js @@ -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; diff --git a/packages/api/src/endpoints/openai/initialize.ts b/packages/api/src/endpoints/openai/initialize.ts index ad44ed4697..babfed0bea 100644 --- a/packages/api/src/endpoints/openai/initialize.ts +++ b/packages/api/src/endpoints/openai/initialize.ts @@ -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]) { diff --git a/packages/api/src/utils/env.spec.ts b/packages/api/src/utils/env.spec.ts index b348644e11..909299ba22 100644 --- a/packages/api/src/utils/env.spec.ts +++ b/packages/api/src/utils/env.spec.ts @@ -36,12 +36,12 @@ describe('resolveHeaders', () => { }); it('should return empty object when headers is null', () => { - const result = resolveHeaders(null as unknown as Record | undefined); + const result = resolveHeaders({ headers: null as unknown as Record | 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'); }); }); diff --git a/packages/api/src/utils/env.ts b/packages/api/src/utils/env.ts index bd7fada171..09b401a594 100644 --- a/packages/api/src/utils/env.ts +++ b/packages/api/src/utils/env.ts @@ -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_}}` where `` ∈ 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 { +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; user?: TUser; - body?: Record; + 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 | undefined, - user?: Partial | { id: string }, - customUserVars?: Record, - body?: Record, -) { - const resolvedHeaders = { ...(headers ?? {}) }; +export function resolveHeaders(options?: { + headers: Record | undefined; + user?: Partial | { id: string }; + body?: RequestBody; + customUserVars?: Record; +}) { + 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 = { ...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, diff --git a/packages/data-provider/src/index.ts b/packages/data-provider/src/index.ts index ea4b8eb498..f7094e7df6 100644 --- a/packages/data-provider/src/index.ts +++ b/packages/data-provider/src/index.ts @@ -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 */ diff --git a/packages/data-provider/src/types/http.ts b/packages/data-provider/src/types/http.ts new file mode 100644 index 0000000000..5b2363ee6f --- /dev/null +++ b/packages/data-provider/src/types/http.ts @@ -0,0 +1,7 @@ +export interface RequestBody { + parentMessageId: string; + messageId: string; + conversationId?: string; + [key: string]: unknown; +} +