Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions api/server/controllers/ModelController.js
Original file line number Diff line number Diff line change
@@ -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<TModelsConfig>} 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);
};

/**
Expand All @@ -23,18 +15,10 @@ const getModelsConfig = async (req) => {
* @returns {Promise<TModelsConfig>} 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) {
Expand Down
43 changes: 41 additions & 2 deletions packages/api/src/endpoints/models.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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({
Expand All @@ -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',
Expand Down Expand Up @@ -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' }],
Expand All @@ -573,6 +611,7 @@ describe('getAnthropicModels', () => {
headers: {
'x-api-key': 'test-anthropic-key',
'anthropic-version': expect.any(String),
'X-Custom-Header': 'custom-value',
},
}),
);
Expand Down
13 changes: 10 additions & 3 deletions packages/api/src/endpoints/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,17 @@ export async function fetchModels({
}

try {
const resolvedCustomHeaders = resolveHeaders({
headers: headers ?? undefined,
user: userObject,
});

const options: {
headers: Record<string, string>;
timeout: number;
httpsAgent?: HttpsProxyAgent<string>;
} = {
headers: {
...(headers ?? {}),
},
headers: {},
timeout: 5000,
};

Expand All @@ -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);
}
Expand Down
Loading