LibreChat/api/server/routes/messages.js
Danny Avila 656e1abaea
🪦 refactor: Remove Legacy Code (#10533)
* 🗑️ chore: Remove unused Legacy Provider clients and related helpers

* Deleted OpenAIClient and GoogleClient files along with their associated tests.
* Removed references to these clients in the clients index file.
* Cleaned up typedefs by removing the OpenAISpecClient export.
* Updated chat controllers to use the OpenAI SDK directly instead of the removed client classes.

* chore/remove-openapi-specs

* 🗑️ chore: Remove unused mergeSort and misc utility functions

* Deleted mergeSort.js and misc.js files as they are no longer needed.
* Removed references to cleanUpPrimaryKeyValue in messages.js and adjusted related logic.
* Updated mongoMeili.ts to eliminate local implementations of removed functions.

* chore: remove legacy endpoints

* chore: remove all plugins endpoint related code

* chore: remove unused prompt handling code and clean up imports

* Deleted handleInputs.js and instructions.js files as they are no longer needed.
* Removed references to these files in the prompts index.js.
* Updated docker-compose.yml to simplify reverse proxy configuration.

* chore: remove unused LightningIcon import from Icons.tsx

* chore: clean up translation.json by removing deprecated and unused keys

* chore: update Jest configuration and remove unused mock file

    * Simplified the setupFiles array in jest.config.js by removing the fetchEventSource mock.
    * Deleted the fetchEventSource.js mock file as it is no longer needed.

* fix: simplify endpoint type check in Landing and ConversationStarters components

    * Updated the endpoint type check to use strict equality for better clarity and performance.
    * Ensured consistency in the handling of the azureOpenAI endpoint across both components.

* chore: remove unused dependencies from package.json and package-lock.json

* chore: remove legacy EditController, associated routes and imports

* chore: update banResponse logic to refine request handling for banned users

* chore: remove unused validateEndpoint middleware and its references

* chore: remove unused 'res' parameter from initializeClient in multiple endpoint files

* chore: remove unused 'isSmallScreen' prop from BookmarkNav and NewChat components; clean up imports in ArchivedChatsTable and useSetIndexOptions hooks; enhance localization in PromptVersions

* chore: remove unused import of Constants and TMessage from MobileNav; retain only necessary QueryKeys import

* chore: remove unused TResPlugin type and related references; clean up imports in types and schemas
2025-12-11 16:36:12 -05:00

325 lines
10 KiB
JavaScript

const express = require('express');
const { logger } = require('@librechat/data-schemas');
const { ContentTypes } = require('librechat-data-provider');
const { unescapeLaTeX, countTokens } = require('@librechat/api');
const {
saveConvo,
getMessage,
saveMessage,
getMessages,
updateMessage,
deleteMessages,
} = require('~/models');
const { findAllArtifacts, replaceArtifactContent } = require('~/server/services/Artifacts/update');
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
const { getConvosQueried } = require('~/models/Conversation');
const { Message } = require('~/db/models');
const router = express.Router();
router.use(requireJwtAuth);
router.get('/', async (req, res) => {
try {
const user = req.user.id ?? '';
const {
cursor = null,
sortBy = 'createdAt',
sortDirection = 'desc',
pageSize: pageSizeRaw,
conversationId,
messageId,
search,
} = req.query;
const pageSize = parseInt(pageSizeRaw, 10) || 25;
let response;
const sortField = ['endpoint', 'createdAt', 'updatedAt'].includes(sortBy)
? sortBy
: 'createdAt';
const sortOrder = sortDirection === 'asc' ? 1 : -1;
if (conversationId && messageId) {
const message = await Message.findOne({
conversationId,
messageId,
user: user,
}).lean();
response = { messages: message ? [message] : [], nextCursor: null };
} else if (conversationId) {
const filter = { conversationId, user: user };
if (cursor) {
filter[sortField] = sortOrder === 1 ? { $gt: cursor } : { $lt: cursor };
}
const messages = await Message.find(filter)
.sort({ [sortField]: sortOrder })
.limit(pageSize + 1)
.lean();
const nextCursor = messages.length > pageSize ? messages.pop()[sortField] : null;
response = { messages, nextCursor };
} else if (search) {
const searchResults = await Message.meiliSearch(search, { filter: `user = "${user}"` }, true);
const messages = searchResults.hits || [];
const result = await getConvosQueried(req.user.id, messages, cursor);
const messageIds = [];
const cleanedMessages = [];
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
if (result.convoMap[message.conversationId]) {
messageIds.push(message.messageId);
cleanedMessages.push(message);
}
}
const dbMessages = await getMessages({
user,
messageId: { $in: messageIds },
});
const dbMessageMap = {};
for (const dbMessage of dbMessages) {
dbMessageMap[dbMessage.messageId] = dbMessage;
}
const activeMessages = [];
for (const message of cleanedMessages) {
const convo = result.convoMap[message.conversationId];
const dbMessage = dbMessageMap[message.messageId];
activeMessages.push({
...message,
title: convo.title,
conversationId: message.conversationId,
model: convo.model,
isCreatedByUser: dbMessage?.isCreatedByUser,
endpoint: dbMessage?.endpoint,
iconURL: dbMessage?.iconURL,
});
}
response = { messages: activeMessages, nextCursor: null };
} else {
response = { messages: [], nextCursor: null };
}
res.status(200).json(response);
} catch (error) {
logger.error('Error fetching messages:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/artifact/:messageId', async (req, res) => {
try {
const { messageId } = req.params;
const { index, original, updated } = req.body;
if (typeof index !== 'number' || index < 0 || original == null || updated == null) {
return res.status(400).json({ error: 'Invalid request parameters' });
}
const message = await getMessage({ user: req.user.id, messageId });
if (!message) {
return res.status(404).json({ error: 'Message not found' });
}
const artifacts = findAllArtifacts(message);
if (index >= artifacts.length) {
return res.status(400).json({ error: 'Artifact index out of bounds' });
}
// Unescape LaTeX preprocessing done by the frontend
// The frontend escapes $ signs for display, but the database has unescaped versions
const unescapedOriginal = unescapeLaTeX(original);
const unescapedUpdated = unescapeLaTeX(updated);
const targetArtifact = artifacts[index];
let updatedText = null;
if (targetArtifact.source === 'content') {
const part = message.content[targetArtifact.partIndex];
updatedText = replaceArtifactContent(
part.text,
targetArtifact,
unescapedOriginal,
unescapedUpdated,
);
if (updatedText) {
part.text = updatedText;
}
} else {
updatedText = replaceArtifactContent(
message.text,
targetArtifact,
unescapedOriginal,
unescapedUpdated,
);
if (updatedText) {
message.text = updatedText;
}
}
if (!updatedText) {
return res.status(400).json({ error: 'Original content not found in target artifact' });
}
const savedMessage = await saveMessage(
req,
{
messageId,
conversationId: message.conversationId,
text: message.text,
content: message.content,
user: req.user.id,
},
{ context: 'POST /api/messages/artifact/:messageId' },
);
res.status(200).json({
conversationId: savedMessage.conversationId,
content: savedMessage.content,
text: savedMessage.text,
});
} catch (error) {
logger.error('Error editing artifact:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/* Note: It's necessary to add `validateMessageReq` within route definition for correct params */
router.get('/:conversationId', validateMessageReq, async (req, res) => {
try {
const { conversationId } = req.params;
const messages = await getMessages({ conversationId }, '-_id -__v -user');
res.status(200).json(messages);
} catch (error) {
logger.error('Error fetching messages:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/:conversationId', validateMessageReq, async (req, res) => {
try {
const message = req.body;
const savedMessage = await saveMessage(
req,
{ ...message, user: req.user.id },
{ context: 'POST /api/messages/:conversationId' },
);
if (!savedMessage) {
return res.status(400).json({ error: 'Message not saved' });
}
await saveConvo(req, savedMessage, { context: 'POST /api/messages/:conversationId' });
res.status(201).json(savedMessage);
} catch (error) {
logger.error('Error saving message:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.get('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
try {
const { conversationId, messageId } = req.params;
const message = await getMessages({ conversationId, messageId }, '-_id -__v -user');
if (!message) {
return res.status(404).json({ error: 'Message not found' });
}
res.status(200).json(message);
} catch (error) {
logger.error('Error fetching message:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
try {
const { conversationId, messageId } = req.params;
const { text, index, model } = req.body;
if (index === undefined) {
const tokenCount = await countTokens(text, model);
const result = await updateMessage(req, { messageId, text, tokenCount });
return res.status(200).json(result);
}
if (typeof index !== 'number' || index < 0) {
return res.status(400).json({ error: 'Invalid index' });
}
const message = (await getMessages({ conversationId, messageId }, 'content tokenCount'))?.[0];
if (!message) {
return res.status(404).json({ error: 'Message not found' });
}
const existingContent = message.content;
if (!Array.isArray(existingContent) || index >= existingContent.length) {
return res.status(400).json({ error: 'Invalid index' });
}
const updatedContent = [...existingContent];
if (!updatedContent[index]) {
return res.status(400).json({ error: 'Content part not found' });
}
const currentPartType = updatedContent[index].type;
if (currentPartType !== ContentTypes.TEXT && currentPartType !== ContentTypes.THINK) {
return res.status(400).json({ error: 'Cannot update non-text content' });
}
const oldText = updatedContent[index][currentPartType];
updatedContent[index] = { type: currentPartType, [currentPartType]: text };
let tokenCount = message.tokenCount;
if (tokenCount !== undefined) {
const oldTokenCount = await countTokens(oldText, model);
const newTokenCount = await countTokens(text, model);
tokenCount = Math.max(0, tokenCount - oldTokenCount) + newTokenCount;
}
const result = await updateMessage(req, { messageId, content: updatedContent, tokenCount });
return res.status(200).json(result);
} catch (error) {
logger.error('Error updating message:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (req, res) => {
try {
const { conversationId, messageId } = req.params;
const { feedback } = req.body;
const updatedMessage = await updateMessage(
req,
{
messageId,
feedback: feedback || null,
},
{ context: 'updateFeedback' },
);
res.json({
messageId,
conversationId,
feedback: updatedMessage.feedback,
});
} catch (error) {
logger.error('Error updating message feedback:', error);
res.status(500).json({ error: 'Failed to update feedback' });
}
});
router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res) => {
try {
const { messageId } = req.params;
await deleteMessages({ messageId });
res.status(204).send();
} catch (error) {
logger.error('Error deleting message:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;