/** * Unit tests for Open Responses API controller * Tests that recordCollectedUsage is called correctly for token spending */ const mockSpendTokens = jest.fn().mockResolvedValue({}); const mockSpendStructuredTokens = jest.fn().mockResolvedValue({}); const mockRecordCollectedUsage = jest .fn() .mockResolvedValue({ input_tokens: 100, output_tokens: 50 }); const mockGetBalanceConfig = jest.fn().mockReturnValue({ enabled: true }); const mockGetTransactionsConfig = jest.fn().mockReturnValue({ enabled: true }); const mockBuildSkillPrimedIdsByName = jest.fn((manualSkillPrimes, alwaysApplySkillPrimes) => { const primed = {}; for (const skill of alwaysApplySkillPrimes ?? []) { primed[skill.name] = skill._id.toString(); } for (const skill of manualSkillPrimes ?? []) { primed[skill.name] = skill._id.toString(); } return Object.keys(primed).length > 0 ? primed : undefined; }); const mockEnrichWithSkillConfigurable = jest.fn((result) => result); const mockBuildAgentToolContext = jest.fn(({ agent, config }) => ({ agent, toolRegistry: config.toolRegistry, userMCPAuthMap: config.userMCPAuthMap, tool_resources: config.tool_resources, actionsEnabled: config.actionsEnabled, accessibleSkillIds: config.accessibleSkillIds, activeSkillNames: config.activeSkillNames, codeEnvAvailable: config.codeEnvAvailable, skillAuthoringAvailable: config.skillAuthoringAvailable, fileAuthoringToolNames: config.fileAuthoringToolNames, skillPrimedIdsByName: mockBuildSkillPrimedIdsByName(config.manualSkillPrimes, config.alwaysApplySkillPrimes) ?? {}, })); const mockEnrichLoadedToolsWithAgentContext = jest.fn(({ result, req, ctx }) => mockEnrichWithSkillConfigurable({ result, context: { req, accessibleSkillIds: ctx.accessibleSkillIds, codeEnvAvailable: ctx.codeEnvAvailable === true, skillPrimedIdsByName: ctx.skillPrimedIdsByName, activeSkillNames: ctx.activeSkillNames, skillAuthoringAvailable: ctx.skillAuthoringAvailable === true, fileAuthoringToolNames: ctx.fileAuthoringToolNames, }, }), ); const mockCanAuthorSkillFiles = jest.fn( ({ scopedEditableSkillIds = [], skillCreateAllowed }) => scopedEditableSkillIds.length > 0 || skillCreateAllowed === true, ); const mockGetSkillToolDeps = jest.fn(() => ({})); const mockBuildAgentScopedContext = jest.fn().mockResolvedValue(new Map()); const mockBuildAgentContextAttachmentsByAgentId = jest.fn().mockReturnValue(new Map()); const mockApplyContextToAgent = jest.fn().mockResolvedValue(undefined); jest.mock('nanoid', () => ({ nanoid: jest.fn(() => 'mock-nanoid-123'), })); jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid-456'), })); jest.mock('@librechat/data-schemas', () => ({ logger: { debug: jest.fn(), error: jest.fn(), warn: jest.fn(), }, })); jest.mock('@librechat/agents', () => ({ Callback: { TOOL_ERROR: 'TOOL_ERROR' }, ToolEndHandler: jest.fn(), formatAgentMessages: jest.fn().mockReturnValue({ messages: [], indexTokenCountMap: {}, }), })); jest.mock('@librechat/api', () => ({ createRun: jest.fn().mockResolvedValue({ processStream: jest.fn().mockResolvedValue(undefined), }), applyContextToAgent: (...args) => mockApplyContextToAgent(...args), buildToolSet: jest.fn().mockReturnValue(new Set()), buildAgentScopedContext: (...args) => mockBuildAgentScopedContext(...args), buildAgentContextAttachmentsByAgentId: (...args) => mockBuildAgentContextAttachmentsByAgentId(...args), scopeSkillIds: jest.fn().mockImplementation((ids) => ids), resolveAgentScopedSkillIds: jest .fn() .mockImplementation(({ accessibleSkillIds }) => accessibleSkillIds), loadSkillStates: jest.fn().mockResolvedValue({ skillStates: {}, defaultActiveOnShare: false }), createSafeUser: jest.fn().mockReturnValue({ id: 'user-123' }), initializeAgent: jest.fn().mockResolvedValue({ id: 'agent-123', model: 'claude-3', model_parameters: {}, toolRegistry: {}, edges: [], agentContextAttachments: [], }), discoverConnectedAgents: jest.fn().mockImplementation(async (computedParams, deps) => { // Call onAgentInitialized for each agent config if provided by the mock setup if (deps?.onAgentInitialized && mockGlobalDiscoveredAgentConfigs) { for (const [agentId, config] of mockGlobalDiscoveredAgentConfigs) { deps.onAgentInitialized(agentId, config, config); } } return { agentConfigs: mockGlobalDiscoveredAgentConfigs ?? new Map(), edges: [], skippedAgentIds: new Set(), userMCPAuthMap: undefined, }; }), getBalanceConfig: mockGetBalanceConfig, getTransactionsConfig: mockGetTransactionsConfig, recordCollectedUsage: mockRecordCollectedUsage, createSubagentUsageSink: jest.fn().mockReturnValue(jest.fn()), extractManualSkills: jest.fn().mockReturnValue(undefined), injectSkillPrimes: jest.fn().mockReturnValue({ initialMessages: [], indexTokenCountMap: {}, inserted: 0, insertIdx: -1, alwaysApplyDropped: 0, alwaysApplyDedupedFromManual: 0, }), createToolExecuteHandler: jest.fn().mockReturnValue({ handle: jest.fn() }), // Responses API writeDone: jest.fn(), buildResponse: jest.fn().mockReturnValue({ id: 'resp_123', output: [] }), generateResponseId: jest.fn().mockReturnValue('resp_mock-123'), isValidationFailure: jest.fn().mockReturnValue(false), findPiiMatchInMessages: jest.fn().mockReturnValue(null), emitResponseCreated: jest.fn(), createResponseContext: jest.fn().mockReturnValue({ responseId: 'resp_123' }), createResponseTracker: jest.fn().mockReturnValue({ usage: { promptTokens: 100, completionTokens: 50 }, }), setupStreamingResponse: jest.fn(), emitResponseInProgress: jest.fn(), convertInputToMessages: jest.fn().mockReturnValue([]), validateResponseRequest: jest.fn().mockReturnValue({ request: { model: 'agent-123', input: 'Hello', stream: false }, }), buildAggregatedResponse: jest.fn().mockReturnValue({ id: 'resp_123', status: 'completed', output: [], usage: { input_tokens: 100, output_tokens: 50, total_tokens: 150 }, }), createResponseAggregator: jest.fn().mockReturnValue({ usage: { promptTokens: 100, completionTokens: 50 }, }), sendResponsesErrorResponse: jest.fn(), createResponsesEventHandlers: jest.fn().mockReturnValue({ handlers: { on_message_delta: { handle: jest.fn() }, on_reasoning_delta: { handle: jest.fn() }, on_run_step: { handle: jest.fn() }, on_run_step_delta: { handle: jest.fn() }, on_chat_model_end: { handle: jest.fn() }, }, finalizeStream: jest.fn(), }), createAggregatorEventHandlers: jest.fn().mockReturnValue({ on_message_delta: { handle: jest.fn() }, on_reasoning_delta: { handle: jest.fn() }, on_run_step: { handle: jest.fn() }, on_run_step_delta: { handle: jest.fn() }, on_chat_model_end: { handle: jest.fn() }, }), })); jest.mock('~/server/services/ToolService', () => ({ loadAgentTools: jest.fn().mockResolvedValue([]), loadToolsForExecution: jest.fn().mockResolvedValue([]), })); const mockGetMultiplier = jest.fn().mockReturnValue(1); const mockGetCacheMultiplier = jest.fn().mockReturnValue(null); jest.mock('~/server/controllers/agents/callbacks', () => { const noop = { handle: jest.fn() }; return { createToolEndCallback: jest.fn().mockReturnValue(jest.fn()), createResponsesToolEndCallback: jest.fn().mockReturnValue(jest.fn()), markSummarizationUsage: jest.fn().mockImplementation((usage) => usage), agentLogHandlerObj: noop, buildSummarizationHandlers: jest.fn().mockReturnValue({ on_summarize_start: noop, on_summarize_delta: noop, on_summarize_complete: noop, }), }; }); jest.mock('~/server/services/PermissionService', () => ({ findAccessibleResources: jest.fn().mockResolvedValue([]), checkPermission: jest.fn().mockResolvedValue(true), })); jest.mock('~/server/controllers/ModelController', () => ({ getModelsConfig: jest.fn().mockResolvedValue({}), })); jest.mock('~/server/services/MCP', () => ({ resolveConfigServers: jest.fn().mockResolvedValue({}), })); jest.mock('~/config', () => ({ getMCPManager: jest.fn().mockReturnValue({}), })); jest.mock('~/server/services/Files/permissions', () => ({ filterFilesByAgentAccess: jest.fn(), })); jest.mock('~/server/services/Endpoints/agents/skillDeps', () => ({ getSkillToolDeps: mockGetSkillToolDeps, getSkillDbMethods: jest.fn(() => ({})), canAuthorSkillFiles: mockCanAuthorSkillFiles, withDeploymentSkillIds: jest.fn((ids = []) => ids), enrichWithSkillConfigurable: mockEnrichWithSkillConfigurable, buildSkillPrimedIdsByName: mockBuildSkillPrimedIdsByName, buildAgentToolContext: mockBuildAgentToolContext, enrichLoadedToolsWithAgentContext: mockEnrichLoadedToolsWithAgentContext, })); jest.mock('~/cache', () => ({ logViolation: jest.fn(), })); jest.mock('~/server/services/Files/strategies', () => ({ getStrategyFunctions: jest.fn().mockReturnValue({}), })); jest.mock('~/server/services/Files/Code/crud', () => ({ batchUploadCodeEnvFiles: jest.fn().mockResolvedValue({ session_id: '', files: [] }), })); jest.mock('~/server/services/Files/Code/process', () => ({ getSessionInfo: jest.fn().mockResolvedValue(null), checkIfActive: jest.fn().mockReturnValue(false), })); const mockUpdateBalance = jest.fn().mockResolvedValue({}); const mockBulkInsertTransactions = jest.fn().mockResolvedValue(undefined); jest.mock('~/models', () => ({ getAgent: jest.fn().mockResolvedValue({ id: 'agent-123', name: 'Test Agent' }), getFiles: jest.fn(), getUserKey: jest.fn(), getMessages: jest.fn().mockResolvedValue([]), saveMessage: jest.fn().mockResolvedValue({}), updateFilesUsage: jest.fn(), getUserKeyValues: jest.fn(), getUserCodeFiles: jest.fn(), getToolFilesByIds: jest.fn(), getCodeGeneratedFiles: jest.fn(), updateBalance: mockUpdateBalance, bulkInsertTransactions: mockBulkInsertTransactions, spendTokens: mockSpendTokens, spendStructuredTokens: mockSpendStructuredTokens, getMultiplier: mockGetMultiplier, getCacheMultiplier: mockGetCacheMultiplier, getConvoFiles: jest.fn().mockResolvedValue([]), saveConvo: jest.fn().mockResolvedValue({}), getConvo: jest.fn().mockResolvedValue(null), })); let mockGlobalDiscoveredAgentConfigs = null; describe('createResponse controller', () => { let createResponse; let req, res; beforeEach(() => { jest.clearAllMocks(); mockGlobalDiscoveredAgentConfigs = null; const controller = require('../responses'); createResponse = controller.createResponse; req = { body: { model: 'agent-123', input: 'Hello', stream: false, }, user: { id: 'user-123' }, config: { endpoints: { agents: { allowedProviders: ['anthropic'] }, }, }, on: jest.fn(), }; res = { status: jest.fn().mockReturnThis(), json: jest.fn(), setHeader: jest.fn(), flushHeaders: jest.fn(), end: jest.fn(), write: jest.fn(), }; }); describe('conversation ownership validation', () => { it('should skip ownership check when previous_response_id is not provided', async () => { const { getConvo } = require('~/models'); await createResponse(req, res); expect(getConvo).not.toHaveBeenCalled(); }); it('should return 400 when previous_response_id is not a string', async () => { const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); validateResponseRequest.mockReturnValueOnce({ request: { model: 'agent-123', input: 'Hello', stream: false, previous_response_id: { $gt: '' }, }, }); await createResponse(req, res); expect(sendResponsesErrorResponse).toHaveBeenCalledWith( res, 400, 'previous_response_id must be a string', 'invalid_request', ); }); it('should return 404 when conversation is not owned by user', async () => { const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); const { getConvo } = require('~/models'); validateResponseRequest.mockReturnValueOnce({ request: { model: 'agent-123', input: 'Hello', stream: false, previous_response_id: 'resp_abc', }, }); getConvo.mockResolvedValueOnce(null); await createResponse(req, res); expect(getConvo).toHaveBeenCalledWith('user-123', 'resp_abc'); expect(sendResponsesErrorResponse).toHaveBeenCalledWith( res, 404, 'Conversation not found', 'not_found', ); }); it('should proceed when conversation is owned by user', async () => { const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); const { getConvo } = require('~/models'); validateResponseRequest.mockReturnValueOnce({ request: { model: 'agent-123', input: 'Hello', stream: false, previous_response_id: 'resp_abc', }, }); getConvo.mockResolvedValueOnce({ conversationId: 'resp_abc', user: 'user-123' }); await createResponse(req, res); expect(getConvo).toHaveBeenCalledWith('user-123', 'resp_abc'); expect(sendResponsesErrorResponse).not.toHaveBeenCalledWith( res, 404, expect.any(String), expect.any(String), ); }); it('should return 500 when getConvo throws a DB error', async () => { const { validateResponseRequest, sendResponsesErrorResponse } = require('@librechat/api'); const { getConvo } = require('~/models'); validateResponseRequest.mockReturnValueOnce({ request: { model: 'agent-123', input: 'Hello', stream: false, previous_response_id: 'resp_abc', }, }); getConvo.mockRejectedValueOnce(new Error('DB connection failed')); await createResponse(req, res); expect(sendResponsesErrorResponse).toHaveBeenCalledWith( res, 500, expect.any(String), expect.any(String), ); }); }); describe('token usage recording - non-streaming', () => { it('should call recordCollectedUsage after successful non-streaming completion', async () => { await createResponse(req, res); expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); expect(mockRecordCollectedUsage).toHaveBeenCalledWith( { spendTokens: mockSpendTokens, spendStructuredTokens: mockSpendStructuredTokens, pricing: { getMultiplier: mockGetMultiplier, getCacheMultiplier: mockGetCacheMultiplier }, bulkWriteOps: { insertMany: mockBulkInsertTransactions, updateBalance: mockUpdateBalance, }, }, expect.objectContaining({ user: 'user-123', conversationId: expect.any(String), collectedUsage: expect.any(Array), context: 'message', }), ); }); it('should pass balance and transactions config to recordCollectedUsage', async () => { mockGetBalanceConfig.mockReturnValue({ enabled: true, startBalance: 2000 }); mockGetTransactionsConfig.mockReturnValue({ enabled: true }); await createResponse(req, res); expect(mockRecordCollectedUsage).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ balance: { enabled: true, startBalance: 2000 }, transactions: { enabled: true }, }), ); }); it('should pass spendTokens, spendStructuredTokens, pricing, and bulkWriteOps as dependencies', async () => { await createResponse(req, res); const [deps] = mockRecordCollectedUsage.mock.calls[0]; expect(deps).toHaveProperty('spendTokens', mockSpendTokens); expect(deps).toHaveProperty('spendStructuredTokens', mockSpendStructuredTokens); expect(deps).toHaveProperty('pricing'); expect(deps.pricing).toHaveProperty('getMultiplier', mockGetMultiplier); expect(deps.pricing).toHaveProperty('getCacheMultiplier', mockGetCacheMultiplier); expect(deps).toHaveProperty('bulkWriteOps'); expect(deps.bulkWriteOps).toHaveProperty('insertMany', mockBulkInsertTransactions); expect(deps.bulkWriteOps).toHaveProperty('updateBalance', mockUpdateBalance); }); it('should include model from primaryConfig in recordCollectedUsage params', async () => { await createResponse(req, res); expect(mockRecordCollectedUsage).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ model: 'claude-3', }), ); }); }); describe('agent context parity with UI path', () => { it('applies agent-scoped attachment context before createRun', async () => { const api = require('@librechat/api'); api.initializeAgent.mockResolvedValueOnce({ id: 'agent-123', model: 'claude-3', model_parameters: {}, toolRegistry: {}, edges: [], agentContextAttachments: [{ file_id: 'file-1', filename: 'ocr_file.pdf' }], }); mockBuildAgentContextAttachmentsByAgentId.mockReturnValueOnce( new Map([['agent-123', [{ file_id: 'file-1', filename: 'ocr_file.pdf' }]]]), ); mockBuildAgentScopedContext.mockResolvedValueOnce( new Map([['agent-123', 'PDF context: ocr_file.pdf']]), ); await createResponse(req, res); expect(mockBuildAgentContextAttachmentsByAgentId).toHaveBeenCalledWith([ expect.objectContaining({ id: 'agent-123' }), ]); expect(mockBuildAgentScopedContext).toHaveBeenCalledWith( expect.objectContaining({ agentIds: ['agent-123'], attachmentsByAgentId: expect.any(Map), req, }), ); expect(mockApplyContextToAgent).toHaveBeenCalledWith( expect.objectContaining({ agent: expect.objectContaining({ id: 'agent-123' }), agentId: 'agent-123', sharedRunContext: 'PDF context: ocr_file.pdf', }), ); }); it('applies context to primary and discovered handoff agents', async () => { const api = require('@librechat/api'); const handoffConfig = { id: 'agent-handoff', model: 'claude-3', model_parameters: {}, toolRegistry: {}, edges: [], agentContextAttachments: [{ file_id: 'file-2', filename: 'handoff_context.pdf' }], }; // Set primary agent to have edges pointing to handoff agent api.initializeAgent.mockResolvedValueOnce({ id: 'agent-123', model: 'claude-3', model_parameters: {}, toolRegistry: {}, edges: [{ source: 'agent-123', target: 'agent-handoff' }], agentContextAttachments: [{ file_id: 'file-1', filename: 'primary_context.pdf' }], }); // Set global config so discoverConnectedAgents mock can invoke onAgentInitialized mockGlobalDiscoveredAgentConfigs = new Map([['agent-handoff', handoffConfig]]); mockBuildAgentScopedContext.mockResolvedValueOnce( new Map([ ['agent-123', 'Primary context'], ['agent-handoff', 'Handoff context'], ]), ); await createResponse(req, res); const appliedAgentIds = mockApplyContextToAgent.mock.calls.map((call) => call[0].agentId); expect(appliedAgentIds).toEqual(expect.arrayContaining(['agent-123', 'agent-handoff'])); expect(mockApplyContextToAgent).toHaveBeenCalledWith( expect.objectContaining({ agentId: 'agent-handoff', sharedRunContext: 'Handoff context', }), ); }); }); describe('token usage recording - streaming', () => { beforeEach(() => { req.body.stream = true; const api = require('@librechat/api'); api.validateResponseRequest.mockReturnValue({ request: { model: 'agent-123', input: 'Hello', stream: true }, }); }); it('should call recordCollectedUsage after successful streaming completion', async () => { await createResponse(req, res); expect(mockRecordCollectedUsage).toHaveBeenCalledTimes(1); expect(mockRecordCollectedUsage).toHaveBeenCalledWith( { spendTokens: mockSpendTokens, spendStructuredTokens: mockSpendStructuredTokens, pricing: { getMultiplier: mockGetMultiplier, getCacheMultiplier: mockGetCacheMultiplier }, bulkWriteOps: { insertMany: mockBulkInsertTransactions, updateBalance: mockUpdateBalance, }, }, expect.objectContaining({ user: 'user-123', context: 'message', }), ); }); }); describe('collectedUsage population', () => { it('should collect usage from on_chat_model_end events', async () => { const api = require('@librechat/api'); api.createRun.mockImplementation(async ({ customHandlers }) => { return { processStream: jest.fn().mockImplementation(async () => { customHandlers.on_chat_model_end.handle('on_chat_model_end', { output: { usage_metadata: { input_tokens: 150, output_tokens: 75, model: 'claude-3', }, }, }); }), }; }); await createResponse(req, res); expect(mockRecordCollectedUsage).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ collectedUsage: expect.arrayContaining([ expect.objectContaining({ input_tokens: 150, output_tokens: 75, }), ]), }), ); }); }); describe('sub-agent skill priming', () => { it('passes the sub-agent primed skill IDs into non-streaming tool execution', async () => { const { initializeAgent, discoverConnectedAgents, createToolExecuteHandler, } = require('@librechat/api'); const { loadToolsForExecution } = require('~/server/services/ToolService'); const subAgent = { id: 'agent-sub', name: 'Sub Agent' }; const subConfig = { id: 'agent-sub', model: 'claude-3', model_parameters: {}, toolRegistry: new Map(), userMCPAuthMap: { sub: { token: 'sub-token' } }, tool_resources: { code_interpreter: { file_ids: ['sub-file'] } }, actionsEnabled: true, accessibleSkillIds: ['sub-skill-id'], activeSkillNames: ['sub-hidden-skill'], codeEnvAvailable: true, skillAuthoringAvailable: true, fileAuthoringToolNames: ['create_file', 'edit_file'], manualSkillPrimes: [{ name: 'sub-hidden-skill', _id: { toString: () => 'sub-manual-id' } }], alwaysApplySkillPrimes: [ { name: 'sub-always-skill', _id: { toString: () => 'sub-always-id' } }, ], }; initializeAgent.mockResolvedValueOnce({ id: 'agent-123', model: 'claude-3', model_parameters: {}, toolRegistry: new Map(), edges: [{ source: 'agent-123', target: 'agent-sub' }], accessibleSkillIds: ['primary-skill-id'], activeSkillNames: ['primary-skill'], codeEnvAvailable: false, skillAuthoringAvailable: false, fileAuthoringToolNames: [], manualSkillPrimes: [{ name: 'primary-skill', _id: { toString: () => 'primary-skill-id' } }], }); discoverConnectedAgents.mockImplementationOnce(async (_params, deps) => { deps.onAgentInitialized('agent-sub', subAgent, subConfig); return { agentConfigs: new Map([['agent-sub', subConfig]]), edges: [], skippedAgentIds: new Set(), userMCPAuthMap: undefined, }; }); await createResponse(req, res); const toolExecuteOptions = createToolExecuteHandler.mock.calls.at(-1)[0]; await toolExecuteOptions.loadTools(['read_file'], 'agent-sub'); expect(loadToolsForExecution).toHaveBeenLastCalledWith( expect.objectContaining({ agent: subAgent, toolRegistry: subConfig.toolRegistry, userMCPAuthMap: subConfig.userMCPAuthMap, tool_resources: subConfig.tool_resources, actionsEnabled: true, }), ); expect(mockEnrichWithSkillConfigurable).toHaveBeenLastCalledWith({ result: expect.anything(), context: { req, accessibleSkillIds: ['sub-skill-id'], codeEnvAvailable: true, skillPrimedIdsByName: { 'sub-always-skill': 'sub-always-id', 'sub-hidden-skill': 'sub-manual-id', }, activeSkillNames: ['sub-hidden-skill'], skillAuthoringAvailable: true, fileAuthoringToolNames: ['create_file', 'edit_file'], }, }); }); }); });