From 826b494578554b16b7edff33a35b60c498a7dbc6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 28 Feb 2026 16:54:07 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=80=20feat:=20update=20OpenRouter=20wi?= =?UTF-8?q?th=20new=20Reasoning=20config=20(#11993)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Update OpenRouter reasoning handling in LLM configuration - Modified the OpenRouter configuration to use a unified `reasoning` object instead of separate `reasoning_effort` and `include_reasoning` properties. - Updated tests to ensure that `reasoning_summary` is excluded from the reasoning object and that the configuration behaves correctly based on the presence of reasoning parameters. - Enhanced test coverage for OpenRouter-specific configurations, ensuring proper handling of various reasoning effort levels. * refactor: Improve OpenRouter reasoning handling in LLM configuration - Updated the handling of the `reasoning` object in the OpenRouter configuration to clarify the relationship between `reasoning_effort` and `include_reasoning`. - Enhanced comments to explain the behavior of the `reasoning` object and its compatibility with legacy parameters. - Ensured that the configuration correctly falls back to legacy behavior when no explicit reasoning effort is provided. * test: Enhance OpenRouter LLM configuration tests - Added a new test to verify the combination of web search plugins and reasoning object for OpenRouter configurations. - Updated existing tests to ensure proper handling of reasoning effort levels and fallback behavior when reasoning_effort is unset. - Improved test coverage for OpenRouter-specific configurations, ensuring accurate validation of reasoning parameters. * chore: Update @librechat/agents dependency to version 3.1.53 - Bumped the version of @librechat/agents in package-lock.json and related package.json files to ensure compatibility with the latest features and fixes. - Updated integrity hashes to reflect the new version. --- api/package.json | 2 +- package-lock.json | 10 +-- packages/api/package.json | 2 +- .../api/src/endpoints/openai/config.spec.ts | 11 +-- packages/api/src/endpoints/openai/llm.spec.ts | 84 ++++++++++++++++++- packages/api/src/endpoints/openai/llm.ts | 18 +++- 6 files changed, 110 insertions(+), 17 deletions(-) diff --git a/api/package.json b/api/package.json index 1447087b38..f9c9601a37 100644 --- a/api/package.json +++ b/api/package.json @@ -44,7 +44,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.52", + "@librechat/agents": "^3.1.53", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/package-lock.json b/package-lock.json index 1ad97628a9..c03ef33c8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.52", + "@librechat/agents": "^3.1.53", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -11836,9 +11836,9 @@ } }, "node_modules/@librechat/agents": { - "version": "3.1.52", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.52.tgz", - "integrity": "sha512-Bg35zp+vEDZ0AEJQPZ+ukWb/UqBrsLcr3YQWRQpuvpftEgfQz0fHM5Wrxn6l5P7PvaD1ViolxoG44nggjCt7Hw==", + "version": "3.1.53", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.53.tgz", + "integrity": "sha512-jK9JHIhQYgr+Ha2FhknEYQmS6Ft3/TGdYIlL6L6EtIq20SIA59r1DvQx/x9sd3wHoHkk6AZumMgqAUTTCaWBIA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -43797,7 +43797,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.52", + "@librechat/agents": "^3.1.53", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/package.json b/packages/api/package.json index 903e15947b..3ceaeb7a12 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -90,7 +90,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.52", + "@librechat/agents": "^3.1.53", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.27.1", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/src/endpoints/openai/config.spec.ts b/packages/api/src/endpoints/openai/config.spec.ts index 93864649f9..2bbe123e63 100644 --- a/packages/api/src/endpoints/openai/config.spec.ts +++ b/packages/api/src/endpoints/openai/config.spec.ts @@ -861,7 +861,7 @@ describe('getOpenAIConfig', () => { expect(result.provider).toBe('openrouter'); }); - it('should handle OpenRouter with reasoning params', () => { + it('should handle OpenRouter with reasoning params (no summary)', () => { const modelOptions = { reasoning_effort: ReasoningEffort.high, reasoning_summary: ReasoningSummary.detailed, @@ -872,10 +872,11 @@ describe('getOpenAIConfig', () => { modelOptions, }); + // OpenRouter reasoning object should only include effort, not summary expect(result.llmConfig.reasoning).toEqual({ effort: ReasoningEffort.high, - summary: ReasoningSummary.detailed, }); + expect(result.llmConfig.include_reasoning).toBeUndefined(); expect(result.provider).toBe('openrouter'); }); @@ -1205,8 +1206,9 @@ describe('getOpenAIConfig', () => { model: 'gpt-4-turbo', temperature: 0.8, streaming: false, - include_reasoning: true, // OpenRouter specific + reasoning: { effort: ReasoningEffort.high }, // OpenRouter reasoning object }); + expect(result.llmConfig.include_reasoning).toBeUndefined(); // Should NOT have useResponsesApi for OpenRouter expect(result.llmConfig.useResponsesApi).toBeUndefined(); expect(result.llmConfig.maxTokens).toBe(2000); @@ -1480,13 +1482,12 @@ describe('getOpenAIConfig', () => { user: 'openrouter-user', temperature: 0.7, maxTokens: 4000, - include_reasoning: true, // OpenRouter specific reasoning: { effort: ReasoningEffort.high, - summary: ReasoningSummary.detailed, }, apiKey: apiKey, }); + expect(result.llmConfig.include_reasoning).toBeUndefined(); expect(result.llmConfig.modelKwargs).toMatchObject({ top_k: 50, repetition_penalty: 1.1, diff --git a/packages/api/src/endpoints/openai/llm.spec.ts b/packages/api/src/endpoints/openai/llm.spec.ts index 8e92332e24..3c10179737 100644 --- a/packages/api/src/endpoints/openai/llm.spec.ts +++ b/packages/api/src/endpoints/openai/llm.spec.ts @@ -381,6 +381,23 @@ describe('getOpenAILLMConfig', () => { expect(result.llmConfig).toHaveProperty('include_reasoning', true); }); + it('should combine web search plugins and reasoning object for OpenRouter', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-3-sonnet', + reasoning_effort: ReasoningEffort.high, + web_search: true, + }, + }); + + expect(result.llmConfig).toHaveProperty('reasoning', { effort: ReasoningEffort.high }); + expect(result.llmConfig).not.toHaveProperty('include_reasoning'); + expect(result.llmConfig.modelKwargs).toHaveProperty('plugins', [{ id: 'web' }]); + }); + it('should disable web search via dropParams', () => { const result = getOpenAILLMConfig({ apiKey: 'test-api-key', @@ -575,7 +592,7 @@ describe('getOpenAILLMConfig', () => { }); describe('OpenRouter Configuration', () => { - it('should include include_reasoning for OpenRouter', () => { + it('should include include_reasoning for OpenRouter when no reasoning_effort set', () => { const result = getOpenAILLMConfig({ apiKey: 'test-api-key', streaming: true, @@ -586,6 +603,71 @@ describe('getOpenAILLMConfig', () => { }); expect(result.llmConfig).toHaveProperty('include_reasoning', true); + expect(result.llmConfig).not.toHaveProperty('reasoning'); + }); + + it('should use reasoning object for OpenRouter when reasoning_effort is set', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-3-sonnet', + reasoning_effort: ReasoningEffort.high, + }, + }); + + expect(result.llmConfig).toHaveProperty('reasoning', { effort: ReasoningEffort.high }); + expect(result.llmConfig).not.toHaveProperty('include_reasoning'); + expect(result.llmConfig).not.toHaveProperty('reasoning_effort'); + }); + + it('should exclude reasoning_summary from OpenRouter reasoning object', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-3-sonnet', + reasoning_effort: ReasoningEffort.high, + reasoning_summary: ReasoningSummary.detailed, + }, + }); + + expect(result.llmConfig).toHaveProperty('reasoning', { effort: ReasoningEffort.high }); + }); + + it.each([ReasoningEffort.xhigh, ReasoningEffort.minimal, ReasoningEffort.none])( + 'should support OpenRouter effort level: %s', + (effort) => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'openai/o3-mini', + reasoning_effort: effort, + }, + }); + + expect(result.llmConfig).toHaveProperty('reasoning', { effort }); + expect(result.llmConfig).not.toHaveProperty('include_reasoning'); + }, + ); + + it('should fall back to include_reasoning when reasoning_effort is unset (empty string)', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-3-sonnet', + reasoning_effort: ReasoningEffort.unset, + }, + }); + + expect(result.llmConfig).toHaveProperty('include_reasoning', true); + expect(result.llmConfig).not.toHaveProperty('reasoning'); }); }); diff --git a/packages/api/src/endpoints/openai/llm.ts b/packages/api/src/endpoints/openai/llm.ts index f25971735c..c659645958 100644 --- a/packages/api/src/endpoints/openai/llm.ts +++ b/packages/api/src/endpoints/openai/llm.ts @@ -1,6 +1,7 @@ import { EModelEndpoint, removeNullishValues } from 'librechat-data-provider'; import type { BindToolsInput } from '@langchain/core/language_models/chat_models'; import type { SettingDefinition } from 'librechat-data-provider'; +import type { OpenRouterReasoning } from '@librechat/agents'; import type { AzureOpenAIInput } from '@langchain/openai'; import type { OpenAI } from 'openai'; import type * as t from '~/types'; @@ -223,10 +224,19 @@ export function getOpenAILLMConfig({ } if (useOpenRouter) { - llmConfig.include_reasoning = true; - } - - if ( + if (hasReasoningParams({ reasoning_effort })) { + /** + * OpenRouter uses a `reasoning` object — `summary` is not supported. + * ChatOpenRouter treats `reasoning` and `include_reasoning` as mutually exclusive: + * `include_reasoning` is legacy compat that maps to `{ enabled: true }` only when + * no `reasoning` object is present, so we intentionally omit it here. + */ + llmConfig.reasoning = { effort: reasoning_effort } as OpenRouterReasoning; + } else { + /** No explicit effort; fall back to legacy `include_reasoning` for reasoning token inclusion */ + llmConfig.include_reasoning = true; + } + } else if ( hasReasoningParams({ reasoning_effort, reasoning_summary }) && (llmConfig.useResponsesApi === true || (endpoint !== EModelEndpoint.openAI && endpoint !== EModelEndpoint.azureOpenAI))