mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: Preserve custom endpoint reasoning params
This commit is contained in:
parent
7dba640c9f
commit
c839e61918
9 changed files with 273 additions and 31 deletions
|
|
@ -395,6 +395,7 @@ describe('getOpenAIConfig - Backward Compatibility', () => {
|
|||
modelOptions: {
|
||||
model: '@cf/deepseek-ai/deepseek-r1-distill-qwen-32b',
|
||||
user: 'some-user',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
reverseProxyUrl:
|
||||
'https://gateway.ai.cloudflare.com/v1/${CF_ACCOUNT_ID}/${CF_GATEWAY_ID}/workers-ai/v1',
|
||||
|
|
@ -419,6 +420,9 @@ describe('getOpenAIConfig - Backward Compatibility', () => {
|
|||
user: 'some-user',
|
||||
disableStreaming: true,
|
||||
apiKey: 'someKey',
|
||||
modelKwargs: {
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
},
|
||||
configOptions: {
|
||||
baseURL:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
EModelEndpoint,
|
||||
ReasoningEffort,
|
||||
ReasoningSummary,
|
||||
ReasoningParameterFormat,
|
||||
} from 'librechat-data-provider';
|
||||
import type { RequestInit } from 'undici';
|
||||
import type { OpenAIParameters, AzureOptions } from '~/types';
|
||||
|
|
@ -97,19 +98,18 @@ describe('getOpenAIConfig', () => {
|
|||
expect((result.llmConfig as Record<string, unknown>).reasoning_summary).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle reasoning params without `useResponsesApi`', () => {
|
||||
it('should pass custom endpoint reasoning through modelKwargs without `useResponsesApi`', () => {
|
||||
const modelOptions = {
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
reasoning_summary: ReasoningSummary.detailed,
|
||||
};
|
||||
|
||||
const result = getOpenAIConfig(mockApiKey, { modelOptions });
|
||||
const result = getOpenAIConfig(mockApiKey, { modelOptions }, 'custom-endpoint');
|
||||
|
||||
/** 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.modelKwargs).toEqual({
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
});
|
||||
expect(result.llmConfig.reasoning).toBeUndefined();
|
||||
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ describe('getOpenAIConfig', () => {
|
|||
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should use reasoning object for non-openAI/azureOpenAI endpoints', () => {
|
||||
it('should pass reasoning_effort through modelKwargs for non-openAI/azureOpenAI endpoints', () => {
|
||||
const modelOptions = {
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
reasoning_summary: ReasoningSummary.detailed,
|
||||
|
|
@ -181,13 +181,79 @@ describe('getOpenAIConfig', () => {
|
|||
|
||||
const result = getOpenAIConfig(mockApiKey, { modelOptions }, 'custom-endpoint');
|
||||
|
||||
expect(result.llmConfig.reasoning).toEqual({
|
||||
effort: ReasoningEffort.high,
|
||||
summary: ReasoningSummary.detailed,
|
||||
expect(result.llmConfig.modelKwargs).toEqual({
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
});
|
||||
expect(result.llmConfig.reasoning).toBeUndefined();
|
||||
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should support custom endpoint reasoning object format', () => {
|
||||
const result = getOpenAIConfig(
|
||||
mockApiKey,
|
||||
{
|
||||
customParams: {
|
||||
reasoningFormat: ReasoningParameterFormat.reasoningObject,
|
||||
},
|
||||
modelOptions: {
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
reasoning_summary: ReasoningSummary.detailed,
|
||||
},
|
||||
},
|
||||
'custom-endpoint',
|
||||
);
|
||||
|
||||
expect(result.llmConfig.modelKwargs).toEqual({
|
||||
reasoning: {
|
||||
effort: ReasoningEffort.high,
|
||||
summary: ReasoningSummary.detailed,
|
||||
},
|
||||
});
|
||||
expect(result.llmConfig.reasoning).toBeUndefined();
|
||||
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should default Vercel custom endpoints to reasoning object format', () => {
|
||||
const result = getOpenAIConfig(
|
||||
mockApiKey,
|
||||
{
|
||||
reverseProxyUrl: 'https://ai-gateway.vercel.sh/v1',
|
||||
modelOptions: {
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
},
|
||||
'Vercel',
|
||||
);
|
||||
|
||||
expect(result.llmConfig.modelKwargs).toEqual({
|
||||
reasoning: {
|
||||
effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
expect(result.llmConfig.reasoning).toBeUndefined();
|
||||
expect((result.llmConfig as Record<string, unknown>).reasoning_effort).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow Vercel reasoning format override', () => {
|
||||
const result = getOpenAIConfig(
|
||||
mockApiKey,
|
||||
{
|
||||
reverseProxyUrl: 'https://ai-gateway.vercel.sh/v1',
|
||||
customParams: {
|
||||
reasoningFormat: ReasoningParameterFormat.reasoningEffort,
|
||||
},
|
||||
modelOptions: {
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
},
|
||||
'Vercel',
|
||||
);
|
||||
|
||||
expect(result.llmConfig.modelKwargs).toEqual({
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle OpenRouter configuration', () => {
|
||||
const reverseProxyUrl = 'https://openrouter.ai/api/v1';
|
||||
|
||||
|
|
@ -1212,6 +1278,7 @@ describe('getOpenAIConfig', () => {
|
|||
expect(result.llmConfig.maxTokens).toBe(2000);
|
||||
expect(result.llmConfig.modelKwargs).toEqual({
|
||||
text: { verbosity: Verbosity.medium },
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
customParam: 'custom-value',
|
||||
});
|
||||
expect(result.tools).toEqual([{ type: 'web_search' }]);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ProxyAgent } from 'undici';
|
||||
import { Providers } from '@librechat/agents';
|
||||
import { KnownEndpoints, EModelEndpoint } from 'librechat-data-provider';
|
||||
import { KnownEndpoints, EModelEndpoint, ReasoningParameterFormat } from 'librechat-data-provider';
|
||||
import type * as t from '~/types';
|
||||
import { getLLMConfig as getAnthropicLLMConfig } from '~/endpoints/anthropic/llm';
|
||||
import { getOpenAILLMConfig, extractDefaultParams } from './llm';
|
||||
|
|
@ -34,6 +34,22 @@ function getDefaultParams({
|
|||
};
|
||||
}
|
||||
|
||||
function getReasoningFormat({
|
||||
customFormat,
|
||||
isVercel,
|
||||
}: {
|
||||
customFormat?: ReasoningParameterFormat;
|
||||
isVercel: boolean;
|
||||
}): ReasoningParameterFormat | undefined {
|
||||
if (customFormat) {
|
||||
return customFormat;
|
||||
}
|
||||
if (isVercel) {
|
||||
return ReasoningParameterFormat.reasoningObject;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function mergeHeadersPreservingAnthropicBeta(
|
||||
headers: Record<string, string> | undefined,
|
||||
defaultHeaders: Record<string, string>,
|
||||
|
|
@ -159,6 +175,10 @@ export function getOpenAIConfig(
|
|||
defaultParams,
|
||||
modelOptions,
|
||||
useOpenRouter,
|
||||
reasoningFormat: getReasoningFormat({
|
||||
customFormat: options.customParams?.reasoningFormat,
|
||||
isVercel: Boolean(isVercel),
|
||||
}),
|
||||
});
|
||||
llmConfig = openaiResult.llmConfig;
|
||||
azure = openaiResult.azure;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
EModelEndpoint,
|
||||
ReasoningEffort,
|
||||
ReasoningSummary,
|
||||
ReasoningParameterFormat,
|
||||
} from 'librechat-data-provider';
|
||||
import { getOpenAILLMConfig, extractDefaultParams, applyDefaultParams } from './llm';
|
||||
import type * as t from '~/types';
|
||||
|
|
@ -463,23 +464,58 @@ describe('getOpenAILLMConfig', () => {
|
|||
expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high);
|
||||
});
|
||||
|
||||
it('should use reasoning object for non-OpenAI endpoints', () => {
|
||||
it('should pass reasoning_effort through modelKwargs for custom endpoints', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: 'custom',
|
||||
modelOptions: {
|
||||
model: 'o1',
|
||||
model: 'provider/reasoning-model',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning_effort', ReasoningEffort.high);
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it('should support reasoning object passthrough for custom endpoints', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: 'custom',
|
||||
reasoningFormat: ReasoningParameterFormat.reasoningObject,
|
||||
modelOptions: {
|
||||
model: 'provider/reasoning-model',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
reasoning_summary: ReasoningSummary.concise,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('reasoning');
|
||||
expect(result.llmConfig.reasoning).toEqual({
|
||||
expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', {
|
||||
effort: ReasoningEffort.high,
|
||||
summary: ReasoningSummary.concise,
|
||||
});
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it('should allow custom endpoints to disable reasoning passthrough', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: 'custom',
|
||||
reasoningFormat: ReasoningParameterFormat.disabled,
|
||||
modelOptions: {
|
||||
model: 'provider/reasoning-model',
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
EModelEndpoint,
|
||||
ReasoningParameterFormat,
|
||||
removeNullishValues,
|
||||
supportsAdaptiveThinking,
|
||||
} from 'librechat-data-provider';
|
||||
|
|
@ -86,6 +87,26 @@ function hasReasoningParams({
|
|||
);
|
||||
}
|
||||
|
||||
function getReasoningObject({
|
||||
reasoningEffort,
|
||||
reasoningSummary,
|
||||
}: {
|
||||
reasoningEffort?: OpenAILLMConfig['reasoning_effort'];
|
||||
reasoningSummary?: OpenAILLMConfig['reasoning_summary'];
|
||||
}): OpenAI.Reasoning {
|
||||
return removeNullishValues(
|
||||
{
|
||||
effort: reasoningEffort,
|
||||
summary: reasoningSummary,
|
||||
},
|
||||
true,
|
||||
) as OpenAI.Reasoning;
|
||||
}
|
||||
|
||||
function isOpenAIEndpoint(endpoint?: EModelEndpoint | string | null): boolean {
|
||||
return endpoint === EModelEndpoint.openAI || endpoint === EModelEndpoint.azureOpenAI;
|
||||
}
|
||||
|
||||
const openRouterAnthropicVerbosityByEffort: Record<
|
||||
string,
|
||||
NonNullable<OpenAILLMConfig['verbosity']>
|
||||
|
|
@ -204,6 +225,60 @@ function applyOpenRouterReasoningConfig({
|
|||
return true;
|
||||
}
|
||||
|
||||
function applyReasoningConfig({
|
||||
endpoint,
|
||||
llmConfig,
|
||||
modelKwargs,
|
||||
reasoningEffort,
|
||||
reasoningFormat,
|
||||
reasoningSummary,
|
||||
}: {
|
||||
endpoint?: EModelEndpoint | string | null;
|
||||
llmConfig: OpenAILLMConfig;
|
||||
modelKwargs: Record<string, unknown>;
|
||||
reasoningEffort?: OpenAILLMConfig['reasoning_effort'];
|
||||
reasoningFormat?: ReasoningParameterFormat;
|
||||
reasoningSummary?: OpenAILLMConfig['reasoning_summary'];
|
||||
}): boolean {
|
||||
if (
|
||||
!hasReasoningParams({
|
||||
reasoning_effort: reasoningEffort,
|
||||
reasoning_summary: reasoningSummary,
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const reasoning = getReasoningObject({ reasoningEffort, reasoningSummary });
|
||||
if (llmConfig.useResponsesApi === true) {
|
||||
llmConfig.reasoning = reasoning;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isOpenAIEndpoint(endpoint)) {
|
||||
if (reasoningEffort) {
|
||||
llmConfig.reasoning_effort = reasoningEffort;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reasoningFormat === ReasoningParameterFormat.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reasoningFormat === ReasoningParameterFormat.reasoningObject) {
|
||||
modelKwargs.reasoning = reasoning;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (reasoningEffort) {
|
||||
modelKwargs.reasoning_effort = reasoningEffort;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getModelKwargsText(modelKwargs: Record<string, unknown>): Record<string, unknown> {
|
||||
const { text } = modelKwargs;
|
||||
if (text == null || typeof text !== 'object' || Array.isArray(text)) {
|
||||
|
|
@ -294,6 +369,7 @@ export function getOpenAILLMConfig({
|
|||
dropParams,
|
||||
defaultParams,
|
||||
useOpenRouter,
|
||||
reasoningFormat = ReasoningParameterFormat.reasoningEffort,
|
||||
modelOptions: _modelOptions,
|
||||
}: {
|
||||
apiKey: string;
|
||||
|
|
@ -305,6 +381,7 @@ export function getOpenAILLMConfig({
|
|||
dropParams?: string[];
|
||||
defaultParams?: Record<string, unknown>;
|
||||
useOpenRouter?: boolean;
|
||||
reasoningFormat?: ReasoningParameterFormat;
|
||||
azure?: false | t.AzureOptions;
|
||||
}): Pick<t.LLMConfigResult, 'llmConfig' | 'tools'> & {
|
||||
azure?: t.AzureOptions;
|
||||
|
|
@ -442,20 +519,16 @@ export function getOpenAILLMConfig({
|
|||
modelKwargs,
|
||||
llmConfig,
|
||||
}) || hasModelKwargs;
|
||||
} else if (
|
||||
hasReasoningParams({ reasoning_effort, reasoning_summary }) &&
|
||||
(llmConfig.useResponsesApi === true ||
|
||||
(endpoint !== EModelEndpoint.openAI && endpoint !== EModelEndpoint.azureOpenAI))
|
||||
) {
|
||||
llmConfig.reasoning = removeNullishValues(
|
||||
{
|
||||
effort: reasoning_effort,
|
||||
summary: reasoning_summary,
|
||||
},
|
||||
true,
|
||||
) as OpenAI.Reasoning;
|
||||
} else if (hasReasoningParams({ reasoning_effort })) {
|
||||
llmConfig.reasoning_effort = reasoning_effort;
|
||||
} else {
|
||||
hasModelKwargs =
|
||||
applyReasoningConfig({
|
||||
endpoint,
|
||||
llmConfig,
|
||||
modelKwargs,
|
||||
reasoningFormat,
|
||||
reasoningEffort: reasoning_effort,
|
||||
reasoningSummary: reasoning_summary,
|
||||
}) || hasModelKwargs;
|
||||
}
|
||||
|
||||
if (llmConfig.max_tokens != null) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
summarizationTriggerSchema,
|
||||
summarizationConfigSchema,
|
||||
} from '../src/config';
|
||||
import { tModelSpecPresetSchema, EModelEndpoint } from '../src/schemas';
|
||||
import { tModelSpecPresetSchema, EModelEndpoint, ReasoningParameterFormat } from '../src/schemas';
|
||||
import { specsConfigSchema } from '../src/models';
|
||||
import { FileSources } from '../src/types/files';
|
||||
|
||||
|
|
@ -305,6 +305,33 @@ describe('endpointSchema addParams validation', () => {
|
|||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts custom reasoning format config', () => {
|
||||
const result = endpointSchema.safeParse({
|
||||
...validEndpoint,
|
||||
customParams: {
|
||||
reasoningFormat: ReasoningParameterFormat.reasoningObject,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.customParams?.reasoningFormat).toBe(
|
||||
ReasoningParameterFormat.reasoningObject,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects invalid custom reasoning format config', () => {
|
||||
const result = endpointSchema.safeParse({
|
||||
...validEndpoint,
|
||||
customParams: {
|
||||
reasoningFormat: 'provider_magic',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-boolean web_search objects in addParams', () => {
|
||||
const result = endpointSchema.safeParse({
|
||||
...validEndpoint,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { z } from 'zod';
|
||||
import type { ZodError } from 'zod';
|
||||
import type { TEndpointsConfig, TModelsConfig, TConfig } from './types';
|
||||
import { EModelEndpoint, eModelEndpointSchema, isAgentsEndpoint } from './schemas';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
eModelEndpointSchema,
|
||||
isAgentsEndpoint,
|
||||
eReasoningParameterFormatSchema,
|
||||
} from './schemas';
|
||||
import { ComponentTypes, SettingTypes, OptionTypes } from './generate';
|
||||
import { specsConfigSchema, TSpecsConfig } from './models';
|
||||
import { fileConfigSchema } from './file-config';
|
||||
|
|
@ -630,6 +635,7 @@ export const endpointSchema = baseEndpointSchema.merge(
|
|||
customParams: z
|
||||
.object({
|
||||
defaultParamsEndpoint: z.string().default('custom'),
|
||||
reasoningFormat: eReasoningParameterFormatSchema.optional(),
|
||||
paramDefinitions: z.array(paramDefinitionSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
|
|
|
|||
|
|
@ -177,6 +177,12 @@ export enum ReasoningEffort {
|
|||
xhigh = 'xhigh',
|
||||
}
|
||||
|
||||
export enum ReasoningParameterFormat {
|
||||
disabled = 'disabled',
|
||||
reasoningEffort = 'reasoning_effort',
|
||||
reasoningObject = 'reasoning_object',
|
||||
}
|
||||
|
||||
export enum AnthropicEffort {
|
||||
unset = '',
|
||||
low = 'low',
|
||||
|
|
@ -250,6 +256,7 @@ export const imageDetailValue = {
|
|||
|
||||
export const eImageDetailSchema = z.nativeEnum(ImageDetail);
|
||||
export const eReasoningEffortSchema = z.nativeEnum(ReasoningEffort);
|
||||
export const eReasoningParameterFormatSchema = z.nativeEnum(ReasoningParameterFormat);
|
||||
export const eAnthropicEffortSchema = z.nativeEnum(AnthropicEffort);
|
||||
export const eThinkingDisplaySchema = z.nativeEnum(ThinkingDisplay);
|
||||
export const eReasoningSummarySchema = z.nativeEnum(ReasoningSummary);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
TAttachment,
|
||||
TMessage,
|
||||
TBanner,
|
||||
ReasoningParameterFormat,
|
||||
} from './schemas';
|
||||
import type { RefillIntervalUnit } from './balance';
|
||||
import type { SettingDefinition } from './generate';
|
||||
|
|
@ -398,6 +399,7 @@ export type TConfig = {
|
|||
capabilities?: string[];
|
||||
customParams?: {
|
||||
defaultParamsEndpoint?: string;
|
||||
reasoningFormat?: ReasoningParameterFormat;
|
||||
paramDefinitions?: Partial<SettingDefinition>[];
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue