diff --git a/packages/api/src/endpoints/custom/initialize.spec.ts b/packages/api/src/endpoints/custom/initialize.spec.ts index 911e17c446..3705f98977 100644 --- a/packages/api/src/endpoints/custom/initialize.spec.ts +++ b/packages/api/src/endpoints/custom/initialize.spec.ts @@ -1,4 +1,4 @@ -import { AuthType } from 'librechat-data-provider'; +import { AuthType, ErrorTypes } from 'librechat-data-provider'; import type { BaseInitializeParams } from '~/types'; const mockValidateEndpointURL = jest.fn(); @@ -68,6 +68,97 @@ function createParams(overrides: { }; } +describe('initializeCustom – Agents API user key resolution', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch user key even when expiresAt is not in request body (Agents API flow)', async () => { + const { checkUserKeyExpiry } = jest.requireMock('~/utils'); + const params = createParams({ + apiKey: AuthType.USER_PROVIDED, + baseURL: 'https://api.example.com/v1', + userApiKey: 'sk-user-key', + }); + // Simulate Agents API request body (no `key` field) + params.req.body = { model: 'agent_123', messages: [] }; + + await initializeCustom(params); + + expect(params.db.getUserKeyValues).toHaveBeenCalledWith({ + userId: 'user-1', + name: 'test-custom', + }); + expect(checkUserKeyExpiry).not.toHaveBeenCalled(); + expect(mockGetOpenAIConfig).toHaveBeenCalledWith( + 'sk-user-key', + expect.any(Object), + 'test-custom', + ); + }); + + it('should fetch user key for user-provided URL without expiresAt (Agents API flow)', async () => { + const { checkUserKeyExpiry } = jest.requireMock('~/utils'); + const params = createParams({ + apiKey: 'sk-system-key', + baseURL: AuthType.USER_PROVIDED, + userBaseURL: 'https://user-api.example.com/v1', + }); + params.req.body = { model: 'agent_123', messages: [] }; + + await initializeCustom(params); + + expect(params.db.getUserKeyValues).toHaveBeenCalledWith({ + userId: 'user-1', + name: 'test-custom', + }); + expect(checkUserKeyExpiry).not.toHaveBeenCalled(); + }); + + it('should still check key expiry when expiresAt is provided (UI flow)', async () => { + const { checkUserKeyExpiry } = jest.requireMock('~/utils'); + const params = createParams({ + apiKey: AuthType.USER_PROVIDED, + baseURL: 'https://api.example.com/v1', + userApiKey: 'sk-user-key', + expiresAt: '2099-01-01', + }); + + await initializeCustom(params); + + expect(checkUserKeyExpiry).toHaveBeenCalledWith('2099-01-01', 'test-custom'); + expect(params.db.getUserKeyValues).toHaveBeenCalled(); + }); + + it('should throw EXPIRED_USER_KEY when expiresAt is expired', async () => { + const { checkUserKeyExpiry } = jest.requireMock('~/utils'); + checkUserKeyExpiry.mockImplementationOnce(() => { + throw new Error(JSON.stringify({ type: ErrorTypes.EXPIRED_USER_KEY })); + }); + + const params = createParams({ + apiKey: AuthType.USER_PROVIDED, + baseURL: 'https://api.example.com/v1', + userApiKey: 'sk-user-key', + expiresAt: '2020-01-01', + }); + + await expect(initializeCustom(params)).rejects.toThrow(ErrorTypes.EXPIRED_USER_KEY); + expect(params.db.getUserKeyValues).not.toHaveBeenCalled(); + }); + + it('should NOT call getUserKeyValues when key and URL are system-defined', async () => { + const params = createParams({ + apiKey: 'sk-system-key', + baseURL: 'https://api.provider.com/v1', + }); + + await initializeCustom(params); + + expect(params.db.getUserKeyValues).not.toHaveBeenCalled(); + }); +}); + describe('initializeCustom – SSRF guard wiring', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/api/src/endpoints/custom/initialize.ts b/packages/api/src/endpoints/custom/initialize.ts index 15b6b873c7..1250721500 100644 --- a/packages/api/src/endpoints/custom/initialize.ts +++ b/packages/api/src/endpoints/custom/initialize.ts @@ -91,9 +91,15 @@ export async function initializeCustom({ const userProvidesKey = isUserProvided(CUSTOM_API_KEY); const userProvidesURL = isUserProvided(CUSTOM_BASE_URL); - let userValues = null; + // Expiry is only checked when present: the Agents API sends an OpenAI-compatible + // request body that does not include `key` (the expiry timestamp), so expiresAt + // will be undefined in that flow. The key is still fetched regardless. if (expiresAt && (userProvidesKey || userProvidesURL)) { checkUserKeyExpiry(expiresAt, endpoint); + } + + let userValues = null; + if (userProvidesKey || userProvidesURL) { userValues = await db.getUserKeyValues({ userId: req.user?.id ?? '', name: endpoint }); }