🧭 feat: Add OpenRouter Prompt Cache Setting (#13029)

* feat: add OpenRouter prompt cache setting

* fix: type OpenRouter schema lookup

* fix: honor proxied OpenRouter prompt cache

* refactor: flatten endpoint schema fallback

* chore: Bump `@librechat/agents` to version 3.1.82

* fix: Default OpenRouter prompt cache params

* test: Align OpenRouter config expectations

* test: Update OpenRouter default cache expectation

* fix: Align OpenRouter Detection

* chore: Bump `@librechat/agents` to version 3.1.83

* docs: Remove OpenRouter prompt cache setup note

* refactor: Use provider enum for OpenRouter defaults

* style: Format OpenRouter defaults guard
This commit is contained in:
Danny Avila 2026-05-09 11:46:09 -04:00 committed by GitHub
parent 0d5c2b339a
commit 8a654dc8b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 487 additions and 38 deletions

View file

@ -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,6 +18,48 @@ const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml');
let i = 0;
const OPENROUTER_PROMPT_CACHE_DEFAULT = {
key: 'promptCache',
default: true,
};
function includesOpenRouter(value) {
return typeof value === 'string' && value.toLowerCase().includes(Providers.OPENROUTER);
}
function isOpenRouterEndpoint(endpoint) {
return includesOpenRouter(endpoint.name) || includesOpenRouter(endpoint.baseURL);
}
function shouldPreserveCustomParams(customParams) {
const defaultEndpoint = customParams?.defaultParamsEndpoint;
return (
defaultEndpoint && defaultEndpoint !== 'custom' && defaultEndpoint !== Providers.OPENROUTER
);
}
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: Providers.OPENROUTER,
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 +162,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));

View file

@ -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,109 @@ 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: [
{
columnSpan: 1,
component: 'switch',
default: true,
key: 'promptCache',
label: '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([
{
columnSpan: 1,
component: 'switch',
default: false,
key: 'promptCache',
label: 'promptCache',
optionType: 'custom',
type: 'boolean',
},
]);
});
});
});