From 1bd69fc742cc820c3d1fd7e3301d87ad26901c77 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 15:52:53 -0400 Subject: [PATCH 01/13] feat: add OpenRouter prompt cache setting --- librechat.example.yaml | 7 +++ .../api/src/endpoints/openai/config.spec.ts | 5 ++ packages/api/src/endpoints/openai/llm.spec.ts | 62 +++++++++++++++++++ packages/api/src/endpoints/openai/llm.ts | 24 ++++++- packages/api/src/types/openai.ts | 1 + packages/data-provider/specs/parsers.spec.ts | 41 +++++++++++- .../data-provider/src/parameterSettings.ts | 8 +++ packages/data-provider/src/parsers.ts | 10 ++- packages/data-provider/src/schemas.ts | 5 ++ 9 files changed, 157 insertions(+), 6 deletions(-) diff --git a/librechat.example.yaml b/librechat.example.yaml index ef6f846c82d..e8bf683610f 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -493,6 +493,13 @@ endpoints: titleModel: 'meta-llama/llama-3-70b-instruct' # Recommended: Drop the stop parameter from the request as Openrouter models use a variety of stop tokens. dropParams: ['stop'] + # Enable OpenRouter-specific parameters. The promptCache toggle adds explicit + # cache_control breakpoints for providers/models that support prompt caching. + customParams: + defaultParamsEndpoint: openrouter + paramDefinitions: + - key: promptCache + default: true modelDisplayLabel: 'OpenRouter' # Helicone Example diff --git a/packages/api/src/endpoints/openai/config.spec.ts b/packages/api/src/endpoints/openai/config.spec.ts index 46ad6a62955..21b2f38cc74 100644 --- a/packages/api/src/endpoints/openai/config.spec.ts +++ b/packages/api/src/endpoints/openai/config.spec.ts @@ -1464,6 +1464,10 @@ describe('getOpenAIConfig', () => { top_k: 50, repetition_penalty: 1.1, }, + customParams: { + defaultParamsEndpoint: 'openrouter', + paramDefinitions: [{ key: 'promptCache', default: true }], + }, modelOptions: { model: 'anthropic/claude-3-sonnet', user: 'openrouter-user', @@ -1482,6 +1486,7 @@ describe('getOpenAIConfig', () => { temperature: 0.7, maxTokens: 4000, apiKey: apiKey, + promptCache: true, }); expect(result.llmConfig.include_reasoning).toBeUndefined(); expect(result.llmConfig.modelKwargs).toMatchObject({ diff --git a/packages/api/src/endpoints/openai/llm.spec.ts b/packages/api/src/endpoints/openai/llm.spec.ts index a78cc4b87d7..5b2bb845feb 100644 --- a/packages/api/src/endpoints/openai/llm.spec.ts +++ b/packages/api/src/endpoints/openai/llm.spec.ts @@ -675,6 +675,68 @@ describe('getOpenAILLMConfig', () => { expect(result.llmConfig).toHaveProperty('include_reasoning', true); expect(result.llmConfig).not.toHaveProperty('reasoning'); }); + + it('should pass promptCache only for OpenRouter', () => { + const openRouterResult = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + modelOptions: { + model: 'anthropic/claude-sonnet-4.6', + promptCache: true, + } as Partial, + }); + const openAIResult = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: false, + modelOptions: { + model: 'gpt-4o', + promptCache: true, + } as Partial, + }); + + expect(openRouterResult.llmConfig).toHaveProperty('promptCache', true); + expect(openRouterResult.llmConfig.modelKwargs).toBeUndefined(); + expect(openAIResult.llmConfig).not.toHaveProperty('promptCache'); + expect(openAIResult.llmConfig.modelKwargs).toBeUndefined(); + }); + + it('should resolve OpenRouter promptCache default/add/drop params', () => { + const enabled = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + defaultParams: { promptCache: true }, + modelOptions: { + model: 'anthropic/claude-sonnet-4.6', + }, + }); + const disabled = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + defaultParams: { promptCache: true }, + addParams: { promptCache: false }, + modelOptions: { + model: 'anthropic/claude-sonnet-4.6', + }, + }); + const dropped = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + useOpenRouter: true, + defaultParams: { promptCache: true }, + dropParams: ['promptCache'], + modelOptions: { + model: 'anthropic/claude-sonnet-4.6', + }, + }); + + expect(enabled.llmConfig).toHaveProperty('promptCache', true); + expect(disabled.llmConfig).not.toHaveProperty('promptCache'); + expect(dropped.llmConfig).not.toHaveProperty('promptCache'); + }); }); describe('Verbosity Handling', () => { diff --git a/packages/api/src/endpoints/openai/llm.ts b/packages/api/src/endpoints/openai/llm.ts index 9351a42cc21..ebdaddfbc85 100644 --- a/packages/api/src/endpoints/openai/llm.ts +++ b/packages/api/src/endpoints/openai/llm.ts @@ -150,10 +150,11 @@ export function getOpenAILLMConfig({ reasoning_summary, verbosity, web_search, + promptCache, frequency_penalty, presence_penalty, ...modelOptions - } = cleanedModelOptions; + } = cleanedModelOptions as Partial; const llmConfig = Object.assign( { @@ -179,17 +180,23 @@ export function getOpenAILLMConfig({ } let enableWebSearch = web_search; + let enablePromptCache = promptCache; /** Apply defaultParams first - only if fields are undefined */ if (defaultParams && typeof defaultParams === 'object') { for (const [key, value] of Object.entries(defaultParams)) { - /** Handle web_search separately - don't add to config */ if (key === 'web_search') { if (enableWebSearch === undefined && typeof value === 'boolean') { enableWebSearch = value; } continue; } + if (key === 'promptCache') { + if (enablePromptCache === undefined && typeof value === 'boolean') { + enablePromptCache = value; + } + continue; + } if (knownOpenAIParams.has(key)) { applyDefaultParams(llmConfig as Record, { [key]: value }); @@ -206,13 +213,18 @@ export function getOpenAILLMConfig({ /** Apply addParams - can override defaultParams */ if (addParams && typeof addParams === 'object') { for (const [key, value] of Object.entries(addParams)) { - /** Handle web_search directly here instead of adding to modelKwargs or llmConfig */ if (key === 'web_search') { if (typeof value === 'boolean') { enableWebSearch = value; } continue; } + if (key === 'promptCache') { + if (typeof value === 'boolean') { + enablePromptCache = value; + } + continue; + } if (knownOpenAIParams.has(key)) { (llmConfig as Record)[key] = value; } else { @@ -263,6 +275,9 @@ export function getOpenAILLMConfig({ if (dropParams && dropParams.includes('web_search')) { enableWebSearch = false; } + if (dropParams && dropParams.includes('promptCache')) { + enablePromptCache = false; + } if (useOpenRouter && enableWebSearch) { /** OpenRouter expects web search as a plugins parameter */ @@ -273,6 +288,9 @@ export function getOpenAILLMConfig({ llmConfig.useResponsesApi = true; tools.push({ type: 'web_search' }); } + if (useOpenRouter && enablePromptCache === true) { + llmConfig.promptCache = true; + } /** * Note: OpenAI reasoning models (o1/o3/gpt-5) do not support temperature and other sampling parameters diff --git a/packages/api/src/types/openai.ts b/packages/api/src/types/openai.ts index 03fbb13b9ea..b82cde86f84 100644 --- a/packages/api/src/types/openai.ts +++ b/packages/api/src/types/openai.ts @@ -30,6 +30,7 @@ export type OpenAIConfiguration = OpenAIClientOptions['configuration']; export type OAIClientOptions = OpenAIClientOptions & { include_reasoning?: boolean; + promptCache?: boolean; _lc_stream_delay?: number; }; diff --git a/packages/data-provider/specs/parsers.spec.ts b/packages/data-provider/specs/parsers.spec.ts index 8f0cf71e8e4..b67b77a79da 100644 --- a/packages/data-provider/specs/parsers.spec.ts +++ b/packages/data-provider/specs/parsers.spec.ts @@ -1,6 +1,6 @@ import { replaceSpecialVars, parseConvo, parseCompactConvo, parseTextParts } from '../src/parsers'; import { specialVariables } from '../src/config'; -import { EModelEndpoint } from '../src/schemas'; +import { EModelEndpoint, Providers } from '../src/schemas'; import { ContentTypes } from '../src/types/runs'; import type { TMessageContentParts } from '../src/types/assistants'; import type { TUser, TConversation } from '../src/types'; @@ -409,6 +409,26 @@ describe('parseConvo - defaultParamsEndpoint', () => { expect(result?.topK).toBe(40); }); + test('should preserve promptCache when defaultParamsEndpoint is openrouter', () => { + const conversation: Partial = { + model: 'anthropic/claude-sonnet-4.6', + temperature: 0.7, + max_tokens: 8192, + promptCache: true, + }; + + const result = parseConvo({ + endpoint: 'OpenRouter' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + conversation, + defaultParamsEndpoint: Providers.OPENROUTER, + }); + + expect(result).not.toBeNull(); + expect(result?.max_tokens).toBe(8192); + expect(result?.promptCache).toBe(true); + }); + test('should not strip fields from non-custom endpoints that already have a schema', () => { const conversation: Partial = { model: 'gpt-4o', @@ -527,6 +547,25 @@ describe('parseCompactConvo - defaultParamsEndpoint', () => { expect(result?.maxOutputTokens).toBe(8192); }); + test('should preserve promptCache when compacting OpenRouter custom endpoints', () => { + const conversation: Partial = { + model: 'anthropic/claude-sonnet-4.6', + promptCache: true, + iconURL: 'https://example.com/icon.png', + }; + + const result = parseCompactConvo({ + endpoint: 'OpenRouter' as EModelEndpoint, + endpointType: EModelEndpoint.custom, + conversation, + defaultParamsEndpoint: Providers.OPENROUTER, + }); + + expect(result).not.toBeNull(); + expect(result?.promptCache).toBe(true); + expect(result?.['iconURL']).toBeUndefined(); + }); + test('should fall back to endpointType when defaultParamsEndpoint is null', () => { const conversation: Partial = { model: 'gpt-4o', diff --git a/packages/data-provider/src/parameterSettings.ts b/packages/data-provider/src/parameterSettings.ts index 6bc8c2b1627..e9b4f36f11b 100644 --- a/packages/data-provider/src/parameterSettings.ts +++ b/packages/data-provider/src/parameterSettings.ts @@ -6,6 +6,7 @@ import { EModelEndpoint, openAISettings, googleSettings, + Providers, ReasoningEffort, AnthropicEffort, ReasoningSummary, @@ -791,6 +792,8 @@ const openAI: SettingsConfiguration = [ librechat.fileTokenLimit, ]; +const openRouter: SettingsConfiguration = [...openAI, anthropic.promptCache]; + const openAICol1: SettingsConfiguration = [ baseDefinitions.model as SettingDefinition, librechat.modelLabel, @@ -1050,6 +1053,7 @@ export const paramSettings: Record = [EModelEndpoint.openAI]: openAI, [EModelEndpoint.azureOpenAI]: openAI, [EModelEndpoint.custom]: openAI, + [Providers.OPENROUTER]: openRouter, [EModelEndpoint.anthropic]: anthropicConfig, [`${EModelEndpoint.bedrock}-${BedrockProviders.Anthropic}`]: bedrockAnthropic, [`${EModelEndpoint.bedrock}-${BedrockProviders.MistralAI}`]: bedrockMistral, @@ -1086,6 +1090,10 @@ export const presetSettings: Record< [EModelEndpoint.openAI]: openAIColumns, [EModelEndpoint.azureOpenAI]: openAIColumns, [EModelEndpoint.custom]: openAIColumns, + [Providers.OPENROUTER]: { + col1: openAICol1, + col2: [...openAICol2, anthropic.promptCache], + }, [EModelEndpoint.anthropic]: { col1: anthropicCol1, col2: anthropicCol2, diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 76cf11f5c7f..1db328c921e 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -6,8 +6,10 @@ import type * as t from './types'; import { ContentTypes } from './types/runs'; import { openAISchema, + openRouterSchema, googleSchema, EModelEndpoint, + Providers, anthropicSchema, assistantSchema, // agentsSchema, @@ -20,6 +22,7 @@ import { alternateName } from './config'; type EndpointSchema = | typeof openAISchema + | typeof openRouterSchema | typeof googleSchema | typeof anthropicSchema | typeof assistantSchema @@ -28,10 +31,11 @@ type EndpointSchema = export type EndpointSchemaKey = EModelEndpoint; -const endpointSchemas: Record = { +const endpointSchemas: Record = { [EModelEndpoint.openAI]: openAISchema, [EModelEndpoint.azureOpenAI]: openAISchema, [EModelEndpoint.custom]: openAISchema, + [Providers.OPENROUTER]: openRouterSchema, [EModelEndpoint.google]: googleSchema, [EModelEndpoint.anthropic]: anthropicSchema, [EModelEndpoint.assistants]: assistantSchema, @@ -289,13 +293,15 @@ type CompactEndpointSchema = | typeof compactAssistantSchema | typeof compactAgentsSchema | typeof compactGoogleSchema + | typeof openRouterSchema | typeof anthropicSchema | typeof bedrockInputSchema; -const compactEndpointSchemas: Record = { +const compactEndpointSchemas: Record = { [EModelEndpoint.openAI]: openAISchema, [EModelEndpoint.azureOpenAI]: openAISchema, [EModelEndpoint.custom]: openAISchema, + [Providers.OPENROUTER]: openRouterSchema, [EModelEndpoint.assistants]: compactAssistantSchema, [EModelEndpoint.azureAssistants]: compactAssistantSchema, [EModelEndpoint.agents]: compactAgentsSchema, diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index c9f35d5d4b0..bf9c119eece 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -1250,6 +1250,11 @@ export const openAISchema = openAIBaseSchema .transform((obj: Partial) => removeNullishValues(obj, true)) .catch(() => ({})); +export const openRouterSchema = openAIBaseSchema + .merge(tConversationSchema.pick({ promptCache: true })) + .transform((obj: Partial) => removeNullishValues(obj, true)) + .catch(() => ({})); + export const compactGoogleSchema = googleBaseSchema .transform((obj) => { const newObj: Partial = { ...obj }; From c7b7568325204ce93525ff3b20c98fabcafe8509 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 17:02:55 -0400 Subject: [PATCH 02/13] fix: type OpenRouter schema lookup --- packages/data-provider/src/parsers.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 1db328c921e..8bcebeb03dd 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -30,8 +30,9 @@ type EndpointSchema = | typeof bedrockInputSchema; export type EndpointSchemaKey = EModelEndpoint; +type EndpointSchemaLookupKey = EModelEndpoint | Providers.OPENROUTER; -const endpointSchemas: Record = { +const endpointSchemas: Record = { [EModelEndpoint.openAI]: openAISchema, [EModelEndpoint.azureOpenAI]: openAISchema, [EModelEndpoint.custom]: openAISchema, @@ -44,6 +45,9 @@ const endpointSchemas: Record = { [EModelEndpoint.bedrock]: bedrockInputSchema, }; +const isEndpointSchemaLookupKey = (value?: string | null): value is EndpointSchemaLookupKey => + value != null && Object.prototype.hasOwnProperty.call(endpointSchemas, value); + // const schemaCreators: Record EndpointSchema> = { // [EModelEndpoint.google]: createGoogleSchema, // }; @@ -161,8 +165,8 @@ export const parseConvo = ({ if (!schema && !endpointType) { throw new Error(`Unknown endpoint: ${endpoint}`); } else if (!schema) { - const overrideSchema = defaultParamsEndpoint - ? endpointSchemas[defaultParamsEndpoint as EndpointSchemaKey] + const overrideSchema = isEndpointSchemaLookupKey(defaultParamsEndpoint) + ? endpointSchemas[defaultParamsEndpoint] : undefined; schema = overrideSchema ?? (endpointType ? endpointSchemas[endpointType] : undefined); } @@ -297,7 +301,7 @@ type CompactEndpointSchema = | typeof anthropicSchema | typeof bedrockInputSchema; -const compactEndpointSchemas: Record = { +const compactEndpointSchemas: Record = { [EModelEndpoint.openAI]: openAISchema, [EModelEndpoint.azureOpenAI]: openAISchema, [EModelEndpoint.custom]: openAISchema, @@ -332,8 +336,8 @@ export const parseCompactConvo = ({ if (!schema && !endpointType) { throw new Error(`Unknown endpoint: ${endpoint}`); } else if (!schema) { - const overrideSchema = defaultParamsEndpoint - ? compactEndpointSchemas[defaultParamsEndpoint as EndpointSchemaKey] + const overrideSchema = isEndpointSchemaLookupKey(defaultParamsEndpoint) + ? compactEndpointSchemas[defaultParamsEndpoint] : undefined; schema = overrideSchema ?? (endpointType ? compactEndpointSchemas[endpointType] : undefined); } From 72a0341175ba61887e5138a77f7bfb815cc37f65 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 18:27:05 -0400 Subject: [PATCH 03/13] fix: honor proxied OpenRouter prompt cache --- .../api/src/endpoints/openai/config.spec.ts | 41 +++++++++++++++++++ packages/api/src/endpoints/openai/config.ts | 4 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/api/src/endpoints/openai/config.spec.ts b/packages/api/src/endpoints/openai/config.spec.ts index 21b2f38cc74..d2480eb1b5a 100644 --- a/packages/api/src/endpoints/openai/config.spec.ts +++ b/packages/api/src/endpoints/openai/config.spec.ts @@ -1501,6 +1501,47 @@ describe('getOpenAIConfig', () => { }); expect(result.provider).toBe('openrouter'); }); + + it('should honor OpenRouter defaults for proxied custom endpoint names', () => { + const endpoint = 'company-gateway'; + const apiKey = 'sk-proxy-key'; + const baseURL = 'https://llm-proxy.example.com/v1'; + + const result = getOpenAIConfig( + apiKey, + { + reverseProxyUrl: baseURL, + headers: { + Authorization: `Bearer ${apiKey}`, + }, + customParams: { + defaultParamsEndpoint: 'openrouter', + paramDefinitions: [{ key: 'promptCache', default: true }], + }, + modelOptions: { + model: 'anthropic/claude-sonnet-4.6', + reasoning_effort: ReasoningEffort.high, + }, + }, + endpoint, + ); + + expect(result.llmConfig).toMatchObject({ + model: 'anthropic/claude-sonnet-4.6', + apiKey, + promptCache: true, + }); + expect(result.llmConfig.include_reasoning).toBeUndefined(); + expect(result.llmConfig.modelKwargs).toMatchObject({ + reasoning: { effort: ReasoningEffort.high }, + }); + expect(result.configOptions?.baseURL).toBe(baseURL); + expect(result.configOptions?.defaultHeaders).toMatchObject({ + 'X-OpenRouter-Title': 'LibreChat', + Authorization: `Bearer ${apiKey}`, + }); + expect(result.provider).toBe('openrouter'); + }); }); describe('Production-like Azure Scenarios', () => { diff --git a/packages/api/src/endpoints/openai/config.ts b/packages/api/src/endpoints/openai/config.ts index 69aed82d803..332d0e0376b 100644 --- a/packages/api/src/endpoints/openai/config.ts +++ b/packages/api/src/endpoints/openai/config.ts @@ -61,11 +61,13 @@ export function getOpenAIConfig( let tools: t.LLMConfigResult['tools']; const isAnthropic = options.customParams?.defaultParamsEndpoint === EModelEndpoint.anthropic; const isGoogle = options.customParams?.defaultParamsEndpoint === EModelEndpoint.google; + const isOpenRouter = options.customParams?.defaultParamsEndpoint === KnownEndpoints.openrouter; const useOpenRouter = !isAnthropic && !isGoogle && - ((baseURL && baseURL.includes(KnownEndpoints.openrouter)) || + (isOpenRouter || + (baseURL && baseURL.includes(KnownEndpoints.openrouter)) || (endpoint != null && endpoint.toLowerCase().includes(KnownEndpoints.openrouter))); const isVercel = !isAnthropic && From a905b4a84d95292e54308aa1e72266051a140f43 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 18:50:52 -0400 Subject: [PATCH 04/13] refactor: flatten endpoint schema fallback --- packages/data-provider/src/parsers.ts | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 8bcebeb03dd..684b233e483 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -48,6 +48,21 @@ const endpointSchemas: Record = { const isEndpointSchemaLookupKey = (value?: string | null): value is EndpointSchemaLookupKey => value != null && Object.prototype.hasOwnProperty.call(endpointSchemas, value); +const getFallbackEndpointSchema = ( + schemas: Record, + endpointType?: EndpointSchemaKey | null, + defaultParamsEndpoint?: string | null, +): TSchema | undefined => { + if (!endpointType) { + return undefined; + } + + const overrideSchema = isEndpointSchemaLookupKey(defaultParamsEndpoint) + ? schemas[defaultParamsEndpoint] + : undefined; + return overrideSchema ?? schemas[endpointType]; +}; + // const schemaCreators: Record EndpointSchema> = { // [EModelEndpoint.google]: createGoogleSchema, // }; @@ -160,17 +175,15 @@ export const parseConvo = ({ possibleValues?: TPossibleValues; defaultParamsEndpoint?: string | null; }) => { - let schema = endpointSchemas[endpoint] as EndpointSchema | undefined; + const primarySchema = endpointSchemas[endpoint] as EndpointSchema | undefined; - if (!schema && !endpointType) { + if (!primarySchema && !endpointType) { throw new Error(`Unknown endpoint: ${endpoint}`); - } else if (!schema) { - const overrideSchema = isEndpointSchemaLookupKey(defaultParamsEndpoint) - ? endpointSchemas[defaultParamsEndpoint] - : undefined; - schema = overrideSchema ?? (endpointType ? endpointSchemas[endpointType] : undefined); } + const schema = + primarySchema ?? + getFallbackEndpointSchema(endpointSchemas, endpointType, defaultParamsEndpoint); const convo = schema?.parse(conversation) as s.TConversation | undefined; const { models } = possibleValues ?? {}; @@ -331,17 +344,16 @@ export const parseCompactConvo = ({ throw new Error(`undefined endpoint: ${endpoint}`); } - let schema = compactEndpointSchemas[endpoint] as CompactEndpointSchema | undefined; + const primarySchema = compactEndpointSchemas[endpoint] as CompactEndpointSchema | undefined; - if (!schema && !endpointType) { + if (!primarySchema && !endpointType) { throw new Error(`Unknown endpoint: ${endpoint}`); - } else if (!schema) { - const overrideSchema = isEndpointSchemaLookupKey(defaultParamsEndpoint) - ? compactEndpointSchemas[defaultParamsEndpoint] - : undefined; - schema = overrideSchema ?? (endpointType ? compactEndpointSchemas[endpointType] : undefined); } + const schema = + primarySchema ?? + getFallbackEndpointSchema(compactEndpointSchemas, endpointType, defaultParamsEndpoint); + if (!schema) { throw new Error(`Unknown endpointType: ${endpointType}`); } From 956efb8e8d5e2b3b36e2693e5ae3b236815ee0fe Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 20:22:46 -0400 Subject: [PATCH 05/13] chore: Bump `@librechat/agents` to version 3.1.82 --- api/package.json | 2 +- package-lock.json | 10 +++++----- packages/api/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/package.json b/api/package.json index cd5a8c563e0..3521a29b550 100644 --- a/api/package.json +++ b/api/package.json @@ -45,7 +45,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@librechat/agents": "^3.1.81", + "@librechat/agents": "^3.1.82", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/package-lock.json b/package-lock.json index 070081c6880..7a5dc636597 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@librechat/agents": "^3.1.81", + "@librechat/agents": "^3.1.82", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -12088,9 +12088,9 @@ } }, "node_modules/@librechat/agents": { - "version": "3.1.81", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.81.tgz", - "integrity": "sha512-5Ivq0qltFin+C63JyeVk254WxtxF1q2cict8BEaHisIIxNGn+DmNaL9SiK9GCmVVsi3mwGQA/wv6vEHWdiPvLw==", + "version": "3.1.82", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.82.tgz", + "integrity": "sha512-N9ggrgfBaMFZzI5Fw0wlAtHtM+z8mNl8I2HnaTSN91FpNPf6ozQKObxI0TfkDYanq0nXW27n1PTvKgUnoGLOAA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.92.0", @@ -44658,7 +44658,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@librechat/agents": "^3.1.81", + "@librechat/agents": "^3.1.82", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.29.0", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/package.json b/packages/api/package.json index 699ee937c1a..28347b5e86e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -98,7 +98,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@librechat/agents": "^3.1.81", + "@librechat/agents": "^3.1.82", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.29.0", "@smithy/node-http-handler": "^4.4.5", From 897b36a4a9758bf9563e0cabb5a56d0459a3d05c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 20:37:06 -0400 Subject: [PATCH 06/13] fix: Default OpenRouter prompt cache params --- .../services/Config/loadCustomConfig.js | 43 +++++++ .../services/Config/loadCustomConfig.spec.js | 119 +++++++++++++++++- librechat.example.yaml | 9 +- .../api/src/endpoints/openai/config.spec.ts | 31 +++++ packages/api/src/endpoints/openai/config.ts | 26 +++- 5 files changed, 215 insertions(+), 13 deletions(-) diff --git a/api/server/services/Config/loadCustomConfig.js b/api/server/services/Config/loadCustomConfig.js index db250499572..fdcfed28e4c 100644 --- a/api/server/services/Config/loadCustomConfig.js +++ b/api/server/services/Config/loadCustomConfig.js @@ -17,6 +17,47 @@ const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml'); let i = 0; +const OPENROUTER_ENDPOINT = 'openrouter'; +const OPENROUTER_PROMPT_CACHE_DEFAULT = { + key: 'promptCache', + default: true, +}; + +function includesOpenRouter(value) { + return typeof value === 'string' && value.toLowerCase().includes(OPENROUTER_ENDPOINT); +} + +function isOpenRouterEndpoint(endpoint) { + return includesOpenRouter(endpoint.name) || includesOpenRouter(endpoint.baseURL); +} + +function shouldPreserveCustomParams(customParams) { + const defaultEndpoint = customParams?.defaultParamsEndpoint; + return defaultEndpoint && defaultEndpoint !== 'custom' && defaultEndpoint !== OPENROUTER_ENDPOINT; +} + +function addOpenRouterDefaults(endpoint) { + if (!isOpenRouterEndpoint(endpoint)) { + return; + } + + if (shouldPreserveCustomParams(endpoint.customParams)) { + return; + } + + const customParams = endpoint.customParams ?? {}; + const paramDefinitions = customParams.paramDefinitions ?? []; + const hasPromptCache = paramDefinitions.some((param) => param.key === 'promptCache'); + + endpoint.customParams = { + ...customParams, + defaultParamsEndpoint: OPENROUTER_ENDPOINT, + paramDefinitions: hasPromptCache + ? paramDefinitions + : [...paramDefinitions, OPENROUTER_PROMPT_CACHE_DEFAULT], + }; +} + /** * Load custom configuration files and caches the object if the `cache` field at root is true. * Validation via parsing the config file with the config schema. @@ -119,6 +160,8 @@ https://www.librechat.ai/docs/configuration/stt_tts`); } } + (customConfig.endpoints?.custom ?? []).forEach(addOpenRouterDefaults); + (customConfig.endpoints?.custom ?? []) .filter((endpoint) => endpoint.customParams) .forEach((endpoint) => parseCustomParams(endpoint.name, endpoint.customParams)); diff --git a/api/server/services/Config/loadCustomConfig.spec.js b/api/server/services/Config/loadCustomConfig.spec.js index f7f11dc8f6c..6be6a652d2e 100644 --- a/api/server/services/Config/loadCustomConfig.spec.js +++ b/api/server/services/Config/loadCustomConfig.spec.js @@ -8,7 +8,19 @@ jest.mock('librechat-data-provider', () => { const actual = jest.requireActual('librechat-data-provider'); return { ...actual, - paramSettings: { foo: {}, bar: {}, custom: {} }, + paramSettings: { + foo: {}, + bar: {}, + custom: {}, + openrouter: [ + { + key: 'promptCache', + type: 'boolean', + component: 'switch', + default: true, + }, + ], + }, agentParamSettings: { custom: [], google: [ @@ -195,7 +207,8 @@ describe('loadCustomConfig', () => { }; process.env.CONFIG_PATH = 'validConfig.yaml'; loadYaml.mockReturnValueOnce(mockConfig); - await loadCustomConfig(); + const result = await loadCustomConfig(); + expect(result).toEqual(mockConfig); }); it('should log the loaded custom config', async () => { @@ -297,7 +310,7 @@ 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, google', + 'defaultParamsEndpoint of "Google" endpoint is invalid. Valid options are foo, bar, custom, openrouter, google', ); }); @@ -340,5 +353,105 @@ describe('loadCustomConfig', () => { }, ]); }); + + it('adds OpenRouter promptCache defaults when custom endpoint name is OpenRouter', async () => { + const openRouterConfig = { + version: '1.0', + cache: false, + endpoints: { + custom: [ + { + name: 'OpenRouter', + apiKey: 'user_provided', + baseURL: 'https://proxy.example.com/v1', + models: { + default: ['anthropic/claude-sonnet-4.6'], + }, + }, + ], + }, + }; + loadYaml.mockReturnValue(openRouterConfig); + + const parsedConfig = await loadCustomConfig(); + expect(parsedConfig.endpoints.custom[0].customParams).toEqual({ + defaultParamsEndpoint: 'openrouter', + paramDefinitions: [ + { + component: 'switch', + default: true, + key: 'promptCache', + optionType: 'custom', + type: 'boolean', + }, + ], + }); + }); + + it('adds OpenRouter promptCache defaults when custom endpoint URL is OpenRouter', async () => { + const openRouterConfig = { + version: '1.0', + cache: false, + endpoints: { + custom: [ + { + name: 'Company Gateway', + apiKey: 'user_provided', + baseURL: 'https://openrouter.ai/api/v1', + models: { + default: ['anthropic/claude-sonnet-4.6'], + }, + }, + ], + }, + }; + loadYaml.mockReturnValue(openRouterConfig); + + const parsedConfig = await loadCustomConfig(); + expect(parsedConfig.endpoints.custom[0].customParams).toMatchObject({ + defaultParamsEndpoint: 'openrouter', + paramDefinitions: [ + { + default: true, + key: 'promptCache', + }, + ], + }); + }); + + it('preserves explicit OpenRouter promptCache defaults', async () => { + const openRouterConfig = { + version: '1.0', + cache: false, + endpoints: { + custom: [ + { + name: 'OpenRouter', + apiKey: 'user_provided', + baseURL: 'https://openrouter.ai/api/v1', + models: { + default: ['anthropic/claude-sonnet-4.6'], + }, + customParams: { + defaultParamsEndpoint: 'openrouter', + paramDefinitions: [{ key: 'promptCache', default: false }], + }, + }, + ], + }, + }; + loadYaml.mockReturnValue(openRouterConfig); + + const parsedConfig = await loadCustomConfig(); + expect(parsedConfig.endpoints.custom[0].customParams.paramDefinitions).toEqual([ + { + component: 'switch', + default: false, + key: 'promptCache', + optionType: 'custom', + type: 'boolean', + }, + ]); + }); }); }); diff --git a/librechat.example.yaml b/librechat.example.yaml index e8bf683610f..f1adf938674 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -493,13 +493,8 @@ endpoints: titleModel: 'meta-llama/llama-3-70b-instruct' # Recommended: Drop the stop parameter from the request as Openrouter models use a variety of stop tokens. dropParams: ['stop'] - # Enable OpenRouter-specific parameters. The promptCache toggle adds explicit - # cache_control breakpoints for providers/models that support prompt caching. - customParams: - defaultParamsEndpoint: openrouter - paramDefinitions: - - key: promptCache - default: true + # OpenRouter custom endpoints are detected by name/baseURL and automatically + # get the OpenRouter promptCache parameter defaulted on. modelDisplayLabel: 'OpenRouter' # Helicone Example diff --git a/packages/api/src/endpoints/openai/config.spec.ts b/packages/api/src/endpoints/openai/config.spec.ts index d2480eb1b5a..efa34519012 100644 --- a/packages/api/src/endpoints/openai/config.spec.ts +++ b/packages/api/src/endpoints/openai/config.spec.ts @@ -201,6 +201,7 @@ describe('getOpenAIConfig', () => { 'X-OpenRouter-Categories': 'general-chat,personal-agent', }); expect(result.llmConfig.include_reasoning).toBe(true); + expect(result.llmConfig.promptCache).toBe(true); expect(result.provider).toBe('openrouter'); }); @@ -784,6 +785,36 @@ describe('getOpenAIConfig', () => { const result = getOpenAIConfig(mockApiKey, {}, 'openrouter'); expect(result.llmConfig.include_reasoning).toBe(true); + expect(result.llmConfig.promptCache).toBe(true); + expect(result.provider).toBe('openrouter'); + }); + + it('should preserve explicit promptCache false for OpenRouter', () => { + const result = getOpenAIConfig( + mockApiKey, + { + customParams: { + defaultParamsEndpoint: 'openrouter', + paramDefinitions: [{ key: 'promptCache', default: false }], + }, + }, + 'openrouter', + ); + + expect(result.llmConfig.promptCache).toBeUndefined(); + expect(result.provider).toBe('openrouter'); + }); + + it('should honor dropParams for the OpenRouter promptCache default', () => { + const result = getOpenAIConfig( + mockApiKey, + { + dropParams: ['promptCache'], + }, + 'openrouter', + ); + + expect(result.llmConfig.promptCache).toBeUndefined(); expect(result.provider).toBe('openrouter'); }); diff --git a/packages/api/src/endpoints/openai/config.ts b/packages/api/src/endpoints/openai/config.ts index 332d0e0376b..a25bec71973 100644 --- a/packages/api/src/endpoints/openai/config.ts +++ b/packages/api/src/endpoints/openai/config.ts @@ -11,6 +11,25 @@ import { createFetch } from '~/utils/generators'; type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise; +const OPENROUTER_DEFAULT_PARAMS = { promptCache: true }; + +function getDefaultParams({ + customDefaultParams, + useOpenRouter, +}: { + customDefaultParams?: Record; + useOpenRouter: boolean; +}): Record | undefined { + if (!useOpenRouter) { + return customDefaultParams; + } + + return { + ...OPENROUTER_DEFAULT_PARAMS, + ...customDefaultParams, + }; +} + function mergeHeadersPreservingAnthropicBeta( headers: Record | undefined, defaultHeaders: Record, @@ -54,9 +73,6 @@ export function getOpenAIConfig( reverseProxyUrl: baseURL, } = options; - /** Extract default params from customParams.paramDefinitions */ - const defaultParams = extractDefaultParams(options.customParams?.paramDefinitions); - let llmConfig: t.OAIClientOptions; let tools: t.LLMConfigResult['tools']; const isAnthropic = options.customParams?.defaultParamsEndpoint === EModelEndpoint.anthropic; @@ -74,6 +90,10 @@ export function getOpenAIConfig( !isGoogle && ((baseURL && baseURL.includes('ai-gateway.vercel.sh')) || (endpoint != null && endpoint.toLowerCase().includes(KnownEndpoints.vercel))); + const defaultParams = getDefaultParams({ + customDefaultParams: extractDefaultParams(options.customParams?.paramDefinitions), + useOpenRouter: Boolean(useOpenRouter), + }); let azure = options.azure; let headers = options.headers; From 19b9f6c1594c1aa127739741c9de8864a35a75e3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 20:44:02 -0400 Subject: [PATCH 07/13] test: Align OpenRouter config expectations --- api/server/services/Config/loadCustomConfig.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/server/services/Config/loadCustomConfig.spec.js b/api/server/services/Config/loadCustomConfig.spec.js index 6be6a652d2e..3fce8777e30 100644 --- a/api/server/services/Config/loadCustomConfig.spec.js +++ b/api/server/services/Config/loadCustomConfig.spec.js @@ -378,9 +378,11 @@ describe('loadCustomConfig', () => { defaultParamsEndpoint: 'openrouter', paramDefinitions: [ { + columnSpan: 1, component: 'switch', default: true, key: 'promptCache', + label: 'promptCache', optionType: 'custom', type: 'boolean', }, @@ -445,9 +447,11 @@ describe('loadCustomConfig', () => { const parsedConfig = await loadCustomConfig(); expect(parsedConfig.endpoints.custom[0].customParams.paramDefinitions).toEqual([ { + columnSpan: 1, component: 'switch', default: false, key: 'promptCache', + label: 'promptCache', optionType: 'custom', type: 'boolean', }, From a80aafdafd67c09b3b678a348504493b9b02cca9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 20:50:28 -0400 Subject: [PATCH 08/13] test: Update OpenRouter default cache expectation --- packages/api/src/endpoints/openai/config.backward-compat.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/src/endpoints/openai/config.backward-compat.spec.ts b/packages/api/src/endpoints/openai/config.backward-compat.spec.ts index 374fe1d188e..f19540481f2 100644 --- a/packages/api/src/endpoints/openai/config.backward-compat.spec.ts +++ b/packages/api/src/endpoints/openai/config.backward-compat.spec.ts @@ -80,6 +80,7 @@ describe('getOpenAIConfig - Backward Compatibility', () => { streaming: true, model: 'qwen/qwen3-max', include_reasoning: true, + promptCache: true, apiKey: 'sk-xxxx', }, configOptions: { From eb2b645af8bee9d48ff06396b29a8736cba7a86d Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 8 May 2026 21:05:22 -0400 Subject: [PATCH 09/13] fix: Align OpenRouter Detection --- packages/api/src/agents/run.spec.ts | 18 +++++++++++++++++- packages/api/src/agents/run.ts | 8 ++++++-- .../api/src/endpoints/openai/config.spec.ts | 10 ++++++++++ packages/api/src/endpoints/openai/config.ts | 8 +++++--- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/api/src/agents/run.spec.ts b/packages/api/src/agents/run.spec.ts index 3e7e4283b0b..59f8959f393 100644 --- a/packages/api/src/agents/run.spec.ts +++ b/packages/api/src/agents/run.spec.ts @@ -1,5 +1,7 @@ +import { Providers } from '@librechat/agents'; import { ToolMessage, AIMessage, HumanMessage } from '@librechat/agents/langchain/messages'; -import { extractDiscoveredToolsFromHistory } from './run'; + +import { extractDiscoveredToolsFromHistory, getReasoningKey } from './run'; describe('extractDiscoveredToolsFromHistory', () => { it('extracts tool names from tool_search JSON output', () => { @@ -131,3 +133,17 @@ describe('extractDiscoveredToolsFromHistory', () => { expect(discovered.size).toBe(0); }); }); + +describe('getReasoningKey', () => { + it('detects OpenRouter baseURL case-insensitively', () => { + const llmConfig = { + configuration: { + baseURL: 'https://gateway.example/v1/OpenRouter', + }, + } as Parameters[1]; + + const reasoningKey = getReasoningKey(Providers.OPENAI, llmConfig); + + expect(reasoningKey).toBe('reasoning'); + }); +}); diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 15514197181..73fa9e5efce 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -210,6 +210,10 @@ const customProviders = new Set([ KnownEndpoints.ollama, ]); +function includesOpenRouter(value?: string | null): boolean { + return typeof value === 'string' && value.toLowerCase().includes(KnownEndpoints.openrouter); +} + export function getReasoningKey( provider: Providers, llmConfig: t.RunLLMConfig, @@ -219,8 +223,8 @@ export function getReasoningKey( if (provider === Providers.GOOGLE) { reasoningKey = 'reasoning'; } else if ( - llmConfig.configuration?.baseURL?.includes(KnownEndpoints.openrouter) || - (agentEndpoint && agentEndpoint.toLowerCase().includes(KnownEndpoints.openrouter)) + includesOpenRouter(llmConfig.configuration?.baseURL) || + includesOpenRouter(agentEndpoint) ) { reasoningKey = 'reasoning'; } else if ( diff --git a/packages/api/src/endpoints/openai/config.spec.ts b/packages/api/src/endpoints/openai/config.spec.ts index efa34519012..baea92b3d92 100644 --- a/packages/api/src/endpoints/openai/config.spec.ts +++ b/packages/api/src/endpoints/openai/config.spec.ts @@ -789,6 +789,16 @@ describe('getOpenAIConfig', () => { expect(result.provider).toBe('openrouter'); }); + it('should detect OpenRouter from baseURL case-insensitively', () => { + const result = getOpenAIConfig(mockApiKey, { + reverseProxyUrl: 'https://gateway.example/v1/OpenRouter', + }); + + expect(result.llmConfig.include_reasoning).toBe(true); + expect(result.llmConfig.promptCache).toBe(true); + expect(result.provider).toBe('openrouter'); + }); + it('should preserve explicit promptCache false for OpenRouter', () => { const result = getOpenAIConfig( mockApiKey, diff --git a/packages/api/src/endpoints/openai/config.ts b/packages/api/src/endpoints/openai/config.ts index a25bec71973..b239a2b7d05 100644 --- a/packages/api/src/endpoints/openai/config.ts +++ b/packages/api/src/endpoints/openai/config.ts @@ -13,6 +13,10 @@ type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise Date: Sat, 9 May 2026 04:54:49 -0400 Subject: [PATCH 10/13] chore: Bump `@librechat/agents` to version 3.1.83 --- api/package.json | 2 +- package-lock.json | 10 +++++----- packages/api/package.json | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/package.json b/api/package.json index 3521a29b550..3c864d1e20f 100644 --- a/api/package.json +++ b/api/package.json @@ -45,7 +45,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@librechat/agents": "^3.1.82", + "@librechat/agents": "^3.1.83", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/package-lock.json b/package-lock.json index 7a5dc636597..93452126a7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@librechat/agents": "^3.1.82", + "@librechat/agents": "^3.1.83", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -12088,9 +12088,9 @@ } }, "node_modules/@librechat/agents": { - "version": "3.1.82", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.82.tgz", - "integrity": "sha512-N9ggrgfBaMFZzI5Fw0wlAtHtM+z8mNl8I2HnaTSN91FpNPf6ozQKObxI0TfkDYanq0nXW27n1PTvKgUnoGLOAA==", + "version": "3.1.83", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.83.tgz", + "integrity": "sha512-6d+GOrR9ORe0a+ofwcJLWXbEP5VIurKxu4bWnOPpLMj5+rPDeTPgfGrmGwPrREQPTkFWpMb8VkEb7iP2ve3XzA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.92.0", @@ -44658,7 +44658,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@librechat/agents": "^3.1.82", + "@librechat/agents": "^3.1.83", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.29.0", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/package.json b/packages/api/package.json index 28347b5e86e..ab0c76608f7 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -98,7 +98,7 @@ "@azure/storage-blob": "^12.30.0", "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", - "@librechat/agents": "^3.1.82", + "@librechat/agents": "^3.1.83", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.29.0", "@smithy/node-http-handler": "^4.4.5", From 6df76f2a73c680e0eb09a4fd436bf997e88e1427 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 9 May 2026 11:36:17 -0400 Subject: [PATCH 11/13] docs: Remove OpenRouter prompt cache setup note --- librechat.example.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/librechat.example.yaml b/librechat.example.yaml index f1adf938674..ef6f846c82d 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -493,8 +493,6 @@ endpoints: titleModel: 'meta-llama/llama-3-70b-instruct' # Recommended: Drop the stop parameter from the request as Openrouter models use a variety of stop tokens. dropParams: ['stop'] - # OpenRouter custom endpoints are detected by name/baseURL and automatically - # get the OpenRouter promptCache parameter defaulted on. modelDisplayLabel: 'OpenRouter' # Helicone Example From e2ee400d7d05c0f4dc582e8251a5cafd13bfbae6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 9 May 2026 11:38:58 -0400 Subject: [PATCH 12/13] refactor: Use provider enum for OpenRouter defaults --- api/server/services/Config/loadCustomConfig.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/server/services/Config/loadCustomConfig.js b/api/server/services/Config/loadCustomConfig.js index fdcfed28e4c..68be6809eb8 100644 --- a/api/server/services/Config/loadCustomConfig.js +++ b/api/server/services/Config/loadCustomConfig.js @@ -3,6 +3,7 @@ const axios = require('axios'); const yaml = require('js-yaml'); const keyBy = require('lodash/keyBy'); const { loadYaml } = require('@librechat/api'); +const { Providers } = require('@librechat/agents'); const { logger } = require('@librechat/data-schemas'); const { configSchema, @@ -17,14 +18,13 @@ const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml'); let i = 0; -const OPENROUTER_ENDPOINT = 'openrouter'; const OPENROUTER_PROMPT_CACHE_DEFAULT = { key: 'promptCache', default: true, }; function includesOpenRouter(value) { - return typeof value === 'string' && value.toLowerCase().includes(OPENROUTER_ENDPOINT); + return typeof value === 'string' && value.toLowerCase().includes(Providers.OPENROUTER); } function isOpenRouterEndpoint(endpoint) { @@ -33,7 +33,7 @@ function isOpenRouterEndpoint(endpoint) { function shouldPreserveCustomParams(customParams) { const defaultEndpoint = customParams?.defaultParamsEndpoint; - return defaultEndpoint && defaultEndpoint !== 'custom' && defaultEndpoint !== OPENROUTER_ENDPOINT; + return defaultEndpoint && defaultEndpoint !== 'custom' && defaultEndpoint !== Providers.OPENROUTER; } function addOpenRouterDefaults(endpoint) { @@ -51,7 +51,7 @@ function addOpenRouterDefaults(endpoint) { endpoint.customParams = { ...customParams, - defaultParamsEndpoint: OPENROUTER_ENDPOINT, + defaultParamsEndpoint: Providers.OPENROUTER, paramDefinitions: hasPromptCache ? paramDefinitions : [...paramDefinitions, OPENROUTER_PROMPT_CACHE_DEFAULT], From e9d710d7790e431ee761c1989c8b89770e6e6dd2 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 9 May 2026 11:42:05 -0400 Subject: [PATCH 13/13] style: Format OpenRouter defaults guard --- api/server/services/Config/loadCustomConfig.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/server/services/Config/loadCustomConfig.js b/api/server/services/Config/loadCustomConfig.js index 68be6809eb8..c9147549749 100644 --- a/api/server/services/Config/loadCustomConfig.js +++ b/api/server/services/Config/loadCustomConfig.js @@ -33,7 +33,9 @@ function isOpenRouterEndpoint(endpoint) { function shouldPreserveCustomParams(customParams) { const defaultEndpoint = customParams?.defaultParamsEndpoint; - return defaultEndpoint && defaultEndpoint !== 'custom' && defaultEndpoint !== Providers.OPENROUTER; + return ( + defaultEndpoint && defaultEndpoint !== 'custom' && defaultEndpoint !== Providers.OPENROUTER + ); } function addOpenRouterDefaults(endpoint) {