mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-13 13:04:24 +01:00
🗃️ refactor: Separate Tool Cache Namespace for Blue/Green Deployments (#11738)
* 🔧 refactor: Introduce TOOL_CACHE for isolated caching of tools - Added TOOL_CACHE key to CacheKeys enum for managing tool-related cache. - Updated various services and controllers to utilize TOOL_CACHE instead of CONFIG_STORE for better separation of concerns in caching logic. - Enhanced .env.example with comments on using in-memory cache for blue/green deployments. * 🔧 refactor: Update cache configuration for in-memory storage handling - Enhanced the handling of `FORCED_IN_MEMORY_CACHE_NAMESPACES` in `cacheConfig.ts` to default to `CONFIG_STORE` and `APP_CONFIG`, ensuring safer blue/green deployments. - Updated `.env.example` with clearer comments regarding the usage of in-memory cache namespaces. - Improved unit tests to validate the new default behavior and handling of empty strings for cache namespaces.
This commit is contained in:
parent
c7531dd029
commit
5b67e48fe1
11 changed files with 284 additions and 18 deletions
|
|
@ -748,8 +748,10 @@ HELP_AND_FAQ_URL=https://librechat.ai
|
|||
# REDIS_PING_INTERVAL=300
|
||||
|
||||
# Force specific cache namespaces to use in-memory storage even when Redis is enabled
|
||||
# Comma-separated list of CacheKeys (e.g., ROLES,MESSAGES)
|
||||
# FORCED_IN_MEMORY_CACHE_NAMESPACES=ROLES,MESSAGES
|
||||
# Comma-separated list of CacheKeys
|
||||
# Defaults to CONFIG_STORE,APP_CONFIG so YAML-derived config stays per-container (safe for blue/green deployments)
|
||||
# Set to empty string to force all namespaces through Redis: FORCED_IN_MEMORY_CACHE_NAMESPACES=
|
||||
# FORCED_IN_MEMORY_CACHE_NAMESPACES=CONFIG_STORE,APP_CONFIG
|
||||
|
||||
# Leader Election Configuration (for multi-instance deployments with Redis)
|
||||
# Duration in seconds that the leader lease is valid before it expires (default: 25)
|
||||
|
|
|
|||
1
api/cache/getLogStores.js
vendored
1
api/cache/getLogStores.js
vendored
|
|
@ -37,6 +37,7 @@ const namespaces = {
|
|||
[CacheKeys.ROLES]: standardCache(CacheKeys.ROLES),
|
||||
[CacheKeys.APP_CONFIG]: standardCache(CacheKeys.APP_CONFIG),
|
||||
[CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE),
|
||||
[CacheKeys.TOOL_CACHE]: standardCache(CacheKeys.TOOL_CACHE),
|
||||
[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),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const { getLogStores } = require('~/cache');
|
|||
|
||||
const getAvailablePluginsController = async (req, res) => {
|
||||
try {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
||||
const cachedPlugins = await cache.get(CacheKeys.PLUGINS);
|
||||
if (cachedPlugins) {
|
||||
res.status(200).json(cachedPlugins);
|
||||
|
|
@ -63,7 +63,7 @@ const getAvailableTools = async (req, res) => {
|
|||
logger.warn('[getAvailableTools] User ID not found in request');
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
||||
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
|
||||
|
||||
const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const { CacheKeys } = require('librechat-data-provider');
|
||||
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
|
|
@ -63,6 +64,28 @@ describe('PluginController', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cache namespace', () => {
|
||||
it('getAvailablePluginsController should use TOOL_CACHE namespace', async () => {
|
||||
mockCache.get.mockResolvedValue([]);
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
|
||||
});
|
||||
|
||||
it('getAvailableTools should use TOOL_CACHE namespace', async () => {
|
||||
mockCache.get.mockResolvedValue([]);
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
|
||||
});
|
||||
|
||||
it('should NOT use CONFIG_STORE namespace for tool/plugin operations', async () => {
|
||||
mockCache.get.mockResolvedValue([]);
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
const allCalls = getLogStores.mock.calls.flat();
|
||||
expect(allCalls).not.toContain(CacheKeys.CONFIG_STORE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvailablePluginsController', () => {
|
||||
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
|
||||
// Add plugins with duplicates to availableTools
|
||||
|
|
|
|||
|
|
@ -1,10 +1,92 @@
|
|||
const { ToolCacheKeys } = require('../getCachedTools');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
|
||||
jest.mock('~/cache/getLogStores');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
|
||||
const mockCache = { get: jest.fn(), set: jest.fn(), delete: jest.fn() };
|
||||
getLogStores.mockReturnValue(mockCache);
|
||||
|
||||
const {
|
||||
ToolCacheKeys,
|
||||
getCachedTools,
|
||||
setCachedTools,
|
||||
getMCPServerTools,
|
||||
invalidateCachedTools,
|
||||
} = require('../getCachedTools');
|
||||
|
||||
describe('getCachedTools', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
getLogStores.mockReturnValue(mockCache);
|
||||
});
|
||||
|
||||
describe('getCachedTools - Cache Isolation Security', () => {
|
||||
describe('ToolCacheKeys.MCP_SERVER', () => {
|
||||
it('should generate cache keys that include userId', () => {
|
||||
const key = ToolCacheKeys.MCP_SERVER('user123', 'github');
|
||||
expect(key).toBe('tools:mcp:user123:github');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TOOL_CACHE namespace usage', () => {
|
||||
it('getCachedTools should use TOOL_CACHE namespace', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
await getCachedTools();
|
||||
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
|
||||
});
|
||||
|
||||
it('getCachedTools with MCP server options should use TOOL_CACHE namespace', async () => {
|
||||
mockCache.get.mockResolvedValue({ tool1: {} });
|
||||
await getCachedTools({ userId: 'user1', serverName: 'github' });
|
||||
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
|
||||
expect(mockCache.get).toHaveBeenCalledWith(ToolCacheKeys.MCP_SERVER('user1', 'github'));
|
||||
});
|
||||
|
||||
it('setCachedTools should use TOOL_CACHE namespace', async () => {
|
||||
mockCache.set.mockResolvedValue(true);
|
||||
const tools = { tool1: { type: 'function' } };
|
||||
await setCachedTools(tools);
|
||||
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
|
||||
expect(mockCache.set).toHaveBeenCalledWith(ToolCacheKeys.GLOBAL, tools, expect.any(Number));
|
||||
});
|
||||
|
||||
it('setCachedTools with MCP server options should use TOOL_CACHE namespace', async () => {
|
||||
mockCache.set.mockResolvedValue(true);
|
||||
const tools = { tool1: { type: 'function' } };
|
||||
await setCachedTools(tools, { userId: 'user1', serverName: 'github' });
|
||||
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
|
||||
expect(mockCache.set).toHaveBeenCalledWith(
|
||||
ToolCacheKeys.MCP_SERVER('user1', 'github'),
|
||||
tools,
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('invalidateCachedTools should use TOOL_CACHE namespace', async () => {
|
||||
mockCache.delete.mockResolvedValue(true);
|
||||
await invalidateCachedTools({ invalidateGlobal: true });
|
||||
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
|
||||
expect(mockCache.delete).toHaveBeenCalledWith(ToolCacheKeys.GLOBAL);
|
||||
});
|
||||
|
||||
it('getMCPServerTools should use TOOL_CACHE namespace', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
await getMCPServerTools('user1', 'github');
|
||||
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
|
||||
expect(mockCache.get).toHaveBeenCalledWith(ToolCacheKeys.MCP_SERVER('user1', 'github'));
|
||||
});
|
||||
|
||||
it('should NOT use CONFIG_STORE namespace', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
await getCachedTools();
|
||||
await getMCPServerTools('user1', 'github');
|
||||
mockCache.set.mockResolvedValue(true);
|
||||
await setCachedTools({ tool1: {} });
|
||||
mockCache.delete.mockResolvedValue(true);
|
||||
await invalidateCachedTools({ invalidateGlobal: true });
|
||||
|
||||
const allCalls = getLogStores.mock.calls.flat();
|
||||
expect(allCalls).not.toContain(CacheKeys.CONFIG_STORE);
|
||||
expect(allCalls.every((key) => key === CacheKeys.TOOL_CACHE)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const ToolCacheKeys = {
|
|||
* @returns {Promise<LCAvailableTools|null>} The available tools object or null if not cached
|
||||
*/
|
||||
async function getCachedTools(options = {}) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
||||
const { userId, serverName } = options;
|
||||
|
||||
// Return MCP server-specific tools if requested
|
||||
|
|
@ -43,7 +43,7 @@ async function getCachedTools(options = {}) {
|
|||
* @returns {Promise<boolean>} Whether the operation was successful
|
||||
*/
|
||||
async function setCachedTools(tools, options = {}) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
||||
const { userId, serverName, ttl = Time.TWELVE_HOURS } = options;
|
||||
|
||||
// Cache by MCP server if specified (requires userId)
|
||||
|
|
@ -65,7 +65,7 @@ async function setCachedTools(tools, options = {}) {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function invalidateCachedTools(options = {}) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
||||
const { userId, serverName, invalidateGlobal = false } = options;
|
||||
|
||||
const keysToDelete = [];
|
||||
|
|
@ -89,7 +89,7 @@ async function invalidateCachedTools(options = {}) {
|
|||
* @returns {Promise<LCAvailableTools|null>} The available tools for the server
|
||||
*/
|
||||
async function getMCPServerTools(userId, serverName) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
||||
const serverTools = await cache.get(ToolCacheKeys.MCP_SERVER(userId, serverName));
|
||||
|
||||
if (serverTools) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ async function updateMCPServerTools({ userId, serverName, tools }) {
|
|||
|
||||
await setCachedTools(serverTools, { userId, serverName });
|
||||
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
||||
await cache.delete(CacheKeys.TOOLS);
|
||||
logger.debug(
|
||||
`[MCP Cache] Updated ${tools.length} tools for server ${serverName} (user: ${userId})`,
|
||||
|
|
@ -61,7 +61,7 @@ async function mergeAppTools(appTools) {
|
|||
const cachedTools = await getCachedTools();
|
||||
const mergedTools = { ...cachedTools, ...appTools };
|
||||
await setCachedTools(mergedTools);
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cache = getLogStores(CacheKeys.TOOL_CACHE);
|
||||
await cache.delete(CacheKeys.TOOLS);
|
||||
logger.debug(`Merged ${count} app-level tools`);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -215,16 +215,30 @@ describe('cacheConfig', () => {
|
|||
}).rejects.toThrow('Invalid cache keys in FORCED_IN_MEMORY_CACHE_NAMESPACES: INVALID_KEY');
|
||||
});
|
||||
|
||||
test('should handle empty string gracefully', async () => {
|
||||
test('should produce empty array when set to empty string (opt out of defaults)', async () => {
|
||||
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = '';
|
||||
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle undefined env var gracefully', async () => {
|
||||
test('should default to CONFIG_STORE and APP_CONFIG when env var is not set', async () => {
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual([]);
|
||||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual(['CONFIG_STORE', 'APP_CONFIG']);
|
||||
});
|
||||
|
||||
test('should accept TOOL_CACHE as a valid namespace', async () => {
|
||||
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'TOOL_CACHE';
|
||||
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual(['TOOL_CACHE']);
|
||||
});
|
||||
|
||||
test('should accept CONFIG_STORE and APP_CONFIG together for blue/green deployments', async () => {
|
||||
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES = 'CONFIG_STORE,APP_CONFIG';
|
||||
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual(['CONFIG_STORE', 'APP_CONFIG']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
135
packages/api/src/cache/__tests__/cacheFactory/standardCache.namespace_isolation.spec.ts
vendored
Normal file
135
packages/api/src/cache/__tests__/cacheFactory/standardCache.namespace_isolation.spec.ts
vendored
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { CacheKeys } from 'librechat-data-provider';
|
||||
|
||||
const mockKeyvRedisInstance = {
|
||||
namespace: '',
|
||||
keyPrefixSeparator: '',
|
||||
on: jest.fn(),
|
||||
};
|
||||
|
||||
const MockKeyvRedis = jest.fn().mockReturnValue(mockKeyvRedisInstance);
|
||||
|
||||
jest.mock('@keyv/redis', () => ({
|
||||
default: MockKeyvRedis,
|
||||
}));
|
||||
|
||||
const mockKeyvRedisClient = { scanIterator: jest.fn() };
|
||||
|
||||
jest.mock('../../redisClients', () => ({
|
||||
keyvRedisClient: mockKeyvRedisClient,
|
||||
ioredisClient: null,
|
||||
}));
|
||||
|
||||
jest.mock('../../redisUtils', () => ({
|
||||
batchDeleteKeys: jest.fn(),
|
||||
scanKeys: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => ({
|
||||
logger: {
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('standardCache - CONFIG_STORE vs TOOL_CACHE namespace isolation', () => {
|
||||
afterEach(() => {
|
||||
jest.resetModules();
|
||||
MockKeyvRedis.mockClear();
|
||||
});
|
||||
|
||||
/**
|
||||
* Core behavioral test for blue/green deployments:
|
||||
* When CONFIG_STORE and APP_CONFIG are forced in-memory,
|
||||
* TOOL_CACHE should still use Redis for cross-container sharing.
|
||||
*/
|
||||
it('should force CONFIG_STORE to in-memory while TOOL_CACHE uses Redis', async () => {
|
||||
jest.doMock('../../cacheConfig', () => ({
|
||||
cacheConfig: {
|
||||
FORCED_IN_MEMORY_CACHE_NAMESPACES: [CacheKeys.CONFIG_STORE, CacheKeys.APP_CONFIG],
|
||||
REDIS_KEY_PREFIX: '',
|
||||
GLOBAL_PREFIX_SEPARATOR: '>>',
|
||||
},
|
||||
}));
|
||||
|
||||
const { standardCache } = await import('../../cacheFactory');
|
||||
|
||||
MockKeyvRedis.mockClear();
|
||||
|
||||
const configCache = standardCache(CacheKeys.CONFIG_STORE);
|
||||
expect(MockKeyvRedis).not.toHaveBeenCalled();
|
||||
expect(configCache).toBeDefined();
|
||||
|
||||
const appConfigCache = standardCache(CacheKeys.APP_CONFIG);
|
||||
expect(MockKeyvRedis).not.toHaveBeenCalled();
|
||||
expect(appConfigCache).toBeDefined();
|
||||
|
||||
const toolCache = standardCache(CacheKeys.TOOL_CACHE);
|
||||
expect(MockKeyvRedis).toHaveBeenCalledTimes(1);
|
||||
expect(MockKeyvRedis).toHaveBeenCalledWith(mockKeyvRedisClient);
|
||||
expect(toolCache).toBeDefined();
|
||||
});
|
||||
|
||||
it('CONFIG_STORE and TOOL_CACHE should be independent stores', async () => {
|
||||
jest.doMock('../../cacheConfig', () => ({
|
||||
cacheConfig: {
|
||||
FORCED_IN_MEMORY_CACHE_NAMESPACES: [CacheKeys.CONFIG_STORE],
|
||||
REDIS_KEY_PREFIX: '',
|
||||
GLOBAL_PREFIX_SEPARATOR: '>>',
|
||||
},
|
||||
}));
|
||||
|
||||
const { standardCache } = await import('../../cacheFactory');
|
||||
|
||||
const configCache = standardCache(CacheKeys.CONFIG_STORE);
|
||||
const toolCache = standardCache(CacheKeys.TOOL_CACHE);
|
||||
|
||||
await configCache.set('STARTUP_CONFIG', { version: 'v2-green' });
|
||||
await toolCache.set('tools:global', { myTool: { type: 'function' } });
|
||||
|
||||
expect(await configCache.get('STARTUP_CONFIG')).toEqual({ version: 'v2-green' });
|
||||
expect(await configCache.get('tools:global')).toBeUndefined();
|
||||
|
||||
expect(await toolCache.get('STARTUP_CONFIG')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should use Redis for all namespaces when nothing is forced in-memory', async () => {
|
||||
jest.doMock('../../cacheConfig', () => ({
|
||||
cacheConfig: {
|
||||
FORCED_IN_MEMORY_CACHE_NAMESPACES: [],
|
||||
REDIS_KEY_PREFIX: '',
|
||||
GLOBAL_PREFIX_SEPARATOR: '>>',
|
||||
},
|
||||
}));
|
||||
|
||||
const { standardCache } = await import('../../cacheFactory');
|
||||
|
||||
MockKeyvRedis.mockClear();
|
||||
|
||||
standardCache(CacheKeys.CONFIG_STORE);
|
||||
standardCache(CacheKeys.TOOL_CACHE);
|
||||
standardCache(CacheKeys.APP_CONFIG);
|
||||
|
||||
expect(MockKeyvRedis).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('forcing TOOL_CACHE to in-memory should not affect CONFIG_STORE', async () => {
|
||||
jest.doMock('../../cacheConfig', () => ({
|
||||
cacheConfig: {
|
||||
FORCED_IN_MEMORY_CACHE_NAMESPACES: [CacheKeys.TOOL_CACHE],
|
||||
REDIS_KEY_PREFIX: '',
|
||||
GLOBAL_PREFIX_SEPARATOR: '>>',
|
||||
},
|
||||
}));
|
||||
|
||||
const { standardCache } = await import('../../cacheFactory');
|
||||
|
||||
MockKeyvRedis.mockClear();
|
||||
|
||||
standardCache(CacheKeys.TOOL_CACHE);
|
||||
expect(MockKeyvRedis).not.toHaveBeenCalled();
|
||||
|
||||
standardCache(CacheKeys.CONFIG_STORE);
|
||||
expect(MockKeyvRedis).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
11
packages/api/src/cache/cacheConfig.ts
vendored
11
packages/api/src/cache/cacheConfig.ts
vendored
|
|
@ -27,9 +27,14 @@ const USE_REDIS_STREAMS =
|
|||
|
||||
// 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())
|
||||
: [];
|
||||
// Defaults to CONFIG_STORE,APP_CONFIG so YAML-derived config stays per-container.
|
||||
// Set to empty string to force all namespaces through Redis.
|
||||
const FORCED_IN_MEMORY_CACHE_NAMESPACES =
|
||||
process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES !== undefined
|
||||
? process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES.split(',')
|
||||
.map((key) => key.trim())
|
||||
.filter(Boolean)
|
||||
: [CacheKeys.CONFIG_STORE, CacheKeys.APP_CONFIG];
|
||||
|
||||
// Validate against CacheKeys enum
|
||||
if (FORCED_IN_MEMORY_CACHE_NAMESPACES.length > 0) {
|
||||
|
|
|
|||
|
|
@ -1364,6 +1364,10 @@ export enum CacheKeys {
|
|||
* Key for the config store namespace.
|
||||
*/
|
||||
CONFIG_STORE = 'CONFIG_STORE',
|
||||
/**
|
||||
* Key for the tool cache namespace (plugins, MCP tools, tool definitions).
|
||||
*/
|
||||
TOOL_CACHE = 'TOOL_CACHE',
|
||||
/**
|
||||
* Key for the roles cache.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue