diff --git a/api/cache/banViolation.js b/api/cache/banViolation.js index 17b23f1c1..3a2d9791b 100644 --- a/api/cache/banViolation.js +++ b/api/cache/banViolation.js @@ -1,7 +1,8 @@ const { logger } = require('@librechat/data-schemas'); +const { isEnabled, math } = require('@librechat/api'); const { ViolationTypes } = require('librechat-data-provider'); -const { isEnabled, math, removePorts } = require('~/server/utils'); const { deleteAllUserSessions } = require('~/models'); +const { removePorts } = require('~/server/utils'); const getLogStores = require('./getLogStores'); const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {}; diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js index 06cadf9f6..0eef7d3fb 100644 --- a/api/cache/getLogStores.js +++ b/api/cache/getLogStores.js @@ -1,7 +1,7 @@ const { Keyv } = require('keyv'); +const { isEnabled, math } = require('@librechat/api'); const { CacheKeys, ViolationTypes, Time } = require('librechat-data-provider'); const { logFile, violationFile } = require('./keyvFiles'); -const { isEnabled, math } = require('~/server/utils'); const keyvRedis = require('./keyvRedis'); const keyvMongo = require('./keyvMongo'); diff --git a/api/models/Conversation.js b/api/models/Conversation.js index 38e2cbb44..698762d43 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -1,4 +1,6 @@ const { logger } = require('@librechat/data-schemas'); +const { createTempChatExpirationDate } = require('@librechat/api'); +const getCustomConfig = require('~/server/services/Config/loadCustomConfig'); const { getMessages, deleteMessages } = require('./Message'); const { Conversation } = require('~/db/models'); @@ -98,10 +100,15 @@ module.exports = { update.conversationId = newConversationId; } - if (req.body.isTemporary) { - const expiredAt = new Date(); - expiredAt.setDate(expiredAt.getDate() + 30); - update.expiredAt = expiredAt; + if (req?.body?.isTemporary) { + try { + const customConfig = await getCustomConfig(); + update.expiredAt = createTempChatExpirationDate(customConfig); + } catch (err) { + logger.error('Error creating temporary chat expiration date:', err); + logger.info(`---\`saveConvo\` context: ${metadata?.context}`); + update.expiredAt = null; + } } else { update.expiredAt = null; } diff --git a/api/models/Message.js b/api/models/Message.js index abd538084..c200c5f4d 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -1,5 +1,7 @@ const { z } = require('zod'); const { logger } = require('@librechat/data-schemas'); +const { createTempChatExpirationDate } = require('@librechat/api'); +const getCustomConfig = require('~/server/services/Config/loadCustomConfig'); const { Message } = require('~/db/models'); const idSchema = z.string().uuid(); @@ -54,9 +56,14 @@ async function saveMessage(req, params, metadata) { }; if (req?.body?.isTemporary) { - const expiredAt = new Date(); - expiredAt.setDate(expiredAt.getDate() + 30); - update.expiredAt = expiredAt; + try { + const customConfig = await getCustomConfig(); + update.expiredAt = createTempChatExpirationDate(customConfig); + } catch (err) { + logger.error('Error creating temporary chat expiration date:', err); + logger.info(`---\`saveMessage\` context: ${metadata?.context}`); + update.expiredAt = null; + } } else { update.expiredAt = null; } diff --git a/api/server/controllers/AuthController.js b/api/server/controllers/AuthController.js index 0f8152de3..3dbb1a2f3 100644 --- a/api/server/controllers/AuthController.js +++ b/api/server/controllers/AuthController.js @@ -1,17 +1,17 @@ const cookies = require('cookie'); const jwt = require('jsonwebtoken'); const openIdClient = require('openid-client'); +const { isEnabled } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); const { - registerUser, - resetPassword, - setAuthTokens, requestPasswordReset, setOpenIDAuthTokens, + resetPassword, + setAuthTokens, + registerUser, } = require('~/server/services/AuthService'); const { findUser, getUserById, deleteAllUserSessions, findSession } = require('~/models'); const { getOpenIdConfig } = require('~/strategies'); -const { isEnabled } = require('~/server/utils'); const registrationController = async (req, res) => { try { diff --git a/api/server/controllers/EditController.js b/api/server/controllers/EditController.js index 574111abf..d24e87ce3 100644 --- a/api/server/controllers/EditController.js +++ b/api/server/controllers/EditController.js @@ -1,3 +1,5 @@ +const { sendEvent } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); const { getResponseSender } = require('librechat-data-provider'); const { handleAbortError, @@ -10,9 +12,8 @@ const { clientRegistry, requestDataMap, } = require('~/server/cleanup'); -const { sendMessage, createOnProgress } = require('~/server/utils'); +const { createOnProgress } = require('~/server/utils'); const { saveMessage } = require('~/models'); -const { logger } = require('~/config'); const EditController = async (req, res, next, initializeClient) => { let { @@ -198,7 +199,7 @@ const EditController = async (req, res, next, initializeClient) => { const finalUserMessage = reqDataContext.userMessage; const finalResponseMessage = { ...response }; - sendMessage(res, { + sendEvent(res, { final: true, conversation, title: conversation.title, diff --git a/api/server/controllers/agents/errors.js b/api/server/controllers/agents/errors.js index fb4de4508..b3bb1cea6 100644 --- a/api/server/controllers/agents/errors.js +++ b/api/server/controllers/agents/errors.js @@ -1,10 +1,10 @@ // errorHandler.js -const { logger } = require('~/config'); -const getLogStores = require('~/cache/getLogStores'); +const { logger } = require('@librechat/data-schemas'); const { CacheKeys, ViolationTypes } = require('librechat-data-provider'); +const { sendResponse } = require('~/server/middleware/error'); const { recordUsage } = require('~/server/services/Threads'); const { getConvo } = require('~/models/Conversation'); -const { sendResponse } = require('~/server/utils'); +const getLogStores = require('~/cache/getLogStores'); /** * @typedef {Object} ErrorHandlerContext @@ -75,7 +75,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch } else if (/Files.*are invalid/.test(error.message)) { const errorMessage = `Files are invalid, or may not have uploaded yet.${ endpoint === 'azureAssistants' - ? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.' + ? " If using Azure OpenAI, files are only available in the region of the assistant's model at the time of upload." : '' }`; return sendResponse(req, res, messageData, errorMessage); diff --git a/api/server/controllers/agents/request.js b/api/server/controllers/agents/request.js index 24b7822c1..5d55991e1 100644 --- a/api/server/controllers/agents/request.js +++ b/api/server/controllers/agents/request.js @@ -1,3 +1,5 @@ +const { sendEvent } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); const { Constants } = require('librechat-data-provider'); const { handleAbortError, @@ -5,9 +7,7 @@ const { cleanupAbortController, } = require('~/server/middleware'); const { disposeClient, clientRegistry, requestDataMap } = require('~/server/cleanup'); -const { sendMessage } = require('~/server/utils'); const { saveMessage } = require('~/models'); -const { logger } = require('~/config'); const AgentController = async (req, res, next, initializeClient, addTitle) => { let { @@ -206,7 +206,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => { // Create a new response object with minimal copies const finalResponse = { ...response }; - sendMessage(res, { + sendEvent(res, { final: true, conversation, title: conversation.title, diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js index 9129a6a1c..b4fe0d901 100644 --- a/api/server/controllers/assistants/chatV1.js +++ b/api/server/controllers/assistants/chatV1.js @@ -1,4 +1,7 @@ const { v4 } = require('uuid'); +const { sleep } = require('@librechat/agents'); +const { sendEvent } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); const { Time, Constants, @@ -19,20 +22,20 @@ const { addThreadMetadata, saveAssistantMessage, } = require('~/server/services/Threads'); -const { sendResponse, sendMessage, sleep, countTokens } = require('~/server/utils'); const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService'); const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts'); 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 { getTransactions } = require('~/models/Transaction'); const { checkBalance } = require('~/models/balanceMethods'); const { getConvo } = require('~/models/Conversation'); const getLogStores = require('~/cache/getLogStores'); +const { countTokens } = require('~/server/utils'); const { getModelMaxTokens } = require('~/utils'); const { getOpenAIClient } = require('./helpers'); -const { logger } = require('~/config'); /** * @route POST / @@ -471,7 +474,7 @@ const chatV1 = async (req, res) => { await Promise.all(promises); const sendInitialResponse = () => { - sendMessage(res, { + sendEvent(res, { sync: true, conversationId, // messages: previousMessages, @@ -587,7 +590,7 @@ const chatV1 = async (req, res) => { iconURL: endpointOption.iconURL, }; - sendMessage(res, { + sendEvent(res, { final: true, conversation, requestMessage: { diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js index 309e5a86c..e1ba93bc2 100644 --- a/api/server/controllers/assistants/chatV2.js +++ b/api/server/controllers/assistants/chatV2.js @@ -1,4 +1,7 @@ const { v4 } = require('uuid'); +const { sleep } = require('@librechat/agents'); +const { sendEvent } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); const { Time, Constants, @@ -22,15 +25,14 @@ const { createErrorHandler } = require('~/server/controllers/assistants/errors') const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { createRun, StreamRunManager } = require('~/server/services/Runs'); const { addTitle } = require('~/server/services/Endpoints/assistants'); -const { sendMessage, sleep, countTokens } = require('~/server/utils'); const { createRunBody } = require('~/server/services/createRunBody'); const { getTransactions } = require('~/models/Transaction'); const { checkBalance } = require('~/models/balanceMethods'); const { getConvo } = require('~/models/Conversation'); const getLogStores = require('~/cache/getLogStores'); +const { countTokens } = require('~/server/utils'); const { getModelMaxTokens } = require('~/utils'); const { getOpenAIClient } = require('./helpers'); -const { logger } = require('~/config'); /** * @route POST / @@ -309,7 +311,7 @@ const chatV2 = async (req, res) => { await Promise.all(promises); const sendInitialResponse = () => { - sendMessage(res, { + sendEvent(res, { sync: true, conversationId, // messages: previousMessages, @@ -432,7 +434,7 @@ const chatV2 = async (req, res) => { iconURL: endpointOption.iconURL, }; - sendMessage(res, { + sendEvent(res, { final: true, conversation, requestMessage: { diff --git a/api/server/controllers/assistants/errors.js b/api/server/controllers/assistants/errors.js index a4b880bf0..182b230fb 100644 --- a/api/server/controllers/assistants/errors.js +++ b/api/server/controllers/assistants/errors.js @@ -1,10 +1,10 @@ // errorHandler.js -const { sendResponse } = require('~/server/utils'); -const { logger } = require('~/config'); -const getLogStores = require('~/cache/getLogStores'); +const { logger } = require('@librechat/data-schemas'); const { CacheKeys, ViolationTypes, ContentTypes } = require('librechat-data-provider'); -const { getConvo } = require('~/models/Conversation'); const { recordUsage, checkMessageGaps } = require('~/server/services/Threads'); +const { sendResponse } = require('~/server/middleware/error'); +const { getConvo } = require('~/models/Conversation'); +const getLogStores = require('~/cache/getLogStores'); /** * @typedef {Object} ErrorHandlerContext @@ -78,7 +78,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch } else if (/Files.*are invalid/.test(error.message)) { const errorMessage = `Files are invalid, or may not have uploaded yet.${ endpoint === 'azureAssistants' - ? ' If using Azure OpenAI, files are only available in the region of the assistant\'s model at the time of upload.' + ? " If using Azure OpenAI, files are only available in the region of the assistant's model at the time of upload." : '' }`; return sendResponse(req, res, messageData, errorMessage); diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index 94d69004b..c5fc48780 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -1,13 +1,13 @@ -// abortMiddleware.js +const { logger } = require('@librechat/data-schemas'); +const { countTokens, isEnabled, sendEvent } = require('@librechat/api'); const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider'); -const { sendMessage, sendError, countTokens, isEnabled } = require('~/server/utils'); const { truncateText, smartTruncateText } = require('~/app/clients/prompts'); const clearPendingReq = require('~/cache/clearPendingReq'); +const { sendError } = require('~/server/middleware/error'); const { spendTokens } = require('~/models/spendTokens'); const abortControllers = require('./abortControllers'); const { saveMessage, getConvo } = require('~/models'); const { abortRun } = require('./abortRun'); -const { logger } = require('~/config'); const abortDataMap = new WeakMap(); @@ -101,7 +101,7 @@ async function abortMessage(req, res) { cleanupAbortController(abortKey); if (res.headersSent && finalEvent) { - return sendMessage(res, finalEvent); + return sendEvent(res, finalEvent); } res.setHeader('Content-Type', 'application/json'); @@ -174,7 +174,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => { * @param {string} responseMessageId */ const onStart = (userMessage, responseMessageId) => { - sendMessage(res, { message: userMessage, created: true }); + sendEvent(res, { message: userMessage, created: true }); const abortKey = userMessage?.conversationId ?? req.user.id; getReqData({ abortKey }); diff --git a/api/server/middleware/abortRun.js b/api/server/middleware/abortRun.js index 01b34aacc..2846c6eef 100644 --- a/api/server/middleware/abortRun.js +++ b/api/server/middleware/abortRun.js @@ -1,11 +1,11 @@ +const { sendEvent } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); const { CacheKeys, RunStatus, isUUID } = require('librechat-data-provider'); const { initializeClient } = require('~/server/services/Endpoints/assistants'); const { checkMessageGaps, recordUsage } = require('~/server/services/Threads'); const { deleteMessages } = require('~/models/Message'); const { getConvo } = require('~/models/Conversation'); const getLogStores = require('~/cache/getLogStores'); -const { sendMessage } = require('~/server/utils'); -const { logger } = require('~/config'); const three_minutes = 1000 * 60 * 3; @@ -34,7 +34,7 @@ async function abortRun(req, res) { const [thread_id, run_id] = runValues.split(':'); if (!run_id) { - logger.warn('[abortRun] Couldn\'t find run for cancel request', { thread_id }); + logger.warn("[abortRun] Couldn't find run for cancel request", { thread_id }); return res.status(204).send({ message: 'Run not found' }); } else if (run_id === 'cancelled') { logger.warn('[abortRun] Run already cancelled', { thread_id }); @@ -93,7 +93,7 @@ async function abortRun(req, res) { }; if (res.headersSent && finalEvent) { - return sendMessage(res, finalEvent); + return sendEvent(res, finalEvent); } res.json(finalEvent); diff --git a/api/server/middleware/denyRequest.js b/api/server/middleware/denyRequest.js index 62efb1aea..20360519c 100644 --- a/api/server/middleware/denyRequest.js +++ b/api/server/middleware/denyRequest.js @@ -1,6 +1,7 @@ const crypto = require('crypto'); +const { sendEvent } = require('@librechat/api'); const { getResponseSender, Constants } = require('librechat-data-provider'); -const { sendMessage, sendError } = require('~/server/utils'); +const { sendError } = require('~/server/middleware/error'); const { saveMessage } = require('~/models'); /** @@ -36,7 +37,7 @@ const denyRequest = async (req, res, errorMessage) => { isCreatedByUser: true, text, }; - sendMessage(res, { message: userMessage, created: true }); + sendEvent(res, { message: userMessage, created: true }); const shouldSaveMessage = _convoId && parentMessageId && parentMessageId !== Constants.NO_PARENT; diff --git a/api/server/utils/streamResponse.js b/api/server/middleware/error.js similarity index 76% rename from api/server/utils/streamResponse.js rename to api/server/middleware/error.js index bb8d63b22..db445c1d4 100644 --- a/api/server/utils/streamResponse.js +++ b/api/server/middleware/error.js @@ -1,31 +1,9 @@ const crypto = require('crypto'); +const { logger } = require('@librechat/data-schemas'); const { parseConvo } = require('librechat-data-provider'); +const { sendEvent, handleError } = require('@librechat/api'); const { saveMessage, getMessages } = require('~/models/Message'); const { getConvo } = require('~/models/Conversation'); -const { logger } = require('~/config'); - -/** - * Sends error data in Server Sent Events format and ends the response. - * @param {object} res - The server response. - * @param {string} message - The error message. - */ -const handleError = (res, message) => { - res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`); - res.end(); -}; - -/** - * Sends message data in Server Sent Events format. - * @param {Express.Response} res - - The server response. - * @param {string | Object} message - The message to be sent. - * @param {'message' | 'error' | 'cancel'} event - [Optional] The type of event. Default is 'message'. - */ -const sendMessage = (res, message, event = 'message') => { - if (typeof message === 'string' && message.length === 0) { - return; - } - res.write(`event: ${event}\ndata: ${JSON.stringify(message)}\n\n`); -}; /** * Processes an error with provided options, saves the error message and sends a corresponding SSE response @@ -91,7 +69,7 @@ const sendError = async (req, res, options, callback) => { convo = parseConvo(errorMessage); } - return sendMessage(res, { + return sendEvent(res, { final: true, requestMessage: query?.[0] ? query[0] : requestMessage, responseMessage: errorMessage, @@ -120,12 +98,10 @@ const sendResponse = (req, res, data, errorMessage) => { if (errorMessage) { return sendError(req, res, { ...data, text: errorMessage }); } - return sendMessage(res, data); + return sendEvent(res, data); }; module.exports = { - sendResponse, - handleError, - sendMessage, sendError, + sendResponse, }; diff --git a/api/server/services/AssistantService.js b/api/server/services/AssistantService.js index 2db0a56b6..5354b2e33 100644 --- a/api/server/services/AssistantService.js +++ b/api/server/services/AssistantService.js @@ -1,4 +1,7 @@ const { klona } = require('klona'); +const { sleep } = require('@librechat/agents'); +const { sendEvent } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); const { StepTypes, RunStatus, @@ -11,11 +14,10 @@ const { } = require('librechat-data-provider'); const { retrieveAndProcessFile } = require('~/server/services/Files/process'); const { processRequiredActions } = require('~/server/services/ToolService'); -const { createOnProgress, sendMessage, sleep } = require('~/server/utils'); const { RunManager, waitForRun } = require('~/server/services/Runs'); const { processMessages } = require('~/server/services/Threads'); +const { createOnProgress } = require('~/server/utils'); const { TextStream } = require('~/app/clients'); -const { logger } = require('~/config'); /** * Sorts, processes, and flattens messages to a single string. @@ -64,7 +66,7 @@ async function createOnTextProgress({ }; logger.debug('Content data:', contentData); - sendMessage(openai.res, contentData); + sendEvent(openai.res, contentData); }; } diff --git a/api/server/services/Config/EndpointService.js b/api/server/services/Config/EndpointService.js index 1f38b70a6..d8277dd67 100644 --- a/api/server/services/Config/EndpointService.js +++ b/api/server/services/Config/EndpointService.js @@ -1,5 +1,6 @@ +const { isUserProvided } = require('@librechat/api'); const { EModelEndpoint } = require('librechat-data-provider'); -const { isUserProvided, generateConfig } = require('~/server/utils'); +const { generateConfig } = require('~/server/utils/handleText'); const { OPENAI_API_KEY: openAIApiKey, diff --git a/api/server/services/Config/loadCustomConfig.js b/api/server/services/Config/loadCustomConfig.js index 18f3a4474..393281daf 100644 --- a/api/server/services/Config/loadCustomConfig.js +++ b/api/server/services/Config/loadCustomConfig.js @@ -1,18 +1,18 @@ const path = require('path'); -const { - CacheKeys, - configSchema, - EImageOutputType, - validateSettingDefinitions, - agentParamSettings, - paramSettings, -} = require('librechat-data-provider'); -const getLogStores = require('~/cache/getLogStores'); -const loadYaml = require('~/utils/loadYaml'); -const { logger } = require('~/config'); const axios = require('axios'); const yaml = require('js-yaml'); const keyBy = require('lodash/keyBy'); +const { loadYaml } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); +const { + CacheKeys, + configSchema, + paramSettings, + EImageOutputType, + agentParamSettings, + validateSettingDefinitions, +} = require('librechat-data-provider'); +const getLogStores = require('~/cache/getLogStores'); const projectRoot = path.resolve(__dirname, '..', '..', '..', '..'); const defaultConfigPath = path.resolve(projectRoot, 'librechat.yaml'); diff --git a/api/server/services/Config/loadCustomConfig.spec.js b/api/server/services/Config/loadCustomConfig.spec.js index ed698e57f..9b905181c 100644 --- a/api/server/services/Config/loadCustomConfig.spec.js +++ b/api/server/services/Config/loadCustomConfig.spec.js @@ -1,6 +1,9 @@ jest.mock('axios'); jest.mock('~/cache/getLogStores'); -jest.mock('~/utils/loadYaml'); +jest.mock('@librechat/api', () => ({ + ...jest.requireActual('@librechat/api'), + loadYaml: jest.fn(), +})); jest.mock('librechat-data-provider', () => { const actual = jest.requireActual('librechat-data-provider'); return { @@ -30,11 +33,22 @@ jest.mock('librechat-data-provider', () => { }; }); +jest.mock('@librechat/data-schemas', () => { + return { + logger: { + info: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + }, + }; +}); + const axios = require('axios'); +const { loadYaml } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); const loadCustomConfig = require('./loadCustomConfig'); const getLogStores = require('~/cache/getLogStores'); -const loadYaml = require('~/utils/loadYaml'); -const { logger } = require('~/config'); describe('loadCustomConfig', () => { const mockSet = jest.fn(); diff --git a/api/server/services/Runs/StreamRunManager.js b/api/server/services/Runs/StreamRunManager.js index 4bab7326b..4f6994e0c 100644 --- a/api/server/services/Runs/StreamRunManager.js +++ b/api/server/services/Runs/StreamRunManager.js @@ -1,3 +1,6 @@ +const { sleep } = require('@librechat/agents'); +const { sendEvent } = require('@librechat/api'); +const { logger } = require('@librechat/data-schemas'); const { Constants, StepTypes, @@ -8,9 +11,8 @@ const { } = require('librechat-data-provider'); const { retrieveAndProcessFile } = require('~/server/services/Files/process'); const { processRequiredActions } = require('~/server/services/ToolService'); -const { createOnProgress, sendMessage, sleep } = require('~/server/utils'); const { processMessages } = require('~/server/services/Threads'); -const { logger } = require('~/config'); +const { createOnProgress } = require('~/server/utils'); /** * Implements the StreamRunManager functionality for managing the streaming @@ -126,7 +128,7 @@ class StreamRunManager { conversationId: this.finalMessage.conversationId, }; - sendMessage(this.res, contentData); + sendEvent(this.res, contentData); } /* <------------------ Misc. Helpers ------------------> */ @@ -302,7 +304,7 @@ class StreamRunManager { for (const d of delta[key]) { if (typeof d === 'object' && !Object.prototype.hasOwnProperty.call(d, 'index')) { - logger.warn('Expected an object with an \'index\' for array updates but got:', d); + logger.warn("Expected an object with an 'index' for array updates but got:", d); continue; } diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js index 680da5da4..36671c44f 100644 --- a/api/server/utils/handleText.js +++ b/api/server/utils/handleText.js @@ -7,9 +7,9 @@ const { defaultAssistantsVersion, defaultAgentCapabilities, } = require('librechat-data-provider'); +const { sendEvent } = require('@librechat/api'); const { Providers } = require('@librechat/agents'); const partialRight = require('lodash/partialRight'); -const { sendMessage } = require('./streamResponse'); /** Helper function to escape special characters in regex * @param {string} string - The string to escape. @@ -37,7 +37,7 @@ const createOnProgress = ( basePayload.text = basePayload.text + chunk; const payload = Object.assign({}, basePayload, rest); - sendMessage(res, payload); + sendEvent(res, payload); if (_onProgress) { _onProgress(payload); } @@ -50,7 +50,7 @@ const createOnProgress = ( const sendIntermediateMessage = (res, payload, extraTokens = '') => { basePayload.text = basePayload.text + extraTokens; const message = Object.assign({}, basePayload, payload); - sendMessage(res, message); + sendEvent(res, message); if (i === 0) { basePayload.initial = false; } diff --git a/api/server/utils/index.js b/api/server/utils/index.js index 2661ff75e..2672f4f2e 100644 --- a/api/server/utils/index.js +++ b/api/server/utils/index.js @@ -1,11 +1,9 @@ -const streamResponse = require('./streamResponse'); const removePorts = require('./removePorts'); const countTokens = require('./countTokens'); const handleText = require('./handleText'); const sendEmail = require('./sendEmail'); const queue = require('./queue'); const files = require('./files'); -const math = require('./math'); /** * Check if email configuration is set @@ -28,7 +26,6 @@ function checkEmailConfig() { } module.exports = { - ...streamResponse, checkEmailConfig, ...handleText, countTokens, @@ -36,5 +33,4 @@ module.exports = { sendEmail, ...files, ...queue, - math, }; diff --git a/api/utils/index.js b/api/utils/index.js index 50b8c46d9..b80c9b0c3 100644 --- a/api/utils/index.js +++ b/api/utils/index.js @@ -1,11 +1,9 @@ -const loadYaml = require('./loadYaml'); const tokenHelpers = require('./tokens'); const deriveBaseURL = require('./deriveBaseURL'); const extractBaseURL = require('./extractBaseURL'); const findMessageContent = require('./findMessageContent'); module.exports = { - loadYaml, deriveBaseURL, extractBaseURL, ...tokenHelpers, diff --git a/api/utils/loadYaml.js b/api/utils/loadYaml.js deleted file mode 100644 index 50e5d23ec..000000000 --- a/api/utils/loadYaml.js +++ /dev/null @@ -1,13 +0,0 @@ -const fs = require('fs'); -const yaml = require('js-yaml'); - -function loadYaml(filepath) { - try { - let fileContents = fs.readFileSync(filepath, 'utf8'); - return yaml.load(fileContents); - } catch (e) { - return e; - } -} - -module.exports = loadYaml; diff --git a/librechat.example.yaml b/librechat.example.yaml index de28dcc32..0fb7975cb 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -73,6 +73,8 @@ interface: bookmarks: true multiConvo: true agents: true + # Temporary chat retention period in hours (default: 720, min: 1, max: 8760) + # temporaryChatRetention: 1 # Example Cloudflare turnstile (optional) #turnstile: diff --git a/package-lock.json b/package-lock.json index 989883c0e..e3a47a4fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46573,6 +46573,7 @@ "diff": "^7.0.0", "eventsource": "^3.0.2", "express": "^4.21.2", + "js-yaml": "^4.1.0", "keyv": "^5.3.2", "librechat-data-provider": "*", "node-fetch": "2.7.0", diff --git a/packages/api/package.json b/packages/api/package.json index 4aaf0f793..ed2b70965 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -76,6 +76,7 @@ "diff": "^7.0.0", "eventsource": "^3.0.2", "express": "^4.21.2", + "js-yaml": "^4.1.0", "keyv": "^5.3.2", "librechat-data-provider": "*", "node-fetch": "2.7.0", diff --git a/packages/api/src/utils/events.ts b/packages/api/src/utils/events.ts index 76d1361d9..20c958399 100644 --- a/packages/api/src/utils/events.ts +++ b/packages/api/src/utils/events.ts @@ -14,3 +14,13 @@ export function sendEvent(res: ServerResponse, event: ServerSentEvent): void { } res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`); } + +/** + * Sends error data in Server Sent Events format and ends the response. + * @param res - The server response. + * @param message - The error message. + */ +export function handleError(res: ServerResponse, message: string): void { + res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`); + res.end(); +} diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts index 807686ca4..dea9472b0 100644 --- a/packages/api/src/utils/index.ts +++ b/packages/api/src/utils/index.ts @@ -6,5 +6,8 @@ export * from './events'; export * from './files'; export * from './generators'; export * from './llm'; +export * from './math'; export * from './openid'; +export * from './tempChatRetention'; export { default as Tokenizer } from './tokenizer'; +export * from './yaml'; diff --git a/api/server/utils/math.js b/packages/api/src/utils/math.ts similarity index 68% rename from api/server/utils/math.js rename to packages/api/src/utils/math.ts index 3cd092989..08ae04f7e 100644 --- a/api/server/utils/math.js +++ b/packages/api/src/utils/math.ts @@ -5,14 +5,14 @@ * If the input is not a string or contains invalid characters, an error is thrown. * If the evaluated result is not a number, an error is thrown. * - * @param {string|number} str - The mathematical expression to evaluate, or a number. - * @param {number} [fallbackValue] - The default value to return if the input is not a string or number, or if the evaluated result is not a number. + * @param str - The mathematical expression to evaluate, or a number. + * @param fallbackValue - The default value to return if the input is not a string or number, or if the evaluated result is not a number. * - * @returns {number} The result of the evaluated expression or the input number. + * @returns The result of the evaluated expression or the input number. * - * @throws {Error} Throws an error if the input is not a string or number, contains invalid characters, or does not evaluate to a number. + * @throws Throws an error if the input is not a string or number, contains invalid characters, or does not evaluate to a number. */ -function math(str, fallbackValue) { +export function math(str: string | number, fallbackValue?: number): number { const fallback = typeof fallbackValue !== 'undefined' && typeof fallbackValue === 'number'; if (typeof str !== 'string' && typeof str === 'number') { return str; @@ -43,5 +43,3 @@ function math(str, fallbackValue) { return value; } - -module.exports = math; diff --git a/packages/api/src/utils/tempChatRetention.spec.ts b/packages/api/src/utils/tempChatRetention.spec.ts new file mode 100644 index 000000000..b0166e952 --- /dev/null +++ b/packages/api/src/utils/tempChatRetention.spec.ts @@ -0,0 +1,133 @@ +import { + MIN_RETENTION_HOURS, + MAX_RETENTION_HOURS, + DEFAULT_RETENTION_HOURS, + getTempChatRetentionHours, + createTempChatExpirationDate, +} from './tempChatRetention'; +import type { TCustomConfig } from 'librechat-data-provider'; + +describe('tempChatRetention', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + delete process.env.TEMP_CHAT_RETENTION_HOURS; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + describe('getTempChatRetentionHours', () => { + it('should return default retention hours when no config or env var is set', () => { + const result = getTempChatRetentionHours(); + expect(result).toBe(DEFAULT_RETENTION_HOURS); + }); + + it('should use environment variable when set', () => { + process.env.TEMP_CHAT_RETENTION_HOURS = '48'; + const result = getTempChatRetentionHours(); + expect(result).toBe(48); + }); + + it('should use config value when set', () => { + const config: Partial = { + interface: { + temporaryChatRetention: 12, + }, + }; + const result = getTempChatRetentionHours(config); + expect(result).toBe(12); + }); + + it('should prioritize config over environment variable', () => { + process.env.TEMP_CHAT_RETENTION_HOURS = '48'; + const config: Partial = { + interface: { + temporaryChatRetention: 12, + }, + }; + const result = getTempChatRetentionHours(config); + expect(result).toBe(12); + }); + + it('should enforce minimum retention period', () => { + const config: Partial = { + interface: { + temporaryChatRetention: 0, + }, + }; + const result = getTempChatRetentionHours(config); + expect(result).toBe(MIN_RETENTION_HOURS); + }); + + it('should enforce maximum retention period', () => { + const config: Partial = { + interface: { + temporaryChatRetention: 10000, + }, + }; + const result = getTempChatRetentionHours(config); + expect(result).toBe(MAX_RETENTION_HOURS); + }); + + it('should handle invalid environment variable', () => { + process.env.TEMP_CHAT_RETENTION_HOURS = 'invalid'; + const result = getTempChatRetentionHours(); + expect(result).toBe(DEFAULT_RETENTION_HOURS); + }); + + it('should handle invalid config value', () => { + const config: Partial = { + interface: { + temporaryChatRetention: 'invalid' as unknown as number, + }, + }; + const result = getTempChatRetentionHours(config); + expect(result).toBe(DEFAULT_RETENTION_HOURS); + }); + }); + + describe('createTempChatExpirationDate', () => { + it('should create expiration date with default retention period', () => { + const result = createTempChatExpirationDate(); + + const expectedDate = new Date(); + expectedDate.setHours(expectedDate.getHours() + DEFAULT_RETENTION_HOURS); + + // Allow for small time differences in test execution + const timeDiff = Math.abs(result.getTime() - expectedDate.getTime()); + expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference + }); + + it('should create expiration date with custom retention period', () => { + const config: Partial = { + interface: { + temporaryChatRetention: 12, + }, + }; + + const result = createTempChatExpirationDate(config); + + const expectedDate = new Date(); + expectedDate.setHours(expectedDate.getHours() + 12); + + // Allow for small time differences in test execution + const timeDiff = Math.abs(result.getTime() - expectedDate.getTime()); + expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference + }); + + it('should return a Date object', () => { + const result = createTempChatExpirationDate(); + expect(result).toBeInstanceOf(Date); + }); + + it('should return a future date', () => { + const now = new Date(); + const result = createTempChatExpirationDate(); + expect(result.getTime()).toBeGreaterThan(now.getTime()); + }); + }); +}); diff --git a/packages/api/src/utils/tempChatRetention.ts b/packages/api/src/utils/tempChatRetention.ts new file mode 100644 index 000000000..6683b4c6a --- /dev/null +++ b/packages/api/src/utils/tempChatRetention.ts @@ -0,0 +1,77 @@ +import { logger } from '@librechat/data-schemas'; +import type { TCustomConfig } from 'librechat-data-provider'; + +/** + * Default retention period for temporary chats in hours + */ +export const DEFAULT_RETENTION_HOURS = 24 * 30; // 30 days + +/** + * Minimum allowed retention period in hours + */ +export const MIN_RETENTION_HOURS = 1; + +/** + * Maximum allowed retention period in hours (1 year = 8760 hours) + */ +export const MAX_RETENTION_HOURS = 8760; + +/** + * Gets the temporary chat retention period from environment variables or config + * @param config - The custom configuration object + * @returns The retention period in hours + */ +export function getTempChatRetentionHours(config?: Partial | null): number { + let retentionHours = DEFAULT_RETENTION_HOURS; + + // Check environment variable first + if (process.env.TEMP_CHAT_RETENTION_HOURS) { + const envValue = parseInt(process.env.TEMP_CHAT_RETENTION_HOURS, 10); + if (!isNaN(envValue)) { + retentionHours = envValue; + } else { + logger.warn( + `Invalid TEMP_CHAT_RETENTION_HOURS environment variable: ${process.env.TEMP_CHAT_RETENTION_HOURS}. Using default: ${DEFAULT_RETENTION_HOURS} hours.`, + ); + } + } + + // Check config file (takes precedence over environment variable) + if (config?.interface?.temporaryChatRetention !== undefined) { + const configValue = config.interface.temporaryChatRetention; + if (typeof configValue === 'number' && !isNaN(configValue)) { + retentionHours = configValue; + } else { + logger.warn( + `Invalid temporaryChatRetention in config: ${configValue}. Using ${retentionHours} hours.`, + ); + } + } + + // Validate the retention period + if (retentionHours < MIN_RETENTION_HOURS) { + logger.warn( + `Temporary chat retention period ${retentionHours} is below minimum ${MIN_RETENTION_HOURS} hours. Using minimum value.`, + ); + retentionHours = MIN_RETENTION_HOURS; + } else if (retentionHours > MAX_RETENTION_HOURS) { + logger.warn( + `Temporary chat retention period ${retentionHours} exceeds maximum ${MAX_RETENTION_HOURS} hours. Using maximum value.`, + ); + retentionHours = MAX_RETENTION_HOURS; + } + + return retentionHours; +} + +/** + * Creates an expiration date for temporary chats + * @param config - The custom configuration object + * @returns The expiration date + */ +export function createTempChatExpirationDate(config?: Partial): Date { + const retentionHours = getTempChatRetentionHours(config); + const expiredAt = new Date(); + expiredAt.setHours(expiredAt.getHours() + retentionHours); + return expiredAt; +} diff --git a/packages/api/src/utils/yaml.ts b/packages/api/src/utils/yaml.ts new file mode 100644 index 000000000..50ea0bd4d --- /dev/null +++ b/packages/api/src/utils/yaml.ts @@ -0,0 +1,11 @@ +import fs from 'fs'; +import yaml from 'js-yaml'; + +export function loadYaml(filepath: string) { + try { + const fileContents = fs.readFileSync(filepath, 'utf8'); + return yaml.load(fileContents); + } catch (e) { + return e; + } +} diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 4c896d023..004ed572c 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -510,6 +510,7 @@ export const intefaceSchema = z prompts: z.boolean().optional(), agents: z.boolean().optional(), temporaryChat: z.boolean().optional(), + temporaryChatRetention: z.number().min(1).max(8760).optional(), runCode: z.boolean().optional(), webSearch: z.boolean().optional(), }) diff --git a/packages/data-schemas/src/methods/session.ts b/packages/data-schemas/src/methods/session.ts index c5af51e93..30700bc26 100644 --- a/packages/data-schemas/src/methods/session.ts +++ b/packages/data-schemas/src/methods/session.ts @@ -13,14 +13,10 @@ export class SessionError extends Error { } const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; -const expires = REFRESH_TOKEN_EXPIRY - ? eval(REFRESH_TOKEN_EXPIRY) - : 1000 * 60 * 60 * 24 * 7; // 7 days default +const expires = REFRESH_TOKEN_EXPIRY ? eval(REFRESH_TOKEN_EXPIRY) : 1000 * 60 * 60 * 24 * 7; // 7 days default // Factory function that takes mongoose instance and returns the methods export function createSessionMethods(mongoose: typeof import('mongoose')) { - const Session = mongoose.models.Session; - /** * Creates a new session for a user */ @@ -33,13 +29,14 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) { } try { - const session = new Session({ + const Session = mongoose.models.Session; + const currentSession = new Session({ user: userId, expiration: options.expiration || new Date(Date.now() + expires), }); - const refreshToken = await generateRefreshToken(session); + const refreshToken = await generateRefreshToken(currentSession); - return { session, refreshToken }; + return { session: currentSession, refreshToken }; } catch (error) { logger.error('[createSession] Error creating session:', error); throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED'); @@ -54,6 +51,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) { options: t.SessionQueryOptions = { lean: true }, ): Promise { try { + const Session = mongoose.models.Session; const query: Record = {}; if (!params.refreshToken && !params.userId && !params.sessionId) { @@ -109,6 +107,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) { newExpiration?: Date, ): Promise { try { + const Session = mongoose.models.Session; const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session; if (!sessionDoc) { @@ -128,6 +127,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) { */ async function deleteSession(params: t.DeleteSessionParams): Promise<{ deletedCount?: number }> { try { + const Session = mongoose.models.Session; if (!params.refreshToken && !params.sessionId) { throw new SessionError( 'Either refreshToken or sessionId is required', @@ -166,6 +166,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) { options: t.DeleteAllSessionsOptions = {}, ): Promise<{ deletedCount?: number }> { try { + const Session = mongoose.models.Session; if (!userId) { throw new SessionError('User ID is required', 'INVALID_USER_ID'); } @@ -237,6 +238,7 @@ export function createSessionMethods(mongoose: typeof import('mongoose')) { */ async function countActiveSessions(userId: string): Promise { try { + const Session = mongoose.models.Session; if (!userId) { throw new SessionError('User ID is required', 'INVALID_USER_ID'); }