diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index 27a54c9874..b781c30b69 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -1,5 +1,6 @@ const { Providers } = require('@librechat/agents'); const { Constants, EModelEndpoint } = require('librechat-data-provider'); +const { getAppConfig } = require('~/server/services/Config'); const AgentClient = require('./client'); jest.mock('@librechat/agents', () => ({ @@ -10,6 +11,10 @@ jest.mock('@librechat/agents', () => ({ }), })); +jest.mock('~/server/services/Config', () => ({ + getAppConfig: jest.fn(), +})); + describe('AgentClient - titleConvo', () => { let client; let mockRun; @@ -39,19 +44,19 @@ describe('AgentClient - titleConvo', () => { }, }; + // Mock getAppConfig to return endpoint configurations + getAppConfig.mockResolvedValue({ + [EModelEndpoint.openAI]: { + // Match the agent endpoint + titleModel: 'gpt-3.5-turbo', + titlePrompt: 'Custom title prompt', + titleMethod: 'structured', + titlePromptTemplate: 'Template: {{content}}', + }, + }); + // Mock request and response mockReq = { - app: { - locals: { - [EModelEndpoint.openAI]: { - // Match the agent endpoint - titleModel: 'gpt-3.5-turbo', - titlePrompt: 'Custom title prompt', - titleMethod: 'structured', - titlePromptTemplate: 'Template: {{content}}', - }, - }, - }, user: { id: 'user-123', }, @@ -143,7 +148,7 @@ describe('AgentClient - titleConvo', () => { it('should handle missing endpoint config gracefully', async () => { // Remove endpoint config - mockReq.app.locals[EModelEndpoint.openAI] = undefined; + getAppConfig.mockResolvedValue({}); const text = 'Test conversation text'; const abortController = new AbortController(); @@ -161,7 +166,14 @@ describe('AgentClient - titleConvo', () => { it('should use agent model when titleModel is not provided', async () => { // Remove titleModel from config - delete mockReq.app.locals[EModelEndpoint.openAI].titleModel; + getAppConfig.mockResolvedValue({ + [EModelEndpoint.openAI]: { + titlePrompt: 'Custom title prompt', + titleMethod: 'structured', + titlePromptTemplate: 'Template: {{content}}', + // titleModel is omitted + }, + }); const text = 'Test conversation text'; const abortController = new AbortController(); @@ -173,7 +185,14 @@ describe('AgentClient - titleConvo', () => { }); it('should not use titleModel when it equals CURRENT_MODEL constant', async () => { - mockReq.app.locals[EModelEndpoint.openAI].titleModel = Constants.CURRENT_MODEL; + getAppConfig.mockResolvedValue({ + [EModelEndpoint.openAI]: { + titleModel: Constants.CURRENT_MODEL, + titlePrompt: 'Custom title prompt', + titleMethod: 'structured', + titlePromptTemplate: 'Template: {{content}}', + }, + }); const text = 'Test conversation text'; const abortController = new AbortController(); @@ -245,10 +264,15 @@ describe('AgentClient - titleConvo', () => { process.env.ANTHROPIC_API_KEY = 'test-api-key'; // Add titleEndpoint to the config - mockReq.app.locals[EModelEndpoint.openAI].titleEndpoint = EModelEndpoint.anthropic; - mockReq.app.locals[EModelEndpoint.openAI].titleMethod = 'structured'; - mockReq.app.locals[EModelEndpoint.openAI].titlePrompt = 'Custom title prompt'; - mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate = 'Custom template'; + getAppConfig.mockResolvedValue({ + [EModelEndpoint.openAI]: { + titleModel: 'gpt-3.5-turbo', + titleEndpoint: EModelEndpoint.anthropic, + titleMethod: 'structured', + titlePrompt: 'Custom title prompt', + titlePromptTemplate: 'Custom template', + }, + }); const text = 'Test conversation text'; const abortController = new AbortController(); @@ -274,19 +298,15 @@ describe('AgentClient - titleConvo', () => { }); it('should use all config when endpoint config is missing', async () => { - // Remove endpoint-specific config - delete mockReq.app.locals[EModelEndpoint.openAI].titleModel; - delete mockReq.app.locals[EModelEndpoint.openAI].titlePrompt; - delete mockReq.app.locals[EModelEndpoint.openAI].titleMethod; - delete mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate; - - // Set 'all' config - mockReq.app.locals.all = { - titleModel: 'gpt-4o-mini', - titlePrompt: 'All config title prompt', - titleMethod: 'completion', - titlePromptTemplate: 'All config template: {{content}}', - }; + // Set 'all' config without endpoint-specific config + getAppConfig.mockResolvedValue({ + all: { + titleModel: 'gpt-4o-mini', + titlePrompt: 'All config title prompt', + titleMethod: 'completion', + titlePromptTemplate: 'All config template: {{content}}', + }, + }); const text = 'Test conversation text'; const abortController = new AbortController(); @@ -309,18 +329,20 @@ describe('AgentClient - titleConvo', () => { it('should prioritize all config over endpoint config for title settings', async () => { // Set both endpoint and 'all' config - mockReq.app.locals[EModelEndpoint.openAI].titleModel = 'gpt-3.5-turbo'; - mockReq.app.locals[EModelEndpoint.openAI].titlePrompt = 'Endpoint title prompt'; - mockReq.app.locals[EModelEndpoint.openAI].titleMethod = 'structured'; - // Remove titlePromptTemplate from endpoint config to test fallback - delete mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate; - - mockReq.app.locals.all = { - titleModel: 'gpt-4o-mini', - titlePrompt: 'All config title prompt', - titleMethod: 'completion', - titlePromptTemplate: 'All config template', - }; + getAppConfig.mockResolvedValue({ + [EModelEndpoint.openAI]: { + titleModel: 'gpt-3.5-turbo', + titlePrompt: 'Endpoint title prompt', + titleMethod: 'structured', + // titlePromptTemplate is omitted to test fallback + }, + all: { + titleModel: 'gpt-4o-mini', + titlePrompt: 'All config title prompt', + titleMethod: 'completion', + titlePromptTemplate: 'All config template', + }, + }); const text = 'Test conversation text'; const abortController = new AbortController(); @@ -346,18 +368,17 @@ describe('AgentClient - titleConvo', () => { const originalApiKey = process.env.ANTHROPIC_API_KEY; process.env.ANTHROPIC_API_KEY = 'test-anthropic-key'; - // Remove endpoint-specific config to test 'all' config - delete mockReq.app.locals[EModelEndpoint.openAI]; - // Set comprehensive 'all' config with all new title options - mockReq.app.locals.all = { - titleConvo: true, - titleModel: 'claude-3-haiku-20240307', - titleMethod: 'completion', // Testing the new default method - titlePrompt: 'Generate a concise, descriptive title for this conversation', - titlePromptTemplate: 'Conversation summary: {{content}}', - titleEndpoint: EModelEndpoint.anthropic, // Should switch provider to Anthropic - }; + getAppConfig.mockResolvedValue({ + all: { + titleConvo: true, + titleModel: 'claude-3-haiku-20240307', + titleMethod: 'completion', // Testing the new default method + titlePrompt: 'Generate a concise, descriptive title for this conversation', + titlePromptTemplate: 'Conversation summary: {{content}}', + titleEndpoint: EModelEndpoint.anthropic, // Should switch provider to Anthropic + }, + }); const text = 'Test conversation about AI and machine learning'; const abortController = new AbortController(); @@ -402,16 +423,15 @@ describe('AgentClient - titleConvo', () => { // Clear previous calls mockRun.generateTitle.mockClear(); - // Remove endpoint config - delete mockReq.app.locals[EModelEndpoint.openAI]; - // Set 'all' config with specific titleMethod - mockReq.app.locals.all = { - titleModel: 'gpt-4o-mini', - titleMethod: method, - titlePrompt: `Testing ${method} method`, - titlePromptTemplate: `Template for ${method}: {{content}}`, - }; + getAppConfig.mockResolvedValue({ + all: { + titleModel: 'gpt-4o-mini', + titleMethod: method, + titlePrompt: `Testing ${method} method`, + titlePromptTemplate: `Template for ${method}: {{content}}`, + }, + }); const text = `Test conversation for ${method} method`; const abortController = new AbortController(); @@ -455,32 +475,34 @@ describe('AgentClient - titleConvo', () => { // Set up Azure endpoint with serverless config mockAgent.endpoint = EModelEndpoint.azureOpenAI; mockAgent.provider = EModelEndpoint.azureOpenAI; - mockReq.app.locals[EModelEndpoint.azureOpenAI] = { - titleConvo: true, - titleModel: 'grok-3', - titleMethod: 'completion', - titlePrompt: 'Azure serverless title prompt', - streamRate: 35, - modelGroupMap: { - 'grok-3': { - group: 'Azure AI Foundry', - deploymentName: 'grok-3', + getAppConfig.mockResolvedValue({ + [EModelEndpoint.azureOpenAI]: { + titleConvo: true, + titleModel: 'grok-3', + titleMethod: 'completion', + titlePrompt: 'Azure serverless title prompt', + streamRate: 35, + modelGroupMap: { + 'grok-3': { + group: 'Azure AI Foundry', + deploymentName: 'grok-3', + }, }, - }, - groupMap: { - 'Azure AI Foundry': { - apiKey: '${AZURE_API_KEY}', - baseURL: 'https://test.services.ai.azure.com/models', - version: '2024-05-01-preview', - serverless: true, - models: { - 'grok-3': { - deploymentName: 'grok-3', + groupMap: { + 'Azure AI Foundry': { + apiKey: '${AZURE_API_KEY}', + baseURL: 'https://test.services.ai.azure.com/models', + version: '2024-05-01-preview', + serverless: true, + models: { + 'grok-3': { + deploymentName: 'grok-3', + }, }, }, }, }, - }; + }); mockReq.body.endpoint = EModelEndpoint.azureOpenAI; mockReq.body.model = 'grok-3'; @@ -503,31 +525,33 @@ describe('AgentClient - titleConvo', () => { // Set up Azure endpoint mockAgent.endpoint = EModelEndpoint.azureOpenAI; mockAgent.provider = EModelEndpoint.azureOpenAI; - mockReq.app.locals[EModelEndpoint.azureOpenAI] = { - titleConvo: true, - titleModel: 'gpt-4o', - titleMethod: 'structured', - titlePrompt: 'Azure instance title prompt', - streamRate: 35, - modelGroupMap: { - 'gpt-4o': { - group: 'eastus', - deploymentName: 'gpt-4o', + getAppConfig.mockResolvedValue({ + [EModelEndpoint.azureOpenAI]: { + titleConvo: true, + titleModel: 'gpt-4o', + titleMethod: 'structured', + titlePrompt: 'Azure instance title prompt', + streamRate: 35, + modelGroupMap: { + 'gpt-4o': { + group: 'eastus', + deploymentName: 'gpt-4o', + }, }, - }, - groupMap: { - eastus: { - apiKey: '${EASTUS_API_KEY}', - instanceName: 'region-instance', - version: '2024-02-15-preview', - models: { - 'gpt-4o': { - deploymentName: 'gpt-4o', + groupMap: { + eastus: { + apiKey: '${EASTUS_API_KEY}', + instanceName: 'region-instance', + version: '2024-02-15-preview', + models: { + 'gpt-4o': { + deploymentName: 'gpt-4o', + }, }, }, }, }, - }; + }); mockReq.body.endpoint = EModelEndpoint.azureOpenAI; mockReq.body.model = 'gpt-4o'; @@ -551,32 +575,34 @@ describe('AgentClient - titleConvo', () => { mockAgent.endpoint = EModelEndpoint.azureOpenAI; mockAgent.provider = EModelEndpoint.azureOpenAI; mockAgent.model_parameters.model = 'gpt-4o-latest'; - mockReq.app.locals[EModelEndpoint.azureOpenAI] = { - titleConvo: true, - titleModel: Constants.CURRENT_MODEL, - titleMethod: 'functions', - streamRate: 35, - modelGroupMap: { - 'gpt-4o-latest': { - group: 'region-eastus', - deploymentName: 'gpt-4o-mini', - version: '2024-02-15-preview', + getAppConfig.mockResolvedValue({ + [EModelEndpoint.azureOpenAI]: { + titleConvo: true, + titleModel: Constants.CURRENT_MODEL, + titleMethod: 'functions', + streamRate: 35, + modelGroupMap: { + 'gpt-4o-latest': { + group: 'region-eastus', + deploymentName: 'gpt-4o-mini', + version: '2024-02-15-preview', + }, }, - }, - groupMap: { - 'region-eastus': { - apiKey: '${EASTUS2_API_KEY}', - instanceName: 'test-instance', - version: '2024-12-01-preview', - models: { - 'gpt-4o-latest': { - deploymentName: 'gpt-4o-mini', - version: '2024-02-15-preview', + groupMap: { + 'region-eastus': { + apiKey: '${EASTUS2_API_KEY}', + instanceName: 'test-instance', + version: '2024-12-01-preview', + models: { + 'gpt-4o-latest': { + deploymentName: 'gpt-4o-mini', + version: '2024-02-15-preview', + }, }, }, }, }, - }; + }); mockReq.body.endpoint = EModelEndpoint.azureOpenAI; mockReq.body.model = 'gpt-4o-latest'; @@ -598,59 +624,61 @@ describe('AgentClient - titleConvo', () => { // Set up Azure endpoint mockAgent.endpoint = EModelEndpoint.azureOpenAI; mockAgent.provider = EModelEndpoint.azureOpenAI; - mockReq.app.locals[EModelEndpoint.azureOpenAI] = { - titleConvo: true, - titleModel: 'o1-mini', - titleMethod: 'completion', - streamRate: 35, - modelGroupMap: { - 'gpt-4o': { - group: 'eastus', - deploymentName: 'gpt-4o', - }, - 'o1-mini': { - group: 'region-eastus', - deploymentName: 'o1-mini', - }, - 'codex-mini': { - group: 'codex-mini', - deploymentName: 'codex-mini', - }, - }, - groupMap: { - eastus: { - apiKey: '${EASTUS_API_KEY}', - instanceName: 'region-eastus', - version: '2024-02-15-preview', - models: { - 'gpt-4o': { - deploymentName: 'gpt-4o', - }, + getAppConfig.mockResolvedValue({ + [EModelEndpoint.azureOpenAI]: { + titleConvo: true, + titleModel: 'o1-mini', + titleMethod: 'completion', + streamRate: 35, + modelGroupMap: { + 'gpt-4o': { + group: 'eastus', + deploymentName: 'gpt-4o', + }, + 'o1-mini': { + group: 'region-eastus', + deploymentName: 'o1-mini', + }, + 'codex-mini': { + group: 'codex-mini', + deploymentName: 'codex-mini', }, }, - 'region-eastus': { - apiKey: '${EASTUS2_API_KEY}', - instanceName: 'region-eastus2', - version: '2024-12-01-preview', - models: { - 'o1-mini': { - deploymentName: 'o1-mini', + groupMap: { + eastus: { + apiKey: '${EASTUS_API_KEY}', + instanceName: 'region-eastus', + version: '2024-02-15-preview', + models: { + 'gpt-4o': { + deploymentName: 'gpt-4o', + }, }, }, - }, - 'codex-mini': { - apiKey: '${AZURE_API_KEY}', - baseURL: 'https://example.cognitiveservices.azure.com/openai/', - version: '2025-04-01-preview', - serverless: true, - models: { - 'codex-mini': { - deploymentName: 'codex-mini', + 'region-eastus': { + apiKey: '${EASTUS2_API_KEY}', + instanceName: 'region-eastus2', + version: '2024-12-01-preview', + models: { + 'o1-mini': { + deploymentName: 'o1-mini', + }, + }, + }, + 'codex-mini': { + apiKey: '${AZURE_API_KEY}', + baseURL: 'https://example.cognitiveservices.azure.com/openai/', + version: '2025-04-01-preview', + serverless: true, + models: { + 'codex-mini': { + deploymentName: 'codex-mini', + }, }, }, }, }, - }; + }); mockReq.body.endpoint = EModelEndpoint.azureOpenAI; mockReq.body.model = 'o1-mini'; @@ -679,36 +707,35 @@ describe('AgentClient - titleConvo', () => { mockReq.body.endpoint = EModelEndpoint.azureOpenAI; mockReq.body.model = 'gpt-4'; - // Remove Azure-specific config - delete mockReq.app.locals[EModelEndpoint.azureOpenAI]; - // Set 'all' config as fallback with a serverless Azure config - mockReq.app.locals.all = { - titleConvo: true, - titleModel: 'gpt-4', - titleMethod: 'structured', - titlePrompt: 'Fallback title prompt from all config', - titlePromptTemplate: 'Template: {{content}}', - modelGroupMap: { - 'gpt-4': { - group: 'default-group', - deploymentName: 'gpt-4', + getAppConfig.mockResolvedValue({ + all: { + titleConvo: true, + titleModel: 'gpt-4', + titleMethod: 'structured', + titlePrompt: 'Fallback title prompt from all config', + titlePromptTemplate: 'Template: {{content}}', + modelGroupMap: { + 'gpt-4': { + group: 'default-group', + deploymentName: 'gpt-4', + }, }, - }, - groupMap: { - 'default-group': { - apiKey: '${AZURE_API_KEY}', - baseURL: 'https://default.openai.azure.com/', - version: '2024-02-15-preview', - serverless: true, - models: { - 'gpt-4': { - deploymentName: 'gpt-4', + groupMap: { + 'default-group': { + apiKey: '${AZURE_API_KEY}', + baseURL: 'https://default.openai.azure.com/', + version: '2024-02-15-preview', + serverless: true, + models: { + 'gpt-4': { + deploymentName: 'gpt-4', + }, }, }, }, }, - }; + }); const text = 'Test Azure with all config fallback'; const abortController = new AbortController(); @@ -982,13 +1009,6 @@ describe('AgentClient - titleConvo', () => { }; mockReq = { - app: { - locals: { - memory: { - messageWindowSize: 3, - }, - }, - }, user: { id: 'user-123', personalization: { @@ -997,6 +1017,13 @@ describe('AgentClient - titleConvo', () => { }, }; + // Mock getAppConfig for memory tests + getAppConfig.mockResolvedValue({ + memory: { + messageWindowSize: 3, + }, + }); + mockRes = {}; mockOptions = {