diff --git a/api/package.json b/api/package.json index 52f50523bc..212a979612 100644 --- a/api/package.json +++ b/api/package.json @@ -49,7 +49,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.80", + "@librechat/agents": "^2.4.81", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/package-lock.json b/package-lock.json index 0de769297b..cce2652ca6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@langchain/google-vertexai": "^0.2.13", "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.4.80", + "@librechat/agents": "^2.4.81", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -21910,9 +21910,9 @@ } }, "node_modules/@librechat/agents": { - "version": "2.4.80", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.80.tgz", - "integrity": "sha512-+GlWHMrgiwkptnUip7jJrz9oAQSz4+uJNoa+X6zR9W6x90NnknFwoQsXhQlnXoDGW75uGb+U7KI9mQz5H/YL6Q==", + "version": "2.4.81", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.81.tgz", + "integrity": "sha512-uPepwOepQS03NJg9jzLvYGonyewy33QDB7iENKHooO8+6eIOv2QC4gm1k/fYKgsIfBfng2caRmn532UTrkE3rQ==", "license": "MIT", "dependencies": { "@langchain/anthropic": "^0.3.26", @@ -51641,7 +51641,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.80", + "@librechat/agents": "^2.4.81", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.12.1", diff --git a/packages/api/package.json b/packages/api/package.json index 036011d30f..c5b48d948a 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -73,7 +73,7 @@ }, "peerDependencies": { "@langchain/core": "^0.3.62", - "@librechat/agents": "^2.4.80", + "@librechat/agents": "^2.4.81", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.17.1", "axios": "^1.12.1", diff --git a/packages/api/src/endpoints/openai/config.spec.ts b/packages/api/src/endpoints/openai/config.spec.ts index 5d6ffa60be..fa718f1043 100644 --- a/packages/api/src/endpoints/openai/config.spec.ts +++ b/packages/api/src/endpoints/openai/config.spec.ts @@ -687,6 +687,82 @@ describe('getOpenAIConfig', () => { expect(result.provider).toBe('openrouter'); }); + it('should handle web_search with OpenRouter using plugins format', () => { + const modelOptions = { + model: 'gpt-4', + web_search: true, + }; + + const result = getOpenAIConfig(mockApiKey, { + reverseProxyUrl: 'https://openrouter.ai/api/v1', + modelOptions, + }); + + // Should use plugins format for OpenRouter, not tools + expect(result.llmConfig.modelKwargs).toEqual({ + plugins: [{ id: 'web' }], + }); + expect(result.tools).toEqual([]); + // Should NOT set useResponsesApi for OpenRouter + expect(result.llmConfig.useResponsesApi).toBeUndefined(); + expect(result.provider).toBe('openrouter'); + }); + + it('should handle web_search false with OpenRouter', () => { + const modelOptions = { + model: 'gpt-4', + web_search: false, + }; + + const result = getOpenAIConfig(mockApiKey, { + reverseProxyUrl: 'https://openrouter.ai/api/v1', + modelOptions, + }); + + // Should not have plugins when web_search is false + expect(result.llmConfig.modelKwargs).toBeUndefined(); + expect(result.tools).toEqual([]); + expect(result.provider).toBe('openrouter'); + }); + + it('should handle web_search with OpenRouter from addParams', () => { + const addParams = { + web_search: true, + customParam: 'value', + }; + + const result = getOpenAIConfig(mockApiKey, { + reverseProxyUrl: 'https://openrouter.ai/api/v1', + addParams, + }); + + // Should use plugins format and include other params + expect(result.llmConfig.modelKwargs).toEqual({ + plugins: [{ id: 'web' }], + customParam: 'value', + }); + expect(result.tools).toEqual([]); + expect(result.provider).toBe('openrouter'); + }); + + it('should handle web_search with OpenRouter and dropParams', () => { + const modelOptions = { + model: 'gpt-4', + web_search: true, + }; + + const result = getOpenAIConfig(mockApiKey, { + reverseProxyUrl: 'https://openrouter.ai/api/v1', + modelOptions, + dropParams: ['web_search'], + }); + + // dropParams should disable web_search even for OpenRouter + expect(result.llmConfig.modelKwargs).toBeUndefined(); + expect(result.tools).toEqual([]); + expect(result.provider).toBe('openrouter'); + }); + it('should handle OpenRouter with reasoning params', () => { const modelOptions = { reasoning_effort: ReasoningEffort.high, @@ -995,6 +1071,45 @@ describe('getOpenAIConfig', () => { }), }); }); + + it('should handle all configuration with OpenRouter and web_search', () => { + const complexConfig = { + modelOptions: { + model: 'gpt-4-turbo', + temperature: 0.7, + max_tokens: 2000, + verbosity: Verbosity.medium, + reasoning_effort: ReasoningEffort.high, + web_search: true, + }, + reverseProxyUrl: 'https://openrouter.ai/api/v1', + headers: { 'X-Custom': 'value' }, + streaming: false, + addParams: { + customParam: 'custom-value', + temperature: 0.8, + }, + }; + + const result = getOpenAIConfig(mockApiKey, complexConfig); + + expect(result.llmConfig).toMatchObject({ + model: 'gpt-4-turbo', + temperature: 0.8, + streaming: false, + include_reasoning: true, // OpenRouter specific + }); + // Should NOT have useResponsesApi for OpenRouter + expect(result.llmConfig.useResponsesApi).toBeUndefined(); + expect(result.llmConfig.maxTokens).toBe(2000); + expect(result.llmConfig.modelKwargs).toEqual({ + verbosity: Verbosity.medium, + customParam: 'custom-value', + plugins: [{ id: 'web' }], // OpenRouter web search format + }); + expect(result.tools).toEqual([]); // No tools for OpenRouter web search + expect(result.provider).toBe('openrouter'); + }); }); describe('Real Usage Integration Tests', () => { diff --git a/packages/api/src/endpoints/openai/llm.ts b/packages/api/src/endpoints/openai/llm.ts index c462b664ef..bcc082c47d 100644 --- a/packages/api/src/endpoints/openai/llm.ts +++ b/packages/api/src/endpoints/openai/llm.ts @@ -180,7 +180,12 @@ export function getOpenAILLMConfig({ enableWebSearch = false; } - if (enableWebSearch) { + if (useOpenRouter && enableWebSearch) { + /** OpenRouter expects web search as a plugins parameter */ + modelKwargs.plugins = [{ id: 'web' }]; + hasModelKwargs = true; + } else if (enableWebSearch) { + /** Standard OpenAI web search uses tools API */ llmConfig.useResponsesApi = true; tools.push({ type: 'web_search_preview' }); }