fix: Address custom reasoning review cases

This commit is contained in:
Danny Avila 2026-06-01 09:14:06 -04:00
parent c839e61918
commit c1b3125b20
6 changed files with 109 additions and 46 deletions

View file

@ -177,7 +177,8 @@ https://www.librechat.ai/docs/configuration/stt_tts`);
// Validate and fill out missing values for custom parameters
function parseCustomParams(endpointName, customParams) {
const paramEndpoint = customParams.defaultParamsEndpoint;
const paramEndpoint = customParams.defaultParamsEndpoint ?? 'custom';
customParams.defaultParamsEndpoint = paramEndpoint;
customParams.paramDefinitions = customParams.paramDefinitions || [];
// Checks if `defaultParamsEndpoint` is a key in `paramSettings`.

View file

@ -11,7 +11,7 @@ jest.mock('librechat-data-provider', () => {
paramSettings: {
foo: {},
bar: {},
custom: {},
custom: [],
openrouter: [
{
key: 'promptCache',
@ -59,6 +59,7 @@ jest.mock('@librechat/data-schemas', () => {
const axios = require('axios');
const { loadYaml } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { ReasoningParameterFormat } = require('librechat-data-provider');
const loadCustomConfig = require('./loadCustomConfig');
describe('loadCustomConfig', () => {
@ -307,11 +308,16 @@ describe('loadCustomConfig', () => {
);
});
it('throws an error when defaultParamsEndpoint is not provided', async () => {
const malformedCustomParams = { defaultParamsEndpoint: undefined };
await expect(loadCustomParams(malformedCustomParams)).rejects.toThrow(
'defaultParamsEndpoint of "Google" endpoint is invalid. Valid options are foo, bar, custom, openrouter, google',
);
it('defaults defaultParamsEndpoint when only reasoningFormat is provided', async () => {
const parsedConfig = await loadCustomParams({
reasoningFormat: ReasoningParameterFormat.reasoningObject,
});
expect(parsedConfig.endpoints.custom[0].customParams).toEqual({
defaultParamsEndpoint: 'custom',
reasoningFormat: ReasoningParameterFormat.reasoningObject,
paramDefinitions: [],
});
});
it('fills the paramDefinitions with missing values', async () => {

View file

@ -10,7 +10,7 @@ describe('getOpenAIConfig - Backward Compatibility', () => {
describe('OpenAI endpoint', () => {
it('should handle GPT-5 model with reasoning and web search', () => {
const apiKey = 'sk-proj-somekey';
const endpoint = undefined;
const endpoint = EModelEndpoint.openAI;
const options = {
modelOptions: {
model: 'gpt-5-nano',
@ -138,7 +138,7 @@ describe('getOpenAIConfig - Backward Compatibility', () => {
it('should handle Azure OpenAI with Responses API and reasoning', () => {
const apiKey = 'some_azure_key';
const endpoint = undefined;
const endpoint = EModelEndpoint.azureOpenAI;
const options = {
modelOptions: {
model: 'gpt-5',

View file

@ -80,7 +80,7 @@ describe('getOpenAIConfig', () => {
expect(result.llmConfig.modelKwargs).toBeUndefined();
});
it('should handle reasoning params for `useResponsesApi`', () => {
it('should pass custom endpoint reasoning object through modelKwargs for `useResponsesApi`', () => {
const modelOptions = {
reasoning_effort: ReasoningEffort.high,
reasoning_summary: ReasoningSummary.detailed,
@ -90,9 +90,12 @@ describe('getOpenAIConfig', () => {
modelOptions: { ...modelOptions, useResponsesApi: true },
});
expect(result.llmConfig.reasoning).toEqual({
effort: ReasoningEffort.high,
summary: ReasoningSummary.detailed,
expect(result.llmConfig.reasoning).toBeUndefined();
expect(result.llmConfig.modelKwargs).toEqual({
reasoning: {
effort: ReasoningEffort.high,
summary: ReasoningSummary.detailed,
},
});
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
expect((result.llmConfig as Record<string, unknown>).reasoning_summary).toBeUndefined();
@ -1072,11 +1075,12 @@ describe('getOpenAIConfig', () => {
const result = getOpenAIConfig(mockApiKey, {
modelOptions: { ...modelOptions, useResponsesApi: true } as Partial<OpenAIParameters>,
});
const reasoning = result.llmConfig?.reasoning ?? result.llmConfig?.modelKwargs?.reasoning;
if (shouldHaveReasoning) {
expect(result.llmConfig?.reasoning).toBeDefined();
expect(reasoning).toBeDefined();
} else {
expect(result.llmConfig?.reasoning).toBeUndefined();
expect(reasoning).toBeUndefined();
}
});
});
@ -1154,6 +1158,7 @@ describe('getOpenAIConfig', () => {
frequency_penalty: 0.5,
presence_penalty: 0.6,
max_tokens: 1000,
reasoning_effort: ReasoningEffort.high,
custom_param: 'should-remain',
};
@ -1168,6 +1173,7 @@ describe('getOpenAIConfig', () => {
/** `presence_penalty` is converted to `presencePenalty` */
expect(result.llmConfig.maxTokens).toBe(1000); // max_tokens is allowed
expect((result.llmConfig as Record<string, unknown>).custom_param).toBe('should-remain');
expect(result.llmConfig.modelKwargs).toBeUndefined();
});
});
@ -1275,10 +1281,11 @@ describe('getOpenAIConfig', () => {
streaming: false,
useResponsesApi: true, // From web_search
});
expect(result.llmConfig.reasoning).toBeUndefined();
expect(result.llmConfig.maxTokens).toBe(2000);
expect(result.llmConfig.modelKwargs).toEqual({
text: { verbosity: Verbosity.medium },
reasoning_effort: ReasoningEffort.high,
reasoning: { effort: ReasoningEffort.high },
customParam: 'custom-value',
});
expect(result.tools).toEqual([{ type: 'web_search' }]);

View file

@ -518,6 +518,44 @@ describe('getOpenAILLMConfig', () => {
expect(result.llmConfig.modelKwargs).toBeUndefined();
});
it('should use Responses API reasoning when web_search enables Responses API', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',
streaming: true,
endpoint: 'custom',
modelOptions: {
model: 'provider/reasoning-model',
reasoning_effort: ReasoningEffort.high,
reasoning_summary: ReasoningSummary.concise,
web_search: true,
},
});
expect(result.llmConfig).toHaveProperty('useResponsesApi', true);
expect(result.llmConfig).not.toHaveProperty('reasoning');
expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', {
effort: ReasoningEffort.high,
summary: ReasoningSummary.concise,
});
expect(result.tools).toContainEqual({ type: 'web_search' });
});
it('should remove reasoning kwargs for GPT-4o search models', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',
streaming: true,
endpoint: 'custom',
modelOptions: {
model: 'gpt-4o-search',
reasoning_effort: ReasoningEffort.high,
},
});
expect(result.llmConfig).not.toHaveProperty('reasoning');
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
expect(result.llmConfig.modelKwargs).toBeUndefined();
});
it('should use reasoning object when useResponsesApi is true', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',

View file

@ -107,6 +107,23 @@ function isOpenAIEndpoint(endpoint?: EModelEndpoint | string | null): boolean {
return endpoint === EModelEndpoint.openAI || endpoint === EModelEndpoint.azureOpenAI;
}
function deleteConfigParam({
param,
llmConfig,
modelKwargs,
}: {
param: string;
llmConfig: OpenAILLMConfig;
modelKwargs: Record<string, unknown>;
}) {
if (param in llmConfig) {
delete llmConfig[param as keyof t.OAIClientOptions];
}
if (param in modelKwargs) {
delete modelKwargs[param];
}
}
const openRouterAnthropicVerbosityByEffort: Record<
string,
NonNullable<OpenAILLMConfig['verbosity']>
@ -250,20 +267,24 @@ function applyReasoningConfig({
}
const reasoning = getReasoningObject({ reasoningEffort, reasoningSummary });
if (llmConfig.useResponsesApi === true) {
llmConfig.reasoning = reasoning;
if (reasoningFormat === ReasoningParameterFormat.disabled) {
return false;
}
if (isOpenAIEndpoint(endpoint)) {
if (llmConfig.useResponsesApi === true) {
llmConfig.reasoning = reasoning;
return false;
}
if (reasoningEffort) {
llmConfig.reasoning_effort = reasoningEffort;
}
return false;
}
if (reasoningFormat === ReasoningParameterFormat.disabled) {
return false;
if (llmConfig.useResponsesApi === true) {
modelKwargs.reasoning = reasoning;
return true;
}
if (reasoningFormat === ReasoningParameterFormat.reasoningObject) {
@ -519,16 +540,6 @@ export function getOpenAILLMConfig({
modelKwargs,
llmConfig,
}) || hasModelKwargs;
} else {
hasModelKwargs =
applyReasoningConfig({
endpoint,
llmConfig,
modelKwargs,
reasoningFormat,
reasoningEffort: reasoning_effort,
reasoningSummary: reasoning_summary,
}) || hasModelKwargs;
}
if (llmConfig.max_tokens != null) {
@ -559,6 +570,18 @@ export function getOpenAILLMConfig({
llmConfig.promptCache = true;
}
if (!useOpenRouter) {
hasModelKwargs =
applyReasoningConfig({
endpoint,
llmConfig,
modelKwargs,
reasoningFormat,
reasoningEffort: reasoning_effort,
reasoningSummary: reasoning_summary,
}) || hasModelKwargs;
}
/** DeepSeek thinking-mode requires `reasoning_content` replay on tool turns (#13366). */
if (
typeof modelOptions.model === 'string' &&
@ -588,11 +611,7 @@ export function getOpenAILLMConfig({
const updatedDropParams = dropParams || [];
const combinedDropParams = [...new Set([...updatedDropParams, ...reasoningExcludeParams])];
combinedDropParams.forEach((param) => {
if (param in llmConfig) {
delete llmConfig[param as keyof t.OAIClientOptions];
}
});
combinedDropParams.forEach((param) => deleteConfigParam({ param, llmConfig, modelKwargs }));
} else if (modelOptions.model && /gpt-4o.*search/.test(modelOptions.model as string)) {
/**
* Note: OpenAI Web Search models do not support any known parameters besides `max_tokens`
@ -617,17 +636,9 @@ export function getOpenAILLMConfig({
const updatedDropParams = dropParams || [];
const combinedDropParams = [...new Set([...updatedDropParams, ...searchExcludeParams])];
combinedDropParams.forEach((param) => {
if (param in llmConfig) {
delete llmConfig[param as keyof t.OAIClientOptions];
}
});
combinedDropParams.forEach((param) => deleteConfigParam({ param, llmConfig, modelKwargs }));
} else if (dropParams && Array.isArray(dropParams)) {
dropParams.forEach((param) => {
if (param in llmConfig) {
delete llmConfig[param as keyof t.OAIClientOptions];
}
});
dropParams.forEach((param) => deleteConfigParam({ param, llmConfig, modelKwargs }));
}
hasModelKwargs =
@ -649,7 +660,7 @@ export function getOpenAILLMConfig({
hasModelKwargs = true;
}
if (hasModelKwargs) {
if (hasModelKwargs && Object.keys(modelKwargs).length > 0) {
llmConfig.modelKwargs = modelKwargs;
}