Compare commits

..

1 Commits

Author SHA1 Message Date
Ruben Talstra
47dff7d387 feat: Add Markdown heading and list components for enhanced formatting 2025-03-12 10:42:08 +01:00
92 changed files with 1661 additions and 5901 deletions

View File

@@ -473,15 +473,6 @@ FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=
#========================#
# S3 AWS Bucket #
#========================#
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_BUCKET_NAME=
#========================#
# Shared Links #
#========================#

View File

@@ -5,7 +5,6 @@ const {
isAgentsEndpoint,
isParamEndpoint,
EModelEndpoint,
ContentTypes,
excludedKeys,
ErrorTypes,
Constants,
@@ -366,14 +365,17 @@ class BaseClient {
* context: TMessage[],
* remainingContextTokens: number,
* messagesToRefine: TMessage[],
* }>} An object with three properties: `context`, `remainingContextTokens`, and `messagesToRefine`.
* summaryIndex: number,
* }>} An object with four properties: `context`, `summaryIndex`, `remainingContextTokens`, and `messagesToRefine`.
* `context` is an array of messages that fit within the token limit.
* `summaryIndex` is the index of the first message in the `messagesToRefine` array.
* `remainingContextTokens` is the number of tokens remaining within the limit after adding the messages to the context.
* `messagesToRefine` is an array of messages that were not added to the context because they would have exceeded the token limit.
*/
async getMessagesWithinTokenLimit({ messages: _messages, maxContextTokens, instructions }) {
// Every reply is primed with <|start|>assistant<|message|>, so we
// start with 3 tokens for the label after all messages have been counted.
let summaryIndex = -1;
let currentTokenCount = 3;
const instructionsTokenCount = instructions?.tokenCount ?? 0;
let remainingContextTokens =
@@ -406,12 +408,14 @@ class BaseClient {
}
const prunedMemory = messages;
summaryIndex = prunedMemory.length - 1;
remainingContextTokens -= currentTokenCount;
return {
context: context.reverse(),
remainingContextTokens,
messagesToRefine: prunedMemory,
summaryIndex,
};
}
@@ -454,7 +458,7 @@ class BaseClient {
let orderedWithInstructions = this.addInstructions(orderedMessages, instructions);
let { context, remainingContextTokens, messagesToRefine } =
let { context, remainingContextTokens, messagesToRefine, summaryIndex } =
await this.getMessagesWithinTokenLimit({
messages: orderedWithInstructions,
instructions,
@@ -524,7 +528,7 @@ class BaseClient {
}
// Make sure to only continue summarization logic if the summary message was generated
shouldSummarize = summaryMessage != null && shouldSummarize === true;
shouldSummarize = summaryMessage && shouldSummarize;
logger.debug('[BaseClient] Context Count (2/2)', {
remainingContextTokens,
@@ -534,18 +538,17 @@ class BaseClient {
/** @type {Record<string, number> | undefined} */
let tokenCountMap;
if (buildTokenMap) {
const currentPayload = shouldSummarize ? orderedWithInstructions : context;
tokenCountMap = currentPayload.reduce((map, message, index) => {
tokenCountMap = orderedWithInstructions.reduce((map, message, index) => {
const { messageId } = message;
if (!messageId) {
return map;
}
if (shouldSummarize && index === messagesToRefine.length - 1 && !usePrevSummary) {
if (shouldSummarize && index === summaryIndex && !usePrevSummary) {
map.summaryMessage = { ...summaryMessage, messageId, tokenCount: summaryTokenCount };
}
map[messageId] = currentPayload[index].tokenCount;
map[messageId] = orderedWithInstructions[index].tokenCount;
return map;
}, {});
}
@@ -1018,17 +1021,11 @@ class BaseClient {
const processValue = (value) => {
if (Array.isArray(value)) {
for (let item of value) {
if (
!item ||
!item.type ||
item.type === ContentTypes.THINK ||
item.type === ContentTypes.ERROR ||
item.type === ContentTypes.IMAGE_URL
) {
if (!item || !item.type || item.type === 'image_url') {
continue;
}
if (item.type === ContentTypes.TOOL_CALL && item.tool_call != null) {
if (item.type === 'tool_call' && item.tool_call != null) {
const toolName = item.tool_call?.name || '';
if (toolName != null && toolName && typeof toolName === 'string') {
numTokens += this.getTokenCount(toolName);

View File

@@ -1272,29 +1272,6 @@ ${convo}
});
}
/** Note: OpenAI Web Search models do not support any known parameters besdies `max_tokens` */
if (modelOptions.model && /gpt-4o.*search/.test(modelOptions.model)) {
const searchExcludeParams = [
'frequency_penalty',
'presence_penalty',
'temperature',
'top_p',
'top_k',
'stop',
'logit_bias',
'seed',
'response_format',
'n',
'logprobs',
'user',
];
this.options.dropParams = this.options.dropParams || [];
this.options.dropParams = [
...new Set([...this.options.dropParams, ...searchExcludeParams]),
];
}
if (this.options.dropParams && Array.isArray(this.options.dropParams)) {
this.options.dropParams.forEach((param) => {
delete modelOptions[param];

View File

@@ -211,7 +211,7 @@ const formatAgentMessages = (payload) => {
} else if (part.type === ContentTypes.THINK) {
hasReasoning = true;
continue;
} else if (part.type === ContentTypes.ERROR || part.type === ContentTypes.AGENT_UPDATE) {
} else if (part.type === ContentTypes.ERROR) {
continue;
} else {
currentContent.push(part);

View File

@@ -164,7 +164,7 @@ describe('BaseClient', () => {
const result = await TestClient.getMessagesWithinTokenLimit({ messages });
expect(result.context).toEqual(expectedContext);
expect(result.messagesToRefine.length - 1).toEqual(expectedIndex);
expect(result.summaryIndex).toEqual(expectedIndex);
expect(result.remainingContextTokens).toBe(expectedRemainingContextTokens);
expect(result.messagesToRefine).toEqual(expectedMessagesToRefine);
});
@@ -200,7 +200,7 @@ describe('BaseClient', () => {
const result = await TestClient.getMessagesWithinTokenLimit({ messages });
expect(result.context).toEqual(expectedContext);
expect(result.messagesToRefine.length - 1).toEqual(expectedIndex);
expect(result.summaryIndex).toEqual(expectedIndex);
expect(result.remainingContextTokens).toBe(expectedRemainingContextTokens);
expect(result.messagesToRefine).toEqual(expectedMessagesToRefine);
});

View File

@@ -172,7 +172,7 @@ Error Message: ${error.message}`);
{
type: ContentTypes.IMAGE_URL,
image_url: {
url: `data:image/png;base64,${base64}`,
url: `data:image/jpeg;base64,${base64}`,
},
},
];

View File

@@ -28,4 +28,4 @@ const getBanner = async (user) => {
}
};
module.exports = { Banner, getBanner };
module.exports = { getBanner };

View File

@@ -61,22 +61,45 @@ const deleteNullOrEmptyConversations = async () => {
};
/**
* Searches for a conversation by conversationId and returns associated file ids.
* @param {string} conversationId - The conversation's ID.
* @returns {Promise<string[] | null>}
* Retrieves files from a conversation that have either embedded=true
* or a metadata.fileIdentifier. Simplified and efficient query.
*
* @param {string} conversationId - The conversation ID
* @returns {Promise<MongoFile[]>} - Filtered array of matching file objects
*/
const getConvoFiles = async (conversationId) => {
const getToolFiles = async (conversationId) => {
try {
return (await Conversation.findOne({ conversationId }, 'files').lean())?.files ?? [];
const [result] = await Conversation.aggregate([
{ $match: { conversationId } },
{
$project: {
files: {
$filter: {
input: '$files',
as: 'file',
cond: {
$or: [
{ $eq: ['$$file.embedded', true] },
{ $ifNull: ['$$file.metadata.fileIdentifier', false] },
],
},
},
},
_id: 0,
},
},
]).exec();
return result?.files || [];
} catch (error) {
logger.error('[getConvoFiles] Error getting conversation files', error);
throw new Error('Error getting conversation files');
logger.error('[getConvoEmbeddedFiles] Error fetching embedded files:', error);
throw new Error('Error fetching embedded files');
}
};
module.exports = {
Conversation,
getConvoFiles,
getToolFiles,
searchConversation,
deleteNullOrEmptyConversations,
/**

View File

@@ -1,6 +1,5 @@
const mongoose = require('mongoose');
const { fileSchema } = require('@librechat/data-schemas');
const { logger } = require('~/config');
const File = mongoose.model('File', fileSchema);
@@ -27,32 +26,6 @@ const getFiles = async (filter, _sortOptions, selectFields = { text: 0 }) => {
return await File.find(filter).select(selectFields).sort(sortOptions).lean();
};
/**
* Retrieves tool files (files that are embedded or have a fileIdentifier) from an array of file IDs
* @param {string[]} fileIds - Array of file_id strings to search for
* @returns {Promise<Array<IMongoFile>>} Files that match the criteria
*/
const getToolFilesByIds = async (fileIds) => {
if (!fileIds || !fileIds.length) {
return [];
}
try {
const filter = {
file_id: { $in: fileIds },
$or: [{ embedded: true }, { 'metadata.fileIdentifier': { $exists: true } }],
};
const selectFields = { text: 0 };
const sortOptions = { updatedAt: -1 };
return await getFiles(filter, sortOptions, selectFields);
} catch (error) {
logger.error('[getToolFilesByIds] Error retrieving tool files:', error);
throw new Error('Error retrieving tool files');
}
};
/**
* Creates a new file with a TTL of 1 hour.
* @param {IMongoFile} data - The file data to be created, must contain file_id.
@@ -138,7 +111,6 @@ module.exports = {
File,
findFileById,
getFiles,
getToolFilesByIds,
createFile,
updateFile,
updateFileUsage,

View File

@@ -1,6 +0,0 @@
const mongoose = require('mongoose');
const { groupSchema } = require('@librechat/data-schemas');
const Group = mongoose.model('Group', groupSchema);
module.exports = Group;

View File

@@ -71,42 +71,7 @@ async function saveMessage(req, params, metadata) {
} catch (err) {
logger.error('Error saving message:', err);
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
// Check if this is a duplicate key error (MongoDB error code 11000)
if (err.code === 11000 && err.message.includes('duplicate key error')) {
// Log the duplicate key error but don't crash the application
logger.warn(`Duplicate messageId detected: ${params.messageId}. Continuing execution.`);
try {
// Try to find the existing message with this ID
const existingMessage = await Message.findOne({
messageId: params.messageId,
user: req.user.id,
});
// If we found it, return it
if (existingMessage) {
return existingMessage.toObject();
}
// If we can't find it (unlikely but possible in race conditions)
return {
...params,
messageId: params.messageId,
user: req.user.id,
};
} catch (findError) {
// If the findOne also fails, log it but don't crash
logger.warn(`Could not retrieve existing message with ID ${params.messageId}: ${findError.message}`);
return {
...params,
messageId: params.messageId,
user: req.user.id,
};
}
}
throw err; // Re-throw other errors
throw err;
}
}

View File

@@ -1,127 +0,0 @@
const User = require('./User');
const Group = require('~/models/Group');
/**
* Retrieve a group by ID and convert the found group document to a plain object.
*
* @param {string} groupId - The ID of the group to find and return as a plain object.
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document.
* @returns {Promise<Object|null>} A plain object representing the group document, or `null` if no group is found.
*/
const getGroupById = (groupId, fieldsToSelect = null) => {
const query = Group.findById(groupId);
if (fieldsToSelect) {
query.select(fieldsToSelect);
}
return query.lean();
};
/**
* Search for a single group or multiple groups based on partial data and return them as plain objects.
*
* @param {Partial<Object>} searchCriteria - The partial data to use for searching groups.
* @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned documents.
* @returns {Promise<Object[]>} An array of plain objects representing the group documents.
*/
const findGroup = (searchCriteria, fieldsToSelect = null) => {
const query = Group.find(searchCriteria);
if (fieldsToSelect) {
query.select(fieldsToSelect);
}
return query.lean();
};
/**
* Update a group with new data without overwriting existing properties.
*
* @param {string} groupId - The ID of the group to update.
* @param {Object} updateData - An object containing the properties to update.
* @returns {Promise<Object|null>} The updated group document as a plain object, or `null` if no group is found.
*/
const updateGroup = (groupId, updateData) => {
return Group.findByIdAndUpdate(
groupId,
{ $set: updateData },
{ new: true, runValidators: true },
).lean();
};
/**
* Create a new group.
*
* @param {Object} data - The group data to be created.
* @returns {Promise<Object>} The created group document.
*/
const createGroup = async (data) => {
return await Group.create(data);
};
/**
* Count the number of group documents in the collection based on the provided filter.
*
* @param {Object} [filter={}] - The filter to apply when counting the documents.
* @returns {Promise<number>} The count of documents that match the filter.
*/
const countGroups = (filter = {}) => {
return Group.countDocuments(filter);
};
/**
* Delete a group by its unique ID only if no user is assigned to it.
*
* @param {string} groupId - The ID of the group to delete.
* @returns {Promise<{ deletedCount: number, message: string }>} An object indicating the number of deleted documents.
*/
const deleteGroupById = async (groupId) => {
// Check if any users reference the group
const userCount = await User.countDocuments({ groups: groupId });
if (userCount > 0) {
return { deletedCount: 0, message: `Cannot delete group; it is assigned to ${userCount} user(s).` };
}
try {
const result = await Group.deleteOne({ _id: groupId });
if (result.deletedCount === 0) {
return { deletedCount: 0, message: 'No group found with that ID.' };
}
return { deletedCount: result.deletedCount, message: 'Group was deleted successfully.' };
} catch (error) {
throw new Error('Error deleting group: ' + error.message);
}
};
/**
* Override deletion of a group by its unique ID.
* This function first removes the group ObjectId from all users' groups arrays,
* then proceeds to delete the group document.
*
* @param {string} groupId - The ID of the group to delete.
* @returns {Promise<{ deletedCount: number, message: string }>} An object indicating the deletion result.
*/
const overrideDeleteGroupById = async (groupId) => {
// Remove group references from all users
await User.updateMany(
{ groups: groupId },
{ $pull: { groups: groupId } },
);
try {
const result = await Group.deleteOne({ _id: groupId });
if (result.deletedCount === 0) {
return { deletedCount: 0, message: 'No group found with that ID.' };
}
return { deletedCount: result.deletedCount, message: 'Group was deleted successfully (override).' };
} catch (error) {
throw new Error('Error deleting group: ' + error.message);
}
};
module.exports = {
getGroupById,
findGroup,
updateGroup,
createGroup,
countGroups,
deleteGroupById,
overrideDeleteGroupById,
};

View File

@@ -40,7 +40,6 @@ const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset')
const { createToken, findToken, updateToken, deleteTokens } = require('./Token');
const Balance = require('./Balance');
const User = require('./User');
const Group = require('./Group');
const Key = require('./Key');
module.exports = {
@@ -93,7 +92,6 @@ module.exports = {
countActiveSessions,
User,
Group,
Key,
Balance,
};

View File

@@ -61,7 +61,6 @@ const bedrockValues = {
'amazon.nova-micro-v1:0': { prompt: 0.035, completion: 0.14 },
'amazon.nova-lite-v1:0': { prompt: 0.06, completion: 0.24 },
'amazon.nova-pro-v1:0': { prompt: 0.8, completion: 3.2 },
'deepseek.r1': { prompt: 1.35, completion: 5.4 },
};
/**

View File

@@ -288,7 +288,7 @@ describe('AWS Bedrock Model Tests', () => {
});
describe('Deepseek Model Tests', () => {
const deepseekModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner', 'deepseek.r1'];
const deepseekModels = ['deepseek-chat', 'deepseek-coder', 'deepseek-reasoner'];
it('should return the correct prompt multipliers for all models', () => {
const results = deepseekModels.map((model) => {

View File

@@ -35,8 +35,6 @@
"homepage": "https://librechat.ai",
"dependencies": {
"@anthropic-ai/sdk": "^0.37.0",
"@aws-sdk/client-s3": "^3.758.0",
"@aws-sdk/s3-request-presigner": "^3.758.0",
"@azure/search-documents": "^12.0.0",
"@google/generative-ai": "^0.23.0",
"@googleapis/youtube": "^20.0.0",
@@ -44,10 +42,10 @@
"@keyv/redis": "^2.8.1",
"@langchain/community": "^0.3.34",
"@langchain/core": "^0.3.40",
"@langchain/google-genai": "^0.1.11",
"@langchain/google-vertexai": "^0.2.2",
"@langchain/google-genai": "^0.1.9",
"@langchain/google-vertexai": "^0.2.0",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.2.8",
"@librechat/agents": "^2.2.0",
"@librechat/data-schemas": "*",
"@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2",

View File

@@ -7,16 +7,7 @@
// validateVisionModel,
// mapModelToAzureConfig,
// } = require('librechat-data-provider');
require('events').EventEmitter.defaultMaxListeners = 100;
const {
Callback,
GraphEvents,
formatMessage,
formatAgentMessages,
formatContentStrings,
getTokenCountForMessage,
createMetadataAggregator,
} = require('@librechat/agents');
const { Callback, createMetadataAggregator } = require('@librechat/agents');
const {
Constants,
VisionModes,
@@ -26,19 +17,24 @@ const {
KnownEndpoints,
anthropicSchema,
isAgentsEndpoint,
AgentCapabilities,
bedrockInputSchema,
removeNullishValues,
} = require('librechat-data-provider');
const { getCustomEndpointConfig, checkCapability } = require('~/server/services/Config');
const { addCacheControl, createContextHandlers } = require('~/app/clients/prompts');
const {
formatMessage,
addCacheControl,
formatAgentMessages,
formatContentStrings,
createContextHandlers,
} = require('~/app/clients/prompts');
const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
const { getBufferString, HumanMessage } = require('@langchain/core/messages');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
const { getCustomEndpointConfig } = require('~/server/services/Config');
const Tokenizer = require('~/server/services/Tokenizer');
const BaseClient = require('~/app/clients/BaseClient');
const { logger, sendEvent } = require('~/config');
const { createRun } = require('./run');
const { logger } = require('~/config');
/** @typedef {import('@librechat/agents').MessageContentComplex} MessageContentComplex */
/** @typedef {import('@langchain/core/runnables').RunnableConfig} RunnableConfig */
@@ -103,8 +99,6 @@ class AgentClient extends BaseClient {
this.outputTokensKey = 'output_tokens';
/** @type {UsageMetadata} */
this.usage;
/** @type {Record<string, number>} */
this.indexTokenCountMap = {};
}
/**
@@ -383,10 +377,6 @@ class AgentClient extends BaseClient {
}));
}
for (let i = 0; i < messages.length; i++) {
this.indexTokenCountMap[i] = messages[i].tokenCount;
}
const result = {
tokenCountMap,
prompt: payload,
@@ -632,9 +622,6 @@ class AgentClient extends BaseClient {
// });
// }
/** @type {TCustomConfig['endpoints']['agents']} */
const agentsEConfig = this.options.req.app.locals[EModelEndpoint.agents];
/** @type {Partial<RunnableConfig> & { version: 'v1' | 'v2'; run_id?: string; streamMode: string }} */
const config = {
configurable: {
@@ -642,30 +629,19 @@ class AgentClient extends BaseClient {
last_agent_index: this.agentConfigs?.size ?? 0,
hide_sequential_outputs: this.options.agent.hide_sequential_outputs,
},
recursionLimit: agentsEConfig?.recursionLimit,
recursionLimit: this.options.req.app.locals[EModelEndpoint.agents]?.recursionLimit,
signal: abortController.signal,
streamMode: 'values',
version: 'v2',
};
const toolSet = new Set((this.options.agent.tools ?? []).map((tool) => tool && tool.name));
let { messages: initialMessages, indexTokenCountMap } = formatAgentMessages(
payload,
this.indexTokenCountMap,
toolSet,
);
const initialMessages = formatAgentMessages(payload);
if (legacyContentEndpoints.has(this.options.agent.endpoint)) {
initialMessages = formatContentStrings(initialMessages);
formatContentStrings(initialMessages);
}
/** @type {ReturnType<createRun>} */
let run;
const countTokens = ((text) => this.getTokenCount(text)).bind(this);
/** @type {(message: BaseMessage) => number} */
const tokenCounter = (message) => {
return getTokenCountForMessage(message, countTokens);
};
/**
*
@@ -673,23 +649,12 @@ class AgentClient extends BaseClient {
* @param {BaseMessage[]} messages
* @param {number} [i]
* @param {TMessageContentParts[]} [contentData]
* @param {Record<string, number>} [currentIndexCountMap]
*/
const runAgent = async (agent, _messages, i = 0, contentData = [], _currentIndexCountMap) => {
const runAgent = async (agent, _messages, i = 0, contentData = []) => {
config.configurable.model = agent.model_parameters.model;
const currentIndexCountMap = _currentIndexCountMap ?? indexTokenCountMap;
if (i > 0) {
this.model = agent.model_parameters.model;
}
if (agent.recursion_limit && typeof agent.recursion_limit === 'number') {
config.recursionLimit = agent.recursion_limit;
}
if (
agentsEConfig?.maxRecursionLimit &&
config.recursionLimit > agentsEConfig?.maxRecursionLimit
) {
config.recursionLimit = agentsEConfig?.maxRecursionLimit;
}
config.configurable.agent_id = agent.id;
config.configurable.name = agent.name;
config.configurable.agent_index = i;
@@ -752,29 +717,11 @@ class AgentClient extends BaseClient {
}
if (contentData.length) {
const agentUpdate = {
type: ContentTypes.AGENT_UPDATE,
[ContentTypes.AGENT_UPDATE]: {
index: contentData.length,
runId: this.responseMessageId,
agentId: agent.id,
},
};
const streamData = {
event: GraphEvents.ON_AGENT_UPDATE,
data: agentUpdate,
};
this.options.aggregateContent(streamData);
sendEvent(this.options.res, streamData);
contentData.push(agentUpdate);
run.Graph.contentData = contentData;
}
await run.processStream({ messages }, config, {
keepContent: i !== 0,
tokenCounter,
indexTokenCountMap: currentIndexCountMap,
maxContextTokens: agent.maxContextTokens,
callbacks: {
[Callback.TOOL_ERROR]: (graph, error, toolId) => {
logger.error(
@@ -788,13 +735,9 @@ class AgentClient extends BaseClient {
};
await runAgent(this.options.agent, initialMessages);
let finalContentStart = 0;
if (
this.agentConfigs &&
this.agentConfigs.size > 0 &&
(await checkCapability(this.options.req, AgentCapabilities.chain))
) {
const windowSize = 5;
if (this.agentConfigs && this.agentConfigs.size > 0) {
let latestMessage = initialMessages.pop().content;
if (typeof latestMessage !== 'string') {
latestMessage = latestMessage[0].text;
@@ -802,16 +745,7 @@ class AgentClient extends BaseClient {
let i = 1;
let runMessages = [];
const windowIndexCountMap = {};
const windowMessages = initialMessages.slice(-windowSize);
let currentIndex = 4;
for (let i = initialMessages.length - 1; i >= 0; i--) {
windowIndexCountMap[currentIndex] = indexTokenCountMap[i];
currentIndex--;
if (currentIndex < 0) {
break;
}
}
const lastFiveMessages = initialMessages.slice(-5);
for (const [agentId, agent] of this.agentConfigs) {
if (abortController.signal.aborted === true) {
break;
@@ -846,9 +780,7 @@ class AgentClient extends BaseClient {
}
try {
const contextMessages = [];
const runIndexCountMap = {};
for (let i = 0; i < windowMessages.length; i++) {
const message = windowMessages[i];
for (const message of lastFiveMessages) {
const messageType = message._getType();
if (
(!agent.tools || agent.tools.length === 0) &&
@@ -856,13 +788,11 @@ class AgentClient extends BaseClient {
) {
continue;
}
runIndexCountMap[contextMessages.length] = windowIndexCountMap[i];
contextMessages.push(message);
}
const bufferMessage = new HumanMessage(bufferString);
runIndexCountMap[contextMessages.length] = tokenCounter(bufferMessage);
const currentMessages = [...contextMessages, bufferMessage];
await runAgent(agent, currentMessages, i, contentData, runIndexCountMap);
const currentMessages = [...contextMessages, new HumanMessage(bufferString)];
await runAgent(agent, currentMessages, i, contentData);
} catch (err) {
logger.error(
`[api/server/controllers/agents/client.js #chatCompletion] Error running agent ${agentId} (${i})`,
@@ -873,7 +803,6 @@ class AgentClient extends BaseClient {
}
}
/** Note: not implemented */
if (config.configurable.hide_sequential_outputs !== true) {
finalContentStart = 0;
}

View File

@@ -1,11 +1,10 @@
const fs = require('fs').promises;
const { nanoid } = require('nanoid');
const {
Tools,
Constants,
FileContext,
Constants,
Tools,
SystemRoles,
EToolResources,
actionDelimiter,
} = require('librechat-data-provider');
const {
@@ -204,21 +203,14 @@ const duplicateAgentHandler = async (req, res) => {
}
const {
id: _id,
_id: __id,
id: _id,
author: _author,
createdAt: _createdAt,
updatedAt: _updatedAt,
tool_resources: _tool_resources = {},
...cloneData
} = agent;
if (_tool_resources?.[EToolResources.ocr]) {
cloneData.tool_resources = {
[EToolResources.ocr]: _tool_resources[EToolResources.ocr],
};
}
const newAgentId = `agent_${nanoid()}`;
const newAgentData = Object.assign(cloneData, {
id: newAgentId,

View File

@@ -161,9 +161,9 @@ async function createActionTool({
if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) {
try {
const action_id = action.action_id;
const identifier = `${req.user.id}:${action.action_id}`;
if (metadata.auth.type === AuthTypeEnum.OAuth && metadata.auth.authorization_url) {
const action_id = action.action_id;
const identifier = `${req.user.id}:${action.action_id}`;
const requestLogin = async () => {
const { args: _args, stepId, ...toolCall } = config.toolCall ?? {};
if (!stepId) {

View File

@@ -2,13 +2,11 @@ const {
FileSources,
EModelEndpoint,
loadOCRConfig,
processMCPEnv,
getConfigDefaults,
} = require('librechat-data-provider');
const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = require('./start/checks');
const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants');
const { initializeFirebase } = require('./Files/Firebase/initialize');
const { initializeS3 } = require('./Files/S3/initialize');
const loadCustomConfig = require('./Config/loadCustomConfig');
const handleRateLimits = require('./Config/handleRateLimits');
const { loadDefaultInterface } = require('./start/interface');
@@ -45,8 +43,6 @@ const AppService = async (app) => {
if (fileStrategy === FileSources.firebase) {
initializeFirebase();
} else if (fileStrategy === FileSources.s3) {
initializeS3();
}
/** @type {Record<string, FunctionTool} */
@@ -58,7 +54,7 @@ const AppService = async (app) => {
if (config.mcpServers != null) {
const mcpManager = await getMCPManager();
await mcpManager.initializeMCP(config.mcpServers, processMCPEnv);
await mcpManager.initializeMCP(config.mcpServers);
await mcpManager.mapAvailableTools(availableTools);
}

View File

@@ -72,15 +72,4 @@ async function getEndpointsConfig(req) {
return endpointsConfig;
}
/**
* @param {ServerRequest} req
* @param {import('librechat-data-provider').AgentCapabilities} capability
* @returns {Promise<boolean>}
*/
const checkCapability = async (req, capability) => {
const endpointsConfig = await getEndpointsConfig(req);
const capabilities = endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [];
return capabilities.includes(capability);
};
module.exports = { getEndpointsConfig, checkCapability };
module.exports = { getEndpointsConfig };

View File

@@ -19,8 +19,7 @@ const { getCustomEndpointConfig } = require('~/server/services/Config');
const { processFiles } = require('~/server/services/Files/process');
const { loadAgentTools } = require('~/server/services/ToolService');
const AgentClient = require('~/server/controllers/agents/client');
const { getConvoFiles } = require('~/models/Conversation');
const { getToolFilesByIds } = require('~/models/File');
const { getToolFiles } = require('~/models/Conversation');
const { getModelMaxTokens } = require('~/utils');
const { getAgent } = require('~/models/Agent');
const { getFiles } = require('~/models/File');
@@ -116,17 +115,15 @@ const initializeAgentOptions = async ({
isInitialAgent = false,
}) => {
let currentFiles;
/** @type {Array<MongoFile>} */
const requestFiles = req.body.files ?? [];
if (
isInitialAgent &&
req.body.conversationId != null &&
(agent.model_parameters?.resendFiles ?? true) === true
agent.model_parameters?.resendFiles === true
) {
const fileIds = (await getConvoFiles(req.body.conversationId)) ?? [];
const toolFiles = await getToolFilesByIds(fileIds);
if (requestFiles.length || toolFiles.length) {
currentFiles = await processFiles(requestFiles.concat(toolFiles));
const fileIds = (await getToolFiles(req.body.conversationId)).map((f) => f.file_id);
if (requestFiles.length || fileIds.length) {
currentFiles = await processFiles(requestFiles, fileIds);
}
} else if (isInitialAgent && requestFiles.length) {
currentFiles = await processFiles(requestFiles);
@@ -181,7 +178,6 @@ const initializeAgentOptions = async ({
agent.provider = options.provider;
}
/** @type {import('@librechat/agents').ClientOptions} */
agent.model_parameters = Object.assign(model_parameters, options.llmConfig);
if (options.configOptions) {
agent.model_parameters.configuration = options.configOptions;
@@ -200,7 +196,6 @@ const initializeAgentOptions = async ({
const tokensModel =
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model;
const maxTokens = agent.model_parameters.maxOutputTokens ?? agent.model_parameters.maxTokens ?? 0;
return {
...agent,
@@ -209,7 +204,8 @@ const initializeAgentOptions = async ({
toolContextMap,
maxContextTokens:
agent.max_context_tokens ??
((getModelMaxTokens(tokensModel, providerEndpointMap[provider]) ?? 4000) - maxTokens) * 0.9,
getModelMaxTokens(tokensModel, providerEndpointMap[provider]) ??
4000,
};
};
@@ -279,13 +275,11 @@ const initializeClient = async ({ req, res, endpointOption }) => {
const client = new AgentClient({
req,
res,
sender,
contentParts,
agentConfigs,
eventHandlers,
collectedUsage,
aggregateContent,
artifactPromises,
agent: primaryConfig,
spec: endpointOption.spec,

View File

@@ -23,9 +23,8 @@ const initializeClient = async ({ req, res, endpointOption }) => {
const agent = {
id: EModelEndpoint.bedrock,
name: endpointOption.name,
provider: EModelEndpoint.bedrock,
endpoint: EModelEndpoint.bedrock,
instructions: endpointOption.promptPrefix,
provider: EModelEndpoint.bedrock,
model: endpointOption.model_parameters.model,
model_parameters: endpointOption.model_parameters,
};
@@ -55,7 +54,6 @@ const initializeClient = async ({ req, res, endpointOption }) => {
const client = new AgentClient({
req,
res,
agent,
sender,
// tools,

View File

@@ -135,9 +135,12 @@ const initializeClient = async ({
}
if (optionsOnly) {
const modelOptions = endpointOption.model_parameters;
modelOptions.model = modelName;
clientOptions = Object.assign({ modelOptions }, clientOptions);
clientOptions = Object.assign(
{
modelOptions: endpointOption.model_parameters,
},
clientOptions,
);
clientOptions.modelOptions.user = req.user.id;
const options = getLLMConfig(apiKey, clientOptions);
if (!clientOptions.streamRate) {

View File

@@ -28,7 +28,7 @@ const { isEnabled } = require('~/server/utils');
* @returns {Object} Configuration options for creating an LLM instance.
*/
function getLLMConfig(apiKey, options = {}, endpoint = null) {
let {
const {
modelOptions = {},
reverseProxyUrl,
defaultQuery,
@@ -50,32 +50,10 @@ function getLLMConfig(apiKey, options = {}, endpoint = null) {
if (addParams && typeof addParams === 'object') {
Object.assign(llmConfig, addParams);
}
/** Note: OpenAI Web Search models do not support any known parameters besdies `max_tokens` */
if (modelOptions.model && /gpt-4o.*search/.test(modelOptions.model)) {
const searchExcludeParams = [
'frequency_penalty',
'presence_penalty',
'temperature',
'top_p',
'top_k',
'stop',
'logit_bias',
'seed',
'response_format',
'n',
'logprobs',
'user',
];
dropParams = dropParams || [];
dropParams = [...new Set([...dropParams, ...searchExcludeParams])];
}
if (dropParams && Array.isArray(dropParams)) {
dropParams.forEach((param) => {
if (llmConfig[param]) {
llmConfig[param] = undefined;
}
delete llmConfig[param];
});
}

View File

@@ -1,162 +0,0 @@
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const fetch = require('node-fetch');
const { getBufferMetadata } = require('~/server/utils');
const { initializeS3 } = require('./initialize');
const { logger } = require('~/config');
const { PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const bucketName = process.env.AWS_BUCKET_NAME;
const s3 = initializeS3();
const defaultBasePath = 'images';
/**
* Constructs the S3 key based on the base path, user ID, and file name.
*/
const getS3Key = (basePath, userId, fileName) => `${basePath}/${userId}/${fileName}`;
/**
* Uploads a buffer to S3 and returns a signed URL.
*
* @param {Object} params
* @param {string} params.userId - The user's unique identifier.
* @param {Buffer} params.buffer - The buffer containing file data.
* @param {string} params.fileName - The file name to use in S3.
* @param {string} [params.basePath='images'] - The base path in the bucket.
* @returns {Promise<string>} Signed URL of the uploaded file.
*/
async function saveBufferToS3({ userId, buffer, fileName, basePath = defaultBasePath }) {
const key = getS3Key(basePath, userId, fileName);
const params = { Bucket: bucketName, Key: key, Body: buffer };
try {
await s3.send(new PutObjectCommand(params));
return await getS3URL({ userId, fileName, basePath });
} catch (error) {
logger.error('[saveBufferToS3] Error uploading buffer to S3:', error.message);
throw error;
}
}
/**
* Retrieves a signed URL for a file stored in S3.
*
* @param {Object} params
* @param {string} params.userId - The user's unique identifier.
* @param {string} params.fileName - The file name in S3.
* @param {string} [params.basePath='images'] - The base path in the bucket.
* @returns {Promise<string>} A signed URL valid for 24 hours.
*/
async function getS3URL({ userId, fileName, basePath = defaultBasePath }) {
const key = getS3Key(basePath, userId, fileName);
const params = { Bucket: bucketName, Key: key };
try {
return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 86400 });
} catch (error) {
logger.error('[getS3URL] Error getting signed URL from S3:', error.message);
throw error;
}
}
/**
* Saves a file from a given URL to S3.
*
* @param {Object} params
* @param {string} params.userId - The user's unique identifier.
* @param {string} params.URL - The source URL of the file.
* @param {string} params.fileName - The file name to use in S3.
* @param {string} [params.basePath='images'] - The base path in the bucket.
* @returns {Promise<string>} Signed URL of the uploaded file.
*/
async function saveURLToS3({ userId, URL, fileName, basePath = defaultBasePath }) {
try {
const response = await fetch(URL);
const buffer = await response.buffer();
// Optionally you can call getBufferMetadata(buffer) if needed.
return await saveBufferToS3({ userId, buffer, fileName, basePath });
} catch (error) {
logger.error('[saveURLToS3] Error uploading file from URL to S3:', error.message);
throw error;
}
}
/**
* Deletes a file from S3.
*
* @param {Object} params
* @param {string} params.userId - The user's unique identifier.
* @param {string} params.fileName - The file name in S3.
* @param {string} [params.basePath='images'] - The base path in the bucket.
* @returns {Promise<void>}
*/
async function deleteFileFromS3({ userId, fileName, basePath = defaultBasePath }) {
const key = getS3Key(basePath, userId, fileName);
const params = { Bucket: bucketName, Key: key };
try {
await s3.send(new DeleteObjectCommand(params));
logger.debug('[deleteFileFromS3] File deleted successfully from S3');
} catch (error) {
logger.error('[deleteFileFromS3] Error deleting file from S3:', error.message);
// If the file is not found, we can safely return.
if (error.code === 'NoSuchKey') {
return;
}
throw error;
}
}
/**
* Uploads a local file to S3.
*
* @param {Object} params
* @param {import('express').Request} params.req - The Express request (must include user).
* @param {Express.Multer.File} params.file - The file object from Multer.
* @param {string} params.file_id - Unique file identifier.
* @param {string} [params.basePath='images'] - The base path in the bucket.
* @returns {Promise<{ filepath: string, bytes: number }>}
*/
async function uploadFileToS3({ req, file, file_id, basePath = defaultBasePath }) {
try {
const inputFilePath = file.path;
const inputBuffer = await fs.promises.readFile(inputFilePath);
const bytes = Buffer.byteLength(inputBuffer);
const userId = req.user.id;
const fileName = `${file_id}__${path.basename(inputFilePath)}`;
const fileURL = await saveBufferToS3({ userId, buffer: inputBuffer, fileName, basePath });
await fs.promises.unlink(inputFilePath);
return { filepath: fileURL, bytes };
} catch (error) {
logger.error('[uploadFileToS3] Error uploading file to S3:', error.message);
throw error;
}
}
/**
* Retrieves a readable stream for a file stored in S3.
*
* @param {string} filePath - The S3 key of the file.
* @returns {Promise<NodeJS.ReadableStream>}
*/
async function getS3FileStream(filePath) {
const params = { Bucket: bucketName, Key: filePath };
try {
const data = await s3.send(new GetObjectCommand(params));
return data.Body; // Returns a Node.js ReadableStream.
} catch (error) {
logger.error('[getS3FileStream] Error retrieving S3 file stream:', error.message);
throw error;
}
}
module.exports = {
saveBufferToS3,
saveURLToS3,
getS3URL,
deleteFileFromS3,
uploadFileToS3,
getS3FileStream,
};

View File

@@ -1,118 +0,0 @@
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');
const { resizeImageBuffer } = require('../images/resize');
const { updateUser } = require('~/models/userMethods');
const { saveBufferToS3 } = require('./crud');
const { updateFile } = require('~/models/File');
const { logger } = require('~/config');
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 {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.
* @param {string} [params.resolution='high'] - Desired image resolution.
* @param {string} [params.basePath='images'] - Base path in the bucket.
* @returns {Promise<{ filepath: string, bytes: number, width: number, height: number }>}
*/
async function uploadImageToS3({
req,
file,
file_id,
endpoint,
resolution = 'high',
basePath = defaultBasePath,
}) {
try {
const inputFilePath = file.path;
const inputBuffer = await fs.promises.readFile(inputFilePath);
const {
buffer: resizedBuffer,
width,
height,
} = await resizeImageBuffer(inputBuffer, resolution, endpoint);
const extension = path.extname(inputFilePath);
const userId = req.user.id;
let processedBuffer;
let fileName = `${file_id}__${path.basename(inputFilePath)}`;
const targetExtension = `.${req.app.locals.imageOutputType}`;
if (extension.toLowerCase() === targetExtension) {
processedBuffer = resizedBuffer;
} else {
processedBuffer = await sharp(resizedBuffer)
.toFormat(req.app.locals.imageOutputType)
.toBuffer();
fileName = fileName.replace(new RegExp(path.extname(fileName) + '$'), targetExtension);
if (!path.extname(fileName)) {
fileName += targetExtension;
}
}
const downloadURL = await saveBufferToS3({
userId,
buffer: processedBuffer,
fileName,
basePath,
});
await fs.promises.unlink(inputFilePath);
const bytes = Buffer.byteLength(processedBuffer);
return { filepath: downloadURL, bytes, width, height };
} catch (error) {
logger.error('[uploadImageToS3] Error uploading image to S3:', error.message);
throw error;
}
}
/**
* Updates a file record and returns its signed URL.
*
* @param {import('express').Request} req - Express request.
* @param {Object} file - File metadata.
* @returns {Promise<[Promise<any>, string]>}
*/
async function prepareImageURLS3(req, file) {
try {
const updatePromise = updateFile({ file_id: file.file_id });
return Promise.all([updatePromise, file.filepath]);
} catch (error) {
logger.error('[prepareImageURLS3] Error preparing image URL:', error.message);
throw error;
}
}
/**
* Processes a user's avatar image by uploading it to S3 and updating the user's avatar URL if required.
*
* @param {Object} params
* @param {Buffer} params.buffer - Avatar image buffer.
* @param {string} params.userId - User's unique identifier.
* @param {string} params.manual - 'true' or 'false' flag for manual update.
* @param {string} [params.basePath='images'] - Base path in the bucket.
* @returns {Promise<string>} Signed URL of the uploaded avatar.
*/
async function processS3Avatar({ buffer, userId, manual, basePath = defaultBasePath }) {
try {
const downloadURL = await saveBufferToS3({ userId, buffer, fileName: 'avatar.png', basePath });
if (manual === 'true') {
await updateUser(userId, { avatar: downloadURL });
}
return downloadURL;
} catch (error) {
logger.error('[processS3Avatar] Error processing S3 avatar:', error.message);
throw error;
}
}
module.exports = {
uploadImageToS3,
prepareImageURLS3,
processS3Avatar,
};

View File

@@ -1,9 +0,0 @@
const crud = require('./crud');
const images = require('./images');
const initialize = require('./initialize');
module.exports = {
...crud,
...images,
...initialize,
};

View File

@@ -1,43 +0,0 @@
const { S3Client } = require('@aws-sdk/client-s3');
const { logger } = require('~/config');
let s3 = null;
/**
* Initializes and returns an instance of the AWS S3 client.
*
* If AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are provided, they will be used.
* Otherwise, the AWS SDK's default credentials chain (including IRSA) is used.
*
* @returns {S3Client|null} An instance of S3Client if the region is provided; otherwise, null.
*/
const initializeS3 = () => {
if (s3) {
return s3;
}
const region = process.env.AWS_REGION;
if (!region) {
logger.error('[initializeS3] AWS_REGION is not set. Cannot initialize S3.');
return null;
}
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
if (accessKeyId && secretAccessKey) {
s3 = new S3Client({
region,
credentials: { accessKeyId, secretAccessKey },
});
logger.info('[initializeS3] S3 initialized with provided credentials.');
} else {
// When using IRSA, credentials are automatically provided via the IAM Role attached to the ServiceAccount.
s3 = new S3Client({ region });
logger.info('[initializeS3] S3 initialized using default credentials (IRSA).');
}
return s3;
};
module.exports = { initializeS3 };

View File

@@ -29,7 +29,7 @@ const { addAgentResourceFile, removeAgentResourceFiles } = require('~/models/Age
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { createFile, updateFileUsage, deleteFiles } = require('~/models/File');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { checkCapability } = require('~/server/services/Config');
const { getEndpointsConfig } = require('~/server/services/Config');
const { LB_QueueAsyncCall } = require('~/server/utils/queue');
const { getStrategyFunctions } = require('./strategies');
const { determineFileType } = require('~/server/utils');
@@ -457,6 +457,17 @@ const processFileUpload = async ({ req, res, metadata }) => {
res.status(200).json({ message: 'File uploaded and processed successfully', ...result });
};
/**
* @param {ServerRequest} req
* @param {AgentCapabilities} capability
* @returns {Promise<boolean>}
*/
const checkCapability = async (req, capability) => {
const endpointsConfig = await getEndpointsConfig(req);
const capabilities = endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? [];
return capabilities.includes(capability);
};
/**
* Applies the current strategy for file uploads.
* Saves file metadata to the database with an expiry TTL.

View File

@@ -21,17 +21,6 @@ const {
processLocalAvatar,
getLocalFileStream,
} = require('./Local');
const {
getS3URL,
saveURLToS3,
saveBufferToS3,
getS3FileStream,
uploadImageToS3,
prepareImageURLS3,
deleteFileFromS3,
processS3Avatar,
uploadFileToS3,
} = require('./S3');
const { uploadOpenAIFile, deleteOpenAIFile, getOpenAIFileStream } = require('./OpenAI');
const { getCodeOutputDownloadStream, uploadCodeEnvFile } = require('./Code');
const { uploadVectors, deleteVectors } = require('./VectorDB');
@@ -69,22 +58,6 @@ const localStrategy = () => ({
getDownloadStream: getLocalFileStream,
});
/**
* S3 Storage Strategy Functions
*
* */
const s3Strategy = () => ({
handleFileUpload: uploadFileToS3,
saveURL: saveURLToS3,
getFileURL: getS3URL,
deleteFile: deleteFileFromS3,
saveBuffer: saveBufferToS3,
prepareImagePayload: prepareImageURLS3,
processAvatar: processS3Avatar,
handleImageUpload: uploadImageToS3,
getDownloadStream: getS3FileStream,
});
/**
* VectorDB Storage Strategy Functions
*
@@ -187,8 +160,6 @@ const getStrategyFunctions = (fileSource) => {
return openAIStrategy();
} else if (fileSource === FileSources.vectordb) {
return vectorStrategy();
} else if (fileSource === FileSources.s3) {
return s3Strategy();
} else if (fileSource === FileSources.execute_code) {
return codeOutputStrategy();
} else if (fileSource === FileSources.mistral_ocr) {

View File

@@ -362,12 +362,7 @@ async function processRequiredActions(client, requiredActions) {
continue;
}
tool = await createActionTool({
req: client.req,
res: client.res,
action: actionSet,
requestBuilder,
});
tool = await createActionTool({ action: actionSet, requestBuilder });
if (!tool) {
logger.warn(
`Invalid action: user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id} | toolName: ${currentAction.tool}`,

View File

@@ -204,7 +204,6 @@ function generateConfig(key, baseURL, endpoint) {
AgentCapabilities.actions,
AgentCapabilities.tools,
AgentCapabilities.ocr,
AgentCapabilities.chain,
];
}

View File

@@ -5,7 +5,6 @@ const { HttpsProxyAgent } = require('https-proxy-agent');
const { Issuer, Strategy: OpenIDStrategy, custom } = require('openid-client');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { findUser, createUser, updateUser } = require('~/models/userMethods');
const { findGroup } = require('~/models/groupMethods');
const { hashToken } = require('~/server/utils/crypto');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
@@ -106,71 +105,6 @@ function convertToUsername(input, defaultValue = '') {
return defaultValue;
}
/**
* Extracts roles from the specified token using configuration from environment variables.
* @param {object} tokenset - The token set returned by the OpenID provider.
* @returns {Array} The roles extracted from the token.
*/
function extractRoles(tokenset) {
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
const token =
requiredRoleTokenKind === 'access'
? jwtDecode(tokenset.access_token)
: jwtDecode(tokenset.id_token);
const pathParts = requiredRoleParameterPath.split('.');
let found = true;
const roles = pathParts.reduce((acc, key) => {
if (!acc || !(key in acc)) {
found = false;
return [];
}
return acc[key];
}, token);
if (!found) {
logger.error(
`[openidStrategy] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`,
);
}
return roles;
}
/**
* Updates the user's groups based on the provided roles.
* It removes any existing OpenID group references and then adds the groups
* that match the roles from the external group collection.
*
* @param {object} user - The user object.
* @param {Array} roles - The roles extracted from the token.
* @returns {Promise<Array>} The updated groups array.
*/
async function updateUserGroups(user, roles) {
user.groups = user.groups || [];
// Remove existing OpenID group references.
const currentOpenIdGroups = await findGroup({
_id: { $in: user.groups },
provider: 'openid',
});
const currentOpenIdGroupIds = new Set(
currentOpenIdGroups.map((g) => g._id.toString()),
);
user.groups = user.groups.filter(
(id) => !currentOpenIdGroupIds.has(id.toString()),
);
// Look up groups matching the roles.
const matchingGroups = await findGroup({
provider: 'openid',
externalId: { $in: roles },
});
matchingGroups.forEach((group) => {
if (!user.groups.some((id) => id.toString() === group._id.toString())) {
user.groups.push(group._id);
}
});
return user.groups;
}
async function setupOpenId() {
try {
if (process.env.PROXY) {
@@ -200,6 +134,8 @@ async function setupOpenId() {
}
const client = new issuer.Client(clientMetadata);
const requiredRole = process.env.OPENID_REQUIRED_ROLE;
const requiredRoleParameterPath = process.env.OPENID_REQUIRED_ROLE_PARAMETER_PATH;
const requiredRoleTokenKind = process.env.OPENID_REQUIRED_ROLE_TOKEN_KIND;
const openidLogin = new OpenIDStrategy(
{
client,
@@ -209,13 +145,8 @@ async function setupOpenId() {
},
async (tokenset, userinfo, done) => {
try {
logger.info(
`[openidStrategy] verify login openidId: ${userinfo.sub}`,
);
logger.debug('[openidStrategy] verify login tokenset and userinfo', {
tokenset,
userinfo,
});
logger.info(`[openidStrategy] verify login openidId: ${userinfo.sub}`);
logger.debug('[openidStrategy] very login tokenset and userinfo', { tokenset, userinfo });
let user = await findUser({ openidId: userinfo.sub });
logger.info(
@@ -233,10 +164,29 @@ async function setupOpenId() {
const fullName = getFullName(userinfo);
// Check for the required role using extracted roles.
let roles = [];
if (requiredRole) {
roles = extractRoles(tokenset);
let decodedToken = '';
if (requiredRoleTokenKind === 'access') {
decodedToken = jwtDecode(tokenset.access_token);
} else if (requiredRoleTokenKind === 'id') {
decodedToken = jwtDecode(tokenset.id_token);
}
const pathParts = requiredRoleParameterPath.split('.');
let found = true;
let roles = pathParts.reduce((o, key) => {
if (o === null || o === undefined || !(key in o)) {
found = false;
return [];
}
return o[key];
}, decodedToken);
if (!found) {
logger.error(
`[openidStrategy] Key '${requiredRoleParameterPath}' not found in ${requiredRoleTokenKind} token!`,
);
}
if (!roles.includes(requiredRole)) {
return done(null, false, {
message: `You must have the "${requiredRole}" role to log in.`,
@@ -293,10 +243,6 @@ async function setupOpenId() {
}
}
if (requiredRole) {
await updateUserGroups(user, roles);
}
user = await updateUser(user._id, user);
logger.info(

View File

@@ -19,9 +19,6 @@ jest.mock('~/models/userMethods', () => ({
createUser: jest.fn(),
updateUser: jest.fn(),
}));
jest.mock('~/models/groupMethods', () => ({
findGroup: jest.fn().mockResolvedValue([]),
}));
jest.mock('~/server/utils/crypto', () => ({
hashToken: jest.fn().mockResolvedValue('hashed-token'),
}));

View File

@@ -92,7 +92,6 @@ const anthropicModels = {
const deepseekModels = {
'deepseek-reasoner': 63000, // -1000 from max (API)
deepseek: 63000, // -1000 from max (API)
'deepseek.r1': 127500,
};
const metaModels = {

View File

@@ -423,9 +423,6 @@ describe('Meta Models Tests', () => {
expect(getModelMaxTokens('deepseek-reasoner')).toBe(
maxTokensMap[EModelEndpoint.openAI]['deepseek-reasoner'],
);
expect(getModelMaxTokens('deepseek.r1')).toBe(
maxTokensMap[EModelEndpoint.openAI]['deepseek.r1'],
);
});
});

View File

@@ -28,5 +28,4 @@ export type AgentForm = {
provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[];
[AgentCapabilities.artifacts]?: ArtifactModes | string;
recursion_limit?: number;
} & TAgentCapabilities;

View File

@@ -131,7 +131,6 @@ export interface DataColumnMeta {
}
export enum Panel {
advanced = 'advanced',
builder = 'builder',
actions = 'actions',
model = 'model',
@@ -182,7 +181,6 @@ export type AgentPanelProps = {
activePanel?: string;
action?: t.Action;
actions?: t.Action[];
createMutation: UseMutationResult<t.Agent, Error, t.AgentCreateParams>;
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
setAction: React.Dispatch<React.SetStateAction<t.Action | undefined>>;
endpointsConfig?: t.TEndpointsConfig;

View File

@@ -1,7 +1,8 @@
import { useState, useMemo, memo, useCallback } from 'react';
import React, { useState, useMemo, memo, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { Atom, ChevronDown } from 'lucide-react';
import type { MouseEvent, FC } from 'react';
import Markdown from '~/components/Chat/Messages/Content/Markdown';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import store from '~/store';
@@ -21,12 +22,18 @@ const CONTENT_STYLES = {
} as const;
export const ThinkingContent: FC<{ children: React.ReactNode; isPart?: boolean }> = memo(
({ isPart, children }) => (
<div className={CONTENT_STYLES.wrapper}>
<div className={isPart === true ? CONTENT_STYLES.partBorder : CONTENT_STYLES.border} />
<p className={CONTENT_STYLES.text}>{children}</p>
</div>
),
({ isPart, children }) => {
return (
<div className={CONTENT_STYLES.wrapper}>
<div className={isPart === true ? CONTENT_STYLES.partBorder : CONTENT_STYLES.border} />
{typeof children === 'string' ? (
<Markdown content={children} isLatestMessage={false} />
) : (
<p className={CONTENT_STYLES.text}>{children}</p>
)}
</div>
);
},
);
export const ThinkingButton = memo(

View File

@@ -12,8 +12,8 @@ const sourceToClassname = {
[FileSources.openai]: 'bg-white/75 dark:bg-black/65',
[FileSources.azure]: 'azure-bg-color opacity-85',
[FileSources.execute_code]: 'bg-black text-white opacity-85',
[FileSources.text]: 'bg-blue-500 dark:bg-blue-900 opacity-85 text-white',
[FileSources.vectordb]: 'bg-yellow-700 dark:bg-yellow-900 opacity-85 text-white',
[FileSources.text]: 'bg-blue-100 dark:bg-blue-900 opacity-85 text-white',
[FileSources.vectordb]: 'bg-yellow-100 dark:bg-yellow-900 opacity-85 text-white',
};
const defaultClassName =

View File

@@ -76,19 +76,6 @@ const MenuItem: FC<MenuItemProps> = ({
<div>
{title}
<div className="text-text-secondary">{description}</div>
{spec.badges && spec.badges.length > 0 && (
<div className="mt-1 flex gap-2">
{spec.badges.map((badge, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold shadow-sm"
style={{ backgroundColor: badge.color, color: '#fff' }}
>
{badge.text}
</span>
))}
</div>
)}
</div>
</div>
</div>

View File

@@ -19,22 +19,9 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
const localize = useLocalize();
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const modularChat = useRecoilValue(store.modularChat);
const user = useRecoilValue(store.user);
const getDefaultConversation = useDefaultConvo();
const assistantMap = useAssistantsMapContext();
const allowedModelSpecs = useMemo(() => {
if (!modelSpecs) {return [];}
return modelSpecs.filter(spec => {
// If no groups defined for spec, allow it.
if (!spec.groups || spec.groups.length === 0) {return true;}
// Otherwise, check if the user exists and has groups.
if (!user || !user.groups || user.groups.length === 0) {return false;}
// Check if at least one of the spec's groups is in the user's groups.
return spec.groups.some(groupId => user.groups.includes(groupId));
});
}, [modelSpecs, user]);
const onSelectSpec = (spec: TModelSpec) => {
const { preset } = spec;
preset.iconURL = getModelSpecIconURL(spec);
@@ -95,15 +82,21 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
};
const selected = useMemo(() => {
const spec = allowedModelSpecs.find((spec) => spec.name === conversation?.spec);
return spec || undefined;
}, [allowedModelSpecs, conversation?.spec]);
const spec = modelSpecs?.find((spec) => spec.name === conversation?.spec);
if (!spec) {
return undefined;
}
return spec;
}, [modelSpecs, conversation?.spec]);
const menuRef = useRef<HTMLDivElement>(null);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
const menuItems = menuRef.current?.querySelectorAll('[role="option"]');
if (!menuItems || !menuItems.length) {
if (!menuItems) {
return;
}
if (!menuItems.length) {
return;
}
@@ -139,7 +132,7 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
endpointsConfig={endpointsConfig}
/>
<Portal>
{allowedModelSpecs && allowedModelSpecs.length > 0 && (
{modelSpecs && modelSpecs.length && (
<div
style={{
position: 'fixed',
@@ -161,7 +154,7 @@ export default function ModelSpecsMenu({ modelSpecs }: { modelSpecs?: TModelSpec
className="models-scrollbar mt-2 max-h-[65vh] min-w-[340px] max-w-xs overflow-y-auto rounded-lg border border-gray-100 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white lg:max-h-[75vh]"
>
<ModelSpecs
specs={allowedModelSpecs}
specs={modelSpecs}
selected={selected}
setSelected={onSelectSpec}
endpointsConfig={endpointsConfig}

View File

@@ -139,7 +139,6 @@ const ContentParts = memo(
isSubmitting={isSubmitting}
key={`part-${messageId}-${idx}`}
isCreatedByUser={isCreatedByUser}
isLast={idx === content.length - 1}
showCursor={idx === content.length - 1 && isLast}
/>
</MessageContext.Provider>

View File

@@ -166,12 +166,55 @@ export const p: React.ElementType = memo(({ children }: TParagraphProps) => {
return <p className="mb-2 whitespace-pre-wrap">{children}</p>;
});
export const h1: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
return (
<h1 className="mb-2 mt-4 text-3xl font-bold" {...props}>
{children}
</h1>
);
});
export const h2: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
return (
<h2 className="mb-2 mt-4 text-2xl font-bold" {...props}>
{children}
</h2>
);
});
export const h3: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
return (
<h3 className="mb-1 mt-3 text-xl font-bold" {...props}>
{children}
</h3>
);
});
export const ol: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
return (
<ol className="ml-6 list-decimal" {...props}>
{children}
</ol>
);
});
export const ul: React.ElementType = memo(({ children, ...props }: TParagraphProps) => {
return (
<ul className="ml-6 list-disc" {...props}>
{children}
</ul>
);
});
const cursor = ' ';
type TContentProps = {
content: string;
showCursor?: boolean;
isLatestMessage: boolean;
};
const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
const Markdown = memo(({ content = '', showCursor, isLatestMessage }: TContentProps) => {
const LaTeXParsing = useRecoilValue<boolean>(store.LaTeXParsing);
const isInitializing = content === '';
@@ -231,13 +274,18 @@ const Markdown = memo(({ content = '', isLatestMessage }: TContentProps) => {
code,
a,
p,
h1,
h2,
h3,
ol,
ul,
artifact: Artifact,
} as {
[nodeType: string]: React.ElementType;
}
}
>
{currentContent}
{isLatestMessage && (showCursor ?? false) ? currentContent + cursor : currentContent}
</ReactMarkdown>
</CodeBlockProvider>
</ArtifactProvider>

View File

@@ -83,7 +83,9 @@ const DisplayMessage = ({ text, isCreatedByUser, message, showCursor }: TDisplay
let content: React.ReactElement;
if (!isCreatedByUser) {
content = <Markdown content={text} isLatestMessage={isLatestMessage} />;
content = (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
);
} else if (enableUserMsgMarkdown) {
content = <MarkdownLite content={text} />;
} else {

View File

@@ -8,11 +8,9 @@ import {
import { memo } from 'react';
import type { TMessageContentParts, TAttachment } from 'librechat-data-provider';
import { ErrorMessage } from './MessageContent';
import AgentUpdate from './Parts/AgentUpdate';
import ExecuteCode from './Parts/ExecuteCode';
import RetrievalCall from './RetrievalCall';
import Reasoning from './Parts/Reasoning';
import EmptyText from './Parts/EmptyText';
import CodeAnalyze from './CodeAnalyze';
import Container from './Container';
import ToolCall from './ToolCall';
@@ -22,159 +20,145 @@ import Image from './Image';
type PartProps = {
part?: TMessageContentParts;
isLast?: boolean;
isSubmitting: boolean;
showCursor: boolean;
isCreatedByUser: boolean;
attachments?: TAttachment[];
};
const Part = memo(
({ part, isSubmitting, attachments, isLast, showCursor, isCreatedByUser }: PartProps) => {
if (!part) {
const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUser }: PartProps) => {
if (!part) {
return null;
}
if (part.type === ContentTypes.ERROR) {
return (
<ErrorMessage
text={part[ContentTypes.ERROR] ?? part[ContentTypes.TEXT]?.value}
className="my-2"
/>
);
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
return (
<Container>
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
} else if (part.type === ContentTypes.THINK) {
const reasoning = typeof part.think === 'string' ? part.think : part.think.value;
if (typeof reasoning !== 'string') {
return null;
}
return <Reasoning reasoning={reasoning} />;
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
return null;
}
if (part.type === ContentTypes.ERROR) {
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
return (
<ErrorMessage
text={part[ContentTypes.ERROR] ?? part[ContentTypes.TEXT]?.value}
className="my-2"
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (part.type === ContentTypes.AGENT_UPDATE) {
} else if (isToolCall) {
return (
<>
<AgentUpdate currentAgentId={part[ContentTypes.AGENT_UPDATE]?.agentId} />
{isLast && showCursor && (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name || ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
auth={toolCall.auth}
expires_at={toolCall.expires_at}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<EmptyText />
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
)}
</>
);
} else if (part.type === ContentTypes.TEXT) {
const text = typeof part.text === 'string' ? part.text : part.text.value;
if (typeof text !== 'string') {
return null;
}
if (part.tool_call_ids != null && !text) {
return null;
}
return (
<Container>
<Text text={text} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
} else if (part.type === ContentTypes.THINK) {
const reasoning = typeof part.think === 'string' ? part.think : part.think.value;
if (typeof reasoning !== 'string') {
return null;
}
return <Reasoning reasoning={reasoning} />;
} else if (part.type === ContentTypes.TOOL_CALL) {
const toolCall = part[ContentTypes.TOOL_CALL];
if (!toolCall) {
return null;
}
const isToolCall =
'args' in toolCall && (!toolCall.type || toolCall.type === ToolCallTypes.TOOL_CALL);
if (isToolCall && toolCall.name === Tools.execute_code) {
return (
<ExecuteCode
args={typeof toolCall.args === 'string' ? toolCall.args : ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
/>
);
} else if (isToolCall) {
return (
<ToolCall
args={toolCall.args ?? ''}
name={toolCall.name || ''}
output={toolCall.output ?? ''}
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
attachments={attachments}
auth={toolCall.auth}
expires_at={toolCall.expires_at}
/>
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
const code_interpreter = toolCall[ToolCallTypes.CODE_INTERPRETER];
return (
<CodeAnalyze
initialProgress={toolCall.progress ?? 0.1}
code={code_interpreter.input}
outputs={code_interpreter.outputs ?? []}
isSubmitting={isSubmitting}
/>
);
} else if (
toolCall.type === ToolCallTypes.RETRIEVAL ||
toolCall.type === ToolCallTypes.FILE_SEARCH
) {
return (
<RetrievalCall initialProgress={toolCall.progress ?? 0.1} isSubmitting={isSubmitting} />
);
} else if (
toolCall.type === ToolCallTypes.FUNCTION &&
ToolCallTypes.FUNCTION in toolCall &&
imageGenTools.has(toolCall.function.name)
) {
return (
<ImageGen
initialProgress={toolCall.progress ?? 0.1}
args={toolCall.function.arguments as string}
/>
);
} else if (toolCall.type === ToolCallTypes.FUNCTION && ToolCallTypes.FUNCTION in toolCall) {
if (isImageVisionTool(toolCall)) {
if (isSubmitting && showCursor) {
return (
<Container>
<Text text={''} isCreatedByUser={isCreatedByUser} showCursor={showCursor} />
</Container>
);
}
return null;
);
}
return (
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
return null;
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
<ToolCall
initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting}
args={toolCall.function.arguments as string}
name={toolCall.function.name}
output={toolCall.function.output}
/>
);
}
} else if (part.type === ContentTypes.IMAGE_FILE) {
const imageFile = part[ContentTypes.IMAGE_FILE];
const height = imageFile.height ?? 1920;
const width = imageFile.width ?? 1080;
return (
<Image
imagePath={imageFile.filepath}
height={height}
width={width}
altText={imageFile.filename ?? 'Uploaded Image'}
placeholderDimensions={{
height: height + 'px',
width: width + 'px',
}}
/>
);
}
return null;
},
);
return null;
});
export default Part;

View File

@@ -1,39 +0,0 @@
import React, { useMemo } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { useAgentsMapContext } from '~/Providers';
import Icon from '~/components/Endpoints/Icon';
interface AgentUpdateProps {
currentAgentId: string;
}
const AgentUpdate: React.FC<AgentUpdateProps> = ({ currentAgentId }) => {
const agentsMap = useAgentsMapContext() || {};
const currentAgent = useMemo(() => agentsMap[currentAgentId], [agentsMap, currentAgentId]);
if (!currentAgentId) {
return null;
}
return (
<div className="relative">
<div className="absolute -left-6 flex h-full w-4 items-center justify-center">
<div className="relative h-full w-4">
<div className="absolute left-0 top-0 h-1/2 w-px border border-border-medium"></div>
<div className="absolute left-0 top-1/2 h-px w-3 border border-border-medium"></div>
</div>
</div>
<div className="my-4 flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon
endpoint={EModelEndpoint.agents}
agentName={currentAgent?.name ?? ''}
iconURL={currentAgent?.avatar?.filepath}
isCreatedByUser={false}
/>
</div>
<div className="font-medium text-text-primary">{currentAgent?.name}</div>
</div>
</div>
);
};
export default AgentUpdate;

View File

@@ -1,17 +0,0 @@
import { memo } from 'react';
const EmptyTextPart = memo(() => {
return (
<div className="text-message mb-[0.625rem] flex min-h-[20px] flex-col items-start gap-3 overflow-visible">
<div className="markdown prose dark:prose-invert light w-full break-words dark:text-gray-100">
<div className="absolute">
<p className="submitting relative">
<span className="result-thinking" />
</p>
</div>
</div>
</div>
);
});
export default EmptyTextPart;

View File

@@ -29,7 +29,9 @@ const TextPart = memo(({ text, isCreatedByUser, showCursor }: TextPartProps) =>
const content: ContentType = useMemo(() => {
if (!isCreatedByUser) {
return <Markdown content={text} isLatestMessage={isLatestMessage} />;
return (
<Markdown content={text} showCursor={showCursorState} isLatestMessage={isLatestMessage} />
);
} else if (enableUserMsgMarkdown) {
return <MarkdownLite content={text} />;
} else {

View File

@@ -142,7 +142,7 @@ const AdminSettings = () => {
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
className="btn btn-neutral border-token-border-light relative mb-4 h-9 w-full gap-1 rounded-lg font-medium"
>
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
{localize('com_ui_admin_settings')}

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { Settings2 } from 'lucide-react';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
interface AdvancedButtonProps {
setActivePanel: (panel: Panel) => void;
}
const AdvancedButton: React.FC<AdvancedButtonProps> = ({ setActivePanel }) => {
const localize = useLocalize();
return (
<Button
size={'sm'}
variant={'outline'}
className="btn btn-neutral border-token-border-light relative h-9 w-full gap-1 rounded-lg font-medium"
onClick={() => setActivePanel(Panel.advanced)}
>
<Settings2 className="h-4 w-4 cursor-pointer" aria-hidden="true" />
{localize('com_ui_advanced')}
</Button>
);
};
export default AdvancedButton;

View File

@@ -1,55 +0,0 @@
import { useMemo } from 'react';
import { ChevronLeft } from 'lucide-react';
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm, AgentPanelProps } from '~/common';
import MaxAgentSteps from './MaxAgentSteps';
import AgentChain from './AgentChain';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
export default function AdvancedPanel({
agentsConfig,
setActivePanel,
}: Pick<AgentPanelProps, 'setActivePanel' | 'agentsConfig'>) {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, watch } = methods;
const currentAgentId = watch('id');
const chainEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.chain) ?? false,
[agentsConfig],
);
return (
<div className="scrollbar-gutter-stable h-full min-h-[40vh] overflow-auto pb-12 text-sm">
<div className="advanced-panel relative flex flex-col items-center px-16 py-4 text-center">
<div className="absolute left-0 top-4">
<button
type="button"
className="btn btn-neutral relative"
onClick={() => {
setActivePanel(Panel.builder);
}}
>
<div className="advanced-panel-content flex w-full items-center justify-center gap-2">
<ChevronLeft />
</div>
</button>
</div>
<div className="mb-2 mt-2 text-xl font-medium">{localize('com_ui_advanced_settings')}</div>
</div>
<div className="flex flex-col gap-4 px-2">
<MaxAgentSteps />
{chainEnabled && (
<Controller
name="agent_ids"
control={control}
defaultValue={[]}
render={({ field }) => <AgentChain field={field} currentAgentId={currentAgentId} />}
/>
)}
</div>
</div>
);
}

View File

@@ -1,179 +0,0 @@
import { X, Link2, PlusCircle } from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import type { ControllerRenderProps } from 'react-hook-form';
import type { AgentForm, OptionWithIcon } from '~/common';
import ControlCombobox from '~/components/ui/ControlCombobox';
import { HoverCard, HoverCardPortal, HoverCardContent, HoverCardTrigger } from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import { useAgentsMapContext } from '~/Providers';
import Icon from '~/components/Endpoints/Icon';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
interface AgentChainProps {
field: ControllerRenderProps<AgentForm, 'agent_ids'>;
currentAgentId: string;
}
/** TODO: make configurable */
const MAX_AGENTS = 10;
const AgentChain: React.FC<AgentChainProps> = ({ field, currentAgentId }) => {
const localize = useLocalize();
const [newAgentId, setNewAgentId] = useState('');
const agentsMap = useAgentsMapContext() || {};
const agentIds = field.value || [];
const agents = useMemo(() => Object.values(agentsMap), [agentsMap]);
const selectableAgents = useMemo(
() =>
agents
.filter((agent) => agent?.id !== currentAgentId)
.map(
(agent) =>
({
label: agent?.name || '',
value: agent?.id,
icon: (
<Icon
endpoint={EModelEndpoint.agents}
agentName={agent?.name ?? ''}
iconURL={agent?.avatar?.filepath}
isCreatedByUser={false}
/>
),
}) as OptionWithIcon,
),
[agents, currentAgentId],
);
const getAgentDetails = useCallback((id: string) => agentsMap[id], [agentsMap]);
useEffect(() => {
if (newAgentId && agentIds.length < MAX_AGENTS) {
field.onChange([...agentIds, newAgentId]);
setNewAgentId('');
}
}, [newAgentId, agentIds, field]);
const removeAgentAt = (index: number) => {
field.onChange(agentIds.filter((_, i) => i !== index));
};
const updateAgentAt = (index: number, id: string) => {
const updated = [...agentIds];
updated[index] = id;
field.onChange(updated);
};
return (
<HoverCard openDelay={50}>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<label className="font-semibold text-text-primary">
{localize('com_ui_agent_chain')}
</label>
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
</div>
<div className="text-xs text-text-secondary">
{agentIds.length} / {MAX_AGENTS}
</div>
</div>
<div className="space-y-1">
{/* Current fixed agent */}
<div className="flex h-10 items-center justify-between rounded-md border border-border-medium bg-surface-primary-contrast px-3 py-2">
<div className="flex items-center gap-2">
<div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
<Icon
endpoint={EModelEndpoint.agents}
agentName={getAgentDetails(currentAgentId)?.name ?? ''}
iconURL={getAgentDetails(currentAgentId)?.avatar?.filepath}
isCreatedByUser={false}
/>
</div>
<div className="font-medium text-text-primary">
{getAgentDetails(currentAgentId)?.name}
</div>
</div>
</div>
{<Link2 className="mx-auto text-text-secondary" size={14} />}
{agentIds.map((agentId, idx) => (
<React.Fragment key={agentId}>
<div className="flex h-10 items-center gap-2 rounded-md border border-border-medium bg-surface-tertiary pr-2">
<ControlCombobox
isCollapsed={false}
ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_select') })}
selectedValue={agentId}
setValue={(id) => updateAgentAt(idx, id)}
selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_select') })}
searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })}
items={selectableAgents}
displayValue={getAgentDetails(agentId)?.name ?? ''}
SelectIcon={
<Icon
endpoint={EModelEndpoint.agents}
isCreatedByUser={false}
agentName={getAgentDetails(agentId)?.name ?? ''}
iconURL={getAgentDetails(agentId)?.avatar?.filepath}
/>
}
className="flex-1 border-border-heavy"
containerClassName="px-0"
/>
{/* Future Settings button? */}
{/* <button className="hover:bg-surface-hover p-1 rounded transition">
<Settings size={16} className="text-text-secondary" />
</button> */}
<button
className="rounded-xl p-1 transition hover:bg-surface-hover"
onClick={() => removeAgentAt(idx)}
>
<X size={18} className="text-text-secondary" />
</button>
</div>
{idx < agentIds.length - 1 && (
<Link2 className="mx-auto text-text-secondary" size={14} />
)}
</React.Fragment>
))}
{agentIds.length < MAX_AGENTS && (
<>
{agentIds.length > 0 && <Link2 className="mx-auto text-text-secondary" size={14} />}
<ControlCombobox
isCollapsed={false}
ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_add') })}
selectedValue=""
setValue={setNewAgentId}
selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_add') })}
searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })}
items={selectableAgents}
className="h-10 w-full border-dashed border-border-heavy text-center text-text-secondary hover:text-text-primary"
containerClassName="px-0"
SelectIcon={<PlusCircle size={16} className="text-text-secondary" />}
/>
</>
)}
{agentIds.length >= MAX_AGENTS && (
<p className="pt-1 text-center text-xs italic text-text-tertiary">
{localize('com_ui_agent_chain_max', { 0: MAX_AGENTS })}
</p>
)}
</div>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">{localize('com_ui_agent_chain_info')}</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
);
};
export default AgentChain;

View File

@@ -1,52 +0,0 @@
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
HoverCard,
FormInput,
HoverCardPortal,
HoverCardContent,
HoverCardTrigger,
} from '~/components/ui';
import { CircleHelpIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { ESide } from '~/common';
export default function AdvancedPanel() {
const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control } = methods;
return (
<HoverCard openDelay={50}>
<Controller
name="recursion_limit"
control={control}
render={({ field }) => (
<FormInput
field={field}
containerClass="w-1/2"
inputClass="w-full"
label={localize('com_ui_agent_recursion_limit')}
placeholder={localize('com_nav_theme_system')}
type="number"
labelClass="w-fit"
labelAdjacent={
<HoverCardTrigger>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</HoverCardTrigger>
}
/>
)}
/>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_ui_agent_recursion_limit_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
);
}

View File

@@ -1,19 +1,32 @@
import React, { useState, useMemo, useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Controller, useWatch, useFormContext } from 'react-hook-form';
import { QueryKeys, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
import {
QueryKeys,
SystemRoles,
Permissions,
EModelEndpoint,
PermissionTypes,
AgentCapabilities,
} from 'librechat-data-provider';
import type { TPlugin } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import { useToastContext, useFileMapContext } from '~/Providers';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import Action from '~/components/SidePanel/Builder/Action';
import { ToolSelectDialog } from '~/components/Tools';
import DuplicateAgent from './DuplicateAgent';
import { processAgentOption } from '~/utils';
import AdminSettings from './AdminSettings';
import DeleteButton from './DeleteButton';
import AgentAvatar from './AgentAvatar';
import FileContext from './FileContext';
import { useLocalize } from '~/hooks';
import { Spinner } from '~/components';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
import Artifacts from './Artifacts';
import AgentTool from './AgentTool';
import CodeForm from './Code/Form';
@@ -30,10 +43,11 @@ export default function AgentConfig({
setAction,
actions = [],
agentsConfig,
createMutation,
setActivePanel,
endpointsConfig,
setActivePanel,
setCurrentAgentId,
}: AgentPanelProps) {
const { user } = useAuthContext();
const fileMap = useFileMapContext();
const queryClient = useQueryClient();
@@ -52,6 +66,11 @@ export default function AgentConfig({
const tools = useWatch({ control, name: 'tools' });
const agent_id = useWatch({ control, name: 'id' });
const hasAccessToShareAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.SHARED_GLOBAL,
});
const toolsEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.tools),
[agentsConfig],
@@ -137,6 +156,46 @@ export default function AgentConfig({
return _agent.code_files ?? [];
}, [agent, agent_id, fileMap]);
/* Mutations */
const update = useUpdateAgentMutation({
onSuccess: (data) => {
showToast({
message: `${localize('com_assistants_update_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
},
onError: (err) => {
const error = err as Error;
showToast({
message: `${localize('com_agents_update_error')}${
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
}`,
status: 'error',
});
},
});
const create = useCreateAgentMutation({
onSuccess: (data) => {
setCurrentAgentId(data.id);
showToast({
message: `${localize('com_assistants_create_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
},
onError: (err) => {
const error = err as Error;
showToast({
message: `${localize('com_agents_create_error')}${
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
}`,
status: 'error',
});
},
});
const handleAddActions = useCallback(() => {
if (!agent_id) {
showToast({
@@ -166,14 +225,26 @@ export default function AgentConfig({
Icon = icons[iconKey];
}
const renderSaveButton = () => {
if (create.isLoading || update.isLoading) {
return <Spinner className="icon-md" aria-hidden="true" />;
}
if (agent_id) {
return localize('com_ui_save');
}
return localize('com_ui_create');
};
return (
<>
<div className="h-auto bg-white px-4 pt-3 dark:bg-transparent">
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
{/* Avatar & Name */}
<div className="mb-4">
<AgentAvatar
createMutation={create}
agent_id={agent_id}
createMutation={createMutation}
avatar={agent?.['avatar'] ?? null}
/>
<label className={labelClass} htmlFor="name">
@@ -295,12 +366,12 @@ export default function AgentConfig({
</label>
{/* Code Execution */}
{codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />}
{/* File Context (OCR) */}
{ocrEnabled && <FileContext agent_id={agent_id} files={context_files} />}
{/* Artifacts */}
{artifactsEnabled && <Artifacts />}
{/* File Search */}
{fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />}
{/* Artifacts */}
{artifactsEnabled && <Artifacts />}
{/* File Context (OCR) */}
{ocrEnabled && <FileContext agent_id={agent_id} files={context_files} />}
</div>
)}
{/* Agent Tools & Actions */}
@@ -360,6 +431,34 @@ export default function AgentConfig({
</div>
</div>
</div>
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<DeleteButton
agent_id={agent_id}
setCurrentAgentId={setCurrentAgentId}
createMutation={create}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
hasAccessToShareAgents && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
isCollaborative={agent?.isCollaborative}
/>
)}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
type="submit"
disabled={create.isLoading || update.isLoading}
aria-busy={create.isLoading || update.isLoading}
>
{renderSaveButton()}
</button>
</div>
</div>
<ToolSelectDialog
isOpen={showToolDialog}

View File

@@ -1,86 +0,0 @@
import React from 'react';
import { useWatch, useFormContext } from 'react-hook-form';
import { SystemRoles, Permissions, PermissionTypes } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import { useUpdateAgentMutation } from '~/data-provider';
import AdvancedButton from './Advanced/AdvancedButton';
import DuplicateAgent from './DuplicateAgent';
import AdminSettings from './AdminSettings';
import DeleteButton from './DeleteButton';
import { Spinner } from '~/components';
import ShareAgent from './ShareAgent';
import { Panel } from '~/common';
export default function AgentFooter({
activePanel,
createMutation,
updateMutation,
setActivePanel,
setCurrentAgentId,
}: Pick<
AgentPanelProps,
'setCurrentAgentId' | 'createMutation' | 'activePanel' | 'setActivePanel'
> & {
updateMutation: ReturnType<typeof useUpdateAgentMutation>;
}) {
const localize = useLocalize();
const { user } = useAuthContext();
const methods = useFormContext<AgentForm>();
const { control } = methods;
const agent = useWatch({ control, name: 'agent' });
const agent_id = useWatch({ control, name: 'id' });
const hasAccessToShareAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.SHARED_GLOBAL,
});
const renderSaveButton = () => {
if (createMutation.isLoading || updateMutation.isLoading) {
return <Spinner className="icon-md" aria-hidden="true" />;
}
if (agent_id) {
return localize('com_ui_save');
}
return localize('com_ui_create');
};
return (
<div className="mx-1 mb-1 flex w-full flex-col gap-2">
{activePanel !== Panel.advanced && <AdvancedButton setActivePanel={setActivePanel} />}
{user?.role === SystemRoles.ADMIN && <AdminSettings />}
{/* Context Button */}
<div className="flex items-center justify-end gap-2">
<DeleteButton
agent_id={agent_id}
setCurrentAgentId={setCurrentAgentId}
createMutation={createMutation}
/>
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN) &&
hasAccessToShareAgents && (
<ShareAgent
agent_id={agent_id}
agentName={agent?.name ?? ''}
projectIds={agent?.projectIds ?? []}
isCollaborative={agent?.isCollaborative}
/>
)}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
type="submit"
disabled={createMutation.isLoading || updateMutation.isLoading}
aria-busy={createMutation.isLoading || updateMutation.isLoading}
>
{renderSaveButton()}
</button>
</div>
</div>
);
}

View File

@@ -19,10 +19,8 @@ import { useSelectAgent, useLocalize, useAuthContext } from '~/hooks';
import AgentPanelSkeleton from './AgentPanelSkeleton';
import { createProviderOption } from '~/utils';
import { useToastContext } from '~/Providers';
import AdvancedPanel from './Advanced/AdvancedPanel';
import AgentConfig from './AgentConfig';
import AgentSelect from './AgentSelect';
import AgentFooter from './AgentFooter';
import { Button } from '~/components';
import ModelPanel from './ModelPanel';
import { Panel } from '~/common';
@@ -132,7 +130,6 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
} = data;
const model = _model ?? '';
@@ -154,7 +151,6 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
},
});
return;
@@ -179,7 +175,6 @@ export default function AgentPanel({
agent_ids,
end_after_tools,
hide_sequential_outputs,
recursion_limit,
});
},
[agent_id, create, update, showToast, localize],
@@ -281,25 +276,12 @@ export default function AgentPanel({
<AgentConfig
actions={actions}
setAction={setAction}
createMutation={create}
agentsConfig={agentsConfig}
setActivePanel={setActivePanel}
endpointsConfig={endpointsConfig}
setCurrentAgentId={setCurrentAgentId}
/>
)}
{canEditAgent && !agentQuery.isInitialLoading && activePanel === Panel.advanced && (
<AdvancedPanel setActivePanel={setActivePanel} agentsConfig={agentsConfig} />
)}
{canEditAgent && !agentQuery.isInitialLoading && (
<AgentFooter
createMutation={create}
updateMutation={update}
activePanel={activePanel}
setActivePanel={setActivePanel}
setCurrentAgentId={setCurrentAgentId}
/>
)}
</form>
</FormProvider>
);

View File

@@ -3,7 +3,7 @@ import { Skeleton } from '~/components/ui';
export default function AgentPanelSkeleton() {
return (
<div className="h-auto bg-white dark:bg-transparent">
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
{/* Avatar */}
<div className="mb-4">
<div className="flex w-full items-center justify-center gap-4">

View File

@@ -81,29 +81,10 @@ export default function AgentSelect({
return;
}
if (capabilities[name] !== undefined) {
formValues[name] = value;
return;
}
if (
name === 'agent_ids' &&
Array.isArray(value) &&
value.every((item) => typeof item === 'string')
) {
formValues[name] = value;
return;
}
if (!keys.has(name)) {
return;
}
if (name === 'recursion_limit' && typeof value === 'number') {
formValues[name] = value;
return;
}
if (typeof value !== 'number' && typeof value !== 'object') {
formValues[name] = value;
}

View File

@@ -77,7 +77,7 @@ export default function Parameters({
};
return (
<div className="mx-1 mb-1 flex h-full min-h-[50vh] w-full flex-col gap-2 text-sm">
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
<div className="model-panel relative flex flex-col items-center px-16 py-4 text-center">
<div className="absolute left-0 top-4">
<button
@@ -224,17 +224,19 @@ export default function Parameters({
);
})}
</div>
{/* Reset Parameters Button */}
<div className="mt-6 flex justify-center">
<button
type="button"
onClick={handleResetParameters}
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</div>
</div>
)}
{/* Reset Parameters Button */}
<button
type="button"
onClick={handleResetParameters}
className="btn btn-neutral my-1 flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</div>
);
}

View File

@@ -0,0 +1,74 @@
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AgentForm } from '~/common';
import {
Checkbox,
HoverCard,
// HoverCardContent,
// HoverCardPortal,
// HoverCardTrigger,
} from '~/components/ui';
// import { CircleHelpIcon } from '~/components/svg';
// import { useLocalize } from '~/hooks';
// import { ESide } from '~/common';
export default function HideSequential() {
// const localize = useLocalize();
const methods = useFormContext<AgentForm>();
const { control, setValue, getValues } = methods;
return (
<>
<HoverCard openDelay={50}>
<div className="my-2 flex items-center">
<Controller
name={AgentCapabilities.hide_sequential_outputs}
control={control}
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value?.toString()}
/>
)}
/>
<button
type="button"
className="flex items-center space-x-2"
onClick={() =>
setValue(
AgentCapabilities.hide_sequential_outputs,
!getValues(AgentCapabilities.hide_sequential_outputs),
{
shouldDirty: true,
},
)
}
>
<label
className="form-check-label text-token-text-primary w-full cursor-pointer"
htmlFor={AgentCapabilities.hide_sequential_outputs}
>
Hide Sequential Agent Outputs except the last agent&apos;s
</label>
{/* <HoverCardTrigger>
<CircleHelpIcon className="h-5 w-5 text-gray-500" />
</HoverCardTrigger> */}
</button>
{/* <HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">
<div className="space-y-2">
<p className="text-sm text-text-secondary">
{localize('com_agents_ttg_info')}
</p>
</div>
</HoverCardContent>
</HoverCardPortal> */}
</div>
</HoverCard>
</>
);
}

View File

@@ -0,0 +1,153 @@
import { Plus, X } from 'lucide-react';
import React, { useRef, useState } from 'react';
import { Transition } from 'react-transition-group';
import { Constants } from 'librechat-data-provider';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
import { TooltipAnchor } from '~/components/ui';
import HideSequential from './HideSequential';
interface SequentialAgentsProps {
field: {
value: string[];
onChange: (value: string[]) => void;
};
}
const labelClass = 'mb-2 text-token-text-primary block font-medium';
const inputClass = cn(
defaultTextProps,
'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800 rounded-xl mb-2',
removeFocusOutlines,
);
const maxAgents = 5;
const SequentialAgents: React.FC<SequentialAgentsProps> = ({ field }) => {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
const nodeRef = useRef(null);
const [newAgentId, setNewAgentId] = useState('');
const handleAddAgentId = () => {
if (newAgentId.trim() && field.value.length < maxAgents) {
const newValues = [...field.value, newAgentId];
field.onChange(newValues);
setNewAgentId('');
}
};
const handleDeleteAgentId = (index: number) => {
const newValues = field.value.filter((_, i) => i !== index);
field.onChange(newValues);
};
const defaultStyle = {
transition: 'opacity 200ms ease-in-out',
opacity: 0,
};
const triggerShake = (element: HTMLElement) => {
element.classList.remove('shake');
void element.offsetWidth;
element.classList.add('shake');
setTimeout(() => {
element.classList.remove('shake');
}, 200);
};
const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
};
const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS;
return (
<div className="relative">
<label className={labelClass} htmlFor="agent_ids">
Sequential Agents
</label>
<div className="mt-4 space-y-2">
<HideSequential />
{/* Display existing agents first */}
{field.value.map((agentId, index) => (
<div key={index} className="relative">
<input
ref={(el) => (inputRefs.current[index] = el)}
value={agentId}
onChange={(e) => {
const newValue = [...field.value];
newValue[index] = e.target.value;
field.onChange(newValue);
}}
className={`${inputClass} pr-10`}
type="text"
maxLength={64}
/>
<TooltipAnchor
side="top"
description={'Remove agent ID'}
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={() => handleDeleteAgentId(index)}
>
<X className="size-4" />
</TooltipAnchor>
</div>
))}
{/* Input for new agent at the bottom */}
<div className="relative">
<input
ref={(el) => (inputRefs.current[field.value.length] = el)}
value={newAgentId}
maxLength={64}
className={`${inputClass} pr-10`}
type="text"
placeholder={hasReachedMax ? 'Max agents reached' : 'Enter agent ID (e.g. agent_1234)'}
onChange={(e) => setNewAgentId(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (hasReachedMax) {
triggerShake(e.currentTarget);
} else {
handleAddAgentId();
}
}
}}
/>
<Transition
nodeRef={nodeRef}
in={field.value.length < Constants.MAX_CONVO_STARTERS}
timeout={200}
unmountOnExit
>
{(state: string) => (
<div
ref={nodeRef}
style={{
...defaultStyle,
...transitionStyles[state as keyof typeof transitionStyles],
transition: state === 'entering' ? 'none' : defaultStyle.transition,
}}
className="absolute right-1 top-1"
>
<TooltipAnchor
side="top"
description={hasReachedMax ? 'Max agents reached' : 'Add agent ID'}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={handleAddAgentId}
disabled={hasReachedMax}
>
<Plus className="size-4" />
</TooltipAnchor>
</div>
)}
</Transition>
</div>
</div>
</div>
);
};
export default SequentialAgents;

View File

@@ -665,7 +665,6 @@ export const settings: Record<string, SettingsConfiguration | undefined> = {
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.DeepSeek}`]: bedrockGeneral,
[EModelEndpoint.google]: googleConfig,
};
@@ -709,7 +708,6 @@ export const presetSettings: Record<
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.DeepSeek}`]: bedrockGeneralColumns,
[EModelEndpoint.google]: {
col1: googleCol1,
col2: googleCol2,

View File

@@ -1,62 +0,0 @@
import React from 'react';
import { Label, Input } from '~/components/ui';
import { cn } from '~/utils';
export default function FormInput({
field,
label,
labelClass,
inputClass,
containerClass,
labelAdjacent,
placeholder = '',
type = 'string',
}: {
field: any;
label: string;
labelClass?: string;
inputClass?: string;
placeholder?: string;
containerClass?: string;
type?: 'string' | 'number';
labelAdjacent?: React.ReactNode;
}) {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (type !== 'number') {
field.onChange(value);
return;
}
if (value === '') {
field.onChange(value);
} else if (!isNaN(Number(value))) {
field.onChange(Number(value));
}
};
return (
<div className={cn('flex w-full flex-col items-center gap-2', containerClass)}>
<div className="flex w-full items-center justify-start gap-2">
<Label
htmlFor={`${field.name}-input`}
className={cn('text-left text-sm font-semibold text-text-primary', labelClass)}
>
{label}
</Label>
{labelAdjacent}
</div>
<Input
id={`${field.name}-input`}
value={field.value ?? ''}
onChange={handleChange}
placeholder={placeholder}
className={cn(
'flex h-10 max-h-10 w-full resize-none border-none bg-surface-secondary px-3 py-2',
inputClass,
)}
/>
</div>
);
}

View File

@@ -29,7 +29,6 @@ export * from './InputOTP';
export { default as Combobox } from './Combobox';
export { default as Dropdown } from './Dropdown';
export { default as FileUpload } from './FileUpload';
export { default as FormInput } from './FormInput';
export { default as DropdownPopup } from './DropdownPopup';
export { default as DelayedRender } from './DelayedRender';
export { default as ThemeSelector } from './ThemeSelector';

View File

@@ -23,7 +23,6 @@ type TStepEvent = {
event: string;
data:
| Agents.MessageDeltaEvent
| Agents.AgentUpdate
| Agents.RunStep
| Agents.ToolEndEvent
| {
@@ -88,17 +87,6 @@ export default function useStepHandler({
if (contentPart.tool_call_ids != null) {
update.tool_call_ids = contentPart.tool_call_ids;
}
updatedContent[index] = update;
} else if (
contentType.startsWith(ContentTypes.AGENT_UPDATE) &&
ContentTypes.AGENT_UPDATE in contentPart &&
contentPart.agent_update
) {
const update: Agents.AgentUpdate = {
type: ContentTypes.AGENT_UPDATE,
agent_update: contentPart.agent_update,
};
updatedContent[index] = update;
} else if (
contentType.startsWith(ContentTypes.THINK) &&
@@ -203,20 +191,29 @@ export default function useStepHandler({
});
}
} else if (event === 'on_agent_update') {
const { agent_update } = data as Agents.AgentUpdate;
const responseMessageId = agent_update.runId || '';
const { runId, message } = data as { runId?: string; message: string };
const responseMessageId = runId ?? '';
if (!responseMessageId) {
console.warn('No message id found in agent update event');
return;
}
const response = messageMap.current.get(responseMessageId);
if (response) {
const updatedResponse = updateContent(response, agent_update.index, data);
messageMap.current.set(responseMessageId, updatedResponse);
const currentMessages = getMessages() || [];
setMessages([...currentMessages.slice(0, -1), updatedResponse]);
}
const responseMessage = messages[messages.length - 1] as TMessage;
const response = {
...responseMessage,
parentMessageId: userMessage.messageId,
conversationId: userMessage.conversationId,
messageId: responseMessageId,
content: [
{
type: ContentTypes.TEXT,
text: message,
},
],
} as TMessage;
setMessages([...messages.slice(0, -1), response]);
} else if (event === 'on_message_delta') {
const messageDelta = data as Agents.MessageDeltaEvent;
const runStep = stepMap.current.get(messageDelta.id);

View File

@@ -465,20 +465,13 @@
"com_ui_admin_access_warning": "Disabling Admin access to this feature may cause unexpected UI issues requiring refresh. If saved, the only way to revert is via the interface setting in librechat.yaml config which affects all roles.",
"com_ui_admin_settings": "Admin Settings",
"com_ui_advanced": "Advanced",
"com_ui_advanced_settings": "Advanced Settings",
"com_ui_agent": "Agent",
"com_ui_agent_chain": "Agent Chain (Mixture-of-Agents)",
"com_ui_agent_chain_info": "Enables creating sequences of agents. Each agent can access outputs from previous agents in the chain. Based on the \"Mixture-of-Agents\" architecture where agents use previous outputs as auxiliary information.",
"com_ui_agent_chain_max": "You have reached the maximum of {{0}} agents.",
"com_ui_agent_delete_error": "There was an error deleting the agent",
"com_ui_agent_deleted": "Successfully deleted agent",
"com_ui_agent_duplicate_error": "There was an error duplicating the agent",
"com_ui_agent_duplicated": "Agent duplicated successfully",
"com_ui_agent_editing_allowed": "Other users can already edit this agent",
"com_ui_agent_recursion_limit": "Max Agent Steps",
"com_ui_agent_recursion_limit_info": "Limits how many steps the agent can take in a run before giving a final response. Default is 25 steps. A step is either an AI API request or a tool usage round. For example, a basic tool interaction takes 3 steps: initial request, tool usage, and follow-up request.",
"com_ui_agent_shared_to_all": "something needs to go here. was empty",
"com_ui_agent_var": "{{0}} agent",
"com_ui_agents": "Agents",
"com_ui_agents_allow_create": "Allow creating Agents",
"com_ui_agents_allow_share_global": "Allow sharing Agents to all users",
@@ -826,9 +819,9 @@
"com_ui_upload_files": "Upload files",
"com_ui_upload_image": "Upload an image",
"com_ui_upload_image_input": "Upload Image",
"com_ui_upload_ocr_text": "Upload as Text",
"com_ui_upload_invalid": "Invalid file for upload. Must be an image not exceeding the limit",
"com_ui_upload_invalid_var": "Invalid file for upload. Must be an image not exceeding {{0}} MB",
"com_ui_upload_ocr_text": "Upload as Text",
"com_ui_upload_success": "Successfully uploaded file",
"com_ui_upload_type": "Select Upload Type",
"com_ui_use_2fa_code": "Use 2FA Code Instead",
@@ -847,4 +840,4 @@
"com_ui_zoom": "Zoom",
"com_user_message": "You",
"com_warning_resubmit_unsupported": "Resubmitting the AI message is not supported for this endpoint."
}
}

View File

@@ -11,9 +11,6 @@
"com_agents_create_error": "Houve um erro ao criar seu agente.",
"com_agents_description_placeholder": "Opcional: Descreva seu Agente aqui",
"com_agents_enable_file_search": "Habilitar pesquisa de arquivos",
"com_agents_file_context": "Contexto de arquivo (OCR)",
"com_agents_file_context_disabled": "O agente deve ser criado antes de carregar arquivos para o Contexto de Arquivo.",
"com_agents_file_context_info": "Os arquivos carregados como \"Contexto\" são processados usando OCR para extrair texto, que é então adicionado às instruções do Agente. Ideal para documentos, imagens com texto ou PDFs onde você precisa do conteúdo de texto completo de um arquivo",
"com_agents_file_search_disabled": "O agente deve ser criado antes de carregar arquivos para Pesquisa de Arquivos.",
"com_agents_file_search_info": "Quando ativado, o agente será informado dos nomes exatos dos arquivos listados abaixo, permitindo que ele recupere o contexto relevante desses arquivos.",
"com_agents_instructions_placeholder": "As instruções do sistema que o agente usa",
@@ -814,14 +811,12 @@
"com_ui_upload_code_files": "Carregar para o interpretador de código",
"com_ui_upload_delay": "O upload de \"{{0}}\" está demorando mais do que o esperado. Por favor, aguarde enquanto o arquivo termina de ser indexado para recuperação.",
"com_ui_upload_error": "Houve um erro ao carregar seu arquivo",
"com_ui_upload_file_context": "Contexto de upload de arquivo",
"com_ui_upload_file_search": "Upload para pesquisa de arquivos",
"com_ui_upload_files": "Carregar arquivos",
"com_ui_upload_image": "Carregar uma imagem",
"com_ui_upload_image_input": "Upload de imagem",
"com_ui_upload_invalid": "Arquivo inválido para upload. Deve ser uma imagem não excedendo o limite",
"com_ui_upload_invalid_var": "Arquivo inválido para upload. Deve ser uma imagem não excedendo {{0}} MB",
"com_ui_upload_ocr_text": "Carregar como texto",
"com_ui_upload_success": "Arquivo carregado com sucesso",
"com_ui_upload_type": "Selecione o tipo de upload",
"com_ui_use_2fa_code": "Use o código 2FA em vez disso",

View File

@@ -361,14 +361,4 @@ div[role="tabpanel"][data-state="active"][data-orientation="horizontal"][aria-la
.cm-content:focus {
outline: none !important;
}
p.whitespace-pre-wrap a, li a {
color: #0066cc;
text-decoration: underline;
font-weight: bold;
}
.dark p.whitespace-pre-wrap a, .dark li a {
color: #52a0ff;
}

View File

@@ -1,69 +0,0 @@
const path = require('path');
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const { askQuestion, silentExit } = require('./helpers');
const User = require('~/models/User');
const Group = require('~/models/Group');
const connect = require('./connect');
(async () => {
await connect();
console.purple('---------------------------------------');
console.purple('Assign a Group to a User');
console.purple('---------------------------------------');
// Read arguments from CLI or prompt the user
const userEmail = process.argv[2] || (await askQuestion('User email: '));
const groupName = process.argv[3] || (await askQuestion('Group name to assign: '));
// Validate email format
if (!userEmail.includes('@')) {
console.red('Error: Invalid email address!');
silentExit(1);
}
// Find the group by name
const group = await Group.findOne({ name: groupName });
if (!group) {
console.red('Error: No group with that name was found!');
silentExit(1);
}
// Find the user by email
const user = await User.findOne({ email: userEmail });
if (!user) {
console.red('Error: No user with that email was found!');
silentExit(1);
}
console.purple(`Found user: ${user.email}`);
// Assign the group to the user if not already assigned
try {
if (!Array.isArray(user.groups)) {
user.groups = [];
}
// Convert both user group IDs and the target group ID to strings for comparison
const groupIdStr = group._id.toString();
const userGroupIds = user.groups.map(id => id.toString());
if (!userGroupIds.includes(groupIdStr)) {
user.groups.push(group._id);
await user.save();
console.green(`User ${user.email} successfully assigned to group ${group.name}!`);
} else {
console.yellow(`User ${user.email} is already assigned to group ${group.name}.`);
}
} catch (error) {
console.red('Error assigning group to user: ' + error.message);
silentExit(1);
}
silentExit(0);
})();
process.on('uncaughtException', (err) => {
console.error('There was an uncaught error:');
console.error(err);
process.exit(1);
});

View File

@@ -1,62 +0,0 @@
const path = require('path');
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const { askQuestion, silentExit } = require('./helpers');
const Group = require('~/models/Group');
const connect = require('./connect');
(async () => {
await connect();
console.purple('---------------------------------------');
console.purple('Create a New Group');
console.purple('---------------------------------------');
// Prompt for basic group info.
const groupName = process.argv[2] || (await askQuestion('Group name: '));
const groupDescription =
process.argv[3] || (await askQuestion('Group description (optional): '));
// Ask for the group type (local or openid; defaults to local)
let groupType =
process.argv[4] ||
(await askQuestion('Group type (local/openid, default is local): '));
groupType = groupType.trim().toLowerCase() || 'local';
let groupData;
if (groupType === 'openid') {
// For OpenID groups, prompt for an external ID.
const externalId =
process.argv[5] ||
(await askQuestion('External ID for OpenID group: '));
groupData = {
name: groupName,
description: groupDescription,
provider: 'openid',
externalId: externalId.trim(),
};
} else {
// For local groups, we only need name and description.
groupData = {
name: groupName,
description: groupDescription,
provider: 'local',
};
}
// Create the group document
let group;
try {
group = await Group.create(groupData);
} catch (error) {
console.red('Error creating group: ' + error.message);
silentExit(1);
}
console.green(`Group created successfully with id: ${group._id}`);
silentExit(0);
})();
process.on('uncaughtException', (err) => {
console.error('There was an uncaught error:');
console.error(err);
process.exit(1);
});

View File

@@ -2,7 +2,7 @@ const path = require('path');
const { v5: uuidv5 } = require('uuid');
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const { askQuestion, askMultiLineQuestion, silentExit } = require('./helpers');
const { Banner } = require('~/models/Banner');
const Banner = require('~/models/schema/banner');
const connect = require('./connect');
(async () => {

View File

@@ -7,9 +7,6 @@ version: 1.2.1
# Cache settings: Set to true to enable caching
cache: true
# File strategy s3/firebase
# fileStrategy: "s3"
# Custom interface configuration
interface:
customWelcome: "Welcome to LibreChat! Enjoy your experience."
@@ -104,31 +101,6 @@ registration:
# userMax: 50
# userWindowInMinutes: 60 # Rate limit window for conversation imports per user
# Example Model Specifications
#modelSpecs:
# enforce: true
# prioritize: true
# list:
# - name: "4o-mini"
# label: "4o-mini"
# groups:
# - "67b9a5c64165f31925e9b25a"
# - "67b9a5c64165f31925e9b25f"
# description: "The most advanced frontier model from Azure OpenAI, suitable to solve complex multi-step problems."
# iconURL: "https://www.librechat.ai/librechat.png"
# badges:
# - text: "Test"
# color: "#FF0000"
# - text: "Beta"
# color: "#00FF00"
# preset:
# default: true
# endpoint: "azureOpenAI"
# model: "gpt-4o-mini"
# modelLabel: "4o-mini"
# Example Actions Object Structure
actions:
allowedDomains:
@@ -137,32 +109,32 @@ actions:
- "google.com"
# Example MCP Servers Object Structure
# mcpServers:
# everything:
# # type: sse # type can optionally be omitted
# url: http://localhost:3001/sse
# timeout: 60000 # 1 minute timeout for this server, this is the default timeout for MCP servers.
# puppeteer:
# type: stdio
# command: npx
# args:
# - -y
# - "@modelcontextprotocol/server-puppeteer"
# timeout: 300000 # 5 minutes timeout for this server
# filesystem:
# # type: stdio
# command: npx
# args:
# - -y
# - "@modelcontextprotocol/server-filesystem"
# - /home/user/LibreChat/
# iconPath: /home/user/LibreChat/client/public/assets/logo.svg
# mcp-obsidian:
# command: npx
# args:
# - -y
# - "mcp-obsidian"
# - /path/to/obsidian/vault
mcpServers:
everything:
# type: sse # type can optionally be omitted
url: http://localhost:3001/sse
timeout: 60000 # 1 minute timeout for this server, this is the default timeout for MCP servers.
puppeteer:
type: stdio
command: npx
args:
- -y
- "@modelcontextprotocol/server-puppeteer"
timeout: 300000 # 5 minutes timeout for this server
filesystem:
# type: stdio
command: npx
args:
- -y
- "@modelcontextprotocol/server-filesystem"
- /home/user/LibreChat/
iconPath: /home/user/LibreChat/client/public/assets/logo.svg
mcp-obsidian:
command: npx
args:
- -y
- "mcp-obsidian"
- /path/to/obsidian/vault
# Definition of custom endpoints
endpoints:
@@ -180,10 +152,8 @@ endpoints:
# # (optional) Assistant Capabilities available to all users. Omit the ones you wish to exclude. Defaults to list below.
# capabilities: ["code_interpreter", "retrieval", "actions", "tools", "image_vision"]
# agents:
# (optional) Default recursion depth for agents, defaults to 25
# (optional) Maximum recursion depth for agents, defaults to 25
# recursionLimit: 50
# (optional) Max recursion depth for agents, defaults to 25
# maxRecursionLimit: 100
# (optional) Disable the builder interface for agents
# disableBuilder: false
# (optional) Agent Capabilities available to all users. Omit the ones you wish to exclude. Defaults to list below.

4765
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,8 +32,6 @@
"reset-password": "node config/reset-password.js",
"ban-user": "node config/ban-user.js",
"delete-user": "node config/delete-user.js",
"create-group": "node config/create-group.js",
"assign-group": "node config/assign-group.js",
"update-banner": "node config/update-banner.js",
"delete-banner": "node config/delete-banner.js",
"backend": "cross-env NODE_ENV=production node api/server/index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.7.73",
"version": "0.7.71",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",

View File

@@ -168,7 +168,6 @@ export enum AgentCapabilities {
artifacts = 'artifacts',
actions = 'actions',
tools = 'tools',
chain = 'chain',
ocr = 'ocr',
}
@@ -235,7 +234,6 @@ export const agentsEndpointSChema = baseEndpointSchema.merge(
/* agents specific */
recursionLimit: z.number().optional(),
disableBuilder: z.boolean().optional(),
maxRecursionLimit: z.number().optional(),
capabilities: z
.array(z.nativeEnum(AgentCapabilities))
.optional()
@@ -246,7 +244,6 @@ export const agentsEndpointSChema = baseEndpointSchema.merge(
AgentCapabilities.actions,
AgentCapabilities.tools,
AgentCapabilities.ocr,
AgentCapabilities.chain,
]),
}),
);
@@ -830,29 +827,28 @@ export const supportsBalanceCheck = {
};
export const visionModels = [
'qwen-vl',
'grok-vision',
'grok-2-vision',
'grok-3',
'gpt-4o-mini',
'grok-2-vision',
'grok-vision',
'gpt-4.5',
'gpt-4o',
'gpt-4o-mini',
'o1',
'gpt-4-turbo',
'gpt-4-vision',
'o1',
'gpt-4.5',
'llava',
'llava-13b',
'gemini-pro-vision',
'claude-3',
'gemini-exp',
'gemini-1.5',
'gemini-2.0',
'gemini-1.5',
'gemini-exp',
'moondream',
'llama3.2-vision',
'llama-3.2-11b-vision',
'llama-3-2-11b-vision',
'llama-3.2-90b-vision',
'llama-3.2-11b-vision',
'llama-3-2-90b-vision',
'llama-3-2-11b-vision',
];
export enum VisionModes {
generative = 'generative',
@@ -1194,7 +1190,7 @@ export enum Constants {
/** Key for the app's version. */
VERSION = 'v0.7.7',
/** Key for the Custom Config's version (librechat.yaml). */
CONFIG_VERSION = '1.2.3',
CONFIG_VERSION = '1.2.2',
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
NO_PARENT = '00000000-0000-0000-0000-000000000000',
/** Standard value for the initial conversationId before a request is sent */

View File

@@ -4,7 +4,6 @@ import { extractEnvVariable } from './utils';
const BaseOptionsSchema = z.object({
iconPath: z.string().optional(),
timeout: z.number().optional(),
initTimeout: z.number().optional(),
});
export const StdioOptionsSchema = BaseOptionsSchema.extend({
@@ -86,26 +85,3 @@ export const MCPOptionsSchema = z.union([
]);
export const MCPServersSchema = z.record(z.string(), MCPOptionsSchema);
export type MCPOptions = z.infer<typeof MCPOptionsSchema>;
/**
* Recursively processes an object to replace environment variables in string values
* @param {MCPOptions} obj - The object to process
* @returns {MCPOptions} - The processed object with environment variables replaced
*/
export function processMCPEnv(obj: MCPOptions): MCPOptions {
if (obj === null || obj === undefined) {
return obj;
}
if ('env' in obj && obj.env) {
const processedEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(obj.env)) {
processedEnv[key] = extractEnvVariable(value);
}
obj.env = processedEnv;
}
return obj;
}

View File

@@ -8,11 +8,6 @@ import {
authTypeSchema,
} from './schemas';
export type TBadge = {
text: string;
color: string;
};
export type TModelSpec = {
name: string;
label: string;
@@ -24,15 +19,8 @@ export type TModelSpec = {
showIconInHeader?: boolean;
iconURL?: string | EModelEndpoint; // Allow using project-included icons
authType?: AuthType;
groups?: string[];
badges?: TBadge[];
};
export const tBadgeSchema = z.object({
text: z.string(),
color: z.string(),
});
export const tModelSpecSchema = z.object({
name: z.string(),
label: z.string(),
@@ -44,8 +32,6 @@ export const tModelSpecSchema = z.object({
showIconInHeader: z.boolean().optional(),
iconURL: z.union([z.string(), eModelEndpointSchema]).optional(),
authType: authTypeSchema.optional(),
groups: z.array(z.string()).optional(),
badges: z.array(tBadgeSchema).optional(),
});
export const specsConfigSchema = z.object({

View File

@@ -47,7 +47,6 @@ export enum BedrockProviders {
Meta = 'meta',
MistralAI = 'mistral',
StabilityAI = 'stability',
DeepSeek = 'deepseek',
}
export const getModelKey = (endpoint: EModelEndpoint | string, model: string) => {
@@ -158,7 +157,6 @@ export const defaultAgentFormValues = {
projectIds: [],
artifacts: '',
isCollaborative: false,
recursion_limit: undefined,
[Tools.execute_code]: false,
[Tools.file_search]: false,
};

View File

@@ -106,16 +106,6 @@ export type TBackupCode = {
usedAt: Date | null;
};
export type TGroup = {
id: string;
name: string;
description?: string;
externalId?: string;
provider: 'local' | 'openid';
createdAt?: string;
updatedAt?: string;
};
export type TUser = {
id: string;
username: string;
@@ -127,7 +117,6 @@ export type TUser = {
plugins?: string[];
twoFactorEnabled?: boolean;
backupCodes?: TBackupCode[];
groups: string[];
createdAt: string;
updatedAt: string;
};

View File

@@ -19,15 +19,6 @@ export namespace Agents {
tool_call_ids?: string[];
};
export type AgentUpdate = {
type: ContentTypes.AGENT_UPDATE;
agent_update: {
index: number;
runId: string;
agentId: string;
};
};
export type MessageContentImageUrl = {
type: ContentTypes.IMAGE_URL;
image_url: string | { url: string; detail?: ImageDetail };
@@ -35,7 +26,6 @@ export namespace Agents {
export type MessageContentComplex =
| ReasoningContentText
| AgentUpdate
| MessageContentText
| MessageContentImageUrl
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -169,7 +159,12 @@ export namespace Agents {
index: number; // #new
stepIndex?: number; // #new
stepDetails: StepDetails;
usage: null | object;
usage: null | {
// Define usage structure if it's ever non-null
// prompt_tokens: number; // #new
// completion_tokens: number; // #new
// total_tokens: number; // #new
};
};
/**
* Represents a run step delta i.e. any changed fields on a run step during

View File

@@ -222,7 +222,6 @@ export type Agent = {
end_after_tools?: boolean;
hide_sequential_outputs?: boolean;
artifacts?: ArtifactModes;
recursion_limit?: number;
};
export type TAgentsMap = Record<string, Agent | undefined>;
@@ -237,10 +236,7 @@ export type AgentCreateParams = {
provider: AgentProvider;
model: string | null;
model_parameters: AgentModelParameters;
} & Pick<
Agent,
'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs' | 'artifacts' | 'recursion_limit'
>;
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs' | 'artifacts'>;
export type AgentUpdateParams = {
name?: string | null;
@@ -256,10 +252,7 @@ export type AgentUpdateParams = {
projectIds?: string[];
removeProjectIds?: string[];
isCollaborative?: boolean;
} & Pick<
Agent,
'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs' | 'artifacts' | 'recursion_limit'
>;
} & Pick<Agent, 'agent_ids' | 'end_after_tools' | 'hide_sequential_outputs' | 'artifacts'>;
export type AgentListParams = {
limit?: number;
@@ -462,7 +455,6 @@ export type TMessageContentParts =
PartMetadata;
}
| { type: ContentTypes.IMAGE_FILE; image_file: ImageFile & PartMetadata }
| Agents.AgentUpdate
| Agents.MessageContentImageUrl;
export type StreamContentData = TMessageContentParts & {

View File

@@ -5,7 +5,6 @@ export enum ContentTypes {
TOOL_CALL = 'tool_call',
IMAGE_FILE = 'image_file',
IMAGE_URL = 'image_url',
AGENT_UPDATE = 'agent_update',
ERROR = 'error',
}

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/data-schemas",
"version": "0.0.3",
"version": "0.0.2",
"type": "module",
"description": "Mongoose schemas and models for LibreChat",
"main": "dist/index.cjs",

View File

@@ -7,7 +7,6 @@ import categoriesSchema from './schema/categories';
import conversationTagSchema from './schema/conversationTag';
import convoSchema from './schema/convo';
import fileSchema from './schema/file';
import groupSchema from './schema/group';
import keySchema from './schema/key';
import messageSchema from './schema/message';
import pluginAuthSchema from './schema/pluginAuth';
@@ -33,7 +32,6 @@ export {
conversationTagSchema,
convoSchema,
fileSchema,
groupSchema,
keySchema,
messageSchema,
pluginAuthSchema,

View File

@@ -13,7 +13,6 @@ export interface IAgent extends Omit<Document, 'model'> {
model_parameters?: Record<string, unknown>;
artifacts?: string;
access_level?: number;
recursion_limit?: number;
tools?: string[];
tool_kwargs?: Array<unknown>;
actions?: string[];
@@ -66,9 +65,6 @@ const agentSchema = new Schema<IAgent>(
access_level: {
type: Number,
},
recursion_limit: {
type: Number,
},
tools: {
type: [String],
default: undefined,

View File

@@ -1,39 +0,0 @@
import { Schema, Document } from 'mongoose';
export interface IGroup extends Document {
name: string;
description?: string;
externalId?: string;
provider: 'local' | 'openid';
createdAt?: Date;
updatedAt?: Date;
}
const groupSchema = new Schema<IGroup>(
{
name: {
type: String,
required: true,
unique: true,
},
description: {
type: String,
},
externalId: {
type: String,
unique: true,
required: function (this: IGroup) {
return this.provider !== 'local';
},
},
provider: {
type: String,
required: true,
default: 'local',
enum: ['local', 'openid'],
},
},
{ timestamps: true },
);
export default groupSchema;

View File

@@ -1,4 +1,4 @@
import { Schema, Document, Types } from 'mongoose';
import { Schema, Document } from 'mongoose';
import { SystemRoles } from 'librechat-data-provider';
export interface IUser extends Document {
@@ -18,7 +18,6 @@ export interface IUser extends Document {
discordId?: string;
appleId?: string;
plugins?: unknown[];
groups?: Types.ObjectId[];
twoFactorEnabled?: boolean;
totpSecret?: string;
backupCodes?: Array<{
@@ -136,11 +135,6 @@ const User = new Schema<IUser>(
plugins: {
type: Array,
},
groups: {
type: [Schema.Types.ObjectId],
ref: 'Group',
default: [],
},
twoFactorEnabled: {
type: Boolean,
default: false,

View File

@@ -269,7 +269,7 @@ export class MCPConnection extends EventEmitter {
this.transport = this.constructTransport(this.options);
this.setupTransportDebugHandlers();
const connectTimeout = this.options.initTimeout ?? 10000;
const connectTimeout = 10000;
await Promise.race([
this.client.connect(this.transport),
new Promise((_resolve, reject) =>

View File

@@ -1,5 +1,5 @@
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
import type { JsonSchemaType, MCPOptions } from 'librechat-data-provider';
import type { JsonSchemaType } from 'librechat-data-provider';
import type { Logger } from 'winston';
import type * as t from './types/mcp';
import { formatToolContent } from './parsers';
@@ -31,17 +31,13 @@ export class MCPManager {
return MCPManager.instance;
}
public async initializeMCP(
mcpServers: t.MCPServers,
processMCPEnv?: (obj: MCPOptions) => MCPOptions,
): Promise<void> {
public async initializeMCP(mcpServers: t.MCPServers): Promise<void> {
this.logger.info('[MCP] Initializing servers');
const entries = Object.entries(mcpServers);
const initializedServers = new Set();
const connectionResults = await Promise.allSettled(
entries.map(async ([serverName, _config], i) => {
const config = processMCPEnv ? processMCPEnv(_config) : _config;
entries.map(async ([serverName, config], i) => {
const connection = new MCPConnection(serverName, config, this.logger);
connection.on('connectionChange', (state) => {