Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api/server/services/Config/loadCustomConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
30 changes: 24 additions & 6 deletions api/server/services/Config/loadCustomConfig.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jest.mock('librechat-data-provider', () => {
paramSettings: {
foo: {},
bar: {},
custom: {},
custom: [],
openrouter: [
{
key: 'promptCache',
Expand Down Expand Up @@ -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, ReasoningResponseKey } = require('librechat-data-provider');
const loadCustomConfig = require('./loadCustomConfig');

describe('loadCustomConfig', () => {
Expand Down Expand Up @@ -307,11 +308,28 @@ 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('defaults defaultParamsEndpoint when only reasoningKey is provided', async () => {
const parsedConfig = await loadCustomParams({
reasoningKey: ReasoningResponseKey.reasoning,
});

expect(parsedConfig.endpoints.custom[0].customParams).toEqual({
defaultParamsEndpoint: 'custom',
reasoningKey: ReasoningResponseKey.reasoning,
paramDefinitions: [],
});
});

it('fills the paramDefinitions with missing values', async () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/agents/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
AgentToolResources,
AgentToolOptions,
TEndpointOption,
ReasoningResponseKey,
TFile,
Agent,
TUser,
Expand Down Expand Up @@ -169,6 +170,8 @@ export type InitializedAgent = Agent & {
actionsEnabled?: boolean;
/** Maximum characters allowed in a single tool result before truncation. */
maxToolResultChars?: number;
/** Response field to read model reasoning from for custom OpenAI-compatible endpoints. */
reasoningKey?: ReasoningResponseKey;
/**
* Whether the code-execution environment is available *for this agent*.
* Narrower than the incoming `params.codeEnvAvailable` admin flag — this
Expand Down Expand Up @@ -1035,6 +1038,7 @@ export async function initializeAgent(
actionsEnabled,
baseContextTokens,
codeEnvAvailable: effectiveCodeEnvAvailable,
reasoningKey: customEndpointConfig?.customParams?.reasoningKey,
skillCount,
accessibleSkillIds: executableSkillIds,
activeSkillNames,
Expand Down
51 changes: 51 additions & 0 deletions packages/api/src/agents/run.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Providers } from '@librechat/agents';
import { ToolMessage, AIMessage, HumanMessage } from '@librechat/agents/langchain/messages';
import { ReasoningResponseKey } from 'librechat-data-provider';

import {
extractDiscoveredToolsFromHistory,
Expand Down Expand Up @@ -150,6 +151,56 @@ describe('getReasoningKey', () => {

expect(reasoningKey).toBe('reasoning');
});

it('keeps Vercel AI Gateway on ChatOpenAI normalized reasoning_content', () => {
const llmConfig = {
configuration: {
baseURL: 'https://ai-gateway.vercel.sh/v1',
},
} as Parameters<typeof getReasoningKey>[1];

const reasoningKey = getReasoningKey(Providers.OPENAI, llmConfig);

expect(reasoningKey).toBe('reasoning_content');
});

it('keeps Vercel custom endpoint names on ChatOpenAI normalized reasoning_content', () => {
const llmConfig = {} as Parameters<typeof getReasoningKey>[1];

const reasoningKey = getReasoningKey(Providers.OPENAI, llmConfig, 'Vercel');

expect(reasoningKey).toBe('reasoning_content');
});

it('uses explicit reasoning response keys for Vercel when configured', () => {
const llmConfig = {
configuration: {
baseURL: 'https://ai-gateway.vercel.sh/v1',
},
} as Parameters<typeof getReasoningKey>[1];

const reasoningKey = getReasoningKey(
Providers.OPENAI,
llmConfig,
'Vercel',
ReasoningResponseKey.reasoning,
);

expect(reasoningKey).toBe('reasoning');
});

it('uses explicit reasoning response keys for otherwise default OpenAI-compatible endpoints', () => {
const llmConfig = {} as Parameters<typeof getReasoningKey>[1];

const reasoningKey = getReasoningKey(
Providers.OPENAI,
llmConfig,
'Company Gateway',
ReasoningResponseKey.reasoning,
);

expect(reasoningKey).toBe('reasoning');
});
});

describe('isDeepSeekReasoningProvider', () => {
Expand Down
16 changes: 13 additions & 3 deletions packages/api/src/agents/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
Agent,
AgentModelParameters,
AgentSubagentsConfig,
ReasoningResponseKey,
SummarizationConfig,
} from 'librechat-data-provider';
import type { BaseMessage } from '@librechat/agents/langchain/messages';
Expand Down Expand Up @@ -212,6 +213,8 @@ const customProviders = new Set([
KnownEndpoints.ollama,
]);

type AgentReasoningKey = 'reasoning_content' | 'reasoning';

function includesOpenRouter(value?: string | null): boolean {
return typeof value === 'string' && value.toLowerCase().includes(KnownEndpoints.openrouter);
}
Expand All @@ -220,8 +223,13 @@ export function getReasoningKey(
provider: Providers,
llmConfig: t.RunLLMConfig,
agentEndpoint?: string | null,
): 'reasoning_content' | 'reasoning' {
let reasoningKey: 'reasoning_content' | 'reasoning' = 'reasoning_content';
customReasoningKey?: ReasoningResponseKey,
): AgentReasoningKey {
if (customReasoningKey) {
return customReasoningKey as AgentReasoningKey;
}

let reasoningKey: AgentReasoningKey = 'reasoning_content';
if (provider === Providers.GOOGLE) {
reasoningKey = 'reasoning';
} else if (
Expand Down Expand Up @@ -293,6 +301,8 @@ type RunAgent = Omit<Agent, 'tools'> & {
codeEnvAvailable?: boolean;
/** Optional per-agent summarization overrides */
summarization?: SummarizationConfig;
/** Response field to read model reasoning from for custom OpenAI-compatible endpoints. */
reasoningKey?: ReasoningResponseKey;
/**
* Maximum characters allowed in a single tool result before truncation.
* Overrides the default computed from maxContextTokens.
Expand Down Expand Up @@ -946,7 +956,7 @@ export async function createRun({
agent.maxContextTokens,
);

const reasoningKey = getReasoningKey(provider, llmConfig, agent.endpoint);
const reasoningKey = getReasoningKey(provider, llmConfig, agent.endpoint, agent.reasoningKey);
return {
provider,
reasoningKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -419,6 +420,9 @@ describe('getOpenAIConfig - Backward Compatibility', () => {
user: 'some-user',
disableStreaming: true,
apiKey: 'someKey',
modelKwargs: {
reasoning_effort: ReasoningEffort.high,
},
},
configOptions: {
baseURL:
Expand Down
Loading
Loading