diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 18ab0ae6e9..5825257257 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1121,6 +1121,13 @@ class AgentClient extends BaseClient { ); } + if (endpointConfig?.titleConvo === false) { + logger.debug( + `[api/server/controllers/agents/client.js #titleConvo] Title generation disabled for endpoint "${endpoint}"`, + ); + return; + } + if (endpointConfig?.titleEndpoint && endpointConfig.titleEndpoint !== endpoint) { try { titleProviderConfig = getProviderConfig({ @@ -1130,7 +1137,7 @@ class AgentClient extends BaseClient { endpoint = endpointConfig.titleEndpoint; } catch (error) { logger.warn( - `[api/server/controllers/agents/client.js #titleConvo] Error getting title endpoint config for ${endpointConfig.titleEndpoint}, falling back to default`, + `[api/server/controllers/agents/client.js #titleConvo] Error getting title endpoint config for "${endpointConfig.titleEndpoint}", falling back to default`, error, ); // Fall back to original provider config diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index 9b5a56474c..2f6c60031e 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -263,6 +263,125 @@ describe('AgentClient - titleConvo', () => { expect(result).toBeUndefined(); }); + it('should skip title generation when titleConvo is set to false', async () => { + // Set titleConvo to false in endpoint config + mockReq.config = { + endpoints: { + [EModelEndpoint.openAI]: { + titleConvo: false, + titleModel: 'gpt-3.5-turbo', + titlePrompt: 'Custom title prompt', + titleMethod: 'structured', + titlePromptTemplate: 'Template: {{content}}', + }, + }, + }; + + const text = 'Test conversation text'; + const abortController = new AbortController(); + + const result = await client.titleConvo({ text, abortController }); + + // Should return undefined without generating title + expect(result).toBeUndefined(); + + // generateTitle should NOT have been called + expect(mockRun.generateTitle).not.toHaveBeenCalled(); + + // recordCollectedUsage should NOT have been called + expect(client.recordCollectedUsage).not.toHaveBeenCalled(); + }); + + it('should skip title generation when titleConvo is false in all config', async () => { + // Set titleConvo to false in "all" config + mockReq.config = { + endpoints: { + all: { + titleConvo: false, + titleModel: 'gpt-4o-mini', + titlePrompt: 'All config title prompt', + titleMethod: 'completion', + titlePromptTemplate: 'All config template', + }, + }, + }; + + const text = 'Test conversation text'; + const abortController = new AbortController(); + + const result = await client.titleConvo({ text, abortController }); + + // Should return undefined without generating title + expect(result).toBeUndefined(); + + // generateTitle should NOT have been called + expect(mockRun.generateTitle).not.toHaveBeenCalled(); + + // recordCollectedUsage should NOT have been called + expect(client.recordCollectedUsage).not.toHaveBeenCalled(); + }); + + it('should skip title generation when titleConvo is false for custom endpoint scenario', async () => { + // This test validates the behavior when customEndpointConfig (retrieved via + // getProviderConfig for custom endpoints) has titleConvo: false. + // + // The code path is: + // 1. endpoints?.all is checked (undefined in this test) + // 2. endpoints?.[endpoint] is checked (our test config) + // 3. Would fall back to titleProviderConfig.customEndpointConfig (for real custom endpoints) + // + // We simulate a custom endpoint scenario using a dynamically named endpoint config + + // Create a unique endpoint name that represents a custom endpoint + const customEndpointName = 'customEndpoint'; + + // Configure the endpoint to have titleConvo: false + // This simulates what would be in customEndpointConfig for a real custom endpoint + mockReq.config = { + endpoints: { + // No 'all' config - so it will check endpoints[endpoint] + // This config represents what customEndpointConfig would contain + [customEndpointName]: { + titleConvo: false, + titleModel: 'custom-model-v1', + titlePrompt: 'Custom endpoint title prompt', + titleMethod: 'completion', + titlePromptTemplate: 'Custom template: {{content}}', + baseURL: 'https://api.custom-llm.com/v1', + apiKey: 'test-custom-key', + // Additional custom endpoint properties + models: { + default: ['custom-model-v1', 'custom-model-v2'], + }, + }, + }, + }; + + // Set up agent to use our custom endpoint + // Use openAI as base but override with custom endpoint name for this test + mockAgent.endpoint = EModelEndpoint.openAI; + mockAgent.provider = EModelEndpoint.openAI; + + // Override the endpoint in the config to point to our custom config + mockReq.config.endpoints[EModelEndpoint.openAI] = + mockReq.config.endpoints[customEndpointName]; + delete mockReq.config.endpoints[customEndpointName]; + + const text = 'Test custom endpoint conversation'; + const abortController = new AbortController(); + + const result = await client.titleConvo({ text, abortController }); + + // Should return undefined without generating title because titleConvo is false + expect(result).toBeUndefined(); + + // generateTitle should NOT have been called + expect(mockRun.generateTitle).not.toHaveBeenCalled(); + + // recordCollectedUsage should NOT have been called + expect(client.recordCollectedUsage).not.toHaveBeenCalled(); + }); + it('should pass titleEndpoint configuration to generateTitle', async () => { // Mock the API key just for this test const originalApiKey = process.env.ANTHROPIC_API_KEY; diff --git a/api/server/services/Endpoints/agents/title.js b/api/server/services/Endpoints/agents/title.js index 5f200fcde6..74cdc0b2c2 100644 --- a/api/server/services/Endpoints/agents/title.js +++ b/api/server/services/Endpoints/agents/title.js @@ -54,6 +54,11 @@ const addTitle = async (req, { text, response, client }) => { clearTimeout(timeoutId); } + if (!title) { + logger.debug(`[${key}] No title generated`); + return; + } + await titleCache.set(key, title, 120000); await saveConvo( req,