🔄 fix: Avatar & Error Handling Enhancements (#6687)

* fix: Ensure safe access to agent capabilities in AgentConfig

* fix: don't show agent builder if agents endpoint is not enabled

* fix: Improve error logging for MCP tool calls

* fix: Enhance error message for MCP tool failures

* feat: Add optional spec and iconURL properties to TEndpointOption type

* chore: Update condition to use constant for new conversation parameter

* feat: Enhance abort error handling with additional endpoint options to properly render error message fields

* fix: Throw error instead of returning message for failed MCP tool calls

* refactor: separate logic to generate new S3 URLs for expired links

* feat: Implement S3 URL refresh for user avatars with error handling

* fix: authcontext error in chats where agent chain is used

* refactor: streamline balance configuration logic in getBalanceConfig function

* fix: enhance icon resolution logic in SpecIcon component

* fix: allow null values for spec and iconURL in TEndpointOption type

* fix: update balance check to allow null tokenCredits
This commit is contained in:
Danny Avila 2025-04-02 18:44:13 -04:00 committed by GitHub
parent cfa44de1c9
commit c4f1da26b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 184 additions and 75 deletions

View file

@ -1,6 +1,8 @@
const { FileSources } = require('librechat-data-provider');
const {
Balance,
getFiles,
updateUser,
deleteFiles,
deleteConvos,
deletePresets,
@ -12,6 +14,7 @@ const User = require('~/models/User');
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud');
const { processDeleteRequest } = require('~/server/services/Files/process');
const { deleteAllSharedLinks } = require('~/models/Share');
const { deleteToolCalls } = require('~/models/ToolCall');
@ -19,8 +22,23 @@ const { Transaction } = require('~/models/Transaction');
const { logger } = require('~/config');
const getUserController = async (req, res) => {
/** @type {MongoUser} */
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
delete userData.totpSecret;
if (req.app.locals.fileStrategy === FileSources.s3 && userData.avatar) {
const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600);
if (!avatarNeedsRefresh) {
return res.status(200).send(userData);
}
const originalAvatar = userData.avatar;
try {
userData.avatar = await getNewS3URL(userData.avatar);
await updateUser(userData.id, { avatar: userData.avatar });
} catch (error) {
userData.avatar = originalAvatar;
logger.error('Error getting new S3 URL for avatar:', error);
}
}
res.status(200).send(userData);
};

View file

@ -148,6 +148,13 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
return { abortController, onStart };
};
/**
* @param {ServerResponse} res
* @param {ServerRequest} req
* @param {Error | unknown} error
* @param {Partial<TMessage> & { partialText?: string }} data
* @returns { Promise<void> }
*/
const handleAbortError = async (res, req, error, data) => {
if (error?.message?.includes('base64')) {
logger.error('[handleAbortError] Error in base64 encoding', {
@ -178,17 +185,30 @@ const handleAbortError = async (res, req, error, data) => {
errorText = `{"type":"${ErrorTypes.NO_SYSTEM_MESSAGES}"}`;
}
/**
* @param {string} partialText
* @returns {Promise<void>}
*/
const respondWithError = async (partialText) => {
const endpointOption = req.body?.endpointOption;
let options = {
sender,
messageId,
conversationId,
parentMessageId,
text: errorText,
shouldSaveMessage: true,
user: req.user.id,
shouldSaveMessage: true,
spec: endpointOption?.spec,
iconURL: endpointOption?.iconURL,
modelLabel: endpointOption?.modelLabel,
model: endpointOption?.modelOptions?.model || req.body?.model,
};
if (req.body?.agent_id) {
options.agent_id = req.body.agent_id;
}
if (partialText) {
options = {
...options,

View file

@ -31,19 +31,16 @@ async function getCustomConfig() {
async function getBalanceConfig() {
const isLegacyEnabled = isEnabled(process.env.CHECK_BALANCE);
const startBalance = process.env.START_BALANCE;
if (isLegacyEnabled || (startBalance != null && startBalance)) {
/** @type {TCustomConfig['balance']} */
const config = {
enabled: isLegacyEnabled,
startBalance: startBalance ? parseInt(startBalance, 10) : undefined,
};
return config;
}
/** @type {TCustomConfig['balance']} */
const config = {
enabled: isLegacyEnabled,
startBalance: startBalance != null && startBalance ? parseInt(startBalance, 10) : undefined,
};
const customConfig = await getCustomConfig();
if (!customConfig) {
return null;
return config;
}
return customConfig?.['balance'] ?? null;
return { ...config, ...(customConfig?.['balance'] ?? {}) };
}
/**

View file

@ -303,6 +303,36 @@ function needsRefresh(signedUrl, bufferSeconds) {
}
}
/**
* Generates a new URL for an expired S3 URL
* @param {string} currentURL - The current file URL
* @returns {Promise<string | undefined>}
*/
async function getNewS3URL(currentURL) {
try {
const s3Key = extractKeyFromS3Url(currentURL);
if (!s3Key) {
return;
}
const keyParts = s3Key.split('/');
if (keyParts.length < 3) {
return;
}
const basePath = keyParts[0];
const userId = keyParts[1];
const fileName = keyParts.slice(2).join('/');
return await getS3URL({
userId,
fileName,
basePath,
});
} catch (error) {
logger.error('Error getting new S3 URL:', error);
}
}
/**
* Refreshes S3 URLs for an array of files if they're expired or close to expiring
*
@ -333,30 +363,15 @@ async function refreshS3FileUrls(files, batchUpdateFiles, bufferSeconds = 3600)
continue;
}
try {
const s3Key = extractKeyFromS3Url(file.filepath);
if (!s3Key) {
const newURL = await getNewS3URL(file.filepath);
if (!newURL) {
continue;
}
const keyParts = s3Key.split('/');
if (keyParts.length < 3) {
continue;
}
const basePath = keyParts[0];
const userId = keyParts[1];
const fileName = keyParts.slice(2).join('/');
const newUrl = await getS3URL({
userId,
fileName,
basePath,
});
filesToUpdate.push({
file_id: file.file_id,
filepath: newUrl,
filepath: newURL,
});
files[i].filepath = newUrl;
files[i].filepath = newURL;
} catch (error) {
logger.error(`Error refreshing S3 URL for file ${file.file_id}:`, error);
}
@ -425,4 +440,6 @@ module.exports = {
getS3FileStream,
refreshS3FileUrls,
refreshS3Url,
needsRefresh,
getNewS3URL,
};

View file

@ -69,7 +69,13 @@ async function createMCPTool({ req, toolKey, provider }) {
}
return result;
} catch (error) {
return `${toolName} MCP server tool call failed.`;
logger.error(
`[MCP][User: ${userId}][${serverName}] Error calling "${toolName}" MCP tool:`,
error,
);
throw new Error(
`"${toolKey}" tool call failed${error?.message ? `: ${error?.message}` : '.'}`,
);
}
};