🌐 feat: OpenRouter Web Search (#9853)

* 🌐 feat: OpenRouter Web Search

- Added tests for handling web_search parameter with OpenRouter in various scenarios.
- Implemented logic to manage web_search in modelOptions and addParams/dropParams.
- Ensured correct configuration of llmConfig and modelKwargs for OpenRouter, including handling of plugins.
- Improved overall integration of OpenRouter with OpenAI API, ensuring expected behavior across different configurations.

* chore: bump @librechat/agents to v2.4.81
This commit is contained in:
Danny Avila 2025-09-26 09:35:41 -04:00 committed by GitHub
parent 823015160c
commit 3d7eaf0fcc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 128 additions and 8 deletions

View file

@ -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",

10
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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', () => {

View file

@ -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' });
}