diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index c1e3a8a57a..9e5b1a8b47 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -11,6 +11,7 @@ const { Constants, } = require('librechat-data-provider'); const { getMessages, saveMessage, updateMessage, saveConvo, getConvo } = require('~/models'); +const { getAppConfig } = require('~/server/services/Config'); const { checkBalance } = require('~/models/balanceMethods'); const { truncateToolCallOutputs } = require('./prompts'); const { getFiles } = require('~/models/File'); @@ -567,6 +568,7 @@ class BaseClient { } async sendMessage(message, opts = {}) { + const appConfig = await getAppConfig({ role: this.options.req?.user?.role }); /** @type {Promise} */ let userMessagePromise; const { user, head, isEdited, conversationId, responseMessageId, saveOptions, userMessage } = @@ -653,7 +655,7 @@ class BaseClient { } } - const balance = this.options.req?.app?.locals?.balance; + const balance = appConfig?.balance; if ( balance?.enabled && supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint] diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 3553e40dd0..6afb5d8cb3 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -34,6 +34,7 @@ const { const { extractBaseURL, getModelMaxTokens, getModelMaxOutputTokens } = require('~/utils'); const { encodeAndFormat } = require('~/server/services/Files/images/encode'); const { addSpaceIfNeeded, sleep } = require('~/server/utils'); +const { getAppConfig } = require('~/server/services/Config'); const { spendTokens } = require('~/models/spendTokens'); const { handleOpenAIErrors } = require('./tools/util'); const { createLLM, RunManager } = require('./llm'); @@ -702,6 +703,7 @@ class OpenAIClient extends BaseClient { * In case of failure, it will return the default title, "New Chat". */ async titleConvo({ text, conversationId, responseText = '' }) { + const appConfig = await getAppConfig({ role: this.options.req?.user?.role }); this.conversationId = conversationId; if (this.options.attachments) { @@ -731,7 +733,7 @@ class OpenAIClient extends BaseClient { }; /** @type {TAzureConfig | undefined} */ - const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig?.[EModelEndpoint.azureOpenAI]; const resetTitleOptions = !!( (this.azure && azureConfig) || @@ -1120,6 +1122,7 @@ ${convo} } async chatCompletion({ payload, onProgress, abortController = null }) { + const appConfig = await getAppConfig({ role: this.options.req?.user?.role }); let error = null; let intermediateReply = []; const errorCallback = (err) => (error = err); @@ -1166,7 +1169,7 @@ ${convo} } /** @type {TAzureConfig | undefined} */ - const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig?.[EModelEndpoint.azureOpenAI]; if ( (this.azure && this.isVisionModel && azureConfig) || diff --git a/api/app/clients/tools/structured/OpenAIImageTools.js b/api/app/clients/tools/structured/OpenAIImageTools.js index 920555da30..d68a2e47e4 100644 --- a/api/app/clients/tools/structured/OpenAIImageTools.js +++ b/api/app/clients/tools/structured/OpenAIImageTools.js @@ -9,6 +9,7 @@ const { logAxiosError } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { ContentTypes, EImageOutputType } = require('librechat-data-provider'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); +const { getAppConfig } = require('~/server/services/Config'); const { extractBaseURL } = require('~/utils'); const { getFiles } = require('~/models/File'); @@ -123,7 +124,7 @@ function createAbortHandler() { * @param {MongoFile[]} [fields.imageFiles] - The images to be used for editing * @returns {Array} - Array of image tools */ -function createOpenAIImageTools(fields = {}) { +async function createOpenAIImageTools(fields = {}) { /** @type {boolean} Used to initialize the Tool without necessary variables. */ const override = fields.override ?? false; /** @type {boolean} */ @@ -131,8 +132,9 @@ function createOpenAIImageTools(fields = {}) { throw new Error('This tool is only available for agents.'); } const { req } = fields; - const imageOutputType = req?.app.locals.imageOutputType || EImageOutputType.PNG; - const appFileStrategy = req?.app.locals.fileStrategy; + const appConfig = await getAppConfig({ role: req?.user?.role }); + const imageOutputType = appConfig?.imageOutputType || EImageOutputType.PNG; + const appFileStrategy = appConfig?.fileStrategy; const getApiKey = () => { const apiKey = process.env.IMAGE_GEN_OAI_API_KEY ?? ''; diff --git a/api/app/clients/tools/util/handleTools.js b/api/app/clients/tools/util/handleTools.js index fc04d0c58a..ff4a4655ba 100644 --- a/api/app/clients/tools/util/handleTools.js +++ b/api/app/clients/tools/util/handleTools.js @@ -24,8 +24,8 @@ const { const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process'); const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch'); const { getUserPluginAuthValue } = require('~/server/services/PluginService'); +const { getCachedTools, getAppConfig } = require('~/server/services/Config'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); -const { getCachedTools } = require('~/server/services/Config'); const { createMCPTool } = require('~/server/services/MCP'); /** @@ -143,6 +143,7 @@ const loadTools = async ({ functions = true, returnMap = false, }) => { + const appConfig = await getAppConfig({ role: options?.req?.user?.role }); const toolConstructors = { flux: FluxAPI, calculator: Calculator, @@ -272,7 +273,7 @@ const loadTools = async ({ }; continue; } else if (tool === Tools.web_search) { - const webSearchConfig = options?.req?.app?.locals?.webSearch; + const webSearchConfig = appConfig?.webSearch; const result = await loadWebSearchAuth({ userId: user, loadAuthValues, diff --git a/api/server/controllers/PluginController.js b/api/server/controllers/PluginController.js index 06b4585a03..730dcacc49 100644 --- a/api/server/controllers/PluginController.js +++ b/api/server/controllers/PluginController.js @@ -6,7 +6,7 @@ const { filterUniquePlugins, convertMCPToolsToPlugins, } = require('@librechat/api'); -const { getCustomConfig, getCachedTools } = require('~/server/services/Config'); +const { getCustomConfig, getCachedTools, getAppConfig } = require('~/server/services/Config'); const { availableTools, toolkits } = require('~/app/clients/tools'); const { getMCPManager, getFlowStateManager } = require('~/config'); const { getLogStores } = require('~/cache'); @@ -20,8 +20,9 @@ const getAvailablePluginsController = async (req, res) => { return; } + const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {{ filteredTools: string[], includedTools: string[] }} */ - const { filteredTools = [], includedTools = [] } = req.app.locals; + const { filteredTools = [], includedTools = [] } = appConfig; const pluginManifest = availableTools; const uniquePlugins = filterUniquePlugins(pluginManifest); diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index cd88cb2de9..75e5509b6b 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -17,11 +17,13 @@ const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud') const { Tools, Constants, FileSources } = require('librechat-data-provider'); const { processDeleteRequest } = require('~/server/services/Files/process'); const { Transaction, Balance, User } = require('~/db/models'); +const { getAppConfig } = require('~/server/services/Config'); const { deleteToolCalls } = require('~/models/ToolCall'); const { deleteAllSharedLinks } = require('~/models'); const { getMCPManager } = require('~/config'); const getUserController = async (req, res) => { + const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {MongoUser} */ const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user }; /** @@ -31,7 +33,7 @@ const getUserController = async (req, res) => { delete userData.password; delete userData.totpSecret; delete userData.backupCodes; - if (req.app.locals.fileStrategy === FileSources.s3 && userData.avatar) { + if (appConfig.fileStrategy === FileSources.s3 && userData.avatar) { const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600); if (!avatarNeedsRefresh) { return res.status(200).send(userData); @@ -87,6 +89,7 @@ const deleteUserFiles = async (req) => { }; const updateUserPluginsController = async (req, res) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const { user } = req; const { pluginKey, action, auth, isEntityTool } = req.body; try { @@ -131,7 +134,7 @@ const updateUserPluginsController = async (req, res) => { if (pluginKey === Tools.web_search) { /** @type {TCustomConfig['webSearch']} */ - const webSearchConfig = req.app.locals?.webSearch; + const webSearchConfig = appConfig?.webSearch; keys = extractWebSearchEnvVars({ keys: action === 'install' ? keys : webSearchKeys, config: webSearchConfig, diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 14c9c38224..774c22f128 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -39,7 +39,12 @@ const { deleteMemory, setMemory, } = require('~/models'); -const { getMCPAuthMap, checkCapability, hasCustomUserVars } = require('~/server/services/Config'); +const { + hasCustomUserVars, + checkCapability, + getMCPAuthMap, + getAppConfig, +} = require('~/server/services/Config'); const { addCacheControl, createContextHandlers } = require('~/app/clients/prompts'); const { initializeAgent } = require('~/server/services/Endpoints/agents/agent'); const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens'); @@ -451,17 +456,15 @@ class AgentClient extends BaseClient { ); return; } - /** @type {TCustomConfig['memory']} */ - const memoryConfig = this.options.req?.app?.locals?.memory; + const appConfig = await getAppConfig({ role: user.role }); + const memoryConfig = appConfig.memory; if (!memoryConfig || memoryConfig.disabled === true) { return; } /** @type {Agent} */ let prelimAgent; - const allowedProviders = new Set( - this.options.req?.app?.locals?.[EModelEndpoint.agents]?.allowedProviders, - ); + const allowedProviders = new Set(appConfig?.[EModelEndpoint.agents]?.allowedProviders); try { if (memoryConfig.agent?.id != null && memoryConfig.agent.id !== this.options.agent.id) { prelimAgent = await loadAgent({ @@ -582,8 +585,8 @@ class AgentClient extends BaseClient { if (this.processMemory == null) { return; } - /** @type {TCustomConfig['memory']} */ - const memoryConfig = this.options.req?.app?.locals?.memory; + const appConfig = await getAppConfig({ role: this.options.req.user?.role }); + const memoryConfig = appConfig.memory; const messageWindowSize = memoryConfig?.messageWindowSize ?? 5; let messagesToProcess = [...messages]; @@ -759,8 +762,9 @@ class AgentClient extends BaseClient { abortController = new AbortController(); } + const appConfig = await getAppConfig({ role: this.options.req.user?.role }); /** @type {TCustomConfig['endpoints']['agents']} */ - const agentsEConfig = this.options.req.app.locals[EModelEndpoint.agents]; + const agentsEConfig = appConfig[EModelEndpoint.agents]; config = { configurable: { @@ -1081,6 +1085,7 @@ class AgentClient extends BaseClient { } const { handleLLMEnd, collected: collectedMetadata } = createMetadataAggregator(); const { req, res, agent } = this.options; + const appConfig = await getAppConfig({ role: req.user?.role }); let endpoint = agent.endpoint; /** @type {import('@librechat/agents').ClientOptions} */ @@ -1092,7 +1097,7 @@ class AgentClient extends BaseClient { /** @type {TEndpoint | undefined} */ const endpointConfig = - req.app.locals.all ?? req.app.locals[endpoint] ?? titleProviderConfig.customEndpointConfig; + appConfig.all ?? appConfig[endpoint] ?? titleProviderConfig.customEndpointConfig; if (!endpointConfig) { logger.warn( '[api/server/controllers/agents/client.js #titleConvo] Error getting endpoint config', diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index df009c0b0c..bffb07934c 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -31,12 +31,12 @@ const { grantPermission, } = require('~/server/services/PermissionService'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); +const { getCachedTools, getAppConfig } = require('~/server/services/Config'); const { resizeAvatar } = require('~/server/services/Files/images/avatar'); const { getFileStrategy } = require('~/server/utils/getFileStrategy'); const { refreshS3Url } = require('~/server/services/Files/S3/crud'); const { filterFile } = require('~/server/services/Files/process'); const { updateAction, getActions } = require('~/models/Action'); -const { getCachedTools } = require('~/server/services/Config'); const { deleteFileByFilter } = require('~/models/File'); const { getCategoriesWithCounts } = require('~/models'); @@ -487,6 +487,7 @@ const getListAgentsHandler = async (req, res) => { */ const uploadAgentAvatarHandler = async (req, res) => { try { + const appConfig = await getAppConfig({ role: req.user?.role }); filterFile({ req, file: req.file, image: true, isAvatar: true }); const { agent_id } = req.params; if (!agent_id) { @@ -510,9 +511,7 @@ const uploadAgentAvatarHandler = async (req, res) => { } const buffer = await fs.readFile(req.file.path); - - const fileStrategy = getFileStrategy(req.app.locals, { isAvatar: true }); - + const fileStrategy = getFileStrategy(appConfig, { isAvatar: true }); const resizedBuffer = await resizeAvatar({ userId: req.user.id, input: buffer, diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js index 09770b56d3..1c9f71da35 100644 --- a/api/server/controllers/assistants/chatV1.js +++ b/api/server/controllers/assistants/chatV1.js @@ -29,6 +29,7 @@ const { createRun, StreamRunManager } = require('~/server/services/Runs'); const { addTitle } = require('~/server/services/Endpoints/assistants'); const { createRunBody } = require('~/server/services/createRunBody'); const { sendResponse } = require('~/server/middleware/error'); +const { getAppConfig } = require('~/server/services/Config'); const { getTransactions } = require('~/models/Transaction'); const { checkBalance } = require('~/models/balanceMethods'); const { getConvo } = require('~/models/Conversation'); @@ -47,6 +48,7 @@ const { getOpenAIClient } = require('./helpers'); * @returns {void} */ const chatV1 = async (req, res) => { + const appConfig = await getAppConfig({ role: req.user?.role }); logger.debug('[/assistants/chat/] req.body', req.body); const { @@ -251,7 +253,7 @@ const chatV1 = async (req, res) => { } const checkBalanceBeforeRun = async () => { - const balance = req.app?.locals?.balance; + const balance = appConfig?.balance; if (!balance?.enabled) { return; } diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js index c569dc8378..32b8e16780 100644 --- a/api/server/controllers/assistants/chatV2.js +++ b/api/server/controllers/assistants/chatV2.js @@ -26,6 +26,7 @@ const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { createRun, StreamRunManager } = require('~/server/services/Runs'); const { addTitle } = require('~/server/services/Endpoints/assistants'); const { createRunBody } = require('~/server/services/createRunBody'); +const { getAppConfig } = require('~/server/services/Config'); const { getTransactions } = require('~/models/Transaction'); const { checkBalance } = require('~/models/balanceMethods'); const { getConvo } = require('~/models/Conversation'); @@ -44,6 +45,7 @@ const { getOpenAIClient } = require('./helpers'); */ const chatV2 = async (req, res) => { logger.debug('[/assistants/chat/] req.body', req.body); + const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {{files: MongoFile[]}} */ const { @@ -126,7 +128,7 @@ const chatV2 = async (req, res) => { } const checkBalanceBeforeRun = async () => { - const balance = req.app?.locals?.balance; + const balance = appConfig?.balance; if (!balance?.enabled) { return; } @@ -374,9 +376,9 @@ const chatV2 = async (req, res) => { }; /** @type {undefined | TAssistantEndpoint} */ - const config = req.app.locals[endpoint] ?? {}; + const config = appConfig[endpoint] ?? {}; /** @type {undefined | TBaseEndpoint} */ - const allConfig = req.app.locals.all; + const allConfig = appConfig.all; const streamRunManager = new StreamRunManager({ req, diff --git a/api/server/controllers/assistants/helpers.js b/api/server/controllers/assistants/helpers.js index 1bbc0915b9..306990078b 100644 --- a/api/server/controllers/assistants/helpers.js +++ b/api/server/controllers/assistants/helpers.js @@ -7,8 +7,8 @@ const { const { initializeClient: initAzureClient, } = require('~/server/services/Endpoints/azureAssistants'); +const { getEndpointsConfig, getAppConfig } = require('~/server/services/Config'); const { initializeClient } = require('~/server/services/Endpoints/assistants'); -const { getEndpointsConfig } = require('~/server/services/Config'); /** * @param {Express.Request} req @@ -210,6 +210,7 @@ async function getOpenAIClient({ req, res, endpointOption, initAppClient, overri * @returns {Promise} 200 - success response - application/json */ const fetchAssistants = async ({ req, res, overrideEndpoint }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const { limit = 100, order = 'desc', @@ -230,20 +231,20 @@ const fetchAssistants = async ({ req, res, overrideEndpoint }) => { if (endpoint === EModelEndpoint.assistants) { ({ body } = await listAllAssistants({ req, res, version, query })); } else if (endpoint === EModelEndpoint.azureAssistants) { - const azureConfig = req.app.locals[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig[EModelEndpoint.azureOpenAI]; body = await listAssistantsForAzure({ req, res, version, azureConfig, query }); } if (req.user.role === SystemRoles.ADMIN) { return body; - } else if (!req.app.locals[endpoint]) { + } else if (!appConfig[endpoint]) { return body; } body.data = filterAssistants({ userId: req.user.id, assistants: body.data, - assistantsConfig: req.app.locals[endpoint], + assistantsConfig: appConfig[endpoint], }); return body; }; diff --git a/api/server/controllers/assistants/v1.js b/api/server/controllers/assistants/v1.js index 10c59d9138..150c712876 100644 --- a/api/server/controllers/assistants/v1.js +++ b/api/server/controllers/assistants/v1.js @@ -5,9 +5,9 @@ const { uploadImageBuffer, filterFile } = require('~/server/services/Files/proce const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { deleteAssistantActions } = require('~/server/services/ActionService'); +const { getCachedTools, getAppConfig } = require('~/server/services/Config'); const { updateAssistantDoc, getAssistants } = require('~/models/Assistant'); const { getOpenAIClient, fetchAssistants } = require('./helpers'); -const { getCachedTools } = require('~/server/services/Config'); const { manifestToolMap } = require('~/app/clients/tools'); const { deleteFileByFilter } = require('~/models/File'); @@ -258,8 +258,9 @@ function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) { */ const getAssistantDocuments = async (req, res) => { try { + const appConfig = await getAppConfig({ role: req.user?.role }); const endpoint = req.query; - const assistantsConfig = req.app.locals[endpoint]; + const assistantsConfig = appConfig[endpoint]; const documents = await getAssistants( {}, { @@ -296,6 +297,7 @@ const getAssistantDocuments = async (req, res) => { */ const uploadAssistantAvatar = async (req, res) => { try { + const appConfig = await getAppConfig({ role: req.user?.role }); filterFile({ req, file: req.file, image: true, isAvatar: true }); const { assistant_id } = req.params; if (!assistant_id) { @@ -337,7 +339,7 @@ const uploadAssistantAvatar = async (req, res) => { const metadata = { ..._metadata, avatar: image.filepath, - avatar_source: req.app.locals.fileStrategy, + avatar_source: appConfig.fileStrategy, }; const promises = []; @@ -347,7 +349,7 @@ const uploadAssistantAvatar = async (req, res) => { { avatar: { filepath: image.filepath, - source: req.app.locals.fileStrategy, + source: appConfig.fileStrategy, }, user: req.user.id, }, diff --git a/api/server/controllers/tools.js b/api/server/controllers/tools.js index 079b4abc85..34a6b67c61 100644 --- a/api/server/controllers/tools.js +++ b/api/server/controllers/tools.js @@ -13,6 +13,7 @@ const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/p const { processCodeOutput } = require('~/server/services/Files/Code/process'); const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); +const { getAppConfig } = require('~/server/services/Config'); const { loadTools } = require('~/app/clients/tools/util'); const { getRoleByName } = require('~/models/Role'); const { getMessage } = require('~/models/Message'); @@ -35,9 +36,10 @@ const toolAccessPermType = { */ const verifyWebSearchAuth = async (req, res) => { try { + const appConfig = await getAppConfig({ role: req.user?.role }); const userId = req.user.id; /** @type {TCustomConfig['webSearch']} */ - const webSearchConfig = req.app.locals?.webSearch || {}; + const webSearchConfig = appConfig?.webSearch || {}; const result = await loadWebSearchAuth({ userId, loadAuthValues, @@ -110,6 +112,7 @@ const verifyToolAuth = async (req, res) => { */ const callTool = async (req, res) => { try { + const appConfig = await getAppConfig({ role: req.user?.role }); const { toolId = '' } = req.params; if (!fieldsMap[toolId]) { logger.warn(`[${toolId}/call] User ${req.user.id} attempted call to invalid tool`); @@ -155,7 +158,7 @@ const callTool = async (req, res) => { returnMetadata: true, processFileURL, uploadImageBuffer, - fileStrategy: req.app.locals.fileStrategy, + fileStrategy: appConfig.fileStrategy, }, }); diff --git a/api/server/index.js b/api/server/index.js index f83fcbb44d..d81a667b6c 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -17,6 +17,7 @@ const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies'); const { checkMigrations } = require('./services/start/migration'); const initializeMCPs = require('./services/initializeMCPs'); const configureSocialLogins = require('./socialLogins'); +const { getAppConfig } = require('./services/Config'); const AppService = require('./services/AppService'); const staticCache = require('./utils/staticCache'); const noIndex = require('./middleware/noIndex'); @@ -46,8 +47,8 @@ const startServer = async () => { app.set('trust proxy', trusted_proxy); await AppService(app); - - const indexPath = path.join(app.locals.paths.dist, 'index.html'); + const appConfig = await getAppConfig(); + const indexPath = path.join(appConfig.paths.dist, 'index.html'); const indexHTML = fs.readFileSync(indexPath, 'utf8'); app.get('/health', (_req, res) => res.status(200).send('OK')); @@ -66,10 +67,9 @@ const startServer = async () => { console.warn('Response compression has been disabled via DISABLE_COMPRESSION.'); } - // Serve static assets with aggressive caching - app.use(staticCache(app.locals.paths.dist)); - app.use(staticCache(app.locals.paths.fonts)); - app.use(staticCache(app.locals.paths.assets)); + app.use(staticCache(appConfig.paths.dist)); + app.use(staticCache(appConfig.paths.fonts)); + app.use(staticCache(appConfig.paths.assets)); if (!ALLOW_SOCIAL_LOGIN) { console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.'); @@ -146,7 +146,7 @@ const startServer = async () => { logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`); } - initializeMCPs(app).then(() => checkMigrations()); + initializeMCPs().then(() => checkMigrations()); }); }; diff --git a/api/server/middleware/assistants/validate.js b/api/server/middleware/assistants/validate.js index a98e8e227f..832a3699bf 100644 --- a/api/server/middleware/assistants/validate.js +++ b/api/server/middleware/assistants/validate.js @@ -1,5 +1,6 @@ const { v4 } = require('uuid'); const { handleAbortError } = require('~/server/middleware/abortMiddleware'); +const { getAppConfig } = require('~/server/services/Config/getAppConfig'); /** * Checks if the assistant is supported or excluded @@ -12,8 +13,9 @@ const { handleAbortError } = require('~/server/middleware/abortMiddleware'); const validateAssistant = async (req, res, next) => { const { endpoint, conversationId, assistant_id, messageId } = req.body; + const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {Partial} */ - const assistantsConfig = req.app.locals?.[endpoint]; + const assistantsConfig = appConfig?.[endpoint]; if (!assistantsConfig) { return next(); } diff --git a/api/server/middleware/assistants/validateAuthor.js b/api/server/middleware/assistants/validateAuthor.js index a17448211e..cf6dfb7a72 100644 --- a/api/server/middleware/assistants/validateAuthor.js +++ b/api/server/middleware/assistants/validateAuthor.js @@ -1,4 +1,5 @@ const { SystemRoles } = require('librechat-data-provider'); +const { getAppConfig } = require('~/server/services/Config/getAppConfig'); const { getAssistant } = require('~/models/Assistant'); /** @@ -20,8 +21,9 @@ const validateAuthor = async ({ req, openai, overrideEndpoint, overrideAssistant const assistant_id = overrideAssistantId ?? req.params.id ?? req.body.assistant_id ?? req.query.assistant_id; + const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {Partial} */ - const assistantsConfig = req.app.locals?.[endpoint]; + const assistantsConfig = appConfig?.[endpoint]; if (!assistantsConfig) { return; } diff --git a/api/server/middleware/buildEndpointOption.js b/api/server/middleware/buildEndpointOption.js index a44fd4b75d..8a9af7925f 100644 --- a/api/server/middleware/buildEndpointOption.js +++ b/api/server/middleware/buildEndpointOption.js @@ -7,6 +7,7 @@ const { parseCompactConvo, } = require('librechat-data-provider'); const azureAssistants = require('~/server/services/Endpoints/azureAssistants'); +const { getAppConfig } = require('~/server/services/Config/getAppConfig'); const assistants = require('~/server/services/Endpoints/assistants'); const { processFiles } = require('~/server/services/Files/process'); const anthropic = require('~/server/services/Endpoints/anthropic'); @@ -40,9 +41,10 @@ async function buildEndpointOption(req, res, next) { return handleError(res, { text: 'Error parsing conversation' }); } - if (req.app.locals.modelSpecs?.list && req.app.locals.modelSpecs?.enforce) { + const appConfig = await getAppConfig({ role: req.user?.role }); + if (appConfig.modelSpecs?.list && appConfig.modelSpecs?.enforce) { /** @type {{ list: TModelSpec[] }}*/ - const { list } = req.app.locals.modelSpecs; + const { list } = appConfig.modelSpecs; const { spec } = parsedBody; if (!spec) { diff --git a/api/server/middleware/validateImageRequest.js b/api/server/middleware/validateImageRequest.js index eb37b9dbb5..d71a294383 100644 --- a/api/server/middleware/validateImageRequest.js +++ b/api/server/middleware/validateImageRequest.js @@ -1,5 +1,6 @@ const cookies = require('cookie'); const jwt = require('jsonwebtoken'); +const { getAppConfig } = require('~/server/services/Config/getAppConfig'); const { logger } = require('~/config'); const OBJECT_ID_LENGTH = 24; @@ -24,8 +25,9 @@ function isValidObjectId(id) { * Middleware to validate image request. * Must be set by `secureImageLinks` via custom config file. */ -function validateImageRequest(req, res, next) { - if (!req.app.locals.secureImageLinks) { +async function validateImageRequest(req, res, next) { + const appConfig = await getAppConfig({ role: req.user?.role }); + if (!appConfig.secureImageLinks) { return next(); } diff --git a/api/server/routes/assistants/actions.js b/api/server/routes/assistants/actions.js index 3dc3923503..77122a5ecf 100644 --- a/api/server/routes/assistants/actions.js +++ b/api/server/routes/assistants/actions.js @@ -6,6 +6,7 @@ const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { updateAction, getActions, deleteAction } = require('~/models/Action'); const { updateAssistantDoc, getAssistant } = require('~/models/Assistant'); const { isActionDomainAllowed } = require('~/server/services/domains'); +const { getAppConfig } = require('~/server/services/Config'); const { logger } = require('~/config'); const router = express.Router(); @@ -20,6 +21,7 @@ const router = express.Router(); * @returns {Object} 200 - success response - application/json */ router.post('/:assistant_id', async (req, res) => { + const appConfig = await getAppConfig({ role: req.user?.role }); try { const { assistant_id } = req.params; @@ -125,7 +127,7 @@ router.post('/:assistant_id', async (req, res) => { } /* Map Azure OpenAI model to the assistant as defined by config */ - if (req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) { + if (appConfig[EModelEndpoint.azureOpenAI]?.assistants) { updatedAssistant = { ...updatedAssistant, model: req.body.model, diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 2abc4b5507..466c7dcb68 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -3,6 +3,7 @@ const { isEnabled } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { CacheKeys, defaultSocialLogins, Constants } = require('librechat-data-provider'); const { getCustomConfig } = require('~/server/services/Config/getCustomConfig'); +const { getAppConfig } = require('~/server/services/Config/getAppConfig'); const { getLdapConfig } = require('~/server/services/Config/ldap'); const { getProjectByName } = require('~/models/Project'); const { getMCPManager } = require('~/config'); @@ -43,6 +44,8 @@ router.get('/', async function (req, res) { const ldap = getLdapConfig(); try { + const appConfig = await getAppConfig({ role: req.user?.role }); + const isOpenIdEnabled = !!process.env.OPENID_CLIENT_ID && !!process.env.OPENID_CLIENT_SECRET && @@ -58,7 +61,7 @@ router.get('/', async function (req, res) { /** @type {TStartupConfig} */ const payload = { appTitle: process.env.APP_TITLE || 'LibreChat', - socialLogins: req.app.locals.socialLogins ?? defaultSocialLogins, + socialLogins: appConfig.socialLogins ?? defaultSocialLogins, discordLoginEnabled: !!process.env.DISCORD_CLIENT_ID && !!process.env.DISCORD_CLIENT_SECRET, facebookLoginEnabled: !!process.env.FACEBOOK_CLIENT_ID && !!process.env.FACEBOOK_CLIENT_SECRET, @@ -91,10 +94,10 @@ router.get('/', async function (req, res) { isEnabled(process.env.SHOW_BIRTHDAY_ICON) || process.env.SHOW_BIRTHDAY_ICON === '', helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai', - interface: req.app.locals.interfaceConfig, - turnstile: req.app.locals.turnstileConfig, - modelSpecs: req.app.locals.modelSpecs, - balance: req.app.locals.balance, + interface: appConfig.interfaceConfig, + turnstile: appConfig.turnstileConfig, + modelSpecs: appConfig.modelSpecs, + balance: appConfig.balance, sharedLinksEnabled, publicSharedLinksEnabled, analyticsGtmId: process.env.ANALYTICS_GTM_ID, @@ -128,8 +131,7 @@ router.get('/', async function (req, res) { } } - /** @type {TCustomConfig['webSearch']} */ - const webSearchConfig = req.app.locals.webSearch; + const webSearchConfig = appConfig.webSearch; if ( webSearchConfig != null && (webSearchConfig.searchProvider || diff --git a/api/server/routes/files/avatar.js b/api/server/routes/files/avatar.js index 23d90a4f3d..d2e6a7bef8 100644 --- a/api/server/routes/files/avatar.js +++ b/api/server/routes/files/avatar.js @@ -2,14 +2,16 @@ const fs = require('fs').promises; const express = require('express'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { resizeAvatar } = require('~/server/services/Files/images/avatar'); -const { filterFile } = require('~/server/services/Files/process'); const { getFileStrategy } = require('~/server/utils/getFileStrategy'); +const { filterFile } = require('~/server/services/Files/process'); +const { getAppConfig } = require('~/server/services/Config'); const { logger } = require('~/config'); const router = express.Router(); router.post('/', async (req, res) => { try { + const appConfig = await getAppConfig({ role: req.user?.role }); filterFile({ req, file: req.file, image: true, isAvatar: true }); const userId = req.user.id; const { manual } = req.body; @@ -19,8 +21,8 @@ router.post('/', async (req, res) => { throw new Error('User ID is undefined'); } - const fileStrategy = getFileStrategy(req.app.locals, { isAvatar: true }); - const desiredFormat = req.app.locals.imageOutputType; + const fileStrategy = getFileStrategy(appConfig, { isAvatar: true }); + const desiredFormat = appConfig.imageOutputType; const resizedBuffer = await resizeAvatar({ userId, input, @@ -39,7 +41,7 @@ router.post('/', async (req, res) => { try { await fs.unlink(req.file.path); logger.debug('[/files/images/avatar] Temp. image upload file deleted'); - } catch (error) { + } catch { logger.debug('[/files/images/avatar] Temp. image upload file already deleted'); } } diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index 1708d99536..57e6f62050 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -26,6 +26,7 @@ const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud'); const { hasAccessToFilesViaAgent } = require('~/server/services/Files'); const { getFiles, batchUpdateFiles } = require('~/models/File'); +const { getAppConfig } = require('~/server/services/Config'); const { cleanFileName } = require('~/server/utils/files'); const { getAssistant } = require('~/models/Assistant'); const { getAgent } = require('~/models/Agent'); @@ -36,8 +37,9 @@ const router = express.Router(); router.get('/', async (req, res) => { try { + const appConfig = await getAppConfig({ role: req.user?.role }); const files = await getFiles({ user: req.user.id }); - if (req.app.locals.fileStrategy === FileSources.s3) { + if (appConfig.fileStrategy === FileSources.s3) { try { const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL); const alreadyChecked = await cache.get(req.user.id); @@ -114,7 +116,8 @@ router.get('/agent/:agent_id', async (req, res) => { router.get('/config', async (req, res) => { try { - res.status(200).json(req.app.locals.fileConfig); + const appConfig = await getAppConfig({ role: req.user?.role }); + res.status(200).json(appConfig.fileConfig); } catch (error) { logger.error('[/files] Error getting fileConfig', error); res.status(400).json({ message: 'Error in request', error: error.message }); diff --git a/api/server/routes/files/images.js b/api/server/routes/files/images.js index d6d04446f8..44ef670812 100644 --- a/api/server/routes/files/images.js +++ b/api/server/routes/files/images.js @@ -7,11 +7,13 @@ const { processImageFile, processAgentFileUpload, } = require('~/server/services/Files/process'); +const { getAppConfig } = require('~/server/services/Config'); const { logger } = require('~/config'); const router = express.Router(); router.post('/', async (req, res) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const metadata = req.body; try { @@ -30,7 +32,7 @@ router.post('/', async (req, res) => { logger.error('[/files/images] Error processing file:', error); try { const filepath = path.join( - req.app.locals.paths.imageOutput, + appConfig.paths.imageOutput, req.user.id, path.basename(req.file.filename), ); @@ -43,7 +45,7 @@ router.post('/', async (req, res) => { try { await fs.unlink(req.file.path); logger.debug('[/files/images] Temp. image upload file deleted'); - } catch (error) { + } catch { logger.debug('[/files/images] Temp. image upload file already deleted'); } } diff --git a/api/server/routes/files/multer.js b/api/server/routes/files/multer.js index 257c309fa2..c73bb6f010 100644 --- a/api/server/routes/files/multer.js +++ b/api/server/routes/files/multer.js @@ -4,11 +4,12 @@ const crypto = require('crypto'); const multer = require('multer'); const { sanitizeFilename } = require('@librechat/api'); const { fileConfig: defaultFileConfig, mergeFileConfig } = require('librechat-data-provider'); -const { getCustomConfig } = require('~/server/services/Config'); +const { getCustomConfig, getAppConfig } = require('~/server/services/Config'); const storage = multer.diskStorage({ - destination: function (req, file, cb) { - const outputPath = path.join(req.app.locals.paths.uploads, 'temp', req.user.id); + destination: async function (req, file, cb) { + const appConfig = await getAppConfig({ role: req.user?.role }); + const outputPath = path.join(appConfig.paths.uploads, 'temp', req.user.id); if (!fs.existsSync(outputPath)) { fs.mkdirSync(outputPath, { recursive: true }); } diff --git a/api/server/routes/memories.js b/api/server/routes/memories.js index b596c89e8a..c69ff53568 100644 --- a/api/server/routes/memories.js +++ b/api/server/routes/memories.js @@ -8,6 +8,7 @@ const { deleteMemory, setMemory, } = require('~/models'); +const { getAppConfig } = require('~/server/services/Config'); const { requireJwtAuth } = require('~/server/middleware'); const { getRoleByName } = require('~/models/Role'); @@ -60,7 +61,8 @@ router.get('/', checkMemoryRead, async (req, res) => { return sum + (memory.tokenCount || 0); }, 0); - const memoryConfig = req.app.locals?.memory; + const appConfig = await getAppConfig({ role: req.user?.role }); + const memoryConfig = appConfig?.memory; const tokenLimit = memoryConfig?.tokenLimit; const charLimit = memoryConfig?.charLimit || 10000; @@ -98,7 +100,8 @@ router.post('/', memoryPayloadLimit, checkMemoryCreate, async (req, res) => { return res.status(400).json({ error: 'Value is required and must be a non-empty string.' }); } - const memoryConfig = req.app.locals?.memory; + const appConfig = await getAppConfig({ role: req.user?.role }); + const memoryConfig = appConfig?.memory; const charLimit = memoryConfig?.charLimit || 10000; if (key.length > 1000) { @@ -117,6 +120,9 @@ router.post('/', memoryPayloadLimit, checkMemoryCreate, async (req, res) => { const tokenCount = Tokenizer.getTokenCount(value, 'o200k_base'); const memories = await getAllUserMemories(req.user.id); + + const appConfig = await getAppConfig({ role: req.user?.role }); + const memoryConfig = appConfig?.memory; const tokenLimit = memoryConfig?.tokenLimit; if (tokenLimit) { @@ -200,8 +206,8 @@ router.patch('/:key', memoryPayloadLimit, checkMemoryUpdate, async (req, res) => } const newKey = bodyKey || urlKey; - - const memoryConfig = req.app.locals?.memory; + const appConfig = await getAppConfig({ role: req.user?.role }); + const memoryConfig = appConfig?.memory; const charLimit = memoryConfig?.charLimit || 10000; if (newKey.length > 1000) { diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index 1cb429b69e..8d28ea8928 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -21,6 +21,7 @@ const { ensureDefaultCategories, seedDefaultRoles, initializeRoles } = require(' const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants'); const { initializeAzureBlobService } = require('./Files/Azure/initialize'); const { initializeFirebase } = require('./Files/Firebase/initialize'); +const { initializeAppConfig } = require('./Config/getAppConfig'); const loadCustomConfig = require('./Config/loadCustomConfig'); const handleRateLimits = require('./Config/handleRateLimits'); const { loadDefaultInterface } = require('./start/interface'); @@ -35,9 +36,8 @@ const paths = require('~/config/paths'); /** * Loads custom config and initializes app-wide variables. * @function AppService - * @param {Express.Application} app - The Express application object. */ -const AppService = async (app) => { +const AppService = async () => { await initializeRoles(); await seedDefaultRoles(); await ensureDefaultCategories(); @@ -109,10 +109,11 @@ const AppService = async (app) => { const agentsDefaults = agentsConfigSetup(config); if (!Object.keys(config).length) { - app.locals = { + const appConfig = { ...defaultLocals, [EModelEndpoint.agents]: agentsDefaults, }; + await initializeAppConfig(appConfig); return; } @@ -167,13 +168,15 @@ const AppService = async (app) => { endpointLocals.all = endpoints.all; } - app.locals = { + const appConfig = { ...defaultLocals, fileConfig: config?.fileConfig, secureImageLinks: config?.secureImageLinks, modelSpecs: processModelSpecs(endpoints, config.modelSpecs, interfaceConfig), ...endpointLocals, }; + + await initializeAppConfig(appConfig); }; module.exports = AppService; diff --git a/api/server/services/AssistantService.js b/api/server/services/AssistantService.js index a9ac26e476..849b27c0f0 100644 --- a/api/server/services/AssistantService.js +++ b/api/server/services/AssistantService.js @@ -16,6 +16,7 @@ const { retrieveAndProcessFile } = require('~/server/services/Files/process'); const { processRequiredActions } = require('~/server/services/ToolService'); const { RunManager, waitForRun } = require('~/server/services/Runs'); const { processMessages } = require('~/server/services/Threads'); +const { getAppConfig } = require('~/server/services/Config'); const { createOnProgress } = require('~/server/utils'); const { TextStream } = require('~/app/clients'); @@ -350,6 +351,7 @@ async function runAssistant({ accumulatedMessages = [], in_progress: inProgress, }) { + const appConfig = await getAppConfig({ role: openai.req.user?.role }); let steps = accumulatedSteps; let messages = accumulatedMessages; const in_progress = inProgress ?? createInProgressHandler(openai, thread_id, messages); @@ -397,7 +399,7 @@ async function runAssistant({ const { endpoint = EModelEndpoint.azureAssistants } = openai.req.body; /** @type {TCustomConfig.endpoints.assistants} */ - const assistantsEndpointConfig = openai.req.app.locals?.[endpoint] ?? {}; + const assistantsEndpointConfig = appConfig?.[endpoint] ?? {}; const { pollIntervalMs, timeoutMs } = assistantsEndpointConfig; const run = await waitForRun({ diff --git a/api/server/services/Config/getAppConfig.js b/api/server/services/Config/getAppConfig.js new file mode 100644 index 0000000000..2f927de213 --- /dev/null +++ b/api/server/services/Config/getAppConfig.js @@ -0,0 +1,110 @@ +const { logger } = require('@librechat/data-schemas'); +const { CacheKeys } = require('librechat-data-provider'); +const getLogStores = require('~/cache/getLogStores'); + +/** + * @typedef {Object} AppConfig + * @property {import('librechat-data-provider').TCustomConfig} config - The main custom configuration + * @property {import('librechat-data-provider').TCustomConfig['ocr']} ocr - OCR configuration + * @property {Object} paths - File paths configuration + * @property {import('librechat-data-provider').TMemoryConfig | undefined} memory - Memory configuration + * @property {import('librechat-data-provider').TCustomConfig['webSearch']} webSearch - Web search configuration + * @property {string} fileStrategy - File storage strategy ('local', 's3', 'firebase', 'azure_blob') + * @property {Array} socialLogins - Social login configurations + * @property {string[]} [filteredTools] - Admin-filtered tools + * @property {string[]} [includedTools] - Admin-included tools + * @property {string} imageOutputType - Image output type configuration + * @property {import('librechat-data-provider').TCustomConfig['interface']} interfaceConfig - Interface configuration + * @property {import('librechat-data-provider').TCustomConfig['registration']} turnstileConfig - Turnstile configuration + * @property {import('librechat-data-provider').TCustomConfig['balance']} balance - Balance configuration + * @property {import('librechat-data-provider').TCustomConfig['mcpServers'] | null} mcpConfig - MCP server configuration + * @property {import('librechat-data-provider').TCustomConfig['fileConfig']} [fileConfig] - File configuration + * @property {import('librechat-data-provider').TCustomConfig['secureImageLinks']} [secureImageLinks] - Secure image links configuration + * @property {import('librechat-data-provider').TCustomConfig['modelSpecs'] | undefined} [modelSpecs] - Processed model specifications + * @property {import('librechat-data-provider').TEndpoint} [openAI] - OpenAI endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [google] - Google endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [bedrock] - Bedrock endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [anthropic] - Anthropic endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [gptPlugins] - GPT plugins endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [azureOpenAI] - Azure OpenAI endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [assistants] - Assistants endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [azureAssistants] - Azure assistants endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [agents] - Agents endpoint configuration + * @property {import('librechat-data-provider').TEndpoint} [all] - Global endpoint configuration + */ + +/** + * Get the app configuration based on user context + * @param {Object} [options] + * @param {string} [options.role] - User role for role-based config + * @param {boolean} [options.refresh] - Force refresh the cache + * @returns {Promise} + */ +async function getAppConfig(options = {}) { + const { role, refresh } = options; + + const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cacheKey = role ? `${CacheKeys.APP_CONFIG}:${role}` : CacheKeys.APP_CONFIG; + + if (!refresh) { + const cached = await cache.get(cacheKey); + if (cached) { + return cached; + } + } + + const baseConfig = await cache.get(CacheKeys.APP_CONFIG); + if (!baseConfig) { + throw new Error('App configuration not initialized. Please ensure AppService has been called.'); + } + + // For now, return the base config + // In the future, this is where we'll apply role-based modifications + if (role) { + // TODO: Apply role-based config modifications + // const roleConfig = await applyRoleBasedConfig(baseConfig, role); + // await cache.set(cacheKey, roleConfig); + // return roleConfig; + } + + return baseConfig; +} + +/** + * Cache the app configuration + * @param {AppConfig} config - The configuration to cache + * @returns {Promise} + */ +async function cacheAppConfig(config) { + const cache = getLogStores(CacheKeys.CONFIG_STORE); + await cache.set(CacheKeys.APP_CONFIG, config); + logger.debug('[getAppConfig] App configuration cached'); +} + +/** + * Clear the app configuration cache + * @returns {Promise} + */ +async function clearAppConfigCache() { + const cache = getLogStores(CacheKeys.CONFIG_STORE); + const cacheKey = CacheKeys.APP_CONFIG; + return await cache.delete(cacheKey); +} + +/** + * Initialize the app configuration during startup + * @param {AppConfig} config - The initial configuration to store + * @returns {Promise} + */ +async function initializeAppConfig(config) { + const cache = getLogStores(CacheKeys.CONFIG_STORE); + await cache.set(CacheKeys.APP_CONFIG, config); + logger.debug('[getAppConfig] App configuration initialized'); +} + +module.exports = { + getAppConfig, + cacheAppConfig, + clearAppConfigCache, + initializeAppConfig, +}; diff --git a/api/server/services/Config/getEndpointsConfig.js b/api/server/services/Config/getEndpointsConfig.js index 670bc22d11..790cb7806c 100644 --- a/api/server/services/Config/getEndpointsConfig.js +++ b/api/server/services/Config/getEndpointsConfig.js @@ -8,6 +8,7 @@ const { const loadDefaultEndpointsConfig = require('./loadDefaultEConfig'); const loadConfigEndpoints = require('./loadConfigEndpoints'); const getLogStores = require('~/cache/getLogStores'); +const { getAppConfig } = require('./getAppConfig'); /** * @@ -23,12 +24,13 @@ async function getEndpointsConfig(req) { const defaultEndpointsConfig = await loadDefaultEndpointsConfig(req); const customConfigEndpoints = await loadConfigEndpoints(req); + const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {TEndpointsConfig} */ const mergedConfig = { ...defaultEndpointsConfig, ...customConfigEndpoints }; - if (mergedConfig[EModelEndpoint.assistants] && req.app.locals?.[EModelEndpoint.assistants]) { + if (mergedConfig[EModelEndpoint.assistants] && appConfig?.[EModelEndpoint.assistants]) { const { disableBuilder, retrievalModels, capabilities, version, ..._rest } = - req.app.locals[EModelEndpoint.assistants]; + appConfig[EModelEndpoint.assistants]; mergedConfig[EModelEndpoint.assistants] = { ...mergedConfig[EModelEndpoint.assistants], @@ -38,9 +40,9 @@ async function getEndpointsConfig(req) { capabilities, }; } - if (mergedConfig[EModelEndpoint.agents] && req.app.locals?.[EModelEndpoint.agents]) { + if (mergedConfig[EModelEndpoint.agents] && appConfig?.[EModelEndpoint.agents]) { const { disableBuilder, capabilities, allowedProviders, ..._rest } = - req.app.locals[EModelEndpoint.agents]; + appConfig[EModelEndpoint.agents]; mergedConfig[EModelEndpoint.agents] = { ...mergedConfig[EModelEndpoint.agents], @@ -50,12 +52,9 @@ async function getEndpointsConfig(req) { }; } - if ( - mergedConfig[EModelEndpoint.azureAssistants] && - req.app.locals?.[EModelEndpoint.azureAssistants] - ) { + if (mergedConfig[EModelEndpoint.azureAssistants] && appConfig?.[EModelEndpoint.azureAssistants]) { const { disableBuilder, retrievalModels, capabilities, version, ..._rest } = - req.app.locals[EModelEndpoint.azureAssistants]; + appConfig[EModelEndpoint.azureAssistants]; mergedConfig[EModelEndpoint.azureAssistants] = { ...mergedConfig[EModelEndpoint.azureAssistants], @@ -66,8 +65,8 @@ async function getEndpointsConfig(req) { }; } - if (mergedConfig[EModelEndpoint.bedrock] && req.app.locals?.[EModelEndpoint.bedrock]) { - const { availableRegions } = req.app.locals[EModelEndpoint.bedrock]; + if (mergedConfig[EModelEndpoint.bedrock] && appConfig?.[EModelEndpoint.bedrock]) { + const { availableRegions } = appConfig[EModelEndpoint.bedrock]; mergedConfig[EModelEndpoint.bedrock] = { ...mergedConfig[EModelEndpoint.bedrock], availableRegions, diff --git a/api/server/services/Config/index.js b/api/server/services/Config/index.js index ad25e57998..67c071661d 100644 --- a/api/server/services/Config/index.js +++ b/api/server/services/Config/index.js @@ -1,3 +1,4 @@ +const getAppConfig = require('./getAppConfig'); const { config } = require('./EndpointService'); const getCachedTools = require('./getCachedTools'); const getCustomConfig = require('./getCustomConfig'); @@ -15,6 +16,7 @@ module.exports = { loadDefaultModels, loadOverrideConfig, loadAsyncEndpoints, + ...getAppConfig, ...getCachedTools, ...getCustomConfig, ...getEndpointsConfig, diff --git a/api/server/services/Config/loadAsyncEndpoints.js b/api/server/services/Config/loadAsyncEndpoints.js index b88744e9ad..7e7be83c7e 100644 --- a/api/server/services/Config/loadAsyncEndpoints.js +++ b/api/server/services/Config/loadAsyncEndpoints.js @@ -2,6 +2,7 @@ const path = require('path'); const { logger } = require('@librechat/data-schemas'); const { loadServiceKey, isUserProvided } = require('@librechat/api'); const { EModelEndpoint } = require('librechat-data-provider'); +const { getAppConfig } = require('./getAppConfig'); const { config } = require('./EndpointService'); const { openAIApiKey, azureOpenAIApiKey, useAzurePlugins, userProvidedOpenAI, googleKey } = config; @@ -11,6 +12,7 @@ const { openAIApiKey, azureOpenAIApiKey, useAzurePlugins, userProvidedOpenAI, go * @param {Express.Request} req - The request object */ async function loadAsyncEndpoints(req) { + const appConfig = await getAppConfig({ role: req.user?.role }); let serviceKey, googleUserProvides; /** Check if GOOGLE_KEY is provided at all(including 'user_provided') */ @@ -34,7 +36,7 @@ async function loadAsyncEndpoints(req) { const google = serviceKey || isGoogleKeyProvided ? { userProvide: googleUserProvides } : false; - const useAzure = req.app.locals[EModelEndpoint.azureOpenAI]?.plugins; + const useAzure = appConfig[EModelEndpoint.azureOpenAI]?.plugins; const gptPlugins = useAzure || openAIApiKey || azureOpenAIApiKey ? { diff --git a/api/server/services/Config/loadConfigEndpoints.js b/api/server/services/Config/loadConfigEndpoints.js index 2e80fb42be..05ea55c161 100644 --- a/api/server/services/Config/loadConfigEndpoints.js +++ b/api/server/services/Config/loadConfigEndpoints.js @@ -1,6 +1,7 @@ const { EModelEndpoint, extractEnvVariable } = require('librechat-data-provider'); const { isUserProvided, normalizeEndpointName } = require('~/server/utils'); const { getCustomConfig } = require('./getCustomConfig'); +const { getAppConfig } = require('./getAppConfig'); /** * Load config endpoints from the cached configuration object @@ -14,6 +15,8 @@ async function loadConfigEndpoints(req) { return {}; } + const appConfig = await getAppConfig({ role: req.user?.role }); + const { endpoints = {} } = customConfig ?? {}; const endpointsConfig = {}; @@ -53,14 +56,14 @@ async function loadConfigEndpoints(req) { } } - if (req.app.locals[EModelEndpoint.azureOpenAI]) { + if (appConfig[EModelEndpoint.azureOpenAI]) { /** @type {Omit} */ endpointsConfig[EModelEndpoint.azureOpenAI] = { userProvide: false, }; } - if (req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) { + if (appConfig[EModelEndpoint.azureOpenAI]?.assistants) { /** @type {Omit} */ endpointsConfig[EModelEndpoint.azureAssistants] = { userProvide: false, diff --git a/api/server/services/Config/loadConfigModels.js b/api/server/services/Config/loadConfigModels.js index fc255b8c47..f1c9630e9b 100644 --- a/api/server/services/Config/loadConfigModels.js +++ b/api/server/services/Config/loadConfigModels.js @@ -2,6 +2,7 @@ const { EModelEndpoint, extractEnvVariable } = require('librechat-data-provider' const { isUserProvided, normalizeEndpointName } = require('~/server/utils'); const { fetchModels } = require('~/server/services/ModelService'); const { getCustomConfig } = require('./getCustomConfig'); +const { getAppConfig } = require('./getAppConfig'); /** * Load config endpoints from the cached configuration object @@ -15,10 +16,11 @@ async function loadConfigModels(req) { return {}; } + const appConfig = await getAppConfig({ role: req.user?.role }); const { endpoints = {} } = customConfig ?? {}; const modelsConfig = {}; const azureEndpoint = endpoints[EModelEndpoint.azureOpenAI]; - const azureConfig = req.app.locals[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig[EModelEndpoint.azureOpenAI]; const { modelNames } = azureConfig ?? {}; if (modelNames && azureEndpoint) { diff --git a/api/server/services/Endpoints/agents/agent.js b/api/server/services/Endpoints/agents/agent.js index a64ce97e78..a6ea4513cc 100644 --- a/api/server/services/Endpoints/agents/agent.js +++ b/api/server/services/Endpoints/agents/agent.js @@ -16,6 +16,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts'); const { getProviderConfig } = require('~/server/services/Endpoints'); const { processFiles } = require('~/server/services/Files/process'); const { getFiles, getToolFilesByIds } = require('~/models/File'); +const { getAppConfig } = require('~/server/services/Config'); const { getConvoFiles } = require('~/models/Conversation'); const { getModelMaxTokens } = require('~/utils'); @@ -43,6 +44,7 @@ const initializeAgent = async ({ allowedProviders, isInitialAgent = false, }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); if ( isAgentsEndpoint(endpointOption?.endpoint) && allowedProviders.size > 0 && @@ -84,10 +86,11 @@ const initializeAgent = async ({ const { attachments, tool_resources } = await primeResources({ req, getFiles, + appConfig, + agentId: agent.id, attachments: currentFiles, tool_resources: agent.tool_resources, requestFileSet: new Set(requestFiles?.map((file) => file.file_id)), - agentId: agent.id, }); const provider = agent.provider; diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index dfe780c414..44002119d6 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -11,9 +11,9 @@ const { createToolEndCallback, getDefaultHandlers, } = require('~/server/controllers/agents/callbacks'); +const { getCustomEndpointConfig, getAppConfig } = require('~/server/services/Config'); const { initializeAgent } = require('~/server/services/Endpoints/agents/agent'); const { getModelsConfig } = require('~/server/controllers/ModelController'); -const { getCustomEndpointConfig } = require('~/server/services/Config'); const { loadAgentTools } = require('~/server/services/ToolService'); const AgentClient = require('~/server/controllers/agents/client'); const { getAgent } = require('~/models/Agent'); @@ -50,6 +50,7 @@ const initializeClient = async ({ req, res, endpointOption }) => { if (!endpointOption) { throw new Error('Endpoint option not provided'); } + const appConfig = await getAppConfig({ role: req.user?.role }); // TODO: use endpointOption to determine options/modelOptions /** @type {Array} */ @@ -90,7 +91,7 @@ const initializeClient = async ({ req, res, endpointOption }) => { const agentConfigs = new Map(); /** @type {Set} */ - const allowedProviders = new Set(req?.app?.locals?.[EModelEndpoint.agents]?.allowedProviders); + const allowedProviders = new Set(appConfig?.[EModelEndpoint.agents]?.allowedProviders); const loadTools = createToolLoader(); /** @type {Array} */ @@ -144,7 +145,7 @@ const initializeClient = async ({ req, res, endpointOption }) => { } } - let endpointConfig = req.app.locals[primaryConfig.endpoint]; + let endpointConfig = appConfig[primaryConfig.endpoint]; if (!isAgentsEndpoint(primaryConfig.endpoint) && !endpointConfig) { try { endpointConfig = await getCustomEndpointConfig(primaryConfig.endpoint); diff --git a/api/server/services/Endpoints/anthropic/initialize.js b/api/server/services/Endpoints/anthropic/initialize.js index 4546fc634c..8986eed34b 100644 --- a/api/server/services/Endpoints/anthropic/initialize.js +++ b/api/server/services/Endpoints/anthropic/initialize.js @@ -2,8 +2,10 @@ const { EModelEndpoint } = require('librechat-data-provider'); const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService'); const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm'); const AnthropicClient = require('~/app/clients/AnthropicClient'); +const { getAppConfig } = require('~/server/services/Config'); const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env; const expiresAt = req.body.key; const isUserProvided = ANTHROPIC_API_KEY === 'user_provided'; @@ -23,7 +25,7 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio let clientOptions = {}; /** @type {undefined | TBaseEndpoint} */ - const anthropicConfig = req.app.locals[EModelEndpoint.anthropic]; + const anthropicConfig = appConfig[EModelEndpoint.anthropic]; if (anthropicConfig) { clientOptions.streamRate = anthropicConfig.streamRate; @@ -31,7 +33,7 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio } /** @type {undefined | TBaseEndpoint} */ - const allConfig = req.app.locals.all; + const allConfig = appConfig.all; if (allConfig) { clientOptions.streamRate = allConfig.streamRate; } diff --git a/api/server/services/Endpoints/azureAssistants/initialize.js b/api/server/services/Endpoints/azureAssistants/initialize.js index e8aaf89e01..200322031b 100644 --- a/api/server/services/Endpoints/azureAssistants/initialize.js +++ b/api/server/services/Endpoints/azureAssistants/initialize.js @@ -7,6 +7,7 @@ const { getUserKeyValues, getUserKeyExpiry, } = require('~/server/services/UserService'); +const { getAppConfig } = require('~/server/services/Config'); const OAIClient = require('~/app/clients/OpenAIClient'); class Files { @@ -48,6 +49,7 @@ class Files { } const initializeClient = async ({ req, res, version, endpointOption, initAppClient = false }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const { PROXY, OPENAI_ORGANIZATION, AZURE_ASSISTANTS_API_KEY, AZURE_ASSISTANTS_BASE_URL } = process.env; @@ -81,7 +83,7 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie }; /** @type {TAzureConfig | undefined} */ - const azureConfig = req.app.locals[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig[EModelEndpoint.azureOpenAI]; /** @type {AzureOptions | undefined} */ let azureOptions; diff --git a/api/server/services/Endpoints/bedrock/options.js b/api/server/services/Endpoints/bedrock/options.js index a31d6e10c4..841854532a 100644 --- a/api/server/services/Endpoints/bedrock/options.js +++ b/api/server/services/Endpoints/bedrock/options.js @@ -9,8 +9,10 @@ const { removeNullishValues, } = require('librechat-data-provider'); const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService'); +const { getAppConfig } = require('~/server/services/Config'); const getOptions = async ({ req, overrideModel, endpointOption }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const { BEDROCK_AWS_SECRET_ACCESS_KEY, BEDROCK_AWS_ACCESS_KEY_ID, @@ -50,14 +52,14 @@ const getOptions = async ({ req, overrideModel, endpointOption }) => { let streamRate = Constants.DEFAULT_STREAM_RATE; /** @type {undefined | TBaseEndpoint} */ - const bedrockConfig = req.app.locals[EModelEndpoint.bedrock]; + const bedrockConfig = appConfig[EModelEndpoint.bedrock]; if (bedrockConfig && bedrockConfig.streamRate) { streamRate = bedrockConfig.streamRate; } /** @type {undefined | TBaseEndpoint} */ - const allConfig = req.app.locals.all; + const allConfig = appConfig.all; if (allConfig && allConfig.streamRate) { streamRate = allConfig.streamRate; } diff --git a/api/server/services/Endpoints/custom/initialize.js b/api/server/services/Endpoints/custom/initialize.js index 184cb612d8..a90d3630ad 100644 --- a/api/server/services/Endpoints/custom/initialize.js +++ b/api/server/services/Endpoints/custom/initialize.js @@ -8,7 +8,7 @@ const { const { Providers } = require('@librechat/agents'); const { getOpenAIConfig, createHandleLLMNewToken, resolveHeaders } = require('@librechat/api'); const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService'); -const { getCustomEndpointConfig } = require('~/server/services/Config'); +const { getCustomEndpointConfig, getAppConfig } = require('~/server/services/Config'); const { fetchModels } = require('~/server/services/ModelService'); const OpenAIClient = require('~/app/clients/OpenAIClient'); const { isUserProvided } = require('~/server/utils'); @@ -17,6 +17,7 @@ const getLogStores = require('~/cache/getLogStores'); const { PROXY } = process.env; const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrideEndpoint }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const { key: expiresAt } = req.body; const endpoint = overrideEndpoint ?? req.body.endpoint; @@ -118,7 +119,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid }; /** @type {undefined | TBaseEndpoint} */ - const allConfig = req.app.locals.all; + const allConfig = appConfig.all; if (allConfig) { customOptions.streamRate = allConfig.streamRate; } diff --git a/api/server/services/Endpoints/google/initialize.js b/api/server/services/Endpoints/google/initialize.js index 75a31a8c09..dc83ff9880 100644 --- a/api/server/services/Endpoints/google/initialize.js +++ b/api/server/services/Endpoints/google/initialize.js @@ -2,6 +2,7 @@ const path = require('path'); const { EModelEndpoint, AuthKeys } = require('librechat-data-provider'); const { getGoogleConfig, isEnabled, loadServiceKey } = require('@librechat/api'); const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService'); +const { getAppConfig } = require('~/server/services/Config'); const { GoogleClient } = require('~/app'); const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => { @@ -46,10 +47,11 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio let clientOptions = {}; + const appConfig = await getAppConfig({ role: req.user?.role }); /** @type {undefined | TBaseEndpoint} */ - const allConfig = req.app.locals.all; + const allConfig = appConfig.all; /** @type {undefined | TBaseEndpoint} */ - const googleConfig = req.app.locals[EModelEndpoint.google]; + const googleConfig = appConfig[EModelEndpoint.google]; if (googleConfig) { clientOptions.streamRate = googleConfig.streamRate; diff --git a/api/server/services/Endpoints/google/title.js b/api/server/services/Endpoints/google/title.js index dd8aa7a220..3451984303 100644 --- a/api/server/services/Endpoints/google/title.js +++ b/api/server/services/Endpoints/google/title.js @@ -1,7 +1,8 @@ +const { isEnabled } = require('@librechat/api'); const { EModelEndpoint, CacheKeys, Constants, googleSettings } = require('librechat-data-provider'); +const { getAppConfig } = require('~/server/services/Config'); const getLogStores = require('~/cache/getLogStores'); const initializeClient = require('./initialize'); -const { isEnabled } = require('~/server/utils'); const { saveConvo } = require('~/models'); const addTitle = async (req, { text, response, client }) => { @@ -14,7 +15,8 @@ const addTitle = async (req, { text, response, client }) => { return; } const { GOOGLE_TITLE_MODEL } = process.env ?? {}; - const providerConfig = req.app.locals[EModelEndpoint.google]; + const appConfig = await getAppConfig({ role: req.user?.role }); + const providerConfig = appConfig[EModelEndpoint.google]; let model = providerConfig?.titleModel ?? GOOGLE_TITLE_MODEL ?? diff --git a/api/server/services/Endpoints/openAI/initialize.js b/api/server/services/Endpoints/openAI/initialize.js index 30673ecb54..6ce9410fa5 100644 --- a/api/server/services/Endpoints/openAI/initialize.js +++ b/api/server/services/Endpoints/openAI/initialize.js @@ -8,6 +8,7 @@ const { createHandleLLMNewToken, } = require('@librechat/api'); const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService'); +const { getAppConfig } = require('~/server/services/Config'); const OpenAIClient = require('~/app/clients/OpenAIClient'); const initializeClient = async ({ @@ -18,6 +19,7 @@ const initializeClient = async ({ overrideEndpoint, overrideModel, }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const { PROXY, OPENAI_API_KEY, @@ -64,7 +66,7 @@ const initializeClient = async ({ const isAzureOpenAI = endpoint === EModelEndpoint.azureOpenAI; /** @type {false | TAzureConfig} */ - const azureConfig = isAzureOpenAI && req.app.locals[EModelEndpoint.azureOpenAI]; + const azureConfig = isAzureOpenAI && appConfig[EModelEndpoint.azureOpenAI]; let serverless = false; if (isAzureOpenAI && azureConfig) { const { modelGroupMap, groupMap } = azureConfig; @@ -113,7 +115,7 @@ const initializeClient = async ({ } /** @type {undefined | TBaseEndpoint} */ - const openAIConfig = req.app.locals[EModelEndpoint.openAI]; + const openAIConfig = appConfig[EModelEndpoint.openAI]; if (!isAzureOpenAI && openAIConfig) { clientOptions.streamRate = openAIConfig.streamRate; @@ -121,7 +123,7 @@ const initializeClient = async ({ } /** @type {undefined | TBaseEndpoint} */ - const allConfig = req.app.locals.all; + const allConfig = appConfig.all; if (allConfig) { clientOptions.streamRate = allConfig.streamRate; } diff --git a/api/server/services/Files/Azure/images.js b/api/server/services/Files/Azure/images.js index 80d5e76290..8321c9e707 100644 --- a/api/server/services/Files/Azure/images.js +++ b/api/server/services/Files/Azure/images.js @@ -3,6 +3,7 @@ const path = require('path'); const sharp = require('sharp'); const { logger } = require('@librechat/data-schemas'); const { resizeImageBuffer } = require('../images/resize'); +const { getAppConfig } = require('~/server/services/Config'); const { updateUser, updateFile } = require('~/models'); const { saveBufferToAzure } = require('./crud'); @@ -30,6 +31,7 @@ async function uploadImageToAzure({ containerName, }) { try { + const appConfig = await getAppConfig({ role: req.user?.role }); const inputFilePath = file.path; const inputBuffer = await fs.promises.readFile(inputFilePath); const { @@ -41,12 +43,12 @@ async function uploadImageToAzure({ const userId = req.user.id; let webPBuffer; let fileName = `${file_id}__${path.basename(inputFilePath)}`; - const targetExtension = `.${req.app.locals.imageOutputType}`; + const targetExtension = `.${appConfig.imageOutputType}`; if (extension.toLowerCase() === targetExtension) { webPBuffer = resizedBuffer; } else { - webPBuffer = await sharp(resizedBuffer).toFormat(req.app.locals.imageOutputType).toBuffer(); + webPBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer(); const extRegExp = new RegExp(path.extname(fileName) + '$'); fileName = fileName.replace(extRegExp, targetExtension); if (!path.extname(fileName)) { diff --git a/api/server/services/Files/Code/crud.js b/api/server/services/Files/Code/crud.js index c696eae0c4..4781219fcf 100644 --- a/api/server/services/Files/Code/crud.js +++ b/api/server/services/Files/Code/crud.js @@ -43,8 +43,7 @@ async function getCodeOutputDownloadStream(fileIdentifier, apiKey) { /** * Uploads a file to the Code Environment server. * @param {Object} params - The params object. - * @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` - * representing the user, and an `app.locals.paths` object with an `uploads` path. + * @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` representing the user * @param {import('fs').ReadStream | import('stream').Readable} params.stream - The read stream for the file. * @param {string} params.filename - The name of the file. * @param {string} params.apiKey - The API key for authentication. diff --git a/api/server/services/Files/Code/process.js b/api/server/services/Files/Code/process.js index 420451ab6d..08afd80c07 100644 --- a/api/server/services/Files/Code/process.js +++ b/api/server/services/Files/Code/process.js @@ -15,6 +15,7 @@ const { filterFilesByAgentAccess } = require('~/server/services/Files/permission const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { convertImage } = require('~/server/services/Files/images/convert'); const { createFile, getFiles, updateFile } = require('~/models/File'); +const { getAppConfig } = require('~/server/services/Config'); /** * Process OpenAI image files, convert to target format, save and return file metadata. @@ -38,6 +39,7 @@ const processCodeOutput = async ({ messageId, session_id, }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const currentDate = new Date(); const baseURL = getCodeBaseURL(); const fileExt = path.extname(name); @@ -77,10 +79,10 @@ const processCodeOutput = async ({ filename: name, conversationId, user: req.user.id, - type: `image/${req.app.locals.imageOutputType}`, + type: `image/${appConfig.imageOutputType}`, createdAt: formattedDate, updatedAt: formattedDate, - source: req.app.locals.fileStrategy, + source: appConfig.fileStrategy, context: FileContext.execute_code, }; createFile(file, true); diff --git a/api/server/services/Files/Firebase/images.js b/api/server/services/Files/Firebase/images.js index 8b0866b5d0..e506d35e23 100644 --- a/api/server/services/Files/Firebase/images.js +++ b/api/server/services/Files/Firebase/images.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); const { logger } = require('@librechat/data-schemas'); +const { getAppConfig } = require('~/server/services/Config'); const { resizeImageBuffer } = require('../images/resize'); const { updateUser, updateFile } = require('~/models'); const { saveBufferToFirebase } = require('./crud'); @@ -11,8 +12,7 @@ const { saveBufferToFirebase } = require('./crud'); * resolution. * * @param {Object} params - The params object. - * @param {Express.Request} params.req - The request object from Express. It should have a `user` property with an `id` - * representing the user, and an `app.locals.paths` object with an `imageOutput` path. + * @param {Express.Request} params.req - The request object from Express. It should have a `user` property with an `id` representing the user * @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should * have a `path` property that points to the location of the uploaded file. * @param {EModelEndpoint} params.endpoint - The params object. @@ -26,6 +26,7 @@ const { saveBufferToFirebase } = require('./crud'); * - height: The height of the converted image. */ async function uploadImageToFirebase({ req, file, file_id, endpoint, resolution = 'high' }) { + const appConfig = await getAppConfig({ role: req.user?.role }); const inputFilePath = file.path; const inputBuffer = await fs.promises.readFile(inputFilePath); const { @@ -38,11 +39,11 @@ async function uploadImageToFirebase({ req, file, file_id, endpoint, resolution let webPBuffer; let fileName = `${file_id}__${path.basename(inputFilePath)}`; - const targetExtension = `.${req.app.locals.imageOutputType}`; + const targetExtension = `.${appConfig.imageOutputType}`; if (extension.toLowerCase() === targetExtension) { webPBuffer = resizedBuffer; } else { - webPBuffer = await sharp(resizedBuffer).toFormat(req.app.locals.imageOutputType).toBuffer(); + webPBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer(); // Replace or append the correct extension const extRegExp = new RegExp(path.extname(fileName) + '$'); fileName = fileName.replace(extRegExp, targetExtension); diff --git a/api/server/services/Files/Local/crud.js b/api/server/services/Files/Local/crud.js index 455d4e0c4f..1095269383 100644 --- a/api/server/services/Files/Local/crud.js +++ b/api/server/services/Files/Local/crud.js @@ -4,6 +4,7 @@ const axios = require('axios'); const { logger } = require('@librechat/data-schemas'); const { EModelEndpoint } = require('librechat-data-provider'); const { generateShortLivedToken } = require('~/server/services/AuthService'); +const { getAppConfig } = require('~/server/services/Config'); const { getBufferMetadata } = require('~/server/utils'); const paths = require('~/config/paths'); @@ -45,7 +46,8 @@ async function saveLocalFile(file, outputPath, outputFilename) { * @throws Will throw an error if the image saving process fails. */ const saveLocalImage = async (req, file, filename) => { - const imagePath = req.app.locals.paths.imageOutput; + const appConfig = await getAppConfig({ role: req.user?.role }); + const imagePath = appConfig.paths.imageOutput; const outputPath = path.join(imagePath, req.user.id ?? ''); await saveLocalFile(file, outputPath, filename); }; @@ -191,8 +193,7 @@ const unlinkFile = async (filepath) => { * Deletes a file from the filesystem. This function takes a file object, constructs the full path, and * verifies the path's validity before deleting the file. If the path is invalid, an error is thrown. * - * @param {Express.Request} req - The request object from Express. It should have an `app.locals.paths` object with - * a `publicPath` property. + * @param {Express.Request} req - The request object from Express. * @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is * a string representing the path of the file relative to the publicPath. * @@ -201,7 +202,8 @@ const unlinkFile = async (filepath) => { * file path is invalid or if there is an error in deletion. */ const deleteLocalFile = async (req, file) => { - const { publicPath, uploads } = req.app.locals.paths; + const appConfig = await getAppConfig({ role: req.user?.role }); + const { publicPath, uploads } = appConfig.paths; /** Filepath stripped of query parameters (e.g., ?manual=true) */ const cleanFilepath = file.filepath.split('?')[0]; @@ -256,8 +258,7 @@ const deleteLocalFile = async (req, file) => { * Uploads a file to the specified upload directory. * * @param {Object} params - The params object. - * @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` - * representing the user, and an `app.locals.paths` object with an `uploads` path. + * @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` representing the user * @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should * have a `path` property that points to the location of the uploaded file. * @param {string} params.file_id - The file ID. @@ -268,11 +269,12 @@ const deleteLocalFile = async (req, file) => { * - bytes: The size of the file in bytes. */ async function uploadLocalFile({ req, file, file_id }) { + const appConfig = await getAppConfig({ role: req.user?.role }); const inputFilePath = file.path; const inputBuffer = await fs.promises.readFile(inputFilePath); const bytes = Buffer.byteLength(inputBuffer); - const { uploads } = req.app.locals.paths; + const { uploads } = appConfig.paths; const userPath = path.join(uploads, req.user.id); if (!fs.existsSync(userPath)) { @@ -295,8 +297,9 @@ async function uploadLocalFile({ req, file, file_id }) { * @param {string} filepath - The filepath. * @returns {ReadableStream} A readable stream of the file. */ -function getLocalFileStream(req, filepath) { +async function getLocalFileStream(req, filepath) { try { + const appConfig = await getAppConfig({ role: req.user?.role }); if (filepath.includes('/uploads/')) { const basePath = filepath.split('/uploads/')[1]; @@ -305,8 +308,8 @@ function getLocalFileStream(req, filepath) { throw new Error(`Invalid file path: ${filepath}`); } - const fullPath = path.join(req.app.locals.paths.uploads, basePath); - const uploadsDir = req.app.locals.paths.uploads; + const fullPath = path.join(appConfig.paths.uploads, basePath); + const uploadsDir = appConfig.paths.uploads; const rel = path.relative(uploadsDir, fullPath); if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) { @@ -323,8 +326,8 @@ function getLocalFileStream(req, filepath) { throw new Error(`Invalid file path: ${filepath}`); } - const fullPath = path.join(req.app.locals.paths.imageOutput, basePath); - const publicDir = req.app.locals.paths.imageOutput; + const fullPath = path.join(appConfig.paths.imageOutput, basePath); + const publicDir = appConfig.paths.imageOutput; const rel = path.relative(publicDir, fullPath); if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) { diff --git a/api/server/services/Files/Local/images.js b/api/server/services/Files/Local/images.js index ea3af87c70..ae8cba5979 100644 --- a/api/server/services/Files/Local/images.js +++ b/api/server/services/Files/Local/images.js @@ -1,6 +1,7 @@ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); +const { getAppConfig } = require('~/server/services/Config'); const { resizeImageBuffer } = require('../images/resize'); const { updateUser, updateFile } = require('~/models'); @@ -13,8 +14,7 @@ const { updateUser, updateFile } = require('~/models'); * * The original image is deleted after conversion. * @param {Object} params - The params object. - * @param {Object} params.req - The request object from Express. It should have a `user` property with an `id` - * representing the user, and an `app.locals.paths` object with an `imageOutput` path. + * @param {Object} params.req - The request object from Express. It should have a `user` property with an `id` representing the user * @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should * have a `path` property that points to the location of the uploaded file. * @param {string} params.file_id - The file ID. @@ -29,6 +29,7 @@ const { updateUser, updateFile } = require('~/models'); * - height: The height of the converted image. */ async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'high' }) { + const appConfig = await getAppConfig({ role: req.user?.role }); const inputFilePath = file.path; const inputBuffer = await fs.promises.readFile(inputFilePath); const { @@ -38,7 +39,7 @@ async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'hi } = await resizeImageBuffer(inputBuffer, resolution, endpoint); const extension = path.extname(inputFilePath); - const { imageOutput } = req.app.locals.paths; + const { imageOutput } = appConfig.paths; const userPath = path.join(imageOutput, req.user.id); if (!fs.existsSync(userPath)) { @@ -47,7 +48,7 @@ async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'hi const fileName = `${file_id}__${path.basename(inputFilePath)}`; const newPath = path.join(userPath, fileName); - const targetExtension = `.${req.app.locals.imageOutputType}`; + const targetExtension = `.${appConfig.imageOutputType}`; if (extension.toLowerCase() === targetExtension) { const bytes = Buffer.byteLength(resizedBuffer); @@ -57,7 +58,7 @@ async function uploadLocalImage({ req, file, file_id, endpoint, resolution = 'hi } const outputFilePath = newPath.replace(extension, targetExtension); - const data = await sharp(resizedBuffer).toFormat(req.app.locals.imageOutputType).toBuffer(); + const data = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer(); await fs.promises.writeFile(outputFilePath, data); const bytes = Buffer.byteLength(data); const filepath = path.posix.join('/', 'images', req.user.id, path.basename(outputFilePath)); @@ -90,7 +91,8 @@ function encodeImage(imagePath) { * @returns {Promise<[MongoFile, string]>} - A promise that resolves to an array of results from updateFile and encodeImage. */ async function prepareImagesLocal(req, file) { - const { publicPath, imageOutput } = req.app.locals.paths; + const appConfig = await getAppConfig({ role: req.user?.role }); + const { publicPath, imageOutput } = appConfig.paths; const userPath = path.join(imageOutput, req.user.id); if (!fs.existsSync(userPath)) { diff --git a/api/server/services/Files/OpenAI/crud.js b/api/server/services/Files/OpenAI/crud.js index a55485fe4b..9afe217f60 100644 --- a/api/server/services/Files/OpenAI/crud.js +++ b/api/server/services/Files/OpenAI/crud.js @@ -7,8 +7,7 @@ const { logger } = require('~/config'); * Uploads a file that can be used across various OpenAI services. * * @param {Object} params - The params object. - * @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` - * representing the user, and an `app.locals.paths` object with an `imageOutput` path. + * @param {ServerRequest} params.req - The request object from Express. It should have a `user` property with an `id` representing the user * @param {Express.Multer.File} params.file - The file uploaded to the server via multer. * @param {OpenAIClient} params.openai - The initialized OpenAI client. * @returns {Promise} diff --git a/api/server/services/Files/S3/images.js b/api/server/services/Files/S3/images.js index 688d5eb68b..31c3a8a797 100644 --- a/api/server/services/Files/S3/images.js +++ b/api/server/services/Files/S3/images.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); const { logger } = require('@librechat/data-schemas'); +const { getAppConfig } = require('~/server/services/Config'); const { resizeImageBuffer } = require('../images/resize'); const { updateUser, updateFile } = require('~/models'); const { saveBufferToS3 } = require('./crud'); @@ -12,7 +13,7 @@ const defaultBasePath = 'images'; * Resizes, converts, and uploads an image file to S3. * * @param {Object} params - * @param {import('express').Request} params.req - Express request (expects user and app.locals.imageOutputType). + * @param {import('express').Request} params.req - Express request (expects `user` and `appConfig.imageOutputType`). * @param {Express.Multer.File} params.file - File object from Multer. * @param {string} params.file_id - Unique file identifier. * @param {any} params.endpoint - Endpoint identifier used in image processing. @@ -29,6 +30,7 @@ async function uploadImageToS3({ basePath = defaultBasePath, }) { try { + const appConfig = await getAppConfig({ role: req.user?.role }); const inputFilePath = file.path; const inputBuffer = await fs.promises.readFile(inputFilePath); const { @@ -41,14 +43,12 @@ async function uploadImageToS3({ let processedBuffer; let fileName = `${file_id}__${path.basename(inputFilePath)}`; - const targetExtension = `.${req.app.locals.imageOutputType}`; + const targetExtension = `.${appConfig.imageOutputType}`; if (extension.toLowerCase() === targetExtension) { processedBuffer = resizedBuffer; } else { - processedBuffer = await sharp(resizedBuffer) - .toFormat(req.app.locals.imageOutputType) - .toBuffer(); + processedBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer(); fileName = fileName.replace(new RegExp(path.extname(fileName) + '$'), targetExtension); if (!path.extname(fileName)) { fileName += targetExtension; diff --git a/api/server/services/Files/VectorDB/crud.js b/api/server/services/Files/VectorDB/crud.js index 18327d7df1..5e00e71b51 100644 --- a/api/server/services/Files/VectorDB/crud.js +++ b/api/server/services/Files/VectorDB/crud.js @@ -10,8 +10,7 @@ const { generateShortLivedToken } = require('~/server/services/AuthService'); * Deletes a file from the vector database. This function takes a file object, constructs the full path, and * verifies the path's validity before deleting the file. If the path is invalid, an error is thrown. * - * @param {ServerRequest} req - The request object from Express. It should have an `app.locals.paths` object with - * a `publicPath` property. + * @param {ServerRequest} req - The request object from Express. * @param {MongoFile} file - The file object to be deleted. It should have a `filepath` property that is * a string representing the path of the file relative to the publicPath. * @@ -54,8 +53,7 @@ const deleteVectors = async (req, file) => { * Uploads a file to the configured Vector database * * @param {Object} params - The params object. - * @param {Object} params.req - The request object from Express. It should have a `user` property with an `id` - * representing the user, and an `app.locals.paths` object with an `uploads` path. + * @param {Object} params.req - The request object from Express. It should have a `user` property with an `id` representing the user * @param {Express.Multer.File} params.file - The file object, which is part of the request. The file object should * have a `path` property that points to the location of the uploaded file. * @param {string} params.file_id - The file ID. diff --git a/api/server/services/Files/images/convert.js b/api/server/services/Files/images/convert.js index 4e7ab75d44..dbd9199b04 100644 --- a/api/server/services/Files/images/convert.js +++ b/api/server/services/Files/images/convert.js @@ -1,8 +1,9 @@ const fs = require('fs'); const path = require('path'); const sharp = require('sharp'); -const { resizeImageBuffer } = require('./resize'); +const { getAppConfig } = require('~/server/services/Config'); const { getStrategyFunctions } = require('../strategies'); +const { resizeImageBuffer } = require('./resize'); const { logger } = require('~/config'); /** @@ -17,6 +18,7 @@ const { logger } = require('~/config'); */ async function convertImage(req, file, resolution = 'high', basename = '') { try { + const appConfig = await getAppConfig({ role: req.user?.role }); let inputBuffer; let outputBuffer; let extension = path.extname(file.path ?? basename).toLowerCase(); @@ -39,11 +41,11 @@ async function convertImage(req, file, resolution = 'high', basename = '') { } = await resizeImageBuffer(inputBuffer, resolution); // Check if the file is already in target format; if it isn't, convert it: - const targetExtension = `.${req.app.locals.imageOutputType}`; + const targetExtension = `.${appConfig.imageOutputType}`; if (extension === targetExtension) { outputBuffer = resizedBuffer; } else { - outputBuffer = await sharp(resizedBuffer).toFormat(req.app.locals.imageOutputType).toBuffer(); + outputBuffer = await sharp(resizedBuffer).toFormat(appConfig.imageOutputType).toBuffer(); extension = targetExtension; } @@ -51,7 +53,7 @@ async function convertImage(req, file, resolution = 'high', basename = '') { const newFileName = path.basename(file.path ?? basename, path.extname(file.path ?? basename)) + extension; - const { saveBuffer } = getStrategyFunctions(req.app.locals.fileStrategy); + const { saveBuffer } = getStrategyFunctions(appConfig.fileStrategy); const savedFilePath = await saveBuffer({ userId: req.user.id, diff --git a/api/server/services/Files/process.js b/api/server/services/Files/process.js index 819e3b6554..6c17b3f08f 100644 --- a/api/server/services/Files/process.js +++ b/api/server/services/Files/process.js @@ -16,8 +16,8 @@ const { removeNullishValues, isAssistantsEndpoint, } = require('librechat-data-provider'); -const { sanitizeFilename } = require('@librechat/api'); const { EnvVar } = require('@librechat/agents'); +const { sanitizeFilename } = require('@librechat/api'); const { convertImage, resizeAndConvert, @@ -27,11 +27,11 @@ const { addResourceFileId, deleteResourceFileId } = require('~/server/controller const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Agent'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { createFile, updateFileUsage, deleteFiles } = require('~/models/File'); +const { checkCapability, getAppConfig } = require('~/server/services/Config'); const { loadAuthValues } = require('~/server/services/Tools/credentials'); -const { checkCapability } = require('~/server/services/Config'); +const { getFileStrategy } = require('~/server/utils/getFileStrategy'); const { LB_QueueAsyncCall } = require('~/server/utils/queue'); const { getStrategyFunctions } = require('./strategies'); -const { getFileStrategy } = require('~/server/utils/getFileStrategy'); const { determineFileType } = require('~/server/utils'); const { logger } = require('~/config'); @@ -157,6 +157,7 @@ function enqueueDeleteOperation({ req, file, deleteFile, promises, resolvedFileI * @returns {Promise} */ const processDeleteRequest = async ({ req, files }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const resolvedFileIds = []; const deletionMethods = {}; const promises = []; @@ -164,7 +165,7 @@ const processDeleteRequest = async ({ req, files }) => { /** @type {Record} */ const client = { [FileSources.openai]: undefined, [FileSources.azure]: undefined }; const initializeClients = async () => { - if (req.app.locals[EModelEndpoint.assistants]) { + if (appConfig[EModelEndpoint.assistants]) { const openAIClient = await getOpenAIClient({ req, overrideEndpoint: EModelEndpoint.assistants, @@ -172,7 +173,7 @@ const processDeleteRequest = async ({ req, files }) => { client[FileSources.openai] = openAIClient.openai; } - if (!req.app.locals[EModelEndpoint.azureOpenAI]?.assistants) { + if (!appConfig[EModelEndpoint.azureOpenAI]?.assistants) { return; } @@ -320,7 +321,8 @@ const processFileURL = async ({ fileStrategy, userId, URL, fileName, basePath, c */ const processImageFile = async ({ req, res, metadata, returnFile = false }) => { const { file } = req; - const source = getFileStrategy(req.app.locals, { isImage: true }); + const appConfig = await getAppConfig({ role: req.user?.role }); + const source = getFileStrategy(appConfig, { isImage: true }); const { handleImageUpload } = getStrategyFunctions(source); const { file_id, temp_file_id, endpoint } = metadata; @@ -341,7 +343,7 @@ const processImageFile = async ({ req, res, metadata, returnFile = false }) => { filename: file.originalname, context: FileContext.message_attachment, source, - type: `image/${req.app.locals.imageOutputType}`, + type: `image/${appConfig.imageOutputType}`, width, height, }, @@ -366,18 +368,19 @@ const processImageFile = async ({ req, res, metadata, returnFile = false }) => { * @returns {Promise<{ filepath: string, filename: string, source: string, type: string}>} */ const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true }) => { - const source = getFileStrategy(req.app.locals, { isImage: true }); + const appConfig = await getAppConfig({ role: req.user?.role }); + const source = getFileStrategy(appConfig, { isImage: true }); const { saveBuffer } = getStrategyFunctions(source); let { buffer, width, height, bytes, filename, file_id, type } = metadata; if (resize) { file_id = v4(); - type = `image/${req.app.locals.imageOutputType}`; + type = `image/${appConfig.imageOutputType}`; ({ buffer, width, height, bytes } = await resizeAndConvert({ inputBuffer: buffer, - desiredFormat: req.app.locals.imageOutputType, + desiredFormat: appConfig.imageOutputType, })); filename = `${path.basename(req.file.originalname, path.extname(req.file.originalname))}.${ - req.app.locals.imageOutputType + appConfig.imageOutputType }`; } const fileName = `${file_id}-${filename}`; @@ -411,11 +414,12 @@ const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true }) * @returns {Promise} */ const processFileUpload = async ({ req, res, metadata }) => { + const appConfig = await getAppConfig({ role: req.user?.role }); const isAssistantUpload = isAssistantsEndpoint(metadata.endpoint); const assistantSource = metadata.endpoint === EModelEndpoint.azureAssistants ? FileSources.azure : FileSources.openai; // Use the configured file strategy for regular file uploads (not vectordb) - const source = isAssistantUpload ? assistantSource : req.app.locals.fileStrategy; + const source = isAssistantUpload ? assistantSource : appConfig.fileStrategy; const { handleFileUpload } = getStrategyFunctions(source); const { file_id, temp_file_id = null } = metadata; @@ -501,6 +505,7 @@ const processFileUpload = async ({ req, res, metadata }) => { */ const processAgentFileUpload = async ({ req, res, metadata }) => { const { file } = req; + const appConfig = await getAppConfig({ role: req.user?.role }); const { agent_id, tool_resource, file_id, temp_file_id = null } = metadata; if (agent_id && !tool_resource) { throw new Error('No tool resource provided for agent file upload'); @@ -553,7 +558,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { } const { handleFileUpload: uploadOCR } = getStrategyFunctions( - req.app.locals?.ocr?.strategy ?? FileSources.mistral_ocr, + appConfig?.ocr?.strategy ?? FileSources.mistral_ocr, ); const { file_id, temp_file_id = null } = metadata; @@ -564,7 +569,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { images: _i, filename, filepath: ocrFileURL, - } = await uploadOCR({ req, file, loadAuthValues }); + } = await uploadOCR({ req, appConfig, file, loadAuthValues }); const fileInfo = removeNullishValues({ text, @@ -597,7 +602,7 @@ const processAgentFileUpload = async ({ req, res, metadata }) => { // Dual storage pattern for RAG files: Storage + Vector DB let storageResult, embeddingResult; const isImageFile = file.mimetype.startsWith('image'); - const source = getFileStrategy(req.app.locals, { isImage: isImageFile }); + const source = getFileStrategy(appConfig, { isImage: isImageFile }); if (tool_resource === EToolResources.file_search) { // FIRST: Upload to Storage for permanent backup (S3/local/etc.) @@ -752,6 +757,7 @@ const processOpenAIFile = async ({ const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileExt }) => { const currentDate = new Date(); const formattedDate = currentDate.toISOString(); + const appConfig = await getAppConfig({ role: req.user?.role }); const _file = await convertImage(req, buffer, undefined, `${file_id}${fileExt}`); // Create only one file record with the correct information @@ -762,7 +768,7 @@ const processOpenAIImageOutput = async ({ req, buffer, file_id, filename, fileEx type: mime.getType(fileExt), createdAt: formattedDate, updatedAt: formattedDate, - source: getFileStrategy(req.app.locals, { isImage: true }), + source: getFileStrategy(appConfig, { isImage: true }), context: FileContext.assistants_output, file_id, filename, @@ -889,7 +895,7 @@ async function saveBase64Image( url, { req, file_id: _file_id, filename: _filename, endpoint, context, resolution }, ) { - const effectiveResolution = resolution ?? req.app.locals.fileConfig?.imageGeneration ?? 'high'; + const effectiveResolution = resolution ?? appConfig.fileConfig?.imageGeneration ?? 'high'; const file_id = _file_id ?? v4(); let filename = `${file_id}-${_filename}`; const { buffer: inputBuffer, type } = base64ToBuffer(url); @@ -903,7 +909,8 @@ async function saveBase64Image( } const image = await resizeImageBuffer(inputBuffer, effectiveResolution, endpoint); - const source = getFileStrategy(req.app.locals, { isImage: true }); + const appConfig = await getAppConfig({ role: req.user?.role }); + const source = getFileStrategy(appConfig, { isImage: true }); const { saveBuffer } = getStrategyFunctions(source); const filepath = await saveBuffer({ userId: req.user.id, @@ -964,7 +971,8 @@ function filterFile({ req, image, isAvatar }) { throw new Error('No endpoint provided'); } - const fileConfig = mergeFileConfig(req.app.locals.fileConfig); + const appConfig = getAppConfig({ role: req.user?.role }); + const fileConfig = mergeFileConfig(appConfig.fileConfig); const { fileSizeLimit: sizeLimit, supportedMimeTypes } = fileConfig.endpoints[endpoint] ?? fileConfig.endpoints.default; diff --git a/api/server/services/Runs/methods.js b/api/server/services/Runs/methods.js index 167b9cc2ba..c68ed5656a 100644 --- a/api/server/services/Runs/methods.js +++ b/api/server/services/Runs/methods.js @@ -1,6 +1,7 @@ const axios = require('axios'); const { logAxiosError } = require('@librechat/api'); const { EModelEndpoint } = require('librechat-data-provider'); +const { getAppConfig } = require('~/server/services/Config'); /** * @typedef {Object} RetrieveOptions @@ -18,6 +19,7 @@ const { EModelEndpoint } = require('librechat-data-provider'); * @returns {Promise} The data retrieved from the API. */ async function retrieveRun({ thread_id, run_id, timeout, openai }) { + const appConfig = await getAppConfig({ role: openai.req.user?.role }); const { apiKey, baseURL, httpAgent, organization } = openai; let url = `${baseURL}/threads/${thread_id}/runs/${run_id}`; @@ -31,7 +33,7 @@ async function retrieveRun({ thread_id, run_id, timeout, openai }) { } /** @type {TAzureConfig | undefined} */ - const azureConfig = openai.req.app.locals[EModelEndpoint.azureOpenAI]; + const azureConfig = appConfig[EModelEndpoint.azureOpenAI]; if (azureConfig && azureConfig.assistants) { delete headers.Authorization; diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 2f2062e14a..2a797cf47a 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -32,8 +32,8 @@ const { manifestToolMap, toolkits, } = require('~/app/clients/tools'); +const { getEndpointsConfig, getCachedTools, getAppConfig } = require('~/server/services/Config'); const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process'); -const { getEndpointsConfig, getCachedTools } = require('~/server/services/Config'); const { createOnSearchResults } = require('~/server/services/Tools/search'); const { isActionDomainAllowed } = require('~/server/services/domains'); const { recordUsage } = require('~/server/services/Threads'); @@ -202,6 +202,7 @@ async function processRequiredActions(client, requiredActions) { `[required actions] user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`, requiredActions, ); + const appConfig = await getAppConfig({ role: client.req.user?.role }); const toolDefinitions = await getCachedTools({ userId: client.req.user.id, includeGlobal: true }); const seenToolkits = new Set(); const tools = requiredActions @@ -233,7 +234,7 @@ async function processRequiredActions(client, requiredActions) { req: client.req, uploadImageBuffer, openAIApiKey: client.apiKey, - fileStrategy: client.req.app.locals.fileStrategy, + fileStrategy: appConfig.fileStrategy, returnMetadata: true, }, }); @@ -480,12 +481,13 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) return {}; } + const appConfig = await getAppConfig({ role: req.user?.role }); const endpointsConfig = await getEndpointsConfig(req); let enabledCapabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []); /** Edge case: use defined/fallback capabilities when the "agents" endpoint is not enabled */ if (enabledCapabilities.size === 0 && agent.id === Constants.EPHEMERAL_AGENT_ID) { enabledCapabilities = new Set( - req.app?.locals?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, + appConfig?.[EModelEndpoint.agents]?.capabilities ?? defaultAgentCapabilities, ); } const checkCapability = (capability) => { @@ -536,7 +538,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) processFileURL, uploadImageBuffer, returnMetadata: true, - fileStrategy: req.app.locals.fileStrategy, + fileStrategy: appConfig.fileStrategy, [Tools.web_search]: webSearchCallbacks, }, }); diff --git a/api/server/services/initializeMCPs.js b/api/server/services/initializeMCPs.js index 0ca402766b..b76599c713 100644 --- a/api/server/services/initializeMCPs.js +++ b/api/server/services/initializeMCPs.js @@ -1,15 +1,15 @@ const { logger } = require('@librechat/data-schemas'); -const { getCachedTools, setCachedTools } = require('./Config'); +const { getCachedTools, setCachedTools, getAppConfig } = require('./Config'); const { CacheKeys } = require('librechat-data-provider'); const { createMCPManager } = require('~/config'); const { getLogStores } = require('~/cache'); /** * Initialize MCP servers - * @param {import('express').Application} app - Express app instance */ -async function initializeMCPs(app) { - const mcpServers = app.locals.mcpConfig; +async function initializeMCPs() { + const appConfig = await getAppConfig(); + const mcpServers = appConfig.mcpConfig; if (!mcpServers) { return; } @@ -17,7 +17,6 @@ async function initializeMCPs(app) { const mcpManager = await createMCPManager(mcpServers); try { - delete app.locals.mcpConfig; const cachedTools = await getCachedTools(); if (!cachedTools) { diff --git a/api/server/utils/getFileStrategy.js b/api/server/utils/getFileStrategy.js index 6c408e5102..4e3cf671e0 100644 --- a/api/server/utils/getFileStrategy.js +++ b/api/server/utils/getFileStrategy.js @@ -24,11 +24,11 @@ const { FileContext } = require('librechat-data-provider'); * { isAvatar: true } * ) // Returns 'local' */ -function getFileStrategy(appLocals, { isAvatar = false, isImage = false, context = null } = {}) { - // Handle both old (config object) and new (app.locals object) calling patterns - const isAppLocals = appLocals.fileStrategy !== undefined; - const config = isAppLocals ? appLocals.config : appLocals; - const fileStrategy = isAppLocals ? appLocals.fileStrategy : appLocals.fileStrategy; +function getFileStrategy(appConfig, { isAvatar = false, isImage = false, context = null } = {}) { + // Handle both old (config object) and new (`appConfig` object) calling patterns + const isAppConfig = appConfig.fileStrategy !== undefined; + const config = isAppConfig ? appConfig.config : appConfig; + const fileStrategy = isAppConfig ? appConfig.fileStrategy : appConfig.fileStrategy; // Fallback to legacy single strategy if no granular config if (!config?.fileStrategies) { diff --git a/packages/api/src/agents/resources.test.ts b/packages/api/src/agents/resources.test.ts index 3735726b84..0877ab141e 100644 --- a/packages/api/src/agents/resources.test.ts +++ b/packages/api/src/agents/resources.test.ts @@ -1,9 +1,10 @@ import { primeResources } from './resources'; import { logger } from '@librechat/data-schemas'; import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider'; +import type { TAgentsEndpoint, TFile } from 'librechat-data-provider'; import type { Request as ServerRequest } from 'express'; -import type { TFile } from 'librechat-data-provider'; import type { TGetFiles } from './resources'; +import type { AppConfig } from '~/types'; // Mock logger jest.mock('@librechat/data-schemas', () => ({ @@ -14,6 +15,7 @@ jest.mock('@librechat/data-schemas', () => ({ describe('primeResources', () => { let mockReq: ServerRequest; + let mockAppConfig: AppConfig; let mockGetFiles: jest.MockedFunction; let requestFileSet: Set; @@ -32,6 +34,13 @@ describe('primeResources', () => { }, } as unknown as ServerRequest; + // Setup mock appConfig + mockAppConfig = { + [EModelEndpoint.agents]: { + capabilities: [AgentCapabilities.ocr], + } as TAgentsEndpoint, + } as AppConfig; + // Setup mock getFiles function mockGetFiles = jest.fn(); @@ -65,6 +74,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments: undefined, @@ -85,6 +95,7 @@ describe('primeResources', () => { describe('when OCR is disabled', () => { it('should not fetch OCR files even if tool_resources has OCR file_ids', async () => { (mockReq.app as ServerRequest['app']).locals[EModelEndpoint.agents].capabilities = []; + (mockAppConfig[EModelEndpoint.agents] as TAgentsEndpoint).capabilities = []; const tool_resources = { [EToolResources.ocr]: { @@ -94,6 +105,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments: undefined, @@ -129,6 +141,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -158,6 +171,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -189,6 +203,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -220,6 +235,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -250,6 +266,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -291,6 +308,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -342,6 +360,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -399,6 +418,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -450,6 +470,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -492,6 +513,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -560,6 +582,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -618,6 +641,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -671,6 +695,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -724,6 +749,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -764,6 +790,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -838,6 +865,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -888,6 +916,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -906,6 +935,7 @@ describe('primeResources', () => { // The function should now handle rejected attachment promises gracefully const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments, @@ -928,9 +958,11 @@ describe('primeResources', () => { describe('edge cases', () => { it('should handle missing app.locals gracefully', async () => { const reqWithoutLocals = {} as ServerRequest; + const emptyAppConfig = {} as AppConfig; const result = await primeResources({ req: reqWithoutLocals, + appConfig: emptyAppConfig, getFiles: mockGetFiles, requestFileSet, attachments: undefined, @@ -950,6 +982,7 @@ describe('primeResources', () => { it('should handle undefined tool_resources', async () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet, attachments: undefined, @@ -982,6 +1015,7 @@ describe('primeResources', () => { const result = await primeResources({ req: mockReq, + appConfig: mockAppConfig, getFiles: mockGetFiles, requestFileSet: emptyRequestFileSet, attachments, diff --git a/packages/api/src/agents/resources.ts b/packages/api/src/agents/resources.ts index ac33291d21..c490c51aad 100644 --- a/packages/api/src/agents/resources.ts +++ b/packages/api/src/agents/resources.ts @@ -4,6 +4,7 @@ import type { AgentToolResources, TFile, AgentBaseResource } from 'librechat-dat import type { FilterQuery, QueryOptions, ProjectionType } from 'mongoose'; import type { IMongoFile, IUser } from '@librechat/data-schemas'; import type { Request as ServerRequest } from 'express'; +import type { AppConfig } from '~/types/'; /** * Function type for retrieving files from the database @@ -134,7 +135,8 @@ const categorizeFileForToolResources = ({ * 4. Prevents duplicate files across all sources * * @param params - Parameters object - * @param params.req - Express request object containing app configuration + * @param params.req - Express request object + * @param params.appConfig - Application configuration object * @param params.getFiles - Function to retrieve files from database * @param params.requestFileSet - Set of file IDs from the current request * @param params.attachments - Promise resolving to array of attachment files @@ -143,6 +145,7 @@ const categorizeFileForToolResources = ({ */ export const primeResources = async ({ req, + appConfig, getFiles, requestFileSet, attachments: _attachments, @@ -150,6 +153,7 @@ export const primeResources = async ({ agentId, }: { req: ServerRequest & { user?: IUser }; + appConfig: AppConfig; requestFileSet: Set; attachments: Promise> | undefined; tool_resources: AgentToolResources | undefined; @@ -198,7 +202,7 @@ export const primeResources = async ({ } } - const isOCREnabled = (req.app.locals?.[EModelEndpoint.agents]?.capabilities ?? []).includes( + const isOCREnabled = (appConfig?.[EModelEndpoint.agents]?.capabilities ?? []).includes( AgentCapabilities.ocr, ); diff --git a/packages/api/src/endpoints/openai/initialize.ts b/packages/api/src/endpoints/openai/initialize.ts index babfed0bea..4319234476 100644 --- a/packages/api/src/endpoints/openai/initialize.ts +++ b/packages/api/src/endpoints/openai/initialize.ts @@ -1,9 +1,9 @@ import { ErrorTypes, EModelEndpoint, mapModelToAzureConfig } from 'librechat-data-provider'; import type { - UserKeyValues, + InitializeOpenAIOptionsParams, OpenAIOptionsResult, OpenAIConfigOptions, - InitializeOpenAIOptionsParams, + UserKeyValues, } from '~/types'; import { createHandleLLMNewToken } from '~/utils/generators'; import { getAzureCredentials } from '~/utils/azure'; @@ -21,6 +21,7 @@ import { getOpenAIConfig } from './llm'; */ export const initializeOpenAI = async ({ req, + appConfig, overrideModel, endpointOption, overrideEndpoint, @@ -71,7 +72,7 @@ export const initializeOpenAI = async ({ }; const isAzureOpenAI = endpoint === EModelEndpoint.azureOpenAI; - const azureConfig = isAzureOpenAI && req.app.locals[EModelEndpoint.azureOpenAI]; + const azureConfig = isAzureOpenAI && appConfig[EModelEndpoint.azureOpenAI]; if (isAzureOpenAI && azureConfig) { const { modelGroupMap, groupMap } = azureConfig; @@ -142,8 +143,8 @@ export const initializeOpenAI = async ({ const options = getOpenAIConfig(apiKey, finalClientOptions, endpoint); - const openAIConfig = req.app.locals[EModelEndpoint.openAI]; - const allConfig = req.app.locals.all; + const openAIConfig = appConfig[EModelEndpoint.openAI]; + const allConfig = appConfig.all; const azureRate = modelName?.includes('gpt-4') ? 30 : 17; let streamRate: number | undefined; diff --git a/packages/api/src/files/mistral/crud.spec.ts b/packages/api/src/files/mistral/crud.spec.ts index a7db60180f..e80e18358e 100644 --- a/packages/api/src/files/mistral/crud.spec.ts +++ b/packages/api/src/files/mistral/crud.spec.ts @@ -46,7 +46,12 @@ import * as fs from 'fs'; import axios from 'axios'; import type { Request as ExpressRequest } from 'express'; import type { Readable } from 'stream'; -import type { MistralFileUploadResponse, MistralSignedUrlResponse, OCRResult } from '~/types'; +import type { + MistralFileUploadResponse, + MistralSignedUrlResponse, + OCRResult, + AppConfig, +} from '~/types'; import { logger as mockLogger } from '@librechat/data-schemas'; import { uploadDocumentToMistral, @@ -497,18 +502,17 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - // Use environment variable syntax to ensure loadAuthValues is called - apiKey: '${OCR_API_KEY}', - baseURL: '${OCR_BASEURL}', - mistralModel: 'mistral-medium', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + // Use environment variable syntax to ensure loadAuthValues is called + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-medium', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -517,6 +521,7 @@ describe('MistralOCR Service', () => { const result = await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -599,17 +604,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user456' }, - app: { - locals: { - ocr: { - apiKey: '${OCR_API_KEY}', - baseURL: '${OCR_BASEURL}', - mistralModel: 'mistral-medium', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-medium', + }, + } as AppConfig; + const file = { path: '/tmp/upload/image.png', originalname: 'image.png', @@ -618,6 +622,7 @@ describe('MistralOCR Service', () => { const result = await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -698,17 +703,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: '${CUSTOM_API_KEY}', - baseURL: '${CUSTOM_BASEURL}', - mistralModel: '${CUSTOM_MODEL}', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${CUSTOM_API_KEY}', + baseURL: '${CUSTOM_BASEURL}', + mistralModel: '${CUSTOM_MODEL}', + }, + } as AppConfig; + // Set environment variable for model process.env.CUSTOM_MODEL = 'mistral-large'; @@ -720,6 +724,7 @@ describe('MistralOCR Service', () => { const result = await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -790,18 +795,17 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - // Use environment variable syntax to ensure loadAuthValues is called - apiKey: '${INVALID_FORMAT}', // Using valid env var format but with an invalid name - baseURL: '${OCR_BASEURL}', // Using valid env var format - mistralModel: 'mistral-ocr-latest', // Plain string value - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + // Use environment variable syntax to ensure loadAuthValues is called + apiKey: '${INVALID_FORMAT}', // Using valid env var format but with an invalid name + baseURL: '${OCR_BASEURL}', // Using valid env var format + mistralModel: 'mistral-ocr-latest', // Plain string value + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -810,6 +814,7 @@ describe('MistralOCR Service', () => { await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -845,16 +850,15 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: 'OCR_API_KEY', - baseURL: 'OCR_BASEURL', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: 'OCR_API_KEY', + baseURL: 'OCR_BASEURL', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -864,6 +868,7 @@ describe('MistralOCR Service', () => { await expect( uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }), @@ -931,17 +936,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: 'OCR_API_KEY', - baseURL: 'OCR_BASEURL', - mistralModel: 'mistral-ocr-latest', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: 'OCR_API_KEY', + baseURL: 'OCR_BASEURL', + mistralModel: 'mistral-ocr-latest', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'single-page.pdf', @@ -950,6 +954,7 @@ describe('MistralOCR Service', () => { const result = await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1019,18 +1024,17 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - // Direct values that should be used as-is, without variable substitution - apiKey: 'actual-api-key-value', - baseURL: 'https://direct-api-url.mistral.ai/v1', - mistralModel: 'mistral-direct-model', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + // Direct values that should be used as-is, without variable substitution + apiKey: 'actual-api-key-value', + baseURL: 'https://direct-api-url.mistral.ai/v1', + mistralModel: 'mistral-direct-model', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'direct-values.pdf', @@ -1039,6 +1043,7 @@ describe('MistralOCR Service', () => { const result = await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1133,18 +1138,17 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - // Empty string values - should fall back to defaults - apiKey: '', - baseURL: '', - mistralModel: '', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + // Empty string values - should fall back to defaults + apiKey: '', + baseURL: '', + mistralModel: '', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'empty-config.pdf', @@ -1153,6 +1157,7 @@ describe('MistralOCR Service', () => { const result = await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1276,17 +1281,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: '${AZURE_MISTRAL_OCR_API_KEY}', - baseURL: 'https://endpoint.models.ai.azure.com/v1', - mistralModel: 'mistral-ocr-2503', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${AZURE_MISTRAL_OCR_API_KEY}', + baseURL: 'https://endpoint.models.ai.azure.com/v1', + mistralModel: 'mistral-ocr-2503', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -1295,6 +1299,7 @@ describe('MistralOCR Service', () => { await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1360,17 +1365,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user456' }, - app: { - locals: { - ocr: { - apiKey: 'hardcoded-api-key-12345', - baseURL: '${CUSTOM_OCR_BASEURL}', - mistralModel: 'mistral-ocr-latest', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: 'hardcoded-api-key-12345', + baseURL: '${CUSTOM_OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -1379,6 +1383,7 @@ describe('MistralOCR Service', () => { await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1484,17 +1489,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: '${OCR_API_KEY}', - baseURL: '${OCR_BASEURL}', - mistralModel: 'mistral-ocr-latest', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -1503,6 +1507,7 @@ describe('MistralOCR Service', () => { await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1553,17 +1558,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: '${OCR_API_KEY}', - baseURL: '${OCR_BASEURL}', - mistralModel: 'mistral-ocr-latest', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -1573,6 +1577,7 @@ describe('MistralOCR Service', () => { await expect( uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }), @@ -1641,17 +1646,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: '${OCR_API_KEY}', - baseURL: '${OCR_BASEURL}', - mistralModel: 'mistral-ocr-latest', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -1661,6 +1665,7 @@ describe('MistralOCR Service', () => { // Should not throw even if delete fails const result = await uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1701,17 +1706,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: '${OCR_API_KEY}', - baseURL: '${OCR_BASEURL}', - mistralModel: 'mistral-ocr-latest', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -1721,6 +1725,7 @@ describe('MistralOCR Service', () => { await expect( uploadMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }), @@ -1775,17 +1780,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: '${OCR_API_KEY}', - baseURL: '${OCR_BASEURL}', - mistralModel: 'mistral-ocr-latest', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${OCR_API_KEY}', + baseURL: '${OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + } as AppConfig; + const file = { path: '/tmp/upload/azure-file.pdf', originalname: 'azure-document.pdf', @@ -1794,6 +1798,7 @@ describe('MistralOCR Service', () => { const result = await uploadAzureMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1851,17 +1856,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user123' }, - app: { - locals: { - ocr: { - apiKey: '${AZURE_MISTRAL_OCR_API_KEY}', - baseURL: 'https://endpoint.models.ai.azure.com/v1', - mistralModel: 'mistral-ocr-2503', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: '${AZURE_MISTRAL_OCR_API_KEY}', + baseURL: 'https://endpoint.models.ai.azure.com/v1', + mistralModel: 'mistral-ocr-2503', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -1870,6 +1874,7 @@ describe('MistralOCR Service', () => { await uploadAzureMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); @@ -1915,17 +1920,16 @@ describe('MistralOCR Service', () => { const req = { user: { id: 'user456' }, - app: { - locals: { - ocr: { - apiKey: 'hardcoded-api-key-12345', - baseURL: '${CUSTOM_OCR_BASEURL}', - mistralModel: 'mistral-ocr-latest', - }, - }, - }, } as unknown as ExpressRequest; + const appConfig = { + ocr: { + apiKey: 'hardcoded-api-key-12345', + baseURL: '${CUSTOM_OCR_BASEURL}', + mistralModel: 'mistral-ocr-latest', + }, + } as AppConfig; + const file = { path: '/tmp/upload/file.pdf', originalname: 'document.pdf', @@ -1934,6 +1938,7 @@ describe('MistralOCR Service', () => { await uploadAzureMistralOCR({ req, + appConfig, file, loadAuthValues: mockLoadAuthValues, }); diff --git a/packages/api/src/files/mistral/crud.ts b/packages/api/src/files/mistral/crud.ts index 077351a7ed..5f93895472 100644 --- a/packages/api/src/files/mistral/crud.ts +++ b/packages/api/src/files/mistral/crud.ts @@ -17,6 +17,7 @@ import type { MistralOCRUploadResult, MistralOCRError, OCRResultPage, + AppConfig, OCRResult, OCRImage, } from '~/types'; @@ -42,14 +43,10 @@ interface GoogleServiceAccount { /** Helper type for OCR request context */ interface OCRContext { - req: Pick & { + req: Pick & { user?: { id: string }; - app: { - locals?: { - ocr?: TCustomConfig['ocr']; - }; - }; }; + appConfig: AppConfig; file: Express.Multer.File; loadAuthValues: (params: { userId: string; @@ -241,7 +238,7 @@ async function resolveConfigValue( * Loads authentication configuration from OCR config */ async function loadAuthConfig(context: OCRContext): Promise { - const ocrConfig = context.req.app.locals?.ocr; + const ocrConfig = context.appConfig?.ocr; const apiKeyConfig = ocrConfig?.apiKey || ''; const baseURLConfig = ocrConfig?.baseURL || ''; @@ -357,6 +354,7 @@ function createOCRError(error: unknown, baseMessage: string): Error { * @param params - The params object. * @param params.req - The request object from Express. It should have a `user` property with an `id` * representing the user + * @param params.appConfig - Application configuration object * @param params.file - The file object, which is part of the request. The file object should * have a `mimetype` property that tells us the file type * @param params.loadAuthValues - Function to load authentication values @@ -372,7 +370,7 @@ export const uploadMistralOCR = async (context: OCRContext): Promise => { try { const { apiKey, baseURL } = await loadAuthConfig(context); - const model = getModelConfig(context.req.app.locals?.ocr); + const model = getModelConfig(context.appConfig?.ocr); const buffer = fs.readFileSync(context.file.path); const base64 = buffer.toString('base64'); @@ -644,6 +643,7 @@ async function performGoogleVertexOCR({ * @param params - The params object. * @param params.req - The request object from Express. It should have a `user` property with an `id` * representing the user + * @param params.appConfig - Application configuration object * @param params.file - The file object, which is part of the request. The file object should * have a `mimetype` property that tells us the file type * @param params.loadAuthValues - Function to load authentication values @@ -655,7 +655,7 @@ export const uploadGoogleVertexMistralOCR = async ( ): Promise => { try { const { serviceAccount, accessToken } = await loadGoogleAuthConfig(); - const model = getModelConfig(context.req.app.locals?.ocr); + const model = getModelConfig(context.appConfig?.ocr); const buffer = fs.readFileSync(context.file.path); const base64 = buffer.toString('base64'); diff --git a/packages/api/src/types/config.ts b/packages/api/src/types/config.ts new file mode 100644 index 0000000000..086732670a --- /dev/null +++ b/packages/api/src/types/config.ts @@ -0,0 +1,78 @@ +import type { + TEndpoint, + FileSources, + TAzureConfig, + TCustomConfig, + TMemoryConfig, + EModelEndpoint, + TAgentsEndpoint, + TAssistantEndpoint, +} from 'librechat-data-provider'; + +/** + * Application configuration object + * Based on the configuration defined in api/server/services/Config/getAppConfig.js + */ +export interface AppConfig { + /** The main custom configuration */ + config: TCustomConfig; + /** OCR configuration */ + ocr?: TCustomConfig['ocr']; + /** File paths configuration */ + paths: { + uploads: string; + imageOutput: string; + publicPath: string; + [key: string]: string; + }; + /** Memory configuration */ + memory?: TMemoryConfig; + /** Web search configuration */ + webSearch?: TCustomConfig['webSearch']; + /** File storage strategy ('local', 's3', 'firebase', 'azure_blob') */ + fileStrategy: FileSources.local | FileSources.s3 | FileSources.firebase | FileSources.azure_blob; + /** Social login configurations */ + socialLogins: Array; + /** Admin-filtered tools */ + filteredTools?: string[]; + /** Admin-included tools */ + includedTools?: string[]; + /** Image output type configuration */ + imageOutputType: string; + /** Interface configuration */ + interfaceConfig?: TCustomConfig['interface']; + /** Turnstile configuration */ + turnstileConfig?: TCustomConfig['registration']; + /** Balance configuration */ + balance?: TCustomConfig['balance']; + /** MCP server configuration */ + mcpConfig?: TCustomConfig['mcpServers'] | null; + /** File configuration */ + fileConfig?: TCustomConfig['fileConfig']; + /** Secure image links configuration */ + secureImageLinks?: TCustomConfig['secureImageLinks']; + /** Processed model specifications */ + modelSpecs?: TCustomConfig['modelSpecs']; + /** OpenAI endpoint configuration */ + openAI?: TEndpoint; + /** Google endpoint configuration */ + google?: TEndpoint; + /** Bedrock endpoint configuration */ + bedrock?: TEndpoint; + /** Anthropic endpoint configuration */ + anthropic?: TEndpoint; + /** GPT plugins endpoint configuration */ + gptPlugins?: TEndpoint; + /** Azure OpenAI endpoint configuration */ + azureOpenAI?: TAzureConfig; + /** Assistants endpoint configuration */ + assistants?: TAssistantEndpoint; + /** Azure assistants endpoint configuration */ + azureAssistants?: TAssistantEndpoint; + /** Agents endpoint configuration */ + [EModelEndpoint.agents]?: TAgentsEndpoint; + /** Global endpoint configuration */ + all?: TEndpoint; + /** Any additional endpoint configurations */ + [key: string]: unknown; +} diff --git a/packages/api/src/types/index.ts b/packages/api/src/types/index.ts index b38f16e703..11cac41332 100644 --- a/packages/api/src/types/index.ts +++ b/packages/api/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './config'; export * from './azure'; export * from './balance'; export * from './events'; diff --git a/packages/api/src/types/openai.ts b/packages/api/src/types/openai.ts index df9d4b5cbb..2d30340ee1 100644 --- a/packages/api/src/types/openai.ts +++ b/packages/api/src/types/openai.ts @@ -4,6 +4,7 @@ import type { TEndpointOption, TAzureConfig, TEndpoint } from 'librechat-data-pr import type { BindToolsInput } from '@langchain/core/language_models/chat_models'; import type { OpenAIClientOptions, Providers } from '@librechat/agents'; import type { AzureOptions } from './azure'; +import type { AppConfig } from './config'; export type OpenAIParameters = z.infer; @@ -85,6 +86,7 @@ export type CheckUserKeyExpiryFunction = (expiresAt: string, endpoint: string) = */ export interface InitializeOpenAIOptionsParams { req: RequestData; + appConfig: AppConfig; overrideModel?: string; overrideEndpoint?: string; endpointOption: Partial; diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index bb333d736a..0aa7c93bd4 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1213,6 +1213,10 @@ export enum CacheKeys { * Key for the static config namespace. */ STATIC_CONFIG = 'STATIC_CONFIG', + /** + * Key for the app config namespace. + */ + APP_CONFIG = 'APP_CONFIG', /** * Key for accessing Abort Keys */