diff --git a/api/server/controllers/ModelController.js b/api/server/controllers/ModelController.js index 805d9eef27ba..426b27eac5f7 100644 --- a/api/server/controllers/ModelController.js +++ b/api/server/controllers/ModelController.js @@ -1,20 +1,12 @@ const { logger } = require('@librechat/data-schemas'); -const { CacheKeys } = require('librechat-data-provider'); const { loadDefaultModels, loadConfigModels } = require('~/server/services/Config'); -const { getLogStores } = require('~/cache'); /** * @param {ServerRequest} req * @returns {Promise} The models config. */ const getModelsConfig = async (req) => { - const cache = getLogStores(CacheKeys.CONFIG_STORE); - let modelsConfig = await cache.get(CacheKeys.MODELS_CONFIG); - if (!modelsConfig) { - modelsConfig = await loadModels(req); - } - - return modelsConfig; + return await loadModels(req); }; /** @@ -23,18 +15,10 @@ const getModelsConfig = async (req) => { * @returns {Promise} The models config. */ async function loadModels(req) { - const cache = getLogStores(CacheKeys.CONFIG_STORE); - const cachedModelsConfig = await cache.get(CacheKeys.MODELS_CONFIG); - if (cachedModelsConfig) { - return cachedModelsConfig; - } const defaultModelsConfig = await loadDefaultModels(req); const customModelsConfig = await loadConfigModels(req); - const modelConfig = { ...defaultModelsConfig, ...customModelsConfig }; - - await cache.set(CacheKeys.MODELS_CONFIG, modelConfig); - return modelConfig; + return { ...defaultModelsConfig, ...customModelsConfig }; } async function modelController(req, res) { diff --git a/packages/api/src/endpoints/models.spec.ts b/packages/api/src/endpoints/models.spec.ts index 575cc5fef894..34dbaaead945 100644 --- a/packages/api/src/endpoints/models.spec.ts +++ b/packages/api/src/endpoints/models.spec.ts @@ -78,12 +78,14 @@ describe('fetchModels', () => { ); }); - it('should pass custom headers to the API request', async () => { + it('should resolve and merge custom headers to the API request', async () => { const customHeaders = { 'X-Custom-Header': 'custom-value', 'X-API-Version': 'v2', }; + (resolveHeaders as jest.Mock).mockReturnValueOnce(customHeaders); + await fetchModels({ user: 'user123', apiKey: 'testApiKey', @@ -92,6 +94,10 @@ describe('fetchModels', () => { headers: customHeaders, }); + expect(resolveHeaders).toHaveBeenCalledWith({ + headers: customHeaders, + user: undefined, + }); expect(mockedAxios.get).toHaveBeenCalledWith( expect.stringContaining('https://api.test.com/models'), expect.objectContaining({ @@ -104,6 +110,36 @@ describe('fetchModels', () => { ); }); + it('should allow custom authorization header to override default Bearer token', async () => { + const customHeaders = { + Authorization: 'Bearer user-specific-token', + }; + + (resolveHeaders as jest.Mock).mockReturnValueOnce(customHeaders); + + await fetchModels({ + user: 'user123', + apiKey: 'testApiKey', + baseURL: 'https://api.test.com', + name: 'TestAPI', + headers: { authorization: 'Bearer {{LIBRECHAT_OPENID_ID_TOKEN}}' }, + userObject: { id: 'user123', email: 'test@example.com' }, + }); + + expect(resolveHeaders).toHaveBeenCalledWith({ + headers: { authorization: 'Bearer {{LIBRECHAT_OPENID_ID_TOKEN}}' }, + user: { id: 'user123', email: 'test@example.com' }, + }); + expect(mockedAxios.get).toHaveBeenCalledWith( + expect.stringContaining('https://api.test.com/models'), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer user-specific-token', + }), + }), + ); + }); + it('should handle null headers gracefully', async () => { await fetchModels({ user: 'user123', @@ -548,11 +584,13 @@ describe('getAnthropicModels', () => { ); }); - it('should pass custom headers for Anthropic endpoint', async () => { + it('should merge custom headers on top of Anthropic defaults', async () => { const customHeaders = { 'X-Custom-Header': 'custom-value', }; + (resolveHeaders as jest.Mock).mockReturnValueOnce(customHeaders); + mockedAxios.get.mockResolvedValue({ data: { data: [{ id: 'claude-3' }], @@ -573,6 +611,7 @@ describe('getAnthropicModels', () => { headers: { 'x-api-key': 'test-anthropic-key', 'anthropic-version': expect.any(String), + 'X-Custom-Header': 'custom-value', }, }), ); diff --git a/packages/api/src/endpoints/models.ts b/packages/api/src/endpoints/models.ts index 715b80656597..9e241294a47d 100644 --- a/packages/api/src/endpoints/models.ts +++ b/packages/api/src/endpoints/models.ts @@ -127,14 +127,17 @@ export async function fetchModels({ } try { + const resolvedCustomHeaders = resolveHeaders({ + headers: headers ?? undefined, + user: userObject, + }); + const options: { headers: Record; timeout: number; httpsAgent?: HttpsProxyAgent; } = { - headers: { - ...(headers ?? {}), - }, + headers: {}, timeout: 5000, }; @@ -147,6 +150,10 @@ export async function fetchModels({ options.headers.Authorization = `Bearer ${apiKey}`; } + // Merge resolved custom headers after defaults so config headers + // (e.g. authorization with {{LIBRECHAT_OPENID_ACCESS_TOKEN}}) take precedence + Object.assign(options.headers, resolvedCustomHeaders); + if (process.env.PROXY) { options.httpsAgent = new HttpsProxyAgent(process.env.PROXY); }