| | const { Keyv } = require('keyv'); |
| | const { Time, CacheKeys, ViolationTypes } = require('librechat-data-provider'); |
| | const { |
| | logFile, |
| | keyvMongo, |
| | cacheConfig, |
| | sessionCache, |
| | standardCache, |
| | violationCache, |
| | } = require('@librechat/api'); |
| |
|
| | const namespaces = { |
| | [ViolationTypes.GENERAL]: new Keyv({ store: logFile, namespace: 'violations' }), |
| | [ViolationTypes.LOGINS]: violationCache(ViolationTypes.LOGINS), |
| | [ViolationTypes.CONCURRENT]: violationCache(ViolationTypes.CONCURRENT), |
| | [ViolationTypes.NON_BROWSER]: violationCache(ViolationTypes.NON_BROWSER), |
| | [ViolationTypes.MESSAGE_LIMIT]: violationCache(ViolationTypes.MESSAGE_LIMIT), |
| | [ViolationTypes.REGISTRATIONS]: violationCache(ViolationTypes.REGISTRATIONS), |
| | [ViolationTypes.TOKEN_BALANCE]: violationCache(ViolationTypes.TOKEN_BALANCE), |
| | [ViolationTypes.TTS_LIMIT]: violationCache(ViolationTypes.TTS_LIMIT), |
| | [ViolationTypes.STT_LIMIT]: violationCache(ViolationTypes.STT_LIMIT), |
| | [ViolationTypes.CONVO_ACCESS]: violationCache(ViolationTypes.CONVO_ACCESS), |
| | [ViolationTypes.TOOL_CALL_LIMIT]: violationCache(ViolationTypes.TOOL_CALL_LIMIT), |
| | [ViolationTypes.FILE_UPLOAD_LIMIT]: violationCache(ViolationTypes.FILE_UPLOAD_LIMIT), |
| | [ViolationTypes.VERIFY_EMAIL_LIMIT]: violationCache(ViolationTypes.VERIFY_EMAIL_LIMIT), |
| | [ViolationTypes.RESET_PASSWORD_LIMIT]: violationCache(ViolationTypes.RESET_PASSWORD_LIMIT), |
| | [ViolationTypes.ILLEGAL_MODEL_REQUEST]: violationCache(ViolationTypes.ILLEGAL_MODEL_REQUEST), |
| | [ViolationTypes.BAN]: new Keyv({ |
| | store: keyvMongo, |
| | namespace: CacheKeys.BANS, |
| | ttl: cacheConfig.BAN_DURATION, |
| | }), |
| |
|
| | [CacheKeys.OPENID_SESSION]: sessionCache(CacheKeys.OPENID_SESSION), |
| | [CacheKeys.SAML_SESSION]: sessionCache(CacheKeys.SAML_SESSION), |
| |
|
| | [CacheKeys.ROLES]: standardCache(CacheKeys.ROLES), |
| | [CacheKeys.APP_CONFIG]: standardCache(CacheKeys.APP_CONFIG), |
| | [CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE), |
| | [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), |
| | [CacheKeys.TOKEN_CONFIG]: standardCache(CacheKeys.TOKEN_CONFIG, Time.THIRTY_MINUTES), |
| | [CacheKeys.GEN_TITLE]: standardCache(CacheKeys.GEN_TITLE, Time.TWO_MINUTES), |
| | [CacheKeys.S3_EXPIRY_INTERVAL]: standardCache(CacheKeys.S3_EXPIRY_INTERVAL, Time.THIRTY_MINUTES), |
| | [CacheKeys.MODEL_QUERIES]: standardCache(CacheKeys.MODEL_QUERIES), |
| | [CacheKeys.AUDIO_RUNS]: standardCache(CacheKeys.AUDIO_RUNS, Time.TEN_MINUTES), |
| | [CacheKeys.MESSAGES]: standardCache(CacheKeys.MESSAGES, Time.ONE_MINUTE), |
| | [CacheKeys.FLOWS]: standardCache(CacheKeys.FLOWS, Time.ONE_MINUTE * 3), |
| | [CacheKeys.OPENID_EXCHANGED_TOKENS]: standardCache( |
| | CacheKeys.OPENID_EXCHANGED_TOKENS, |
| | Time.TEN_MINUTES, |
| | ), |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | function getTTLStores() { |
| | return Object.values(namespaces).filter( |
| | (store) => |
| | store instanceof Keyv && |
| | parseInt(store.opts?.ttl ?? '0') > 0 && |
| | !store.opts?.store?.constructor?.name?.includes('Redis'), |
| | ); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | async function clearExpiredFromCache(cache) { |
| | if (!cache?.opts?.store?.entries) { |
| | return; |
| | } |
| |
|
| | const ttl = cache.opts.ttl; |
| | if (!ttl) { |
| | return; |
| | } |
| |
|
| | const expiryTime = Date.now() - ttl; |
| | let cleared = 0; |
| |
|
| | |
| | const keys = Array.from(cache.opts.store.keys()); |
| |
|
| | for (const key of keys) { |
| | try { |
| | const raw = cache.opts.store.get(key); |
| | if (!raw) { |
| | continue; |
| | } |
| |
|
| | const data = cache.opts.deserialize(raw); |
| | |
| | if (data?.expires && data.expires <= expiryTime) { |
| | const deleted = await cache.opts.store.delete(key); |
| | if (!deleted) { |
| | cacheConfig.DEBUG_MEMORY_CACHE && |
| | console.warn(`[Cache] Error deleting entry: ${key} from ${cache.opts.namespace}`); |
| | continue; |
| | } |
| | cleared++; |
| | } |
| | } catch (error) { |
| | cacheConfig.DEBUG_MEMORY_CACHE && |
| | console.log(`[Cache] Error processing entry from ${cache.opts.namespace}:`, error); |
| | const deleted = await cache.opts.store.delete(key); |
| | if (!deleted) { |
| | cacheConfig.DEBUG_MEMORY_CACHE && |
| | console.warn(`[Cache] Error deleting entry: ${key} from ${cache.opts.namespace}`); |
| | continue; |
| | } |
| | cleared++; |
| | } |
| | } |
| |
|
| | if (cleared > 0) { |
| | cacheConfig.DEBUG_MEMORY_CACHE && |
| | console.log( |
| | `[Cache] Cleared ${cleared} entries older than ${ttl}ms from ${cache.opts.namespace}`, |
| | ); |
| | } |
| | } |
| |
|
| | const auditCache = () => { |
| | const ttlStores = getTTLStores(); |
| | console.log('[Cache] Starting audit'); |
| |
|
| | ttlStores.forEach((store) => { |
| | if (!store?.opts?.store?.entries) { |
| | return; |
| | } |
| |
|
| | console.log(`[Cache] ${store.opts.namespace} entries:`, { |
| | count: store.opts.store.size, |
| | ttl: store.opts.ttl, |
| | keys: Array.from(store.opts.store.keys()), |
| | entriesWithTimestamps: Array.from(store.opts.store.entries()).map(([key, value]) => ({ |
| | key, |
| | value, |
| | })), |
| | }); |
| | }); |
| | }; |
| |
|
| | |
| | |
| | |
| | async function clearAllExpiredFromCache() { |
| | const ttlStores = getTTLStores(); |
| | await Promise.all(ttlStores.map((store) => clearExpiredFromCache(store))); |
| |
|
| | |
| | if (global.gc) { |
| | global.gc(); |
| | } |
| | } |
| |
|
| | if (!cacheConfig.USE_REDIS && !cacheConfig.CI) { |
| | |
| | const cleanupIntervals = new Set(); |
| |
|
| | |
| | const cleanup = setInterval(() => { |
| | clearAllExpiredFromCache(); |
| | }, Time.THIRTY_SECONDS); |
| |
|
| | cleanupIntervals.add(cleanup); |
| |
|
| | if (cacheConfig.DEBUG_MEMORY_CACHE) { |
| | const monitor = setInterval(() => { |
| | const ttlStores = getTTLStores(); |
| | const memory = process.memoryUsage(); |
| | const totalSize = ttlStores.reduce((sum, store) => sum + (store.opts?.store?.size ?? 0), 0); |
| |
|
| | console.log('[Cache] Memory usage:', { |
| | heapUsed: `${(memory.heapUsed / 1024 / 1024).toFixed(2)} MB`, |
| | heapTotal: `${(memory.heapTotal / 1024 / 1024).toFixed(2)} MB`, |
| | rss: `${(memory.rss / 1024 / 1024).toFixed(2)} MB`, |
| | external: `${(memory.external / 1024 / 1024).toFixed(2)} MB`, |
| | totalCacheEntries: totalSize, |
| | }); |
| |
|
| | auditCache(); |
| | }, Time.ONE_MINUTE); |
| |
|
| | cleanupIntervals.add(monitor); |
| | } |
| |
|
| | const dispose = () => { |
| | cacheConfig.DEBUG_MEMORY_CACHE && console.log('[Cache] Cleaning up and shutting down...'); |
| | cleanupIntervals.forEach((interval) => clearInterval(interval)); |
| | cleanupIntervals.clear(); |
| |
|
| | |
| | clearAllExpiredFromCache().then(() => { |
| | cacheConfig.DEBUG_MEMORY_CACHE && console.log('[Cache] Final cleanup completed'); |
| | process.exit(0); |
| | }); |
| | }; |
| |
|
| | |
| | process.on('SIGTERM', dispose); |
| | process.on('SIGINT', dispose); |
| | process.on('SIGQUIT', dispose); |
| | process.on('SIGHUP', dispose); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const getLogStores = (key) => { |
| | if (!key || !namespaces[key]) { |
| | throw new Error(`Invalid store key: ${key}`); |
| | } |
| | return namespaces[key]; |
| | }; |
| |
|
| | module.exports = getLogStores; |
| |
|