🌅 fix: Agent Avatar S3 URL Refresh Pagination and Persistence (#11323)

* Refresh all S3 avatars for this user's accessible agent set, not the first page

* Cleaner debug messages

* Log errors as errors

* refactor: avatar refresh logic to process agents in batches and improve error handling. Introduced new utility functions for refreshing S3 avatars and updating agent records. Updated tests to cover various scenarios including cache hits, user ownership checks, and error handling. Added constants for maximum refresh limits.

* refactor: update avatar refresh logic to allow users with VIEW access to refresh avatars for all accessible agents. Removed checks for agent ownership and author presence, and updated related tests to reflect new behavior.

* chore: Remove YouTube toolkit due to #11331

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
David Newman 2026-01-14 04:01:11 +10:00 committed by GitHub
parent 10f591ab1c
commit a95fea19bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 743 additions and 51 deletions

View file

@ -5,7 +5,9 @@ const { logger } = require('@librechat/data-schemas');
const {
agentCreateSchema,
agentUpdateSchema,
refreshListAvatars,
mergeAgentOcrConversion,
MAX_AVATAR_REFRESH_AGENTS,
convertOcrToContextInPlace,
} = require('@librechat/api');
const {
@ -56,46 +58,6 @@ const systemTools = {
const MAX_SEARCH_LEN = 100;
const escapeRegex = (str = '') => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
/**
* Opportunistically refreshes S3-backed avatars for agent list responses.
* Only list responses are refreshed because they're the highest-traffic surface and
* the avatar URLs have a short-lived TTL. The refresh is cached per-user for 30 minutes
* via {@link CacheKeys.S3_EXPIRY_INTERVAL} so we refresh once per interval at most.
* @param {Array} agents - Agents being enriched with S3-backed avatars
* @param {string} userId - User identifier used for the cache refresh key
*/
const refreshListAvatars = async (agents, userId) => {
if (!agents?.length) {
return;
}
const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL);
const refreshKey = `${userId}:agents_list`;
const alreadyChecked = await cache.get(refreshKey);
if (alreadyChecked) {
return;
}
await Promise.all(
agents.map(async (agent) => {
if (agent?.avatar?.source !== FileSources.s3 || !agent?.avatar?.filepath) {
return;
}
try {
const newPath = await refreshS3Url(agent.avatar);
if (newPath && newPath !== agent.avatar.filepath) {
agent.avatar = { ...agent.avatar, filepath: newPath };
}
} catch (err) {
logger.debug('[/Agents] Avatar refresh error for list item', err);
}
}),
);
await cache.set(refreshKey, true, Time.THIRTY_MINUTES);
};
/**
* Creates an Agent.
* @route POST /Agents
@ -544,6 +506,35 @@ const getListAgentsHandler = async (req, res) => {
requiredPermissions: PermissionBits.VIEW,
});
/**
* Refresh all S3 avatars for this user's accessible agent set (not only the current page)
* This addresses page-size limits preventing refresh of agents beyond the first page
*/
const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL);
const refreshKey = `${userId}:agents_avatar_refresh`;
const alreadyChecked = await cache.get(refreshKey);
if (alreadyChecked) {
logger.debug('[/Agents] S3 avatar refresh already checked, skipping');
} else {
try {
const fullList = await getListAgentsByAccess({
accessibleIds,
otherParams: {},
limit: MAX_AVATAR_REFRESH_AGENTS,
after: null,
});
await refreshListAvatars({
agents: fullList?.data ?? [],
userId,
refreshS3Url,
updateAgent,
});
await cache.set(refreshKey, true, Time.THIRTY_MINUTES);
} catch (err) {
logger.error('[/Agents] Error refreshing avatars for full list: %o', err);
}
}
// Use the new ACL-aware function
const data = await getListAgentsByAccess({
accessibleIds,
@ -571,15 +562,9 @@ const getListAgentsHandler = async (req, res) => {
return agent;
});
// Opportunistically refresh S3 avatar URLs for list results with caching
try {
await refreshListAvatars(data.data, req.user.id);
} catch (err) {
logger.debug('[/Agents] Skipping avatar refresh for list', err);
}
return res.json(data);
} catch (error) {
logger.error('[/Agents] Error listing Agents', error);
logger.error('[/Agents] Error listing Agents: %o', error);
res.status(500).json({ error: error.message });
}
};