diff --git a/api/server/services/Endpoints/custom/initialize.js b/api/server/services/Endpoints/custom/initialize.js index 8cedf598bc..1f9026fc0a 100644 --- a/api/server/services/Endpoints/custom/initialize.js +++ b/api/server/services/Endpoints/custom/initialize.js @@ -28,23 +28,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid const CUSTOM_API_KEY = extractEnvVariable(endpointConfig.apiKey); const CUSTOM_BASE_URL = extractEnvVariable(endpointConfig.baseURL); - const customUserVars = {}; - if (req.body.conversationId) { - customUserVars.LIBRECHAT_CONVERSATION_ID = req.body.conversationId; - } - - let resolvedHeaders = resolveHeaders(endpointConfig.headers, req.user, customUserVars); - - // Filter out headers with unresolved placeholders - const filteredHeaders = {}; - for (const [key, value] of Object.entries(resolvedHeaders)) { - if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) { - continue; - } - filteredHeaders[key] = value; - } - - resolvedHeaders = filteredHeaders; + let resolvedHeaders = resolveHeaders(endpointConfig.headers, req.user, undefined, 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 42125e6373..e71489477e 100644 --- a/api/server/services/Endpoints/custom/initialize.spec.js +++ b/api/server/services/Endpoints/custom/initialize.spec.js @@ -64,26 +64,14 @@ describe('custom/initializeClient', () => { jest.clearAllMocks(); }); - it('calls resolveHeaders with conversation ID when provided', async () => { - const { resolveHeaders } = require('@librechat/api'); - const requestWithConversationId = { - ...mockRequest, - body: { ...mockRequest.body, conversationId: 'existing-conversation-123' }, - }; - await initializeClient({ req: requestWithConversationId, res: mockResponse, optionsOnly: true }); - expect(resolveHeaders).toHaveBeenCalledWith( - { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' }, - { id: 'user-123', email: 'test@example.com' }, - { LIBRECHAT_CONVERSATION_ID: 'existing-conversation-123' }, - ); - }); - - it('calls resolveHeaders with headers and user', async () => { + 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 ); }); diff --git a/librechat.example.yaml b/librechat.example.yaml index 3a9230b508..4c2575b489 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -259,6 +259,8 @@ endpoints: # recommended environment variables: apiKey: '${OPENROUTER_KEY}' baseURL: 'https://openrouter.ai/api/v1' + headers: + x-librechat-body-parentmessageid: '{{LIBRECHAT_BODY_PARENTMESSAGEID}}' models: default: ['meta-llama/llama-3-70b-instruct'] fetch: true diff --git a/packages/api/src/utils/env.spec.ts b/packages/api/src/utils/env.spec.ts index 4cb8da0d6b..b348644e11 100644 --- a/packages/api/src/utils/env.spec.ts +++ b/packages/api/src/utils/env.spec.ts @@ -426,4 +426,11 @@ describe('resolveHeaders', () => { expect(result['X-Empty']).toBe(''); expect(result['X-Boolean']).toBe('true'); }); + + 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); + expect(result['X-Conversation']).toBe('conv-123'); + }); }); diff --git a/packages/api/src/utils/env.ts b/packages/api/src/utils/env.ts index 54169d9b93..bd7fada171 100644 --- a/packages/api/src/utils/env.ts +++ b/packages/api/src/utils/env.ts @@ -25,6 +25,16 @@ const ALLOWED_USER_FIELDS = [ 'termsAccepted', ] as const; +/** + * List of allowed request body fields that can be used in header placeholders. + * These are common fields from the request body that are safe to expose in headers. + */ +const ALLOWED_BODY_FIELDS = [ + 'conversationId', + 'parentMessageId', + 'messageId' +] as const; + /** * Processes a string value to replace user field placeholders * @param value - The string value to process @@ -61,21 +71,46 @@ function processUserPlaceholders(value: string, user?: TUser): string { return value; } +/** + * Processes a string value to replace request body field placeholders + * @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 { + + for (const field of ALLOWED_BODY_FIELDS) { + const placeholder = `{{LIBRECHAT_BODY_${field.toUpperCase()}}}`; + if (!value.includes(placeholder)) { + continue; + } + + const fieldValue = body[field]; + const replacementValue = fieldValue == null ? '' : String(fieldValue); + value = value.replace(new RegExp(placeholder, 'g'), replacementValue); + } + + return value; +} + /** * Processes a single string value by replacing various types of placeholders * @param originalValue - The original string value to process * @param customUserVars - Optional custom user variables to replace placeholders * @param user - Optional user object for replacing user field placeholders + * @param body - Optional request body object for replacing body field placeholders * @returns The processed string with all placeholders replaced */ function processSingleValue({ originalValue, customUserVars, user, + body = undefined, }: { originalValue: string; customUserVars?: Record; user?: TUser; + body?: Record; }): string { let value = originalValue; @@ -92,7 +127,12 @@ function processSingleValue({ // 2. Replace user field placeholders (e.g., {{LIBRECHAT_USER_EMAIL}}, {{LIBRECHAT_USER_ID}}) value = processUserPlaceholders(value, user); - // 3. Replace system environment variables + // 3. Replace body field placeholders (e.g., {{LIBRECHAT_BODY_CONVERSATIONID}}, {{LIBRECHAT_BODY_PARENTMESSAGEID}}) + if (body) { + value = processBodyPlaceholders(value, body); + } + + // 4. Replace system environment variables value = extractEnvVariable(value); return value; @@ -151,16 +191,18 @@ export function processMCPEnv( } /** - * Resolves header values by replacing user placeholders, custom variables, and environment variables + * 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 */ export function resolveHeaders( headers: Record | undefined, user?: Partial | { id: string }, customUserVars?: Record, + body?: Record, ) { const resolvedHeaders = { ...(headers ?? {}) }; @@ -170,6 +212,7 @@ export function resolveHeaders( originalValue: headers[key], customUserVars, user: user as TUser, + body, }); }); }