mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
✨ feat: Add support for forced in-memory cache namespaces configuration (#8586)
* ✨ feat: Add support for forced in-memory cache keys configuration
* refactor: Update cache keys to use uppercase constants and moved cache for `librechat.yaml` into its own cache namespace (STATIC_CONFIG) and with a more descriptive key (LIBRECHAT_YAML_CONFIG)
This commit is contained in:
parent
3dc9e85fab
commit
21005b66cc
9 changed files with 121 additions and 30 deletions
|
@ -627,6 +627,10 @@ HELP_AND_FAQ_URL=https://librechat.ai
|
|||
# Redis connection limits
|
||||
# REDIS_MAX_LISTENERS=40
|
||||
|
||||
# Force specific cache namespaces to use in-memory storage even when Redis is enabled
|
||||
# Comma-separated list of CacheKeys (e.g., STATIC_CONFIG,ROLES,MESSAGES)
|
||||
# FORCED_IN_MEMORY_CACHE_NAMESPACES=STATIC_CONFIG,ROLES
|
||||
|
||||
#==================================================#
|
||||
# Others #
|
||||
#==================================================#
|
||||
|
|
20
api/cache/cacheConfig.js
vendored
20
api/cache/cacheConfig.js
vendored
|
@ -1,5 +1,6 @@
|
|||
const fs = require('fs');
|
||||
const { math, isEnabled } = require('@librechat/api');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
|
||||
// To ensure that different deployments do not interfere with each other's cache, we use a prefix for the Redis keys.
|
||||
// This prefix is usually the deployment ID, which is often passed to the container or pod as an env var.
|
||||
|
@ -15,7 +16,26 @@ if (USE_REDIS && !process.env.REDIS_URI) {
|
|||
throw new Error('USE_REDIS is enabled but REDIS_URI is not set.');
|
||||
}
|
||||
|
||||
// Comma-separated list of cache namespaces that should be forced to use in-memory storage
|
||||
// even when Redis is enabled. This allows selective performance optimization for specific caches.
|
||||
const FORCED_IN_MEMORY_CACHE_NAMESPACES = process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES
|
||||
? process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES.split(',').map((key) => key.trim())
|
||||
: [];
|
||||
|
||||
// Validate against CacheKeys enum
|
||||
if (FORCED_IN_MEMORY_CACHE_NAMESPACES.length > 0) {
|
||||
const validKeys = Object.values(CacheKeys);
|
||||
const invalidKeys = FORCED_IN_MEMORY_CACHE_NAMESPACES.filter((key) => !validKeys.includes(key));
|
||||
|
||||
if (invalidKeys.length > 0) {
|
||||
throw new Error(
|
||||
`Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: ${invalidKeys.join(', ')}. Valid keys: ${validKeys.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cacheConfig = {
|
||||
FORCED_IN_MEMORY_CACHE_NAMESPACES,
|
||||
USE_REDIS,
|
||||
REDIS_URI: process.env.REDIS_URI,
|
||||
REDIS_USERNAME: process.env.REDIS_USERNAME,
|
||||
|
|
34
api/cache/cacheConfig.spec.js
vendored
34
api/cache/cacheConfig.spec.js
vendored
|
@ -14,6 +14,7 @@ describe('cacheConfig', () => {
|
|||
delete process.env.REDIS_KEY_PREFIX_VAR;
|
||||
delete process.env.REDIS_KEY_PREFIX;
|
||||
delete process.env.USE_REDIS;
|
||||
delete process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES;
|
||||
|
||||
// Clear require cache
|
||||
jest.resetModules();
|
||||
|
@ -105,4 +106,37 @@ describe('cacheConfig', () => {
|
|||
expect(cacheConfig.REDIS_CA).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FORCED_IN_MEMORY_CACHE_NAMESPACES validation', () => {
|
||||
test('should parse comma-separated cache keys correctly', () => {
|
||||
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = ' ROLES, STATIC_CONFIG ,MESSAGES ';
|
||||
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([
|
||||
'ROLES',
|
||||
'STATIC_CONFIG',
|
||||
'MESSAGES',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should throw error for invalid cache keys', () => {
|
||||
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'INVALID_KEY,ROLES';
|
||||
|
||||
expect(() => {
|
||||
require('./cacheConfig');
|
||||
}).toThrow('Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: INVALID_KEY');
|
||||
});
|
||||
|
||||
test('should handle empty string gracefully', () => {
|
||||
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = '';
|
||||
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle undefined env var gracefully', () => {
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
5
api/cache/cacheFactory.js
vendored
5
api/cache/cacheFactory.js
vendored
|
@ -16,7 +16,10 @@ const { RedisStore } = require('rate-limit-redis');
|
|||
* @returns {Keyv} Cache instance.
|
||||
*/
|
||||
const standardCache = (namespace, ttl = undefined, fallbackStore = undefined) => {
|
||||
if (cacheConfig.USE_REDIS) {
|
||||
if (
|
||||
cacheConfig.USE_REDIS &&
|
||||
!cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES?.includes(namespace)
|
||||
) {
|
||||
const keyvRedis = new KeyvRedis(keyvRedisClient);
|
||||
const cache = new Keyv(keyvRedis, { namespace, ttl });
|
||||
keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX;
|
||||
|
|
26
api/cache/cacheFactory.spec.js
vendored
26
api/cache/cacheFactory.spec.js
vendored
|
@ -31,6 +31,7 @@ jest.mock('./cacheConfig', () => ({
|
|||
cacheConfig: {
|
||||
USE_REDIS: false,
|
||||
REDIS_KEY_PREFIX: 'test',
|
||||
FORCED_IN_MEMORY_CACHE_NAMESPACES: [],
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -63,6 +64,7 @@ describe('cacheFactory', () => {
|
|||
// Reset cache config mock
|
||||
cacheConfig.USE_REDIS = false;
|
||||
cacheConfig.REDIS_KEY_PREFIX = 'test';
|
||||
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = [];
|
||||
});
|
||||
|
||||
describe('redisCache', () => {
|
||||
|
@ -116,6 +118,30 @@ describe('cacheFactory', () => {
|
|||
|
||||
expect(mockKeyv).toHaveBeenCalledWith({ namespace: undefined, ttl: undefined });
|
||||
});
|
||||
|
||||
it('should use fallback when namespace is in FORCED_IN_MEMORY_CACHE_NAMESPACES', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = ['forced-memory'];
|
||||
const namespace = 'forced-memory';
|
||||
const ttl = 3600;
|
||||
|
||||
standardCache(namespace, ttl);
|
||||
|
||||
expect(require('@keyv/redis').default).not.toHaveBeenCalled();
|
||||
expect(mockKeyv).toHaveBeenCalledWith({ namespace, ttl });
|
||||
});
|
||||
|
||||
it('should use Redis when namespace is not in FORCED_IN_MEMORY_CACHE_NAMESPACES', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES = ['other-namespace'];
|
||||
const namespace = 'test-namespace';
|
||||
const ttl = 3600;
|
||||
|
||||
standardCache(namespace, ttl);
|
||||
|
||||
expect(require('@keyv/redis').default).toHaveBeenCalledWith(mockKeyvRedisClient);
|
||||
expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl });
|
||||
});
|
||||
});
|
||||
|
||||
describe('violationCache', () => {
|
||||
|
|
1
api/cache/getLogStores.js
vendored
1
api/cache/getLogStores.js
vendored
|
@ -33,6 +33,7 @@ const namespaces = {
|
|||
[CacheKeys.ROLES]: standardCache(CacheKeys.ROLES),
|
||||
[CacheKeys.MCP_TOOLS]: standardCache(CacheKeys.MCP_TOOLS),
|
||||
[CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE),
|
||||
[CacheKeys.STATIC_CONFIG]: standardCache(CacheKeys.STATIC_CONFIG),
|
||||
[CacheKeys.PENDING_REQ]: standardCache(CacheKeys.PENDING_REQ),
|
||||
[CacheKeys.ENCODED_DOMAINS]: new Keyv({ store: keyvMongo, namespace: CacheKeys.ENCODED_DOMAINS }),
|
||||
[CacheKeys.ABORT_KEYS]: standardCache(CacheKeys.ABORT_KEYS, Time.TEN_MINUTES),
|
||||
|
|
|
@ -11,8 +11,8 @@ const getLogStores = require('~/cache/getLogStores');
|
|||
* @returns {Promise<TCustomConfig | null>}
|
||||
* */
|
||||
async function getCustomConfig() {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
return (await cache.get(CacheKeys.CUSTOM_CONFIG)) || (await loadCustomConfig());
|
||||
const cache = getLogStores(CacheKeys.STATIC_CONFIG);
|
||||
return (await cache.get(CacheKeys.LIBRECHAT_YAML_CONFIG)) || (await loadCustomConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -120,8 +120,8 @@ https://www.librechat.ai/docs/configuration/stt_tts`);
|
|||
.forEach((endpoint) => parseCustomParams(endpoint.name, endpoint.customParams));
|
||||
|
||||
if (customConfig.cache) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
await cache.set(CacheKeys.CUSTOM_CONFIG, customConfig);
|
||||
const cache = getLogStores(CacheKeys.STATIC_CONFIG);
|
||||
await cache.set(CacheKeys.LIBRECHAT_YAML_CONFIG, customConfig);
|
||||
}
|
||||
|
||||
if (result.data.modelSpecs) {
|
||||
|
|
|
@ -1122,85 +1122,88 @@ export enum CacheKeys {
|
|||
/**
|
||||
* Key for the config store namespace.
|
||||
*/
|
||||
CONFIG_STORE = 'configStore',
|
||||
CONFIG_STORE = 'CONFIG_STORE',
|
||||
/**
|
||||
* Key for the config store namespace.
|
||||
* Key for the roles cache.
|
||||
*/
|
||||
ROLES = 'roles',
|
||||
ROLES = 'ROLES',
|
||||
/**
|
||||
* Key for the plugins cache.
|
||||
*/
|
||||
PLUGINS = 'plugins',
|
||||
PLUGINS = 'PLUGINS',
|
||||
/**
|
||||
* Key for the title generation cache.
|
||||
*/
|
||||
GEN_TITLE = 'genTitle',
|
||||
/**
|
||||
GEN_TITLE = 'GEN_TITLE',
|
||||
/**
|
||||
* Key for the tools cache.
|
||||
*/
|
||||
TOOLS = 'tools',
|
||||
TOOLS = 'TOOLS',
|
||||
/**
|
||||
* Key for the model config cache.
|
||||
*/
|
||||
MODELS_CONFIG = 'modelsConfig',
|
||||
MODELS_CONFIG = 'MODELS_CONFIG',
|
||||
/**
|
||||
* Key for the model queries cache.
|
||||
*/
|
||||
MODEL_QUERIES = 'modelQueries',
|
||||
MODEL_QUERIES = 'MODEL_QUERIES',
|
||||
/**
|
||||
* Key for the default startup config cache.
|
||||
*/
|
||||
STARTUP_CONFIG = 'startupConfig',
|
||||
STARTUP_CONFIG = 'STARTUP_CONFIG',
|
||||
/**
|
||||
* Key for the default endpoint config cache.
|
||||
*/
|
||||
ENDPOINT_CONFIG = 'endpointsConfig',
|
||||
ENDPOINT_CONFIG = 'ENDPOINT_CONFIG',
|
||||
/**
|
||||
* Key for accessing the model token config cache.
|
||||
*/
|
||||
TOKEN_CONFIG = 'tokenConfig',
|
||||
TOKEN_CONFIG = 'TOKEN_CONFIG',
|
||||
/**
|
||||
* Key for the custom config cache.
|
||||
* Key for the librechat yaml config cache.
|
||||
*/
|
||||
CUSTOM_CONFIG = 'customConfig',
|
||||
LIBRECHAT_YAML_CONFIG = 'LIBRECHAT_YAML_CONFIG',
|
||||
/**
|
||||
* Key for the static config namespace.
|
||||
*/
|
||||
STATIC_CONFIG = 'STATIC_CONFIG',
|
||||
/**
|
||||
* Key for accessing Abort Keys
|
||||
*/
|
||||
ABORT_KEYS = 'abortKeys',
|
||||
ABORT_KEYS = 'ABORT_KEYS',
|
||||
/**
|
||||
* Key for the override config cache.
|
||||
*/
|
||||
OVERRIDE_CONFIG = 'overrideConfig',
|
||||
OVERRIDE_CONFIG = 'OVERRIDE_CONFIG',
|
||||
/**
|
||||
* Key for the bans cache.
|
||||
*/
|
||||
BANS = 'bans',
|
||||
BANS = 'BANS',
|
||||
/**
|
||||
* Key for the encoded domains cache.
|
||||
* Used by Azure OpenAI Assistants.
|
||||
*/
|
||||
ENCODED_DOMAINS = 'encoded_domains',
|
||||
ENCODED_DOMAINS = 'ENCODED_DOMAINS',
|
||||
/**
|
||||
* Key for the cached audio run Ids.
|
||||
*/
|
||||
AUDIO_RUNS = 'audioRuns',
|
||||
AUDIO_RUNS = 'AUDIO_RUNS',
|
||||
/**
|
||||
* Key for in-progress messages.
|
||||
*/
|
||||
MESSAGES = 'messages',
|
||||
MESSAGES = 'MESSAGES',
|
||||
/**
|
||||
* Key for in-progress flow states.
|
||||
*/
|
||||
FLOWS = 'flows',
|
||||
FLOWS = 'FLOWS',
|
||||
/**
|
||||
* Key for individual MCP Tool Manifests.
|
||||
*/
|
||||
MCP_TOOLS = 'mcp_tools',
|
||||
MCP_TOOLS = 'MCP_TOOLS',
|
||||
/**
|
||||
* Key for pending chat requests (concurrency check)
|
||||
*/
|
||||
PENDING_REQ = 'pending_req',
|
||||
PENDING_REQ = 'PENDING_REQ',
|
||||
/**
|
||||
* Key for s3 check intervals per user
|
||||
*/
|
||||
|
@ -1212,11 +1215,11 @@ export enum CacheKeys {
|
|||
/**
|
||||
* Key for OpenID session.
|
||||
*/
|
||||
OPENID_SESSION = 'openid_session',
|
||||
OPENID_SESSION = 'OPENID_SESSION',
|
||||
/**
|
||||
* Key for SAML session.
|
||||
*/
|
||||
SAML_SESSION = 'saml_session',
|
||||
SAML_SESSION = 'SAML_SESSION',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue