🎚️ feat: Reasoning Parameters for Custom Endpoints (#10297)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

This commit is contained in:
Danny Avila 2025-10-29 13:41:35 -04:00 committed by GitHub
parent 861ef98d29
commit e6aeec9f25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 99 additions and 13 deletions

View file

@ -48,7 +48,7 @@
"@langchain/google-genai": "^0.2.13", "@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13", "@langchain/google-vertexai": "^0.2.13",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.89", "@librechat/agents": "^2.4.90",
"@librechat/api": "*", "@librechat/api": "*",
"@librechat/data-schemas": "*", "@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7", "@microsoft/microsoft-graph-client": "^3.0.7",

View file

@ -143,7 +143,7 @@ const initializeClient = async ({
modelOptions.model = modelName; modelOptions.model = modelName;
clientOptions = Object.assign({ modelOptions }, clientOptions); clientOptions = Object.assign({ modelOptions }, clientOptions);
clientOptions.modelOptions.user = req.user.id; clientOptions.modelOptions.user = req.user.id;
const options = getOpenAIConfig(apiKey, clientOptions); const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
if (options != null && serverless === true) { if (options != null && serverless === true) {
options.useLegacyContent = true; options.useLegacyContent = true;
} }

12
package-lock.json generated
View file

@ -64,7 +64,7 @@
"@langchain/google-genai": "^0.2.13", "@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13", "@langchain/google-vertexai": "^0.2.13",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.89", "@librechat/agents": "^2.4.90",
"@librechat/api": "*", "@librechat/api": "*",
"@librechat/data-schemas": "*", "@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7", "@microsoft/microsoft-graph-client": "^3.0.7",
@ -21690,9 +21690,9 @@
} }
}, },
"node_modules/@librechat/agents": { "node_modules/@librechat/agents": {
"version": "2.4.89", "version": "2.4.90",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.89.tgz", "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.90.tgz",
"integrity": "sha512-QMqaNkkfcDHI8mpaqpgdb1Zz3KT3uVLaaU4NCeXafNH6JvGdG4ueORQH2dM8xtVm3+5DEXwauTdSAi5gHV5tJQ==", "integrity": "sha512-CZI0K0NjIO1mvw4f4tAAMN8x4fFXFyZyrP+NLTCHDvdrIpyTY2k9qRwQHneCGnMgltuUQ+53eXbS3s9psjsAOA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@langchain/anthropic": "^0.3.26", "@langchain/anthropic": "^0.3.26",
@ -51984,7 +51984,7 @@
}, },
"packages/api": { "packages/api": {
"name": "@librechat/api", "name": "@librechat/api",
"version": "1.4.1", "version": "1.5.0",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.21.5", "@babel/preset-env": "^7.21.5",
@ -52023,7 +52023,7 @@
"@azure/storage-blob": "^12.27.0", "@azure/storage-blob": "^12.27.0",
"@keyv/redis": "^4.3.3", "@keyv/redis": "^4.3.3",
"@langchain/core": "^0.3.62", "@langchain/core": "^0.3.62",
"@librechat/agents": "^2.4.89", "@librechat/agents": "^2.4.90",
"@librechat/data-schemas": "*", "@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.17.1", "@modelcontextprotocol/sdk": "^1.17.1",
"axios": "^1.12.1", "axios": "^1.12.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "@librechat/api", "name": "@librechat/api",
"version": "1.4.1", "version": "1.5.0",
"type": "commonjs", "type": "commonjs",
"description": "MCP services for LibreChat", "description": "MCP services for LibreChat",
"main": "dist/index.js", "main": "dist/index.js",
@ -80,7 +80,7 @@
"@azure/storage-blob": "^12.27.0", "@azure/storage-blob": "^12.27.0",
"@keyv/redis": "^4.3.3", "@keyv/redis": "^4.3.3",
"@langchain/core": "^0.3.62", "@langchain/core": "^0.3.62",
"@librechat/agents": "^2.4.89", "@librechat/agents": "^2.4.90",
"@librechat/data-schemas": "*", "@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.17.1", "@modelcontextprotocol/sdk": "^1.17.1",
"axios": "^1.12.1", "axios": "^1.12.1",

View file

@ -1,4 +1,9 @@
import { Verbosity, ReasoningEffort, ReasoningSummary } from 'librechat-data-provider'; import {
Verbosity,
EModelEndpoint,
ReasoningEffort,
ReasoningSummary,
} from 'librechat-data-provider';
import type { RequestInit } from 'undici'; import type { RequestInit } from 'undici';
import type { OpenAIParameters, AzureOptions } from '~/types'; import type { OpenAIParameters, AzureOptions } from '~/types';
import { getOpenAIConfig } from './config'; import { getOpenAIConfig } from './config';
@ -103,12 +108,89 @@ describe('getOpenAIConfig', () => {
const result = getOpenAIConfig(mockApiKey, { modelOptions }); const result = getOpenAIConfig(mockApiKey, { modelOptions });
/** When no endpoint is specified, it's treated as non-openAI/azureOpenAI, so uses reasoning object */
expect(result.llmConfig.reasoning).toEqual({
effort: ReasoningEffort.high,
summary: ReasoningSummary.detailed,
});
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
});
it('should use reasoning_effort for openAI endpoint without useResponsesApi', () => {
const modelOptions = {
reasoning_effort: ReasoningEffort.high,
reasoning_summary: ReasoningSummary.detailed,
};
const result = getOpenAIConfig(mockApiKey, { modelOptions }, EModelEndpoint.openAI);
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBe( expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBe(
ReasoningEffort.high, ReasoningEffort.high,
); );
expect(result.llmConfig.reasoning).toBeUndefined(); expect(result.llmConfig.reasoning).toBeUndefined();
}); });
it('should use reasoning_effort for azureOpenAI endpoint without useResponsesApi', () => {
const modelOptions = {
reasoning_effort: ReasoningEffort.high,
reasoning_summary: ReasoningSummary.detailed,
};
const result = getOpenAIConfig(mockApiKey, { modelOptions }, EModelEndpoint.azureOpenAI);
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBe(
ReasoningEffort.high,
);
expect(result.llmConfig.reasoning).toBeUndefined();
});
it('should use reasoning object for openAI endpoint with useResponsesApi=true', () => {
const modelOptions = {
reasoning_effort: ReasoningEffort.high,
reasoning_summary: ReasoningSummary.detailed,
useResponsesApi: true,
};
const result = getOpenAIConfig(mockApiKey, { modelOptions }, EModelEndpoint.openAI);
expect(result.llmConfig.reasoning).toEqual({
effort: ReasoningEffort.high,
summary: ReasoningSummary.detailed,
});
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
});
it('should use reasoning object for azureOpenAI endpoint with useResponsesApi=true', () => {
const modelOptions = {
reasoning_effort: ReasoningEffort.high,
reasoning_summary: ReasoningSummary.detailed,
useResponsesApi: true,
};
const result = getOpenAIConfig(mockApiKey, { modelOptions }, EModelEndpoint.azureOpenAI);
expect(result.llmConfig.reasoning).toEqual({
effort: ReasoningEffort.high,
summary: ReasoningSummary.detailed,
});
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
});
it('should use reasoning object for non-openAI/azureOpenAI endpoints', () => {
const modelOptions = {
reasoning_effort: ReasoningEffort.high,
reasoning_summary: ReasoningSummary.detailed,
};
const result = getOpenAIConfig(mockApiKey, { modelOptions }, 'custom-endpoint');
expect(result.llmConfig.reasoning).toEqual({
effort: ReasoningEffort.high,
summary: ReasoningSummary.detailed,
});
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
});
it('should handle OpenRouter configuration', () => { it('should handle OpenRouter configuration', () => {
const reverseProxyUrl = 'https://openrouter.ai/api/v1'; const reverseProxyUrl = 'https://openrouter.ai/api/v1';

View file

@ -68,6 +68,7 @@ export function getOpenAIConfig(
azure, azure,
apiKey, apiKey,
baseURL, baseURL,
endpoint,
streaming, streaming,
addParams, addParams,
dropParams, dropParams,

View file

@ -1,4 +1,4 @@
import { removeNullishValues } from 'librechat-data-provider'; import { EModelEndpoint, removeNullishValues } from 'librechat-data-provider';
import type { BindToolsInput } from '@langchain/core/language_models/chat_models'; import type { BindToolsInput } from '@langchain/core/language_models/chat_models';
import type { AzureOpenAIInput } from '@langchain/openai'; import type { AzureOpenAIInput } from '@langchain/openai';
import type { OpenAI } from 'openai'; import type { OpenAI } from 'openai';
@ -79,6 +79,7 @@ export function getOpenAILLMConfig({
azure, azure,
apiKey, apiKey,
baseURL, baseURL,
endpoint,
streaming, streaming,
addParams, addParams,
dropParams, dropParams,
@ -88,6 +89,7 @@ export function getOpenAILLMConfig({
apiKey: string; apiKey: string;
streaming: boolean; streaming: boolean;
baseURL?: string | null; baseURL?: string | null;
endpoint?: EModelEndpoint | string | null;
modelOptions: Partial<t.OpenAIParameters>; modelOptions: Partial<t.OpenAIParameters>;
addParams?: Record<string, unknown>; addParams?: Record<string, unknown>;
dropParams?: string[]; dropParams?: string[];
@ -155,7 +157,8 @@ export function getOpenAILLMConfig({
if ( if (
hasReasoningParams({ reasoning_effort, reasoning_summary }) && hasReasoningParams({ reasoning_effort, reasoning_summary }) &&
(llmConfig.useResponsesApi === true || useOpenRouter) (llmConfig.useResponsesApi === true ||
(endpoint !== EModelEndpoint.openAI && endpoint !== EModelEndpoint.azureOpenAI))
) { ) {
llmConfig.reasoning = removeNullishValues( llmConfig.reasoning = removeNullishValues(
{ {