From 240e3bd59ee4d3e4e66d5992d54542d94f906097 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 18 Aug 2025 15:20:58 -0400 Subject: [PATCH] refactor: update appConfig access to use endpoints structure across various services --- api/app/clients/OpenAIClient.js | 6 +- api/server/controllers/agents/client.js | 12 +- api/server/controllers/agents/client.test.js | 374 ++++++++++-------- api/server/controllers/assistants/chatV2.js | 4 +- api/server/controllers/assistants/helpers.js | 6 +- api/server/controllers/assistants/v1.js | 2 +- api/server/middleware/assistants/validate.js | 2 +- .../middleware/assistants/validateAuthor.js | 2 +- api/server/routes/assistants/actions.js | 2 +- api/server/services/AppService.js | 6 +- api/server/services/AppService.spec.js | 242 +++++++----- api/server/services/AssistantService.js | 4 +- api/server/services/Config/getCustomConfig.js | 2 +- .../services/Config/getEndpointsConfig.js | 22 +- api/server/services/Config/index.js | 2 + .../services/Config/loadAsyncEndpoints.js | 2 +- .../services/Config/loadConfigEndpoints.js | 8 +- .../services/Config/loadConfigModels.js | 6 +- .../services/Config/loadConfigModels.spec.js | 236 +++++------ .../services/Endpoints/agents/initialize.js | 5 +- .../Endpoints/anthropic/initialize.js | 5 +- .../Endpoints/azureAssistants/initialize.js | 2 +- .../services/Endpoints/bedrock/options.js | 5 +- .../services/Endpoints/custom/initialize.js | 3 +- .../services/Endpoints/google/initialize.js | 4 +- api/server/services/Endpoints/google/title.js | 2 +- .../services/Endpoints/openAI/initialize.js | 7 +- .../Endpoints/openAI/initialize.spec.js | 44 ++- api/server/services/Files/Citations/index.js | 8 +- api/server/services/Files/process.js | 4 +- api/server/services/Runs/methods.js | 2 +- api/server/services/ToolService.js | 2 +- packages/api/src/agents/resources.test.ts | 10 +- packages/api/src/agents/resources.ts | 6 +- .../api/src/endpoints/openai/initialize.ts | 6 +- packages/api/src/types/config.ts | 46 +-- 36 files changed, 591 insertions(+), 510 deletions(-) diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 78679493ef..7618f3d98c 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -718,8 +718,7 @@ class OpenAIClient extends BaseClient { max_tokens: 16, }; - /** @type {TAzureConfig | undefined} */ - const azureConfig = appConfig?.[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig?.endpoints?.[EModelEndpoint.azureOpenAI]; const resetTitleOptions = !!( (this.azure && azureConfig) || @@ -1154,8 +1153,7 @@ ${convo} opts.fetchOptions.agent = new HttpsProxyAgent(this.options.proxy); } - /** @type {TAzureConfig | undefined} */ - const azureConfig = appConfig?.[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig?.endpoints?.[EModelEndpoint.azureOpenAI]; if ( (this.azure && this.isVisionModel && azureConfig) || diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 7a598c69d2..5773c3788d 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -464,7 +464,9 @@ class AgentClient extends BaseClient { /** @type {Agent} */ let prelimAgent; - const allowedProviders = new Set(appConfig?.[EModelEndpoint.agents]?.allowedProviders); + const allowedProviders = new Set( + appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders, + ); try { if (memoryConfig.agent?.id != null && memoryConfig.agent.id !== this.options.agent.id) { prelimAgent = await loadAgent({ @@ -770,8 +772,8 @@ class AgentClient extends BaseClient { } const appConfig = await getAppConfig({ role: this.options.req.user?.role }); - /** @type {TCustomConfig['endpoints']['agents']} */ - const agentsEConfig = appConfig[EModelEndpoint.agents]; + /** @type {AppConfig['endpoints']['agents']} */ + const agentsEConfig = appConfig.endpoints?.[EModelEndpoint.agents]; config = { configurable: { @@ -1104,7 +1106,9 @@ class AgentClient extends BaseClient { /** @type {TEndpoint | undefined} */ const endpointConfig = - appConfig.all ?? appConfig[endpoint] ?? titleProviderConfig.customEndpointConfig; + appConfig.endpoints?.all ?? + appConfig.endpoints?.[endpoint] ?? + titleProviderConfig.customEndpointConfig; if (!endpointConfig) { logger.warn( '[api/server/controllers/agents/client.js #titleConvo] Error getting endpoint config', diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index b781c30b69..c1d7d7175f 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -46,12 +46,14 @@ 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}}', + endpoints: { + [EModelEndpoint.openAI]: { + // Match the agent endpoint + titleModel: 'gpt-3.5-turbo', + titlePrompt: 'Custom title prompt', + titleMethod: 'structured', + titlePromptTemplate: 'Template: {{content}}', + }, }, }); @@ -148,7 +150,7 @@ describe('AgentClient - titleConvo', () => { it('should handle missing endpoint config gracefully', async () => { // Remove endpoint config - getAppConfig.mockResolvedValue({}); + getAppConfig.mockResolvedValue({ endpoints: {} }); const text = 'Test conversation text'; const abortController = new AbortController(); @@ -167,11 +169,13 @@ describe('AgentClient - titleConvo', () => { it('should use agent model when titleModel is not provided', async () => { // Remove titleModel from config getAppConfig.mockResolvedValue({ - [EModelEndpoint.openAI]: { - titlePrompt: 'Custom title prompt', - titleMethod: 'structured', - titlePromptTemplate: 'Template: {{content}}', - // titleModel is omitted + endpoints: { + [EModelEndpoint.openAI]: { + titlePrompt: 'Custom title prompt', + titleMethod: 'structured', + titlePromptTemplate: 'Template: {{content}}', + // titleModel is omitted + }, }, }); @@ -186,11 +190,13 @@ describe('AgentClient - titleConvo', () => { it('should not use titleModel when it equals CURRENT_MODEL constant', async () => { getAppConfig.mockResolvedValue({ - [EModelEndpoint.openAI]: { - titleModel: Constants.CURRENT_MODEL, - titlePrompt: 'Custom title prompt', - titleMethod: 'structured', - titlePromptTemplate: 'Template: {{content}}', + endpoints: { + [EModelEndpoint.openAI]: { + titleModel: Constants.CURRENT_MODEL, + titlePrompt: 'Custom title prompt', + titleMethod: 'structured', + titlePromptTemplate: 'Template: {{content}}', + }, }, }); @@ -265,12 +271,14 @@ describe('AgentClient - titleConvo', () => { // Add titleEndpoint to the config getAppConfig.mockResolvedValue({ - [EModelEndpoint.openAI]: { - titleModel: 'gpt-3.5-turbo', - titleEndpoint: EModelEndpoint.anthropic, - titleMethod: 'structured', - titlePrompt: 'Custom title prompt', - titlePromptTemplate: 'Custom template', + endpoints: { + [EModelEndpoint.openAI]: { + titleModel: 'gpt-3.5-turbo', + titleEndpoint: EModelEndpoint.anthropic, + titleMethod: 'structured', + titlePrompt: 'Custom title prompt', + titlePromptTemplate: 'Custom template', + }, }, }); @@ -300,11 +308,13 @@ describe('AgentClient - titleConvo', () => { it('should use all config when endpoint config is missing', async () => { // 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}}', + endpoints: { + all: { + titleModel: 'gpt-4o-mini', + titlePrompt: 'All config title prompt', + titleMethod: 'completion', + titlePromptTemplate: 'All config template: {{content}}', + }, }, }); @@ -330,17 +340,19 @@ describe('AgentClient - titleConvo', () => { it('should prioritize all config over endpoint config for title settings', async () => { // Set both endpoint and 'all' config 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', + endpoints: { + [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', + }, }, }); @@ -370,13 +382,15 @@ describe('AgentClient - titleConvo', () => { // Set comprehensive 'all' config with all new title options 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 + endpoints: { + 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 + }, }, }); @@ -425,11 +439,13 @@ describe('AgentClient - titleConvo', () => { // Set 'all' config with specific titleMethod getAppConfig.mockResolvedValue({ - all: { - titleModel: 'gpt-4o-mini', - titleMethod: method, - titlePrompt: `Testing ${method} method`, - titlePromptTemplate: `Template for ${method}: {{content}}`, + endpoints: { + all: { + titleModel: 'gpt-4o-mini', + titleMethod: method, + titlePrompt: `Testing ${method} method`, + titlePromptTemplate: `Template for ${method}: {{content}}`, + }, }, }); @@ -476,27 +492,29 @@ describe('AgentClient - titleConvo', () => { mockAgent.endpoint = EModelEndpoint.azureOpenAI; mockAgent.provider = EModelEndpoint.azureOpenAI; 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', + endpoints: { + [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', + }, }, }, }, @@ -526,26 +544,28 @@ describe('AgentClient - titleConvo', () => { mockAgent.endpoint = EModelEndpoint.azureOpenAI; mockAgent.provider = EModelEndpoint.azureOpenAI; 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', + endpoints: { + [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', + }, }, }, }, @@ -576,27 +596,29 @@ describe('AgentClient - titleConvo', () => { mockAgent.provider = EModelEndpoint.azureOpenAI; mockAgent.model_parameters.model = 'gpt-4o-latest'; 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', + endpoints: { + [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', + }, }, }, }, @@ -625,54 +647,56 @@ describe('AgentClient - titleConvo', () => { mockAgent.endpoint = EModelEndpoint.azureOpenAI; mockAgent.provider = EModelEndpoint.azureOpenAI; 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', - }, - }, - groupMap: { - eastus: { - apiKey: '${EASTUS_API_KEY}', - instanceName: 'region-eastus', - version: '2024-02-15-preview', - models: { - 'gpt-4o': { - deploymentName: 'gpt-4o', - }, + endpoints: { + [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', + }, }, }, }, @@ -709,27 +733,29 @@ describe('AgentClient - titleConvo', () => { // Set 'all' config as fallback with a serverless Azure config 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', + endpoints: { + 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', + }, }, }, }, diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js index 32b8e16780..e4190eb9cc 100644 --- a/api/server/controllers/assistants/chatV2.js +++ b/api/server/controllers/assistants/chatV2.js @@ -376,9 +376,9 @@ const chatV2 = async (req, res) => { }; /** @type {undefined | TAssistantEndpoint} */ - const config = appConfig[endpoint] ?? {}; + const config = appConfig.endpoints?.[endpoint] ?? {}; /** @type {undefined | TBaseEndpoint} */ - const allConfig = appConfig.all; + const allConfig = appConfig.endpoints?.all; const streamRunManager = new StreamRunManager({ req, diff --git a/api/server/controllers/assistants/helpers.js b/api/server/controllers/assistants/helpers.js index 306990078b..7e5ce949a5 100644 --- a/api/server/controllers/assistants/helpers.js +++ b/api/server/controllers/assistants/helpers.js @@ -231,20 +231,20 @@ const fetchAssistants = async ({ req, res, overrideEndpoint }) => { if (endpoint === EModelEndpoint.assistants) { ({ body } = await listAllAssistants({ req, res, version, query })); } else if (endpoint === EModelEndpoint.azureAssistants) { - const azureConfig = appConfig[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig.endpoints?.[EModelEndpoint.azureOpenAI]; body = await listAssistantsForAzure({ req, res, version, azureConfig, query }); } if (req.user.role === SystemRoles.ADMIN) { return body; - } else if (!appConfig[endpoint]) { + } else if (!appConfig.endpoints?.[endpoint]) { return body; } body.data = filterAssistants({ userId: req.user.id, assistants: body.data, - assistantsConfig: appConfig[endpoint], + assistantsConfig: appConfig.endpoints?.[endpoint], }); return body; }; diff --git a/api/server/controllers/assistants/v1.js b/api/server/controllers/assistants/v1.js index 150c712876..355871287a 100644 --- a/api/server/controllers/assistants/v1.js +++ b/api/server/controllers/assistants/v1.js @@ -260,7 +260,7 @@ const getAssistantDocuments = async (req, res) => { try { const appConfig = await getAppConfig({ role: req.user?.role }); const endpoint = req.query; - const assistantsConfig = appConfig[endpoint]; + const assistantsConfig = appConfig.endpoints?.[endpoint]; const documents = await getAssistants( {}, { diff --git a/api/server/middleware/assistants/validate.js b/api/server/middleware/assistants/validate.js index 5bc3552434..b820c3ab74 100644 --- a/api/server/middleware/assistants/validate.js +++ b/api/server/middleware/assistants/validate.js @@ -15,7 +15,7 @@ const validateAssistant = async (req, res, next) => { const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {Partial} */ - const assistantsConfig = appConfig?.[endpoint]; + const assistantsConfig = appConfig.endpoints?.[endpoint]; if (!assistantsConfig) { return next(); } diff --git a/api/server/middleware/assistants/validateAuthor.js b/api/server/middleware/assistants/validateAuthor.js index fa943ca316..1395672d50 100644 --- a/api/server/middleware/assistants/validateAuthor.js +++ b/api/server/middleware/assistants/validateAuthor.js @@ -23,7 +23,7 @@ const validateAuthor = async ({ req, openai, overrideEndpoint, overrideAssistant const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {Partial} */ - const assistantsConfig = appConfig?.[endpoint]; + const assistantsConfig = appConfig.endpoints?.[endpoint]; if (!assistantsConfig) { return; } diff --git a/api/server/routes/assistants/actions.js b/api/server/routes/assistants/actions.js index 4cab980e19..b0366b8f4b 100644 --- a/api/server/routes/assistants/actions.js +++ b/api/server/routes/assistants/actions.js @@ -130,7 +130,7 @@ router.post('/:assistant_id', async (req, res) => { } /* Map Azure OpenAI model to the assistant as defined by config */ - if (appConfig[EModelEndpoint.azureOpenAI]?.assistants) { + if (appConfig.endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) { updatedAssistant = { ...updatedAssistant, model: req.body.model, diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index f06311f76a..bafeec66a8 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -111,7 +111,9 @@ const AppService = async () => { if (!Object.keys(config).length) { const appConfig = { ...defaultConfig, - [EModelEndpoint.agents]: agentsDefaults, + endpoints: { + [EModelEndpoint.agents]: agentsDefaults, + }, }; await setAppConfig(appConfig); return; @@ -126,7 +128,7 @@ const AppService = async () => { fileConfig: config?.fileConfig, secureImageLinks: config?.secureImageLinks, modelSpecs: processModelSpecs(config?.endpoints, config.modelSpecs, interfaceConfig), - ...loadedEndpoints, + endpoints: loadedEndpoints, }; await setAppConfig(appConfig); diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index f50101ffeb..87efd09b0c 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -172,12 +172,14 @@ describe('AppService', () => { searxngInstanceUrl: '${SEARXNG_INSTANCE_URL}', }), memory: undefined, - agents: expect.objectContaining({ - disableBuilder: false, - capabilities: expect.arrayContaining([...defaultAgentCapabilities]), - maxCitations: 30, - maxCitationsPerFile: 7, - minRelevanceScore: 0.45, + endpoints: expect.objectContaining({ + agents: expect.objectContaining({ + disableBuilder: false, + capabilities: expect.arrayContaining([...defaultAgentCapabilities]), + maxCitations: 30, + maxCitationsPerFile: 7, + minRelevanceScore: 0.45, + }), }), }), ); @@ -328,12 +330,14 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - [EModelEndpoint.assistants]: expect.objectContaining({ - disableBuilder: true, - pollIntervalMs: 5000, - timeoutMs: 30000, - supportedIds: expect.arrayContaining(['id1', 'id2']), - privateAssistants: false, + endpoints: expect.objectContaining({ + [EModelEndpoint.assistants]: expect.objectContaining({ + disableBuilder: true, + pollIntervalMs: 5000, + timeoutMs: 30000, + supportedIds: expect.arrayContaining(['id1', 'id2']), + privateAssistants: false, + }), }), }), ); @@ -358,15 +362,17 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - [EModelEndpoint.agents]: expect.objectContaining({ - disableBuilder: true, - recursionLimit: 10, - maxRecursionLimit: 20, - allowedProviders: expect.arrayContaining(['openai', 'anthropic']), - capabilities: expect.arrayContaining([ - AgentCapabilities.tools, - AgentCapabilities.actions, - ]), + endpoints: expect.objectContaining({ + [EModelEndpoint.agents]: expect.objectContaining({ + disableBuilder: true, + recursionLimit: 10, + maxRecursionLimit: 20, + allowedProviders: expect.arrayContaining(['openai', 'anthropic']), + capabilities: expect.arrayContaining([ + AgentCapabilities.tools, + AgentCapabilities.actions, + ]), + }), }), }), ); @@ -379,9 +385,11 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - [EModelEndpoint.agents]: expect.objectContaining({ - disableBuilder: false, - capabilities: expect.arrayContaining([...defaultAgentCapabilities]), + endpoints: expect.objectContaining({ + [EModelEndpoint.agents]: expect.objectContaining({ + disableBuilder: false, + capabilities: expect.arrayContaining([...defaultAgentCapabilities]), + }), }), }), ); @@ -402,12 +410,14 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - [EModelEndpoint.agents]: expect.objectContaining({ - disableBuilder: false, - capabilities: expect.arrayContaining([...defaultAgentCapabilities]), - }), - [EModelEndpoint.openAI]: expect.objectContaining({ - titleConvo: true, + endpoints: expect.objectContaining({ + [EModelEndpoint.agents]: expect.objectContaining({ + disableBuilder: false, + capabilities: expect.arrayContaining([...defaultAgentCapabilities]), + }), + [EModelEndpoint.openAI]: expect.objectContaining({ + titleConvo: true, + }), }), }), ); @@ -432,12 +442,14 @@ describe('AppService', () => { await AppService(); expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - [EModelEndpoint.azureAssistants]: expect.objectContaining({ - capabilities: expect.arrayContaining([ - expect.any(String), - expect.any(String), - expect.any(String), - ]), + endpoints: expect.objectContaining({ + [EModelEndpoint.azureAssistants]: expect.objectContaining({ + capabilities: expect.arrayContaining([ + expect.any(String), + expect.any(String), + expect.any(String), + ]), + }), }), }), ); @@ -462,10 +474,12 @@ describe('AppService', () => { const { modelNames, modelGroupMap, groupMap } = validateAzureGroups(azureGroups); expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - [EModelEndpoint.azureOpenAI]: expect.objectContaining({ - modelNames, - modelGroupMap, - groupMap, + endpoints: expect.objectContaining({ + [EModelEndpoint.azureOpenAI]: expect.objectContaining({ + modelNames, + modelGroupMap, + groupMap, + }), }), }), ); @@ -619,27 +633,29 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - // Check OpenAI endpoint configuration - [EModelEndpoint.openAI]: expect.objectContaining({ - titleConvo: true, - titleModel: 'gpt-3.5-turbo', - titleMethod: 'structured', - titlePrompt: 'Custom title prompt for conversation', - titlePromptTemplate: 'Summarize this conversation: {{conversation}}', - }), - // Check Assistants endpoint configuration - [EModelEndpoint.assistants]: expect.objectContaining({ - titleMethod: 'functions', - titlePrompt: 'Generate a title for this assistant conversation', - titlePromptTemplate: 'Assistant conversation template: {{messages}}', - }), - // Check Azure OpenAI endpoint configuration - [EModelEndpoint.azureOpenAI]: expect.objectContaining({ - titleConvo: true, - titleMethod: 'completion', - titleModel: 'gpt-4', - titlePrompt: 'Azure title prompt', - titlePromptTemplate: 'Azure conversation: {{context}}', + endpoints: expect.objectContaining({ + // Check OpenAI endpoint configuration + [EModelEndpoint.openAI]: expect.objectContaining({ + titleConvo: true, + titleModel: 'gpt-3.5-turbo', + titleMethod: 'structured', + titlePrompt: 'Custom title prompt for conversation', + titlePromptTemplate: 'Summarize this conversation: {{conversation}}', + }), + // Check Assistants endpoint configuration + [EModelEndpoint.assistants]: expect.objectContaining({ + titleMethod: 'functions', + titlePrompt: 'Generate a title for this assistant conversation', + titlePromptTemplate: 'Assistant conversation template: {{messages}}', + }), + // Check Azure OpenAI endpoint configuration + [EModelEndpoint.azureOpenAI]: expect.objectContaining({ + titleConvo: true, + titleMethod: 'completion', + titleModel: 'gpt-4', + titlePrompt: 'Azure title prompt', + titlePromptTemplate: 'Azure conversation: {{context}}', + }), }), }), ); @@ -667,18 +683,20 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - [EModelEndpoint.agents]: expect.objectContaining({ - disableBuilder: false, - titleConvo: true, - titleModel: 'gpt-4', - titleMethod: 'structured', - titlePrompt: 'Generate a descriptive title for this agent conversation', - titlePromptTemplate: 'Agent conversation summary: {{content}}', - recursionLimit: 15, - capabilities: expect.arrayContaining([ - AgentCapabilities.tools, - AgentCapabilities.actions, - ]), + endpoints: expect.objectContaining({ + [EModelEndpoint.agents]: expect.objectContaining({ + disableBuilder: false, + titleConvo: true, + titleModel: 'gpt-4', + titleMethod: 'structured', + titlePrompt: 'Generate a descriptive title for this agent conversation', + titlePromptTemplate: 'Agent conversation summary: {{content}}', + recursionLimit: 15, + capabilities: expect.arrayContaining([ + AgentCapabilities.tools, + AgentCapabilities.actions, + ]), + }), }), }), ); @@ -700,17 +718,19 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - [EModelEndpoint.openAI]: expect.objectContaining({ - titleConvo: true, + endpoints: expect.objectContaining({ + [EModelEndpoint.openAI]: expect.objectContaining({ + titleConvo: true, + }), }), }), ); // Verify that optional fields are not set when not provided const initCall = setAppConfig.mock.calls[0][0]; - expect(initCall[EModelEndpoint.openAI].titlePrompt).toBeUndefined(); - expect(initCall[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined(); - expect(initCall[EModelEndpoint.openAI].titleMethod).toBeUndefined(); + expect(initCall.endpoints[EModelEndpoint.openAI].titlePrompt).toBeUndefined(); + expect(initCall.endpoints[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined(); + expect(initCall.endpoints[EModelEndpoint.openAI].titleMethod).toBeUndefined(); }); it('should correctly configure titleEndpoint when specified', async () => { @@ -735,17 +755,19 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - // Check OpenAI endpoint has titleEndpoint - [EModelEndpoint.openAI]: expect.objectContaining({ - titleConvo: true, - titleModel: 'gpt-3.5-turbo', - titleEndpoint: EModelEndpoint.anthropic, - titlePrompt: 'Generate a concise title', - }), - // Check Agents endpoint has titleEndpoint - [EModelEndpoint.agents]: expect.objectContaining({ - titleEndpoint: 'custom-provider', - titleMethod: 'structured', + endpoints: expect.objectContaining({ + // Check OpenAI endpoint has titleEndpoint + [EModelEndpoint.openAI]: expect.objectContaining({ + titleConvo: true, + titleModel: 'gpt-3.5-turbo', + titleEndpoint: EModelEndpoint.anthropic, + titlePrompt: 'Generate a concise title', + }), + // Check Agents endpoint has titleEndpoint + [EModelEndpoint.agents]: expect.objectContaining({ + titleEndpoint: 'custom-provider', + titleMethod: 'structured', + }), }), }), ); @@ -777,19 +799,21 @@ describe('AppService', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ // Check that 'all' endpoint config is loaded - all: expect.objectContaining({ - titleConvo: true, - titleModel: 'gpt-4o-mini', - titleMethod: 'structured', - titlePrompt: 'Default title prompt for all endpoints', - titlePromptTemplate: 'Default template: {{conversation}}', - titleEndpoint: EModelEndpoint.anthropic, - streamRate: 50, - }), - // Check that OpenAI endpoint has its own config - [EModelEndpoint.openAI]: expect.objectContaining({ - titleConvo: true, - titleModel: 'gpt-3.5-turbo', + endpoints: expect.objectContaining({ + all: expect.objectContaining({ + titleConvo: true, + titleModel: 'gpt-4o-mini', + titleMethod: 'structured', + titlePrompt: 'Default title prompt for all endpoints', + titlePromptTemplate: 'Default template: {{conversation}}', + titleEndpoint: EModelEndpoint.anthropic, + streamRate: 50, + }), + // Check that OpenAI endpoint has its own config + [EModelEndpoint.openAI]: expect.objectContaining({ + titleConvo: true, + titleModel: 'gpt-3.5-turbo', + }), }), }), ); @@ -883,18 +907,20 @@ describe('AppService updating app config and issuing warnings', () => { expect(setAppConfig).toHaveBeenCalledWith( expect.objectContaining({ - assistants: expect.objectContaining({ - disableBuilder: true, - pollIntervalMs: 5000, - timeoutMs: 30000, - supportedIds: ['id1', 'id2'], + endpoints: expect.objectContaining({ + assistants: expect.objectContaining({ + disableBuilder: true, + pollIntervalMs: 5000, + timeoutMs: 30000, + supportedIds: ['id1', 'id2'], + }), }), }), ); // Verify excludedIds is undefined when not provided const initCall = setAppConfig.mock.calls[0][0]; - expect(initCall.assistants.excludedIds).toBeUndefined(); + expect(initCall.endpoints.assistants.excludedIds).toBeUndefined(); }); it('should log a warning when both supportedIds and excludedIds are provided', async () => { diff --git a/api/server/services/AssistantService.js b/api/server/services/AssistantService.js index 849b27c0f0..ff7a739897 100644 --- a/api/server/services/AssistantService.js +++ b/api/server/services/AssistantService.js @@ -398,8 +398,8 @@ async function runAssistant({ }); const { endpoint = EModelEndpoint.azureAssistants } = openai.req.body; - /** @type {TCustomConfig.endpoints.assistants} */ - const assistantsEndpointConfig = appConfig?.[endpoint] ?? {}; + /** @type {AppConfig['endpoints']['assistants']} */ + const assistantsEndpointConfig = appConfig.endpoints?.[endpoint] ?? {}; const { pollIntervalMs, timeoutMs } = assistantsEndpointConfig; const run = await waitForRun({ diff --git a/api/server/services/Config/getCustomConfig.js b/api/server/services/Config/getCustomConfig.js index a25ca8b76a..0b1b766641 100644 --- a/api/server/services/Config/getCustomConfig.js +++ b/api/server/services/Config/getCustomConfig.js @@ -36,7 +36,7 @@ const getCustomEndpointConfig = async (endpoint) => { throw new Error(`Config not found for the ${endpoint} custom endpoint.`); } - const customEndpoints = appConfig[EModelEndpoint.custom] ?? []; + const customEndpoints = appConfig.endpoints?.[EModelEndpoint.custom] ?? []; return customEndpoints.find( (endpointConfig) => normalizeEndpointName(endpointConfig.name) === endpoint, ); diff --git a/api/server/services/Config/getEndpointsConfig.js b/api/server/services/Config/getEndpointsConfig.js index da93de487d..c08ceb8a7d 100644 --- a/api/server/services/Config/getEndpointsConfig.js +++ b/api/server/services/Config/getEndpointsConfig.js @@ -28,9 +28,12 @@ async function getEndpointsConfig(req) { /** @type {TEndpointsConfig} */ const mergedConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints }; - if (mergedConfig[EModelEndpoint.assistants] && appConfig?.[EModelEndpoint.assistants]) { + if ( + mergedConfig[EModelEndpoint.assistants] && + appConfig?.endpoints?.[EModelEndpoint.assistants] + ) { const { disableBuilder, retrievalModels, capabilities, version, ..._rest } = - appConfig[EModelEndpoint.assistants]; + appConfig.endpoints[EModelEndpoint.assistants]; mergedConfig[EModelEndpoint.assistants] = { ...mergedConfig[EModelEndpoint.assistants], @@ -40,9 +43,9 @@ async function getEndpointsConfig(req) { capabilities, }; } - if (mergedConfig[EModelEndpoint.agents] && appConfig?.[EModelEndpoint.agents]) { + if (mergedConfig[EModelEndpoint.agents] && appConfig?.endpoints?.[EModelEndpoint.agents]) { const { disableBuilder, capabilities, allowedProviders, ..._rest } = - appConfig[EModelEndpoint.agents]; + appConfig.endpoints[EModelEndpoint.agents]; mergedConfig[EModelEndpoint.agents] = { ...mergedConfig[EModelEndpoint.agents], @@ -52,9 +55,12 @@ async function getEndpointsConfig(req) { }; } - if (mergedConfig[EModelEndpoint.azureAssistants] && appConfig?.[EModelEndpoint.azureAssistants]) { + if ( + mergedConfig[EModelEndpoint.azureAssistants] && + appConfig?.endpoints?.[EModelEndpoint.azureAssistants] + ) { const { disableBuilder, retrievalModels, capabilities, version, ..._rest } = - appConfig[EModelEndpoint.azureAssistants]; + appConfig.endpoints[EModelEndpoint.azureAssistants]; mergedConfig[EModelEndpoint.azureAssistants] = { ...mergedConfig[EModelEndpoint.azureAssistants], @@ -65,8 +71,8 @@ async function getEndpointsConfig(req) { }; } - if (mergedConfig[EModelEndpoint.bedrock] && appConfig?.[EModelEndpoint.bedrock]) { - const { availableRegions } = appConfig[EModelEndpoint.bedrock]; + if (mergedConfig[EModelEndpoint.bedrock] && appConfig?.endpoints?.[EModelEndpoint.bedrock]) { + const { availableRegions } = appConfig.endpoints[EModelEndpoint.bedrock]; mergedConfig[EModelEndpoint.bedrock] = { ...mergedConfig[EModelEndpoint.bedrock], availableRegions, diff --git a/api/server/services/Config/index.js b/api/server/services/Config/index.js index c6eee234f3..efb4d19a88 100644 --- a/api/server/services/Config/index.js +++ b/api/server/services/Config/index.js @@ -1,6 +1,7 @@ const appConfig = require('./app'); const { config } = require('./EndpointService'); const getCachedTools = require('./getCachedTools'); +const getCustomConfig = require('./getCustomConfig'); const loadCustomConfig = require('./loadCustomConfig'); const loadConfigModels = require('./loadConfigModels'); const loadDefaultModels = require('./loadDefaultModels'); @@ -17,5 +18,6 @@ module.exports = { loadAsyncEndpoints, ...appConfig, ...getCachedTools, + ...getCustomConfig, ...getEndpointsConfig, }; diff --git a/api/server/services/Config/loadAsyncEndpoints.js b/api/server/services/Config/loadAsyncEndpoints.js index 88af40459c..69958c2c61 100644 --- a/api/server/services/Config/loadAsyncEndpoints.js +++ b/api/server/services/Config/loadAsyncEndpoints.js @@ -36,7 +36,7 @@ async function loadAsyncEndpoints(req) { const google = serviceKey || isGoogleKeyProvided ? { userProvide: googleUserProvides } : false; - const useAzure = appConfig[EModelEndpoint.azureOpenAI]?.plugins; + const useAzure = appConfig.endpoints?.[EModelEndpoint.azureOpenAI]?.plugins; const gptPlugins = useAzure || openAIApiKey || azureOpenAIApiKey ? { diff --git a/api/server/services/Config/loadConfigEndpoints.js b/api/server/services/Config/loadConfigEndpoints.js index 493a08159d..b69ebec25b 100644 --- a/api/server/services/Config/loadConfigEndpoints.js +++ b/api/server/services/Config/loadConfigEndpoints.js @@ -15,8 +15,8 @@ async function loadConfigEndpoints(req) { const endpointsConfig = {}; - if (Array.isArray(appConfig[EModelEndpoint.custom])) { - const customEndpoints = appConfig[EModelEndpoint.custom].filter( + if (Array.isArray(appConfig.endpoints?.[EModelEndpoint.custom])) { + const customEndpoints = appConfig.endpoints[EModelEndpoint.custom].filter( (endpoint) => endpoint.baseURL && endpoint.apiKey && @@ -51,14 +51,14 @@ async function loadConfigEndpoints(req) { } } - if (appConfig[EModelEndpoint.azureOpenAI]) { + if (appConfig.endpoints?.[EModelEndpoint.azureOpenAI]) { /** @type {Omit} */ endpointsConfig[EModelEndpoint.azureOpenAI] = { userProvide: false, }; } - if (appConfig[EModelEndpoint.azureOpenAI]?.assistants) { + if (appConfig.endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) { /** @type {Omit} */ endpointsConfig[EModelEndpoint.azureAssistants] = { userProvide: false, diff --git a/api/server/services/Config/loadConfigModels.js b/api/server/services/Config/loadConfigModels.js index 13f12e68c2..6c5e9f9536 100644 --- a/api/server/services/Config/loadConfigModels.js +++ b/api/server/services/Config/loadConfigModels.js @@ -14,7 +14,7 @@ async function loadConfigModels(req) { return {}; } const modelsConfig = {}; - const azureConfig = appConfig[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig.endpoints?.[EModelEndpoint.azureOpenAI]; const { modelNames } = azureConfig ?? {}; if (modelNames && azureConfig) { @@ -29,11 +29,11 @@ async function loadConfigModels(req) { modelsConfig[EModelEndpoint.azureAssistants] = azureConfig.assistantModels; } - if (!Array.isArray(appConfig[EModelEndpoint.custom])) { + if (!Array.isArray(appConfig.endpoints?.[EModelEndpoint.custom])) { return modelsConfig; } - const customEndpoints = appConfig[EModelEndpoint.custom].filter( + const customEndpoints = appConfig.endpoints[EModelEndpoint.custom].filter( (endpoint) => endpoint.baseURL && endpoint.apiKey && diff --git a/api/server/services/Config/loadConfigModels.spec.js b/api/server/services/Config/loadConfigModels.spec.js index 15fa6f545b..b8d577667a 100644 --- a/api/server/services/Config/loadConfigModels.spec.js +++ b/api/server/services/Config/loadConfigModels.spec.js @@ -6,55 +6,57 @@ jest.mock('~/server/services/ModelService'); jest.mock('./app'); const exampleConfig = { - custom: [ - { - name: 'Mistral', - apiKey: '${MY_PRECIOUS_MISTRAL_KEY}', - baseURL: 'https://api.mistral.ai/v1', - models: { - default: ['mistral-tiny', 'mistral-small', 'mistral-medium', 'mistral-large-latest'], - fetch: true, + endpoints: { + custom: [ + { + name: 'Mistral', + apiKey: '${MY_PRECIOUS_MISTRAL_KEY}', + baseURL: 'https://api.mistral.ai/v1', + models: { + default: ['mistral-tiny', 'mistral-small', 'mistral-medium', 'mistral-large-latest'], + fetch: true, + }, + dropParams: ['stop', 'user', 'frequency_penalty', 'presence_penalty'], }, - dropParams: ['stop', 'user', 'frequency_penalty', 'presence_penalty'], - }, - { - name: 'OpenRouter', - apiKey: '${MY_OPENROUTER_API_KEY}', - baseURL: 'https://openrouter.ai/api/v1', - models: { - default: ['gpt-3.5-turbo'], - fetch: true, + { + name: 'OpenRouter', + apiKey: '${MY_OPENROUTER_API_KEY}', + baseURL: 'https://openrouter.ai/api/v1', + models: { + default: ['gpt-3.5-turbo'], + fetch: true, + }, + dropParams: ['stop'], }, - dropParams: ['stop'], - }, - { - name: 'groq', - apiKey: 'user_provided', - baseURL: 'https://api.groq.com/openai/v1/', - models: { - default: ['llama2-70b-4096', 'mixtral-8x7b-32768'], - fetch: false, + { + name: 'groq', + apiKey: 'user_provided', + baseURL: 'https://api.groq.com/openai/v1/', + models: { + default: ['llama2-70b-4096', 'mixtral-8x7b-32768'], + fetch: false, + }, }, - }, - { - name: 'Ollama', - apiKey: 'user_provided', - baseURL: 'http://localhost:11434/v1/', - models: { - default: ['mistral', 'llama2:13b'], - fetch: false, + { + name: 'Ollama', + apiKey: 'user_provided', + baseURL: 'http://localhost:11434/v1/', + models: { + default: ['mistral', 'llama2:13b'], + fetch: false, + }, }, - }, - { - name: 'MLX', - apiKey: 'user_provided', - baseURL: 'http://localhost:8080/v1/', - models: { - default: ['Meta-Llama-3-8B-Instruct-4bit'], - fetch: false, + { + name: 'MLX', + apiKey: 'user_provided', + baseURL: 'http://localhost:8080/v1/', + models: { + default: ['Meta-Llama-3-8B-Instruct-4bit'], + fetch: false, + }, }, - }, - ], + ], + }, }; describe('loadConfigModels', () => { @@ -83,7 +85,9 @@ describe('loadConfigModels', () => { it('handles azure models and endpoint correctly', async () => { getAppConfig.mockResolvedValue({ - azureOpenAI: { modelNames: ['model1', 'model2'] }, + endpoints: { + azureOpenAI: { modelNames: ['model1', 'model2'] }, + }, }); const result = await loadConfigModels(mockRequest); @@ -102,7 +106,7 @@ describe('loadConfigModels', () => { }, ]; - getAppConfig.mockResolvedValue({ custom: customEndpoints }); + getAppConfig.mockResolvedValue({ endpoints: { custom: customEndpoints } }); fetchModels.mockResolvedValue(['customModel1', 'customModel2']); const result = await loadConfigModels(mockRequest); @@ -112,20 +116,22 @@ describe('loadConfigModels', () => { it('correctly associates models to names using unique keys', async () => { getAppConfig.mockResolvedValue({ - custom: [ - { - baseURL: 'http://example.com', - apiKey: 'API_KEY1', - name: 'Model1', - models: { fetch: true }, - }, - { - baseURL: 'http://example.com', - apiKey: 'API_KEY2', - name: 'Model2', - models: { fetch: true }, - }, - ], + endpoints: { + custom: [ + { + baseURL: 'http://example.com', + apiKey: 'API_KEY1', + name: 'Model1', + models: { fetch: true }, + }, + { + baseURL: 'http://example.com', + apiKey: 'API_KEY2', + name: 'Model2', + models: { fetch: true }, + }, + ], + }, }); fetchModels.mockImplementation(({ apiKey }) => Promise.resolve(apiKey === 'API_KEY1' ? ['model1Data'] : ['model2Data']), @@ -139,26 +145,28 @@ describe('loadConfigModels', () => { it('correctly handles multiple endpoints with the same baseURL but different apiKeys', async () => { // Mock the custom configuration to simulate the user's scenario getAppConfig.mockResolvedValue({ - custom: [ - { - name: 'LiteLLM', - apiKey: '${LITELLM_ALL_MODELS}', - baseURL: '${LITELLM_HOST}', - models: { fetch: true }, - }, - { - name: 'OpenAI', - apiKey: '${LITELLM_OPENAI_MODELS}', - baseURL: '${LITELLM_SECOND_HOST}', - models: { fetch: true }, - }, - { - name: 'Google', - apiKey: '${LITELLM_GOOGLE_MODELS}', - baseURL: '${LITELLM_SECOND_HOST}', - models: { fetch: true }, - }, - ], + endpoints: { + custom: [ + { + name: 'LiteLLM', + apiKey: '${LITELLM_ALL_MODELS}', + baseURL: '${LITELLM_HOST}', + models: { fetch: true }, + }, + { + name: 'OpenAI', + apiKey: '${LITELLM_OPENAI_MODELS}', + baseURL: '${LITELLM_SECOND_HOST}', + models: { fetch: true }, + }, + { + name: 'Google', + apiKey: '${LITELLM_GOOGLE_MODELS}', + baseURL: '${LITELLM_SECOND_HOST}', + models: { fetch: true }, + }, + ], + }, }); // Mock `fetchModels` to return different models based on the apiKey @@ -246,8 +254,8 @@ describe('loadConfigModels', () => { // For groq and ollama, since the apiKey is "user_provided", models should not be fetched // Depending on your implementation's behavior regarding "default" models without fetching, // you may need to adjust the following assertions: - expect(result.groq).toBe(exampleConfig.custom[2].models.default); - expect(result.ollama).toBe(exampleConfig.custom[3].models.default); + expect(result.groq).toBe(exampleConfig.endpoints.custom[2].models.default); + expect(result.ollama).toBe(exampleConfig.endpoints.custom[3].models.default); // Verifying fetchModels was not called for groq and ollama expect(fetchModels).not.toHaveBeenCalledWith( @@ -264,26 +272,28 @@ describe('loadConfigModels', () => { it('falls back to default models if fetching returns an empty array', async () => { getAppConfig.mockResolvedValue({ - custom: [ - { - name: 'EndpointWithSameFetchKey', - apiKey: 'API_KEY', - baseURL: 'http://example.com', - models: { - fetch: true, - default: ['defaultModel1'], + endpoints: { + custom: [ + { + name: 'EndpointWithSameFetchKey', + apiKey: 'API_KEY', + baseURL: 'http://example.com', + models: { + fetch: true, + default: ['defaultModel1'], + }, }, - }, - { - name: 'EmptyFetchModel', - apiKey: 'API_KEY', - baseURL: 'http://example.com', - models: { - fetch: true, - default: ['defaultModel1', 'defaultModel2'], + { + name: 'EmptyFetchModel', + apiKey: 'API_KEY', + baseURL: 'http://example.com', + models: { + fetch: true, + default: ['defaultModel1', 'defaultModel2'], + }, }, - }, - ], + ], + }, }); fetchModels.mockResolvedValue([]); @@ -295,17 +305,19 @@ describe('loadConfigModels', () => { it('falls back to default models if fetching returns a falsy value', async () => { getAppConfig.mockResolvedValue({ - custom: [ - { - name: 'FalsyFetchModel', - apiKey: 'API_KEY', - baseURL: 'http://example.com', - models: { - fetch: true, - default: ['defaultModel1', 'defaultModel2'], + endpoints: { + custom: [ + { + name: 'FalsyFetchModel', + apiKey: 'API_KEY', + baseURL: 'http://example.com', + models: { + fetch: true, + default: ['defaultModel1', 'defaultModel2'], + }, }, - }, - ], + ], + }, }); fetchModels.mockResolvedValue(false); @@ -354,7 +366,9 @@ describe('loadConfigModels', () => { ]; getAppConfig.mockResolvedValue({ - custom: testCases, + endpoints: { + custom: testCases, + }, }); const result = await loadConfigModels(mockRequest); diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 44002119d6..3dbc0df609 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -90,8 +90,7 @@ const initializeClient = async ({ req, res, endpointOption }) => { } const agentConfigs = new Map(); - /** @type {Set} */ - const allowedProviders = new Set(appConfig?.[EModelEndpoint.agents]?.allowedProviders); + const allowedProviders = new Set(appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders); const loadTools = createToolLoader(); /** @type {Array} */ @@ -145,7 +144,7 @@ const initializeClient = async ({ req, res, endpointOption }) => { } } - let endpointConfig = appConfig[primaryConfig.endpoint]; + let endpointConfig = appConfig.endpoints?.[primaryConfig.endpoint]; if (!isAgentsEndpoint(primaryConfig.endpoint) && !endpointConfig) { try { endpointConfig = await getCustomEndpointConfig(primaryConfig.endpoint); diff --git a/api/server/services/Endpoints/anthropic/initialize.js b/api/server/services/Endpoints/anthropic/initialize.js index 8986eed34b..f349ad49d3 100644 --- a/api/server/services/Endpoints/anthropic/initialize.js +++ b/api/server/services/Endpoints/anthropic/initialize.js @@ -25,15 +25,14 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio let clientOptions = {}; /** @type {undefined | TBaseEndpoint} */ - const anthropicConfig = appConfig[EModelEndpoint.anthropic]; + const anthropicConfig = appConfig.endpoints?.[EModelEndpoint.anthropic]; if (anthropicConfig) { clientOptions.streamRate = anthropicConfig.streamRate; clientOptions.titleModel = anthropicConfig.titleModel; } - /** @type {undefined | TBaseEndpoint} */ - const allConfig = appConfig.all; + const allConfig = appConfig.endpoints?.all; if (allConfig) { clientOptions.streamRate = allConfig.streamRate; } diff --git a/api/server/services/Endpoints/azureAssistants/initialize.js b/api/server/services/Endpoints/azureAssistants/initialize.js index 200322031b..f04a646780 100644 --- a/api/server/services/Endpoints/azureAssistants/initialize.js +++ b/api/server/services/Endpoints/azureAssistants/initialize.js @@ -83,7 +83,7 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie }; /** @type {TAzureConfig | undefined} */ - const azureConfig = appConfig[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig.endpoints?.[EModelEndpoint.azureOpenAI]; /** @type {AzureOptions | undefined} */ let azureOptions; diff --git a/api/server/services/Endpoints/bedrock/options.js b/api/server/services/Endpoints/bedrock/options.js index 841854532a..808406ab98 100644 --- a/api/server/services/Endpoints/bedrock/options.js +++ b/api/server/services/Endpoints/bedrock/options.js @@ -52,14 +52,13 @@ const getOptions = async ({ req, overrideModel, endpointOption }) => { let streamRate = Constants.DEFAULT_STREAM_RATE; /** @type {undefined | TBaseEndpoint} */ - const bedrockConfig = appConfig[EModelEndpoint.bedrock]; + const bedrockConfig = appConfig.endpoints?.[EModelEndpoint.bedrock]; if (bedrockConfig && bedrockConfig.streamRate) { streamRate = bedrockConfig.streamRate; } - /** @type {undefined | TBaseEndpoint} */ - const allConfig = appConfig.all; + const allConfig = appConfig.endpoints?.all; if (allConfig && allConfig.streamRate) { streamRate = allConfig.streamRate; } diff --git a/api/server/services/Endpoints/custom/initialize.js b/api/server/services/Endpoints/custom/initialize.js index a90d3630ad..73d37e10c1 100644 --- a/api/server/services/Endpoints/custom/initialize.js +++ b/api/server/services/Endpoints/custom/initialize.js @@ -118,8 +118,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid endpointTokenConfig, }; - /** @type {undefined | TBaseEndpoint} */ - const allConfig = appConfig.all; + const allConfig = appConfig.endpoints?.all; if (allConfig) { customOptions.streamRate = allConfig.streamRate; } diff --git a/api/server/services/Endpoints/google/initialize.js b/api/server/services/Endpoints/google/initialize.js index dc83ff9880..a77147ef78 100644 --- a/api/server/services/Endpoints/google/initialize.js +++ b/api/server/services/Endpoints/google/initialize.js @@ -49,9 +49,9 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {undefined | TBaseEndpoint} */ - const allConfig = appConfig.all; + const allConfig = appConfig.endpoints?.all; /** @type {undefined | TBaseEndpoint} */ - const googleConfig = appConfig[EModelEndpoint.google]; + const googleConfig = appConfig.endpoints?.[EModelEndpoint.google]; if (googleConfig) { clientOptions.streamRate = googleConfig.streamRate; diff --git a/api/server/services/Endpoints/google/title.js b/api/server/services/Endpoints/google/title.js index 3451984303..ff74b93eee 100644 --- a/api/server/services/Endpoints/google/title.js +++ b/api/server/services/Endpoints/google/title.js @@ -16,7 +16,7 @@ const addTitle = async (req, { text, response, client }) => { } const { GOOGLE_TITLE_MODEL } = process.env ?? {}; const appConfig = await getAppConfig({ role: req.user?.role }); - const providerConfig = appConfig[EModelEndpoint.google]; + const providerConfig = appConfig.endpoints?.[EModelEndpoint.google]; let model = providerConfig?.titleModel ?? GOOGLE_TITLE_MODEL ?? diff --git a/api/server/services/Endpoints/openAI/initialize.js b/api/server/services/Endpoints/openAI/initialize.js index 6ce9410fa5..c45c453e34 100644 --- a/api/server/services/Endpoints/openAI/initialize.js +++ b/api/server/services/Endpoints/openAI/initialize.js @@ -66,7 +66,7 @@ const initializeClient = async ({ const isAzureOpenAI = endpoint === EModelEndpoint.azureOpenAI; /** @type {false | TAzureConfig} */ - const azureConfig = isAzureOpenAI && appConfig[EModelEndpoint.azureOpenAI]; + const azureConfig = isAzureOpenAI && appConfig.endpoints?.[EModelEndpoint.azureOpenAI]; let serverless = false; if (isAzureOpenAI && azureConfig) { const { modelGroupMap, groupMap } = azureConfig; @@ -115,15 +115,14 @@ const initializeClient = async ({ } /** @type {undefined | TBaseEndpoint} */ - const openAIConfig = appConfig[EModelEndpoint.openAI]; + const openAIConfig = appConfig.endpoints?.[EModelEndpoint.openAI]; if (!isAzureOpenAI && openAIConfig) { clientOptions.streamRate = openAIConfig.streamRate; clientOptions.titleModel = openAIConfig.titleModel; } - /** @type {undefined | TBaseEndpoint} */ - const allConfig = appConfig.all; + const allConfig = appConfig.endpoints?.all; if (allConfig) { clientOptions.streamRate = allConfig.streamRate; } diff --git a/api/server/services/Endpoints/openAI/initialize.spec.js b/api/server/services/Endpoints/openAI/initialize.spec.js index 40a6e2973c..7a5000d554 100644 --- a/api/server/services/Endpoints/openAI/initialize.spec.js +++ b/api/server/services/Endpoints/openAI/initialize.spec.js @@ -22,28 +22,30 @@ jest.mock('~/server/services/UserService', () => ({ jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn().mockResolvedValue({ - openAI: { - apiKey: 'test-key', - }, - azureOpenAI: { - apiKey: 'test-azure-key', - modelNames: ['gpt-4-vision-preview', 'gpt-3.5-turbo', 'gpt-4'], - modelGroupMap: { - 'gpt-4-vision-preview': { - group: 'librechat-westus', - deploymentName: 'gpt-4-vision-preview', - version: '2024-02-15-preview', - }, + endpoints: { + openAI: { + apiKey: 'test-key', }, - groupMap: { - 'librechat-westus': { - apiKey: 'WESTUS_API_KEY', - instanceName: 'librechat-westus', - version: '2023-12-01-preview', - models: { - 'gpt-4-vision-preview': { - deploymentName: 'gpt-4-vision-preview', - version: '2024-02-15-preview', + azureOpenAI: { + apiKey: 'test-azure-key', + modelNames: ['gpt-4-vision-preview', 'gpt-3.5-turbo', 'gpt-4'], + modelGroupMap: { + 'gpt-4-vision-preview': { + group: 'librechat-westus', + deploymentName: 'gpt-4-vision-preview', + version: '2024-02-15-preview', + }, + }, + groupMap: { + 'librechat-westus': { + apiKey: 'WESTUS_API_KEY', + instanceName: 'librechat-westus', + version: '2023-12-01-preview', + models: { + 'gpt-4-vision-preview': { + deploymentName: 'gpt-4-vision-preview', + version: '2024-02-15-preview', + }, }, }, }, diff --git a/api/server/services/Files/Citations/index.js b/api/server/services/Files/Citations/index.js index 810abe548e..5f6106937c 100644 --- a/api/server/services/Files/Citations/index.js +++ b/api/server/services/Files/Citations/index.js @@ -51,9 +51,11 @@ async function processFileCitations({ user, toolArtifact, toolCallId, metadata } } const appConfig = await getAppConfig({ role: user?.role }); - const maxCitations = appConfig?.[EModelEndpoint.agents]?.maxCitations ?? 30; - const maxCitationsPerFile = appConfig?.[EModelEndpoint.agents]?.maxCitationsPerFile ?? 5; - const minRelevanceScore = appConfig?.[EModelEndpoint.agents]?.minRelevanceScore ?? 0.45; + const maxCitations = appConfig.endpoints?.[EModelEndpoint.agents]?.maxCitations ?? 30; + const maxCitationsPerFile = + appConfig.endpoints?.[EModelEndpoint.agents]?.maxCitationsPerFile ?? 5; + const minRelevanceScore = + appConfig.endpoints?.[EModelEndpoint.agents]?.minRelevanceScore ?? 0.45; const sources = toolArtifact[Tools.file_search].sources || []; const filteredSources = sources.filter((source) => source.relevance >= minRelevanceScore); diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 6c17b3f08f..6addf70ad0 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -165,7 +165,7 @@ const processDeleteRequest = async ({ req, files }) => { /** @type {Record} */ const client = { [FileSources.openai]: undefined, [FileSources.azure]: undefined }; const initializeClients = async () => { - if (appConfig[EModelEndpoint.assistants]) { + if (appConfig.endpoints?.[EModelEndpoint.assistants]) { const openAIClient = await getOpenAIClient({ req, overrideEndpoint: EModelEndpoint.assistants, @@ -173,7 +173,7 @@ const processDeleteRequest = async ({ req, files }) => { client[FileSources.openai] = openAIClient.openai; } - if (!appConfig[EModelEndpoint.azureOpenAI]?.assistants) { + if (!appConfig.endpoints?.[EModelEndpoint.azureOpenAI]?.assistants) { return; } diff --git a/api/server/services/Runs/methods.js b/api/server/services/Runs/methods.js index c68ed5656a..2c4bcacc07 100644 --- a/api/server/services/Runs/methods.js +++ b/api/server/services/Runs/methods.js @@ -33,7 +33,7 @@ async function retrieveRun({ thread_id, run_id, timeout, openai }) { } /** @type {TAzureConfig | undefined} */ - const azureConfig = appConfig[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig.endpoints?.[EModelEndpoint.azureOpenAI]; if (azureConfig && azureConfig.assistants) { delete headers.Authorization; diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 62cc175d73..e42a35b1ec 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -504,7 +504,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) /** Edge case: use defined/fallback capabilities when the "agents" endpoint is not enabled */ if (enabledCapabilities.size === 0 && agent.id === Constants.EPHEMERAL_AGENT_ID) { enabledCapabilities = new Set( - appConfig?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, + appConfig.endpoints?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, ); } const checkCapability = (capability) => { diff --git a/packages/api/src/agents/resources.test.ts b/packages/api/src/agents/resources.test.ts index e5215c9ccb..df6e0a1217 100644 --- a/packages/api/src/agents/resources.test.ts +++ b/packages/api/src/agents/resources.test.ts @@ -29,9 +29,11 @@ describe('primeResources', () => { // Setup mock appConfig mockAppConfig = { - [EModelEndpoint.agents]: { - capabilities: [AgentCapabilities.ocr], - } as TAgentsEndpoint, + endpoints: { + [EModelEndpoint.agents]: { + capabilities: [AgentCapabilities.ocr], + } as TAgentsEndpoint, + }, } as AppConfig; // Setup mock getFiles function @@ -87,7 +89,7 @@ describe('primeResources', () => { describe('when OCR is disabled', () => { it('should not fetch OCR files even if tool_resources has OCR file_ids', async () => { - (mockAppConfig[EModelEndpoint.agents] as TAgentsEndpoint).capabilities = []; + (mockAppConfig.endpoints![EModelEndpoint.agents] as TAgentsEndpoint).capabilities = []; const tool_resources = { [EToolResources.ocr]: { diff --git a/packages/api/src/agents/resources.ts b/packages/api/src/agents/resources.ts index c490c51aad..e0ad1443bc 100644 --- a/packages/api/src/agents/resources.ts +++ b/packages/api/src/agents/resources.ts @@ -202,9 +202,9 @@ export const primeResources = async ({ } } - const isOCREnabled = (appConfig?.[EModelEndpoint.agents]?.capabilities ?? []).includes( - AgentCapabilities.ocr, - ); + const isOCREnabled = ( + appConfig?.endpoints?.[EModelEndpoint.agents]?.capabilities ?? [] + ).includes(AgentCapabilities.ocr); if (tool_resources[EToolResources.ocr]?.file_ids && isOCREnabled) { const context = await getFiles( diff --git a/packages/api/src/endpoints/openai/initialize.ts b/packages/api/src/endpoints/openai/initialize.ts index 4319234476..425aa3d558 100644 --- a/packages/api/src/endpoints/openai/initialize.ts +++ b/packages/api/src/endpoints/openai/initialize.ts @@ -72,7 +72,7 @@ export const initializeOpenAI = async ({ }; const isAzureOpenAI = endpoint === EModelEndpoint.azureOpenAI; - const azureConfig = isAzureOpenAI && appConfig[EModelEndpoint.azureOpenAI]; + const azureConfig = isAzureOpenAI && appConfig.endpoints?.[EModelEndpoint.azureOpenAI]; if (isAzureOpenAI && azureConfig) { const { modelGroupMap, groupMap } = azureConfig; @@ -143,8 +143,8 @@ export const initializeOpenAI = async ({ const options = getOpenAIConfig(apiKey, finalClientOptions, endpoint); - const openAIConfig = appConfig[EModelEndpoint.openAI]; - const allConfig = appConfig.all; + const openAIConfig = appConfig.endpoints?.[EModelEndpoint.openAI]; + const allConfig = appConfig.endpoints?.all; const azureRate = modelName?.includes('gpt-4') ? 30 : 17; let streamRate: number | undefined; diff --git a/packages/api/src/types/config.ts b/packages/api/src/types/config.ts index a3a205fef7..ea21052ba6 100644 --- a/packages/api/src/types/config.ts +++ b/packages/api/src/types/config.ts @@ -59,26 +59,28 @@ export interface AppConfig { secureImageLinks?: TCustomConfig['secureImageLinks']; /** Processed model specifications */ modelSpecs?: TCustomConfig['modelSpecs']; - /** OpenAI endpoint configuration */ - openAI?: TEndpoint; - /** Google endpoint configuration */ - google?: TEndpoint; - /** Bedrock endpoint configuration */ - bedrock?: TEndpoint; - /** Anthropic endpoint configuration */ - anthropic?: TEndpoint; - /** GPT plugins endpoint configuration */ - gptPlugins?: TEndpoint; - /** Azure OpenAI endpoint configuration */ - azureOpenAI?: TAzureConfig; - /** Assistants endpoint configuration */ - assistants?: TAssistantEndpoint; - /** Azure assistants endpoint configuration */ - azureAssistants?: TAssistantEndpoint; - /** Agents endpoint configuration */ - [EModelEndpoint.agents]?: TAgentsEndpoint; - /** Global endpoint configuration */ - all?: TEndpoint; - /** Any additional endpoint configurations */ - [key: string]: unknown; + endpoints?: { + /** OpenAI endpoint configuration */ + openAI?: TEndpoint; + /** Google endpoint configuration */ + google?: TEndpoint; + /** Bedrock endpoint configuration */ + bedrock?: TEndpoint; + /** Anthropic endpoint configuration */ + anthropic?: TEndpoint; + /** GPT plugins endpoint configuration */ + gptPlugins?: TEndpoint; + /** Azure OpenAI endpoint configuration */ + azureOpenAI?: TAzureConfig; + /** Assistants endpoint configuration */ + assistants?: TAssistantEndpoint; + /** Azure assistants endpoint configuration */ + azureAssistants?: TAssistantEndpoint; + /** Agents endpoint configuration */ + [EModelEndpoint.agents]?: TAgentsEndpoint; + /** Global endpoint configuration */ + all?: TEndpoint; + /** Any additional endpoint configurations */ + [key: string]: unknown; + }; }