Compare commits
24 Commits
refactor/a
...
v0.8.0-rc3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ad235f17 | ||
|
|
4a0b329e3e | ||
|
|
a22359de5e | ||
|
|
bbfe4002eb | ||
|
|
94426a3cae | ||
|
|
e559f0f4dc | ||
|
|
15c9c7e1f4 | ||
|
|
ac641e7cba | ||
|
|
1915d7b195 | ||
|
|
c2f4b383f2 | ||
|
|
939af59950 | ||
|
|
7d08da1a8a | ||
|
|
543b617e1c | ||
|
|
c827fdd10e | ||
|
|
ac608ded46 | ||
|
|
0e00f357a6 | ||
|
|
c465d7b732 | ||
|
|
aba0a93d1d | ||
|
|
a49b2b2833 | ||
|
|
e0ebb7097e | ||
|
|
9a79635012 | ||
|
|
ce19abc968 | ||
|
|
49cd3894aa | ||
|
|
da4aa37493 |
33
.github/workflows/i18n-unused-keys.yml
vendored
33
.github/workflows/i18n-unused-keys.yml
vendored
@@ -1,5 +1,10 @@
|
||||
name: Detect Unused i18next Strings
|
||||
|
||||
# This workflow checks for unused i18n keys in translation files.
|
||||
# It has special handling for:
|
||||
# - com_ui_special_var_* keys that are dynamically constructed
|
||||
# - com_agents_category_* keys that are stored in the database and used dynamically
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -7,6 +12,7 @@ on:
|
||||
- "api/**"
|
||||
- "packages/data-provider/src/**"
|
||||
- "packages/client/**"
|
||||
- "packages/data-schemas/src/**"
|
||||
|
||||
jobs:
|
||||
detect-unused-i18n-keys:
|
||||
@@ -24,7 +30,7 @@ jobs:
|
||||
|
||||
# Define paths
|
||||
I18N_FILE="client/src/locales/en/translation.json"
|
||||
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client")
|
||||
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client" "packages/data-schemas/src")
|
||||
|
||||
# Check if translation file exists
|
||||
if [[ ! -f "$I18N_FILE" ]]; then
|
||||
@@ -52,6 +58,31 @@ jobs:
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check if the key is directly used somewhere
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Special case for agent category keys that are dynamically used from database
|
||||
elif [[ "$KEY" == com_agents_category_* ]]; then
|
||||
# Check if agent category localization is being used
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
# Check for dynamic category label/description usage
|
||||
if grep -r --include=\*.{js,jsx,ts,tsx} -E "category\.(label|description).*startsWith.*['\"]com_" "$DIR" > /dev/null 2>&1 || \
|
||||
# Check for the method that defines these keys
|
||||
grep -r --include=\*.{js,jsx,ts,tsx} "ensureDefaultCategories" "$DIR" > /dev/null 2>&1 || \
|
||||
# Check for direct usage in agentCategory.ts
|
||||
grep -r --include=\*.ts -E "label:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1 || \
|
||||
grep -r --include=\*.ts -E "description:.*['\"]$KEY['\"]" "$DIR" > /dev/null 2>&1; then
|
||||
FOUND=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Also check if the key is directly used somewhere
|
||||
if [[ "$FOUND" == false ]]; then
|
||||
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# v0.8.0-rc2
|
||||
# v0.8.0-rc3
|
||||
|
||||
# Base node image
|
||||
FROM node:20-alpine AS node
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Dockerfile.multi
|
||||
# v0.8.0-rc2
|
||||
# v0.8.0-rc3
|
||||
|
||||
# Base for all builds
|
||||
FROM node:20-alpine AS base-min
|
||||
|
||||
15
README.md
15
README.md
@@ -65,8 +65,10 @@
|
||||
|
||||
- 🔦 **Agents & Tools Integration**:
|
||||
- **[LibreChat Agents](https://www.librechat.ai/docs/features/agents)**:
|
||||
- No-Code Custom Assistants: Build specialized, AI-driven helpers without coding
|
||||
- Flexible & Extensible: Use MCP Servers, tools, file search, code execution, and more
|
||||
- No-Code Custom Assistants: Build specialized, AI-driven helpers
|
||||
- Agent Marketplace: Discover and deploy community-built agents
|
||||
- Collaborative Sharing: Share agents with specific users and groups
|
||||
- Flexible & Extensible: Use MCP Servers, tools, file search, code execution, and more
|
||||
- Compatible with Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, Google, Vertex AI, Responses API, and more
|
||||
- [Model Context Protocol (MCP) Support](https://modelcontextprotocol.io/clients#librechat) for Tools
|
||||
|
||||
@@ -87,15 +89,18 @@
|
||||
- Create, Save, & Share Custom Presets
|
||||
- Switch between AI Endpoints and Presets mid-chat
|
||||
- Edit, Resubmit, and Continue Messages with Conversation branching
|
||||
- Create and share prompts with specific users and groups
|
||||
- [Fork Messages & Conversations](https://www.librechat.ai/docs/features/fork) for Advanced Context control
|
||||
|
||||
- 💬 **Multimodal & File Interactions**:
|
||||
- Upload and analyze images with Claude 3, GPT-4.5, GPT-4o, o1, Llama-Vision, and Gemini 📸
|
||||
- Chat with Files using Custom Endpoints, OpenAI, Azure, Anthropic, AWS Bedrock, & Google 🗃️
|
||||
|
||||
- 🌎 **Multilingual UI**:
|
||||
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro
|
||||
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
|
||||
- 🌎 **Multilingual UI**:
|
||||
- English, 中文 (简体), 中文 (繁體), العربية, Deutsch, Español, Français, Italiano
|
||||
- Polski, Português (PT), Português (BR), Русский, 日本語, Svenska, 한국어, Tiếng Việt
|
||||
- Türkçe, Nederlands, עברית, Català, Čeština, Dansk, Eesti, فارسی
|
||||
- Suomi, Magyar, Հայերեն, Bahasa Indonesia, ქართული, Latviešu, ไทย, ئۇيغۇرچە
|
||||
|
||||
- 🧠 **Reasoning UI**:
|
||||
- Dynamic Reasoning UI for Chain-of-Thought/Reasoning AI models like DeepSeek-R1
|
||||
|
||||
@@ -187,7 +187,8 @@ class BaseClient {
|
||||
this.user = user;
|
||||
const saveOptions = this.getSaveOptions();
|
||||
this.abortController = opts.abortController ?? new AbortController();
|
||||
const conversationId = overrideConvoId ?? opts.conversationId ?? crypto.randomUUID();
|
||||
const requestConvoId = overrideConvoId ?? opts.conversationId;
|
||||
const conversationId = requestConvoId ?? crypto.randomUUID();
|
||||
const parentMessageId = opts.parentMessageId ?? Constants.NO_PARENT;
|
||||
const userMessageId =
|
||||
overrideUserMessageId ?? opts.overrideParentMessageId ?? crypto.randomUUID();
|
||||
@@ -212,11 +213,12 @@ class BaseClient {
|
||||
...opts,
|
||||
user,
|
||||
head,
|
||||
saveOptions,
|
||||
userMessageId,
|
||||
requestConvoId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
userMessageId,
|
||||
responseMessageId,
|
||||
saveOptions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -235,11 +237,12 @@ class BaseClient {
|
||||
const {
|
||||
user,
|
||||
head,
|
||||
saveOptions,
|
||||
userMessageId,
|
||||
requestConvoId,
|
||||
conversationId,
|
||||
parentMessageId,
|
||||
userMessageId,
|
||||
responseMessageId,
|
||||
saveOptions,
|
||||
} = await this.setMessageOptions(opts);
|
||||
|
||||
const userMessage = opts.isEdited
|
||||
@@ -261,7 +264,8 @@ class BaseClient {
|
||||
}
|
||||
|
||||
if (typeof opts?.onStart === 'function') {
|
||||
opts.onStart(userMessage, responseMessageId);
|
||||
const isNewConvo = !requestConvoId && parentMessageId === Constants.NO_PARENT;
|
||||
opts.onStart(userMessage, responseMessageId, isNewConvo);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -579,6 +579,8 @@ describe('BaseClient', () => {
|
||||
expect(onStart).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ text: 'Hello, world!' }),
|
||||
expect.any(String),
|
||||
/** `isNewConvo` */
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ const { SerpAPI } = require('@langchain/community/tools/serpapi');
|
||||
const { Calculator } = require('@langchain/community/tools/calculator');
|
||||
const { mcpToolPattern, loadWebSearchAuth } = require('@librechat/api');
|
||||
const { EnvVar, createCodeExecutionTool, createSearchTool } = require('@librechat/agents');
|
||||
const { Tools, EToolResources, replaceSpecialVars } = require('librechat-data-provider');
|
||||
const { Tools, Constants, EToolResources, replaceSpecialVars } = require('librechat-data-provider');
|
||||
const {
|
||||
availableTools,
|
||||
manifestToolMap,
|
||||
@@ -24,9 +24,9 @@ const {
|
||||
const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/process');
|
||||
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
const { createMCPTool, createMCPTools } = require('~/server/services/MCP');
|
||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||
const { getCachedTools } = require('~/server/services/Config');
|
||||
const { createMCPTool } = require('~/server/services/MCP');
|
||||
|
||||
/**
|
||||
* Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values.
|
||||
@@ -123,6 +123,8 @@ const getAuthFields = (toolKey) => {
|
||||
*
|
||||
* @param {object} object
|
||||
* @param {string} object.user
|
||||
* @param {Record<string, Record<string, string>>} [object.userMCPAuthMap]
|
||||
* @param {AbortSignal} [object.signal]
|
||||
* @param {Pick<Agent, 'id' | 'provider' | 'model'>} [object.agent]
|
||||
* @param {string} [object.model]
|
||||
* @param {EModelEndpoint} [object.endpoint]
|
||||
@@ -137,7 +139,9 @@ const loadTools = async ({
|
||||
user,
|
||||
agent,
|
||||
model,
|
||||
signal,
|
||||
endpoint,
|
||||
userMCPAuthMap,
|
||||
tools = [],
|
||||
options = {},
|
||||
functions = true,
|
||||
@@ -231,6 +235,7 @@ const loadTools = async ({
|
||||
/** @type {Record<string, string>} */
|
||||
const toolContextMap = {};
|
||||
const cachedTools = (await getCachedTools({ userId: user, includeGlobal: true })) ?? {};
|
||||
const requestedMCPTools = {};
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool === Tools.execute_code) {
|
||||
@@ -299,14 +304,35 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
|
||||
};
|
||||
continue;
|
||||
} else if (tool && cachedTools && mcpToolPattern.test(tool)) {
|
||||
requestedTools[tool] = async () =>
|
||||
const [toolName, serverName] = tool.split(Constants.mcp_delimiter);
|
||||
if (toolName === Constants.mcp_all) {
|
||||
const currentMCPGenerator = async (index) =>
|
||||
createMCPTools({
|
||||
req: options.req,
|
||||
res: options.res,
|
||||
index,
|
||||
serverName,
|
||||
userMCPAuthMap,
|
||||
model: agent?.model ?? model,
|
||||
provider: agent?.provider ?? endpoint,
|
||||
signal,
|
||||
});
|
||||
requestedMCPTools[serverName] = [currentMCPGenerator];
|
||||
continue;
|
||||
}
|
||||
const currentMCPGenerator = async (index) =>
|
||||
createMCPTool({
|
||||
index,
|
||||
req: options.req,
|
||||
res: options.res,
|
||||
toolKey: tool,
|
||||
userMCPAuthMap,
|
||||
model: agent?.model ?? model,
|
||||
provider: agent?.provider ?? endpoint,
|
||||
signal,
|
||||
});
|
||||
requestedMCPTools[serverName] = requestedMCPTools[serverName] || [];
|
||||
requestedMCPTools[serverName].push(currentMCPGenerator);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -346,6 +372,34 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
|
||||
}
|
||||
|
||||
const loadedTools = (await Promise.all(toolPromises)).flatMap((plugin) => plugin || []);
|
||||
const mcpToolPromises = [];
|
||||
/** MCP server tools are initialized sequentially by server */
|
||||
let index = -1;
|
||||
for (const [serverName, generators] of Object.entries(requestedMCPTools)) {
|
||||
index++;
|
||||
for (const generator of generators) {
|
||||
try {
|
||||
if (generator && generators.length === 1) {
|
||||
mcpToolPromises.push(
|
||||
generator(index).catch((error) => {
|
||||
logger.error(`Error loading ${serverName} tools:`, error);
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const mcpTool = await generator(index);
|
||||
if (Array.isArray(mcpTool)) {
|
||||
loadedTools.push(...mcpTool);
|
||||
} else if (mcpTool) {
|
||||
loadedTools.push(mcpTool);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error loading MCP tool for server ${serverName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
loadedTools.push(...(await Promise.all(mcpToolPromises)).flatMap((plugin) => plugin || []));
|
||||
return { loadedTools, toolContextMap };
|
||||
};
|
||||
|
||||
|
||||
1
api/cache/getLogStores.js
vendored
1
api/cache/getLogStores.js
vendored
@@ -31,7 +31,6 @@ const namespaces = {
|
||||
[CacheKeys.SAML_SESSION]: sessionCache(CacheKeys.SAML_SESSION),
|
||||
|
||||
[CacheKeys.ROLES]: standardCache(CacheKeys.ROLES),
|
||||
[CacheKeys.MCP_TOOLS]: standardCache(CacheKeys.MCP_TOOLS),
|
||||
[CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE),
|
||||
[CacheKeys.STATIC_CONFIG]: standardCache(CacheKeys.STATIC_CONFIG),
|
||||
[CacheKeys.PENDING_REQ]: standardCache(CacheKeys.PENDING_REQ),
|
||||
|
||||
@@ -2,7 +2,7 @@ const mongoose = require('mongoose');
|
||||
const crypto = require('node:crypto');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { ResourceType, SystemRoles, Tools, actionDelimiter } = require('librechat-data-provider');
|
||||
const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } =
|
||||
const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_all, mcp_delimiter } =
|
||||
require('librechat-data-provider').Constants;
|
||||
const {
|
||||
removeAgentFromAllProjects,
|
||||
@@ -78,6 +78,7 @@ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _
|
||||
tools.push(Tools.web_search);
|
||||
}
|
||||
|
||||
const addedServers = new Set();
|
||||
if (mcpServers.size > 0) {
|
||||
for (const toolName of Object.keys(availableTools)) {
|
||||
if (!toolName.includes(mcp_delimiter)) {
|
||||
@@ -85,9 +86,17 @@ const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _
|
||||
}
|
||||
const mcpServer = toolName.split(mcp_delimiter)?.[1];
|
||||
if (mcpServer && mcpServers.has(mcpServer)) {
|
||||
addedServers.add(mcpServer);
|
||||
tools.push(toolName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const mcpServer of mcpServers) {
|
||||
if (addedServers.has(mcpServer)) {
|
||||
continue;
|
||||
}
|
||||
tools.push(`${mcp_all}${mcp_delimiter}${mcpServer}`);
|
||||
}
|
||||
}
|
||||
|
||||
const instructions = req.body.promptPrefix;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/backend",
|
||||
"version": "v0.8.0-rc2",
|
||||
"version": "v0.8.0-rc3",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
|
||||
@@ -4,11 +4,18 @@ const {
|
||||
getToolkitKey,
|
||||
checkPluginAuth,
|
||||
filterUniquePlugins,
|
||||
convertMCPToolToPlugin,
|
||||
convertMCPToolsToPlugins,
|
||||
} = require('@librechat/api');
|
||||
const { getCustomConfig, getCachedTools } = require('~/server/services/Config');
|
||||
const {
|
||||
getCachedTools,
|
||||
setCachedTools,
|
||||
mergeUserTools,
|
||||
getCustomConfig,
|
||||
} = require('~/server/services/Config');
|
||||
const { loadAndFormatTools } = require('~/server/services/ToolService');
|
||||
const { availableTools, toolkits } = require('~/app/clients/tools');
|
||||
const { getMCPManager, getFlowStateManager } = require('~/config');
|
||||
const { getMCPManager } = require('~/config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
const getAvailablePluginsController = async (req, res) => {
|
||||
@@ -22,6 +29,7 @@ const getAvailablePluginsController = async (req, res) => {
|
||||
|
||||
/** @type {{ filteredTools: string[], includedTools: string[] }} */
|
||||
const { filteredTools = [], includedTools = [] } = req.app.locals;
|
||||
/** @type {import('@librechat/api').LCManifestTool[]} */
|
||||
const pluginManifest = availableTools;
|
||||
|
||||
const uniquePlugins = filterUniquePlugins(pluginManifest);
|
||||
@@ -47,45 +55,6 @@ const getAvailablePluginsController = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
function createServerToolsCallback() {
|
||||
/**
|
||||
* @param {string} serverName
|
||||
* @param {TPlugin[] | null} serverTools
|
||||
*/
|
||||
return async function (serverName, serverTools) {
|
||||
try {
|
||||
const mcpToolsCache = getLogStores(CacheKeys.MCP_TOOLS);
|
||||
if (!serverName || !mcpToolsCache) {
|
||||
return;
|
||||
}
|
||||
await mcpToolsCache.set(serverName, serverTools);
|
||||
logger.debug(`MCP tools for ${serverName} added to cache.`);
|
||||
} catch (error) {
|
||||
logger.error('Error retrieving MCP tools from cache:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createGetServerTools() {
|
||||
/**
|
||||
* Retrieves cached server tools
|
||||
* @param {string} serverName
|
||||
* @returns {Promise<TPlugin[] | null>}
|
||||
*/
|
||||
return async function (serverName) {
|
||||
try {
|
||||
const mcpToolsCache = getLogStores(CacheKeys.MCP_TOOLS);
|
||||
if (!mcpToolsCache) {
|
||||
return null;
|
||||
}
|
||||
return await mcpToolsCache.get(serverName);
|
||||
} catch (error) {
|
||||
logger.error('Error retrieving MCP tools from cache:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and returns a list of available tools, either from a cache or by reading a plugin manifest file.
|
||||
*
|
||||
@@ -101,11 +70,18 @@ function createGetServerTools() {
|
||||
const getAvailableTools = async (req, res) => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) {
|
||||
logger.warn('[getAvailableTools] User ID not found in request');
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
const customConfig = await getCustomConfig();
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
|
||||
const cachedUserTools = await getCachedTools({ userId });
|
||||
const userPlugins = convertMCPToolsToPlugins({ functionTools: cachedUserTools, customConfig });
|
||||
const userPlugins =
|
||||
cachedUserTools != null
|
||||
? convertMCPToolsToPlugins({ functionTools: cachedUserTools, customConfig })
|
||||
: undefined;
|
||||
|
||||
if (cachedToolsArray != null && userPlugins != null) {
|
||||
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
|
||||
@@ -113,31 +89,51 @@ const getAvailableTools = async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {Record<string, FunctionTool> | null} Get tool definitions to filter which tools are actually available */
|
||||
let toolDefinitions = await getCachedTools({ includeGlobal: true });
|
||||
let prelimCachedTools;
|
||||
|
||||
// TODO: this is a temp fix until app config is refactored
|
||||
if (!toolDefinitions) {
|
||||
toolDefinitions = loadAndFormatTools({
|
||||
adminFilter: req.app.locals?.filteredTools,
|
||||
adminIncluded: req.app.locals?.includedTools,
|
||||
directory: req.app.locals?.paths.structuredTools,
|
||||
});
|
||||
prelimCachedTools = toolDefinitions;
|
||||
}
|
||||
|
||||
/** @type {import('@librechat/api').LCManifestTool[]} */
|
||||
let pluginManifest = availableTools;
|
||||
if (customConfig?.mcpServers != null) {
|
||||
try {
|
||||
const mcpManager = getMCPManager();
|
||||
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||
const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null;
|
||||
const serverToolsCallback = createServerToolsCallback();
|
||||
const getServerTools = createGetServerTools();
|
||||
const mcpTools = await mcpManager.loadManifestTools({
|
||||
flowManager,
|
||||
serverToolsCallback,
|
||||
getServerTools,
|
||||
});
|
||||
pluginManifest = [...mcpTools, ...pluginManifest];
|
||||
const mcpTools = await mcpManager.getAllToolFunctions(userId);
|
||||
prelimCachedTools = prelimCachedTools ?? {};
|
||||
for (const [toolKey, toolData] of Object.entries(mcpTools)) {
|
||||
const plugin = convertMCPToolToPlugin({
|
||||
toolKey,
|
||||
toolData,
|
||||
customConfig,
|
||||
});
|
||||
if (plugin) {
|
||||
pluginManifest.push(plugin);
|
||||
}
|
||||
prelimCachedTools[toolKey] = toolData;
|
||||
}
|
||||
await mergeUserTools({ userId, cachedUserTools, userTools: prelimCachedTools });
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[getAvailableTools] Error loading MCP Tools, servers may still be initializing:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else if (prelimCachedTools != null) {
|
||||
await setCachedTools(prelimCachedTools, { isGlobal: true });
|
||||
}
|
||||
|
||||
/** @type {TPlugin[]} */
|
||||
/** @type {TPlugin[]} Deduplicate and authenticate plugins */
|
||||
const uniquePlugins = filterUniquePlugins(pluginManifest);
|
||||
|
||||
const authenticatedPlugins = uniquePlugins.map((plugin) => {
|
||||
if (checkPluginAuth(plugin)) {
|
||||
return { ...plugin, authenticated: true };
|
||||
@@ -146,8 +142,7 @@ const getAvailableTools = async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const toolDefinitions = (await getCachedTools({ includeGlobal: true })) || {};
|
||||
|
||||
/** Filter plugins based on availability and add MCP-specific auth config */
|
||||
const toolsOutput = [];
|
||||
for (const plugin of authenticatedPlugins) {
|
||||
const isToolDefined = toolDefinitions[plugin.pluginKey] !== undefined;
|
||||
@@ -163,41 +158,36 @@ const getAvailableTools = async (req, res) => {
|
||||
|
||||
const toolToAdd = { ...plugin };
|
||||
|
||||
if (!plugin.pluginKey.includes(Constants.mcp_delimiter)) {
|
||||
toolsOutput.push(toolToAdd);
|
||||
continue;
|
||||
}
|
||||
if (plugin.pluginKey.includes(Constants.mcp_delimiter)) {
|
||||
const parts = plugin.pluginKey.split(Constants.mcp_delimiter);
|
||||
const serverName = parts[parts.length - 1];
|
||||
const serverConfig = customConfig?.mcpServers?.[serverName];
|
||||
|
||||
const parts = plugin.pluginKey.split(Constants.mcp_delimiter);
|
||||
const serverName = parts[parts.length - 1];
|
||||
const serverConfig = customConfig?.mcpServers?.[serverName];
|
||||
|
||||
if (!serverConfig?.customUserVars) {
|
||||
toolsOutput.push(toolToAdd);
|
||||
continue;
|
||||
}
|
||||
|
||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
||||
|
||||
if (customVarKeys.length === 0) {
|
||||
toolToAdd.authConfig = [];
|
||||
toolToAdd.authenticated = true;
|
||||
} else {
|
||||
toolToAdd.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
|
||||
authField: key,
|
||||
label: value.title || key,
|
||||
description: value.description || '',
|
||||
}));
|
||||
toolToAdd.authenticated = false;
|
||||
if (serverConfig?.customUserVars) {
|
||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
||||
if (customVarKeys.length === 0) {
|
||||
toolToAdd.authConfig = [];
|
||||
toolToAdd.authenticated = true;
|
||||
} else {
|
||||
toolToAdd.authConfig = Object.entries(serverConfig.customUserVars).map(
|
||||
([key, value]) => ({
|
||||
authField: key,
|
||||
label: value.title || key,
|
||||
description: value.description || '',
|
||||
}),
|
||||
);
|
||||
toolToAdd.authenticated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toolsOutput.push(toolToAdd);
|
||||
}
|
||||
|
||||
const finalTools = filterUniquePlugins(toolsOutput);
|
||||
await cache.set(CacheKeys.TOOLS, finalTools);
|
||||
|
||||
const dedupedTools = filterUniquePlugins([...userPlugins, ...finalTools]);
|
||||
|
||||
const dedupedTools = filterUniquePlugins([...(userPlugins ?? []), ...finalTools]);
|
||||
res.status(200).json(dedupedTools);
|
||||
} catch (error) {
|
||||
logger.error('[getAvailableTools]', error);
|
||||
|
||||
@@ -13,15 +13,18 @@ jest.mock('@librechat/data-schemas', () => ({
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getCustomConfig: jest.fn(),
|
||||
getCachedTools: jest.fn(),
|
||||
setCachedTools: jest.fn(),
|
||||
mergeUserTools: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/ToolService', () => ({
|
||||
getToolkitKey: jest.fn(),
|
||||
loadAndFormatTools: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/config', () => ({
|
||||
getMCPManager: jest.fn(() => ({
|
||||
loadManifestTools: jest.fn().mockResolvedValue([]),
|
||||
loadAllManifestTools: jest.fn().mockResolvedValue([]),
|
||||
})),
|
||||
getFlowStateManager: jest.fn(),
|
||||
}));
|
||||
@@ -35,31 +38,31 @@ jest.mock('~/cache', () => ({
|
||||
getLogStores: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
getToolkitKey: jest.fn(),
|
||||
checkPluginAuth: jest.fn(),
|
||||
filterUniquePlugins: jest.fn(),
|
||||
convertMCPToolsToPlugins: jest.fn(),
|
||||
}));
|
||||
|
||||
// Import the actual module with the function we want to test
|
||||
const { getAvailableTools, getAvailablePluginsController } = require('./PluginController');
|
||||
const {
|
||||
filterUniquePlugins,
|
||||
checkPluginAuth,
|
||||
convertMCPToolsToPlugins,
|
||||
getToolkitKey,
|
||||
} = require('@librechat/api');
|
||||
const { loadAndFormatTools } = require('~/server/services/ToolService');
|
||||
|
||||
describe('PluginController', () => {
|
||||
let mockReq, mockRes, mockCache;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockReq = { user: { id: 'test-user-id' } };
|
||||
mockReq = {
|
||||
user: { id: 'test-user-id' },
|
||||
app: {
|
||||
locals: {
|
||||
paths: { structuredTools: '/mock/path' },
|
||||
filteredTools: null,
|
||||
includedTools: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };
|
||||
mockCache = { get: jest.fn(), set: jest.fn() };
|
||||
getLogStores.mockReturnValue(mockCache);
|
||||
|
||||
// Clear availableTools and toolkits arrays before each test
|
||||
require('~/app/clients/tools').availableTools.length = 0;
|
||||
require('~/app/clients/tools').toolkits.length = 0;
|
||||
});
|
||||
|
||||
describe('getAvailablePluginsController', () => {
|
||||
@@ -68,38 +71,39 @@ describe('PluginController', () => {
|
||||
});
|
||||
|
||||
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
|
||||
// Add plugins with duplicates to availableTools
|
||||
const mockPlugins = [
|
||||
{ name: 'Plugin1', pluginKey: 'key1', description: 'First' },
|
||||
{ name: 'Plugin1', pluginKey: 'key1', description: 'First duplicate' },
|
||||
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
|
||||
];
|
||||
|
||||
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
filterUniquePlugins.mockReturnValue(mockPlugins);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
expect(filterUniquePlugins).toHaveBeenCalled();
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
// The response includes authenticated: true for each plugin when checkPluginAuth returns true
|
||||
expect(mockRes.json).toHaveBeenCalledWith([
|
||||
{ name: 'Plugin1', pluginKey: 'key1', description: 'First', authenticated: true },
|
||||
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second', authenticated: true },
|
||||
]);
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
expect(responseData).toHaveLength(2);
|
||||
expect(responseData[0].pluginKey).toBe('key1');
|
||||
expect(responseData[1].pluginKey).toBe('key2');
|
||||
});
|
||||
|
||||
it('should use checkPluginAuth to verify plugin authentication', async () => {
|
||||
// checkPluginAuth returns false for plugins without authConfig
|
||||
// so authenticated property won't be added
|
||||
const mockPlugin = { name: 'Plugin1', pluginKey: 'key1', description: 'First' };
|
||||
|
||||
require('~/app/clients/tools').availableTools.push(mockPlugin);
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
filterUniquePlugins.mockReturnValue([mockPlugin]);
|
||||
checkPluginAuth.mockReturnValueOnce(true);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
expect(checkPluginAuth).toHaveBeenCalledWith(mockPlugin);
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
expect(responseData[0].authenticated).toBe(true);
|
||||
// checkPluginAuth returns false, so authenticated property is not added
|
||||
expect(responseData[0].authenticated).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return cached plugins when available', async () => {
|
||||
@@ -111,8 +115,7 @@ describe('PluginController', () => {
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
expect(filterUniquePlugins).not.toHaveBeenCalled();
|
||||
expect(checkPluginAuth).not.toHaveBeenCalled();
|
||||
// When cache is hit, we return immediately without processing
|
||||
expect(mockRes.json).toHaveBeenCalledWith(cachedPlugins);
|
||||
});
|
||||
|
||||
@@ -122,10 +125,9 @@ describe('PluginController', () => {
|
||||
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
|
||||
];
|
||||
|
||||
require('~/app/clients/tools').availableTools.push(...mockPlugins);
|
||||
mockReq.app.locals.includedTools = ['key1'];
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
filterUniquePlugins.mockReturnValue(mockPlugins);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
@@ -139,70 +141,102 @@ describe('PluginController', () => {
|
||||
it('should use convertMCPToolsToPlugins for user-specific MCP tools', async () => {
|
||||
const mockUserTools = {
|
||||
[`tool1${Constants.mcp_delimiter}server1`]: {
|
||||
function: { name: 'tool1', description: 'Tool 1' },
|
||||
type: 'function',
|
||||
function: {
|
||||
name: `tool1${Constants.mcp_delimiter}server1`,
|
||||
description: 'Tool 1',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockConvertedPlugins = [
|
||||
{
|
||||
name: 'tool1',
|
||||
pluginKey: `tool1${Constants.mcp_delimiter}server1`,
|
||||
description: 'Tool 1',
|
||||
},
|
||||
];
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||
convertMCPToolsToPlugins.mockReturnValue(mockConvertedPlugins);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock second call to return tool definitions
|
||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||
functionTools: mockUserTools,
|
||||
customConfig: null,
|
||||
});
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
// convertMCPToolsToPlugins should have converted the tool
|
||||
expect(responseData.length).toBeGreaterThan(0);
|
||||
const convertedTool = responseData.find(
|
||||
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}server1`,
|
||||
);
|
||||
expect(convertedTool).toBeDefined();
|
||||
expect(convertedTool.name).toBe('tool1');
|
||||
});
|
||||
|
||||
it('should use filterUniquePlugins to deduplicate combined tools', async () => {
|
||||
const mockUserPlugins = [
|
||||
{ name: 'UserTool', pluginKey: 'user-tool', description: 'User tool' },
|
||||
];
|
||||
const mockManifestPlugins = [
|
||||
const mockUserTools = {
|
||||
'user-tool': {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'user-tool',
|
||||
description: 'User tool',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockCachedPlugins = [
|
||||
{ name: 'user-tool', pluginKey: 'user-tool', description: 'Duplicate user tool' },
|
||||
{ name: 'ManifestTool', pluginKey: 'manifest-tool', description: 'Manifest tool' },
|
||||
];
|
||||
|
||||
mockCache.get.mockResolvedValue(mockManifestPlugins);
|
||||
getCachedTools.mockResolvedValueOnce({});
|
||||
convertMCPToolsToPlugins.mockReturnValue(mockUserPlugins);
|
||||
filterUniquePlugins.mockReturnValue([...mockUserPlugins, ...mockManifestPlugins]);
|
||||
mockCache.get.mockResolvedValue(mockCachedPlugins);
|
||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock second call to return tool definitions
|
||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
// Should be called to deduplicate the combined array
|
||||
expect(filterUniquePlugins).toHaveBeenLastCalledWith([
|
||||
...mockUserPlugins,
|
||||
...mockManifestPlugins,
|
||||
]);
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
// Should have deduplicated tools with same pluginKey
|
||||
const userToolCount = responseData.filter((tool) => tool.pluginKey === 'user-tool').length;
|
||||
expect(userToolCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should use checkPluginAuth to verify authentication status', async () => {
|
||||
const mockPlugin = { name: 'Tool1', pluginKey: 'tool1', description: 'Tool 1' };
|
||||
// Add a plugin to availableTools that will be checked
|
||||
const mockPlugin = {
|
||||
name: 'Tool1',
|
||||
pluginKey: 'tool1',
|
||||
description: 'Tool 1',
|
||||
// No authConfig means checkPluginAuth returns false
|
||||
};
|
||||
|
||||
require('~/app/clients/tools').availableTools.push(mockPlugin);
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue({});
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
filterUniquePlugins.mockReturnValue([mockPlugin]);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
getCachedTools.mockResolvedValue(null);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock getCachedTools second call to return tool definitions
|
||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({ tool1: true });
|
||||
// Mock loadAndFormatTools to return tool definitions including our tool
|
||||
loadAndFormatTools.mockReturnValue({
|
||||
tool1: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'tool1',
|
||||
description: 'Tool 1',
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(checkPluginAuth).toHaveBeenCalledWith(mockPlugin);
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
expect(Array.isArray(responseData)).toBe(true);
|
||||
const tool = responseData.find((t) => t.pluginKey === 'tool1');
|
||||
expect(tool).toBeDefined();
|
||||
// checkPluginAuth returns false, so authenticated property is not added
|
||||
expect(tool.authenticated).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should use getToolkitKey for toolkit validation', async () => {
|
||||
@@ -213,22 +247,38 @@ describe('PluginController', () => {
|
||||
toolkit: true,
|
||||
};
|
||||
|
||||
require('~/app/clients/tools').availableTools.push(mockToolkit);
|
||||
|
||||
// Mock toolkits to have a mapping
|
||||
require('~/app/clients/tools').toolkits.push({
|
||||
name: 'Toolkit1',
|
||||
pluginKey: 'toolkit1',
|
||||
tools: ['toolkit1_function'],
|
||||
});
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue({});
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
filterUniquePlugins.mockReturnValue([mockToolkit]);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
getToolkitKey.mockReturnValue('toolkit1');
|
||||
getCachedTools.mockResolvedValue(null);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock getCachedTools second call to return tool definitions
|
||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({
|
||||
toolkit1_function: true,
|
||||
// Mock loadAndFormatTools to return tool definitions
|
||||
loadAndFormatTools.mockReturnValue({
|
||||
toolkit1_function: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'toolkit1_function',
|
||||
description: 'Toolkit function',
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(getToolkitKey).toHaveBeenCalled();
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
expect(Array.isArray(responseData)).toBe(true);
|
||||
const toolkit = responseData.find((t) => t.pluginKey === 'toolkit1');
|
||||
expect(toolkit).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -239,32 +289,33 @@ describe('PluginController', () => {
|
||||
|
||||
const functionTools = {
|
||||
[`test-tool${Constants.mcp_delimiter}test-server`]: {
|
||||
function: { name: 'test-tool', description: 'A test tool' },
|
||||
type: 'function',
|
||||
function: {
|
||||
name: `test-tool${Constants.mcp_delimiter}test-server`,
|
||||
description: 'A test tool',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockConvertedPlugin = {
|
||||
name: 'test-tool',
|
||||
pluginKey: `test-tool${Constants.mcp_delimiter}test-server`,
|
||||
description: 'A test tool',
|
||||
icon: mcpServers['test-server']?.iconPath,
|
||||
authenticated: true,
|
||||
authConfig: [],
|
||||
// Mock the MCP manager to return tools
|
||||
const mockMCPManager = {
|
||||
getAllToolFunctions: jest.fn().mockResolvedValue(functionTools),
|
||||
};
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||
|
||||
getCachedTools.mockResolvedValueOnce({});
|
||||
|
||||
// Mock loadAndFormatTools to return empty object since these are MCP tools
|
||||
loadAndFormatTools.mockReturnValue({});
|
||||
|
||||
getCachedTools.mockResolvedValueOnce(functionTools);
|
||||
convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
getToolkitKey.mockReturnValue(undefined);
|
||||
|
||||
getCachedTools.mockResolvedValueOnce({
|
||||
[`test-tool${Constants.mcp_delimiter}test-server`]: true,
|
||||
});
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
return responseData.find((tool) => tool.name === 'test-tool');
|
||||
return responseData.find(
|
||||
(tool) => tool.pluginKey === `test-tool${Constants.mcp_delimiter}test-server`,
|
||||
);
|
||||
};
|
||||
|
||||
it('should set plugin.icon when iconPath is defined', async () => {
|
||||
@@ -298,19 +349,21 @@ describe('PluginController', () => {
|
||||
},
|
||||
};
|
||||
|
||||
// We need to test the actual flow where MCP manager tools are included
|
||||
const mcpManagerTools = [
|
||||
{
|
||||
name: 'tool1',
|
||||
pluginKey: `tool1${Constants.mcp_delimiter}test-server`,
|
||||
description: 'Tool 1',
|
||||
authenticated: true,
|
||||
// Mock MCP tools returned by getAllToolFunctions
|
||||
const mcpToolFunctions = {
|
||||
[`tool1${Constants.mcp_delimiter}test-server`]: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: `tool1${Constants.mcp_delimiter}test-server`,
|
||||
description: 'Tool 1',
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// Mock the MCP manager to return tools
|
||||
const mockMCPManager = {
|
||||
loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools),
|
||||
getAllToolFunctions: jest.fn().mockResolvedValue(mcpToolFunctions),
|
||||
};
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||
|
||||
@@ -320,19 +373,11 @@ describe('PluginController', () => {
|
||||
// First call returns user tools (empty in this case)
|
||||
getCachedTools.mockResolvedValueOnce({});
|
||||
|
||||
// Mock convertMCPToolsToPlugins to return empty array for user tools
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
// Mock loadAndFormatTools to return empty object for MCP tools
|
||||
loadAndFormatTools.mockReturnValue({});
|
||||
|
||||
// Mock filterUniquePlugins to pass through
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||
|
||||
// Mock checkPluginAuth
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
|
||||
// Second call returns tool definitions
|
||||
getCachedTools.mockResolvedValueOnce({
|
||||
[`tool1${Constants.mcp_delimiter}test-server`]: true,
|
||||
});
|
||||
// Second call returns tool definitions including our MCP tool
|
||||
getCachedTools.mockResolvedValueOnce(mcpToolFunctions);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
@@ -373,25 +418,23 @@ describe('PluginController', () => {
|
||||
it('should handle null cachedTools and cachedUserTools', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue(null);
|
||||
convertMCPToolsToPlugins.mockReturnValue(undefined);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock loadAndFormatTools to return empty object when getCachedTools returns null
|
||||
loadAndFormatTools.mockReturnValue({});
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||
functionTools: null,
|
||||
customConfig: null,
|
||||
});
|
||||
// Should handle null values gracefully
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it('should handle when getCachedTools returns undefined', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue(undefined);
|
||||
convertMCPToolsToPlugins.mockReturnValue(undefined);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
|
||||
// Mock loadAndFormatTools to return empty object when getCachedTools returns undefined
|
||||
loadAndFormatTools.mockReturnValue({});
|
||||
|
||||
// Mock getCachedTools to return undefined for both calls
|
||||
getCachedTools.mockReset();
|
||||
@@ -399,37 +442,40 @@ describe('PluginController', () => {
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||
functionTools: undefined,
|
||||
customConfig: null,
|
||||
});
|
||||
// Should handle undefined values gracefully
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it('should handle cachedToolsArray and userPlugins both being defined', async () => {
|
||||
const cachedTools = [{ name: 'CachedTool', pluginKey: 'cached-tool', description: 'Cached' }];
|
||||
// Use MCP delimiter for the user tool so convertMCPToolsToPlugins works
|
||||
const userTools = {
|
||||
'user-tool': { function: { name: 'user-tool', description: 'User tool' } },
|
||||
[`user-tool${Constants.mcp_delimiter}server1`]: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: `user-tool${Constants.mcp_delimiter}server1`,
|
||||
description: 'User tool',
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const userPlugins = [{ name: 'UserTool', pluginKey: 'user-tool', description: 'User tool' }];
|
||||
|
||||
mockCache.get.mockResolvedValue(cachedTools);
|
||||
getCachedTools.mockResolvedValue(userTools);
|
||||
convertMCPToolsToPlugins.mockReturnValue(userPlugins);
|
||||
filterUniquePlugins.mockReturnValue([...userPlugins, ...cachedTools]);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith([...userPlugins, ...cachedTools]);
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
// Should have both cached and user tools
|
||||
expect(responseData.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should handle empty toolDefinitions object', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({});
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
@@ -456,18 +502,6 @@ describe('PluginController', () => {
|
||||
getCustomConfig.mockResolvedValue(customConfig);
|
||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||
|
||||
const mockPlugin = {
|
||||
name: 'tool1',
|
||||
pluginKey: `tool1${Constants.mcp_delimiter}test-server`,
|
||||
description: 'Tool 1',
|
||||
authenticated: true,
|
||||
authConfig: [],
|
||||
};
|
||||
|
||||
convertMCPToolsToPlugins.mockReturnValue([mockPlugin]);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
|
||||
getCachedTools.mockResolvedValueOnce({
|
||||
[`tool1${Constants.mcp_delimiter}test-server`]: true,
|
||||
});
|
||||
@@ -483,8 +517,6 @@ describe('PluginController', () => {
|
||||
it('should handle req.app.locals with undefined filteredTools and includedTools', async () => {
|
||||
mockReq.app = { locals: {} };
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
filterUniquePlugins.mockReturnValue([]);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
@@ -500,14 +532,25 @@ describe('PluginController', () => {
|
||||
toolkit: true,
|
||||
};
|
||||
|
||||
// Ensure req.app.locals is properly mocked
|
||||
mockReq.app = {
|
||||
locals: {
|
||||
filteredTools: [],
|
||||
includedTools: [],
|
||||
paths: { structuredTools: '/mock/path' },
|
||||
},
|
||||
};
|
||||
|
||||
// Add the toolkit to availableTools
|
||||
require('~/app/clients/tools').availableTools.push(mockToolkit);
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue({});
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
filterUniquePlugins.mockReturnValue([mockToolkit]);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
getToolkitKey.mockReturnValue(undefined);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock loadAndFormatTools to return an empty object when toolDefinitions is null
|
||||
loadAndFormatTools.mockReturnValue({});
|
||||
|
||||
// Mock getCachedTools second call to return null
|
||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce(null);
|
||||
|
||||
|
||||
@@ -33,18 +33,13 @@ const {
|
||||
bedrockInputSchema,
|
||||
removeNullishValues,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
findPluginAuthsByKeys,
|
||||
getFormattedMemories,
|
||||
deleteMemory,
|
||||
setMemory,
|
||||
} = require('~/models');
|
||||
const { getMCPAuthMap, checkCapability, hasCustomUserVars } = require('~/server/services/Config');
|
||||
const { addCacheControl, createContextHandlers } = require('~/app/clients/prompts');
|
||||
const { initializeAgent } = require('~/server/services/Endpoints/agents/agent');
|
||||
const { spendTokens, spendStructuredTokens } = require('~/models/spendTokens');
|
||||
const { getFormattedMemories, deleteMemory, setMemory } = require('~/models');
|
||||
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
|
||||
const { getProviderConfig } = require('~/server/services/Endpoints');
|
||||
const { checkCapability } = require('~/server/services/Config');
|
||||
const BaseClient = require('~/app/clients/BaseClient');
|
||||
const { getRoleByName } = require('~/models/Role');
|
||||
const { loadAgent } = require('~/models/Agent');
|
||||
@@ -615,6 +610,7 @@ class AgentClient extends BaseClient {
|
||||
await this.chatCompletion({
|
||||
payload,
|
||||
onProgress: opts.onProgress,
|
||||
userMCPAuthMap: opts.userMCPAuthMap,
|
||||
abortController: opts.abortController,
|
||||
});
|
||||
return this.contentParts;
|
||||
@@ -747,7 +743,13 @@ class AgentClient extends BaseClient {
|
||||
return currentMessageTokens > 0 ? currentMessageTokens : originalEstimate;
|
||||
}
|
||||
|
||||
async chatCompletion({ payload, abortController = null }) {
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {string | ChatCompletionMessageParam[]} params.payload
|
||||
* @param {Record<string, Record<string, string>>} [params.userMCPAuthMap]
|
||||
* @param {AbortController} [params.abortController]
|
||||
*/
|
||||
async chatCompletion({ payload, userMCPAuthMap, abortController = null }) {
|
||||
/** @type {Partial<GraphRunnableConfig>} */
|
||||
let config;
|
||||
/** @type {ReturnType<createRun>} */
|
||||
@@ -903,21 +905,9 @@ class AgentClient extends BaseClient {
|
||||
run.Graph.contentData = contentData;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await hasCustomUserVars()) {
|
||||
config.configurable.userMCPAuthMap = await getMCPAuthMap({
|
||||
tools: agent.tools,
|
||||
userId: this.options.req.user.id,
|
||||
findPluginAuthsByKeys,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[api/server/controllers/agents/client.js #chatCompletion] Error getting custom user vars for agent ${agent.id}`,
|
||||
err,
|
||||
);
|
||||
if (userMCPAuthMap != null) {
|
||||
config.configurable.userMCPAuthMap = userMCPAuthMap;
|
||||
}
|
||||
|
||||
await run.processStream({ messages }, config, {
|
||||
keepContent: i !== 0,
|
||||
tokenCounter: createTokenCounter(this.getEncoding()),
|
||||
|
||||
@@ -9,6 +9,24 @@ const {
|
||||
const { disposeClient, clientRegistry, requestDataMap } = require('~/server/cleanup');
|
||||
const { saveMessage } = require('~/models');
|
||||
|
||||
function createCloseHandler(abortController) {
|
||||
return function (manual) {
|
||||
if (!manual) {
|
||||
logger.debug('[AgentController] Request closed');
|
||||
}
|
||||
if (!abortController) {
|
||||
return;
|
||||
} else if (abortController.signal.aborted) {
|
||||
return;
|
||||
} else if (abortController.requestCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
logger.debug('[AgentController] Request aborted on close');
|
||||
};
|
||||
}
|
||||
|
||||
const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
let {
|
||||
text,
|
||||
@@ -31,7 +49,6 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
let userMessagePromise;
|
||||
let getAbortData;
|
||||
let client = null;
|
||||
// Initialize as an array
|
||||
let cleanupHandlers = [];
|
||||
|
||||
const newConvo = !conversationId;
|
||||
@@ -62,9 +79,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
// Create a function to handle final cleanup
|
||||
const performCleanup = () => {
|
||||
logger.debug('[AgentController] Performing cleanup');
|
||||
// Make sure cleanupHandlers is an array before iterating
|
||||
if (Array.isArray(cleanupHandlers)) {
|
||||
// Execute all cleanup handlers
|
||||
for (const handler of cleanupHandlers) {
|
||||
try {
|
||||
if (typeof handler === 'function') {
|
||||
@@ -105,8 +120,33 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
};
|
||||
|
||||
try {
|
||||
/** @type {{ client: TAgentClient }} */
|
||||
const result = await initializeClient({ req, res, endpointOption });
|
||||
let prelimAbortController = new AbortController();
|
||||
const prelimCloseHandler = createCloseHandler(prelimAbortController);
|
||||
res.on('close', prelimCloseHandler);
|
||||
const removePrelimHandler = (manual) => {
|
||||
try {
|
||||
prelimCloseHandler(manual);
|
||||
res.removeListener('close', prelimCloseHandler);
|
||||
} catch (e) {
|
||||
logger.error('[AgentController] Error removing close listener', e);
|
||||
}
|
||||
};
|
||||
cleanupHandlers.push(removePrelimHandler);
|
||||
/** @type {{ client: TAgentClient; userMCPAuthMap?: Record<string, Record<string, string>> }} */
|
||||
const result = await initializeClient({
|
||||
req,
|
||||
res,
|
||||
endpointOption,
|
||||
signal: prelimAbortController.signal,
|
||||
});
|
||||
if (prelimAbortController.signal?.aborted) {
|
||||
prelimAbortController = null;
|
||||
throw new Error('Request was aborted before initialization could complete');
|
||||
} else {
|
||||
prelimAbortController = null;
|
||||
removePrelimHandler(true);
|
||||
cleanupHandlers.pop();
|
||||
}
|
||||
client = result.client;
|
||||
|
||||
// Register client with finalization registry if available
|
||||
@@ -138,22 +178,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
};
|
||||
|
||||
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
|
||||
|
||||
// Simple handler to avoid capturing scope
|
||||
const closeHandler = () => {
|
||||
logger.debug('[AgentController] Request closed');
|
||||
if (!abortController) {
|
||||
return;
|
||||
} else if (abortController.signal.aborted) {
|
||||
return;
|
||||
} else if (abortController.requestCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
abortController.abort();
|
||||
logger.debug('[AgentController] Request aborted on close');
|
||||
};
|
||||
|
||||
const closeHandler = createCloseHandler(abortController);
|
||||
res.on('close', closeHandler);
|
||||
cleanupHandlers.push(() => {
|
||||
try {
|
||||
@@ -175,6 +200,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
abortController,
|
||||
overrideParentMessageId,
|
||||
isEdited: !!editedContent,
|
||||
userMCPAuthMap: result.userMCPAuthMap,
|
||||
responseMessageId: editedResponseMessageId,
|
||||
progressOptions: {
|
||||
res,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { countTokens, isEnabled, sendEvent } = require('@librechat/api');
|
||||
const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider');
|
||||
const { isAssistantsEndpoint, ErrorTypes, Constants } = require('librechat-data-provider');
|
||||
const { truncateText, smartTruncateText } = require('~/app/clients/prompts');
|
||||
const clearPendingReq = require('~/cache/clearPendingReq');
|
||||
const { sendError } = require('~/server/middleware/error');
|
||||
@@ -11,6 +11,10 @@ const { abortRun } = require('./abortRun');
|
||||
|
||||
const abortDataMap = new WeakMap();
|
||||
|
||||
/**
|
||||
* @param {string} abortKey
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function cleanupAbortController(abortKey) {
|
||||
if (!abortControllers.has(abortKey)) {
|
||||
return false;
|
||||
@@ -71,6 +75,20 @@ function cleanupAbortController(abortKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} abortKey
|
||||
* @returns {function(): void}
|
||||
*/
|
||||
function createCleanUpHandler(abortKey) {
|
||||
return function () {
|
||||
try {
|
||||
cleanupAbortController(abortKey);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function abortMessage(req, res) {
|
||||
let { abortKey, endpoint } = req.body;
|
||||
|
||||
@@ -172,11 +190,15 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
|
||||
/**
|
||||
* @param {TMessage} userMessage
|
||||
* @param {string} responseMessageId
|
||||
* @param {boolean} [isNewConvo]
|
||||
*/
|
||||
const onStart = (userMessage, responseMessageId) => {
|
||||
const onStart = (userMessage, responseMessageId, isNewConvo) => {
|
||||
sendEvent(res, { message: userMessage, created: true });
|
||||
|
||||
const abortKey = userMessage?.conversationId ?? req.user.id;
|
||||
const prelimAbortKey = userMessage?.conversationId ?? req.user.id;
|
||||
const abortKey = isNewConvo
|
||||
? `${prelimAbortKey}${Constants.COMMON_DIVIDER}${Constants.NEW_CONVO}`
|
||||
: prelimAbortKey;
|
||||
getReqData({ abortKey });
|
||||
const prevRequest = abortControllers.get(abortKey);
|
||||
const { overrideUserMessageId } = req?.body ?? {};
|
||||
@@ -194,16 +216,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
|
||||
};
|
||||
|
||||
abortControllers.set(addedAbortKey, { abortController, ...minimalOptions });
|
||||
|
||||
// Use a simple function for cleanup to avoid capturing context
|
||||
const cleanupHandler = () => {
|
||||
try {
|
||||
cleanupAbortController(addedAbortKey);
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupHandler = createCleanUpHandler(addedAbortKey);
|
||||
res.on('finish', cleanupHandler);
|
||||
return;
|
||||
}
|
||||
@@ -216,16 +229,7 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
|
||||
};
|
||||
|
||||
abortControllers.set(abortKey, { abortController, ...minimalOptions });
|
||||
|
||||
// Use a simple function for cleanup to avoid capturing context
|
||||
const cleanupHandler = () => {
|
||||
try {
|
||||
cleanupAbortController(abortKey);
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupHandler = createCleanUpHandler(abortKey);
|
||||
res.on('finish', cleanupHandler);
|
||||
};
|
||||
|
||||
@@ -364,15 +368,7 @@ const handleAbortError = async (res, req, error, data) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Create a simple callback without capturing parent scope
|
||||
const callback = async () => {
|
||||
try {
|
||||
cleanupAbortController(conversationId);
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
};
|
||||
|
||||
const callback = createCleanUpHandler(conversationId);
|
||||
await sendError(req, res, options, callback);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
const mongoose = require('mongoose');
|
||||
const express = require('express');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
...jest.requireActual('@librechat/api'),
|
||||
MCPOAuthHandler: {
|
||||
initiateOAuthFlow: jest.fn(),
|
||||
getFlowState: jest.fn(),
|
||||
completeOAuthFlow: jest.fn(),
|
||||
generateFlowId: jest.fn(),
|
||||
},
|
||||
getUserMCPAuthMap: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => ({
|
||||
@@ -36,6 +38,7 @@ jest.mock('~/models', () => ({
|
||||
updateToken: jest.fn(),
|
||||
createToken: jest.fn(),
|
||||
deleteTokens: jest.fn(),
|
||||
findPluginAuthsByKeys: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
@@ -44,6 +47,10 @@ jest.mock('~/server/services/Config', () => ({
|
||||
loadCustomConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Config/mcpToolsCache', () => ({
|
||||
updateMCPUserTools: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/MCP', () => ({
|
||||
getMCPSetupData: jest.fn(),
|
||||
getServerConnectionStatus: jest.fn(),
|
||||
@@ -66,6 +73,10 @@ jest.mock('~/server/middleware', () => ({
|
||||
requireJwtAuth: (req, res, next) => next(),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/Tools/mcp', () => ({
|
||||
reinitMCPServer: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('MCP Routes', () => {
|
||||
let app;
|
||||
let mongoServer;
|
||||
@@ -677,6 +688,13 @@ describe('MCP Routes', () => {
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
require('~/config').getFlowStateManager.mockReturnValue({});
|
||||
require('~/cache').getLogStores.mockReturnValue({});
|
||||
require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue({
|
||||
success: true,
|
||||
message: "MCP server 'oauth-server' ready for OAuth authentication",
|
||||
serverName: 'oauth-server',
|
||||
oauthRequired: true,
|
||||
oauthUrl: 'https://oauth.example.com/auth',
|
||||
});
|
||||
|
||||
const response = await request(app).post('/api/mcp/oauth-server/reinitialize');
|
||||
|
||||
@@ -701,6 +719,7 @@ describe('MCP Routes', () => {
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
require('~/config').getFlowStateManager.mockReturnValue({});
|
||||
require('~/cache').getLogStores.mockReturnValue({});
|
||||
require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue(null);
|
||||
|
||||
const response = await request(app).post('/api/mcp/error-server/reinitialize');
|
||||
|
||||
@@ -759,8 +778,18 @@ describe('MCP Routes', () => {
|
||||
require('~/cache').getLogStores.mockReturnValue({});
|
||||
|
||||
const { getCachedTools, setCachedTools } = require('~/server/services/Config');
|
||||
const { updateMCPUserTools } = require('~/server/services/Config/mcpToolsCache');
|
||||
getCachedTools.mockResolvedValue({});
|
||||
setCachedTools.mockResolvedValue();
|
||||
updateMCPUserTools.mockResolvedValue();
|
||||
|
||||
require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue({
|
||||
success: true,
|
||||
message: "MCP server 'test-server' reinitialized successfully",
|
||||
serverName: 'test-server',
|
||||
oauthRequired: false,
|
||||
oauthUrl: null,
|
||||
});
|
||||
|
||||
const response = await request(app).post('/api/mcp/test-server/reinitialize');
|
||||
|
||||
@@ -776,7 +805,6 @@ describe('MCP Routes', () => {
|
||||
'test-user-id',
|
||||
'test-server',
|
||||
);
|
||||
expect(setCachedTools).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle server with custom user variables', async () => {
|
||||
@@ -798,21 +826,38 @@ describe('MCP Routes', () => {
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
|
||||
require('~/config').getFlowStateManager.mockReturnValue({});
|
||||
require('~/cache').getLogStores.mockReturnValue({});
|
||||
require('~/server/services/PluginService').getUserPluginAuthValue.mockResolvedValue(
|
||||
'api-key-value',
|
||||
);
|
||||
require('@librechat/api').getUserMCPAuthMap.mockResolvedValue({
|
||||
'mcp:test-server': {
|
||||
API_KEY: 'api-key-value',
|
||||
},
|
||||
});
|
||||
require('~/models').findPluginAuthsByKeys.mockResolvedValue([
|
||||
{ key: 'API_KEY', value: 'api-key-value' },
|
||||
]);
|
||||
|
||||
const { getCachedTools, setCachedTools } = require('~/server/services/Config');
|
||||
const { updateMCPUserTools } = require('~/server/services/Config/mcpToolsCache');
|
||||
getCachedTools.mockResolvedValue({});
|
||||
setCachedTools.mockResolvedValue();
|
||||
updateMCPUserTools.mockResolvedValue();
|
||||
|
||||
require('~/server/services/Tools/mcp').reinitMCPServer.mockResolvedValue({
|
||||
success: true,
|
||||
message: "MCP server 'test-server' reinitialized successfully",
|
||||
serverName: 'test-server',
|
||||
oauthRequired: false,
|
||||
oauthUrl: null,
|
||||
});
|
||||
|
||||
const response = await request(app).post('/api/mcp/test-server/reinitialize');
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(
|
||||
require('~/server/services/PluginService').getUserPluginAuthValue,
|
||||
).toHaveBeenCalledWith('test-user-id', 'API_KEY', false);
|
||||
expect(require('@librechat/api').getUserMCPAuthMap).toHaveBeenCalledWith({
|
||||
userId: 'test-user-id',
|
||||
servers: ['test-server'],
|
||||
findPluginAuthsByKeys: require('~/models').findPluginAuthsByKeys,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ router.use('/tools', tools);
|
||||
|
||||
/**
|
||||
* Get all agent categories with counts
|
||||
* @route GET /agents/marketplace/categories
|
||||
* @route GET /agents/categories
|
||||
*/
|
||||
router.get('/categories', v1.getAgentCategories);
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { MCPOAuthHandler } = require('@librechat/api');
|
||||
const { Router } = require('express');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { MCPOAuthHandler, getUserMCPAuthMap } = require('@librechat/api');
|
||||
const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP');
|
||||
const { findToken, updateToken, createToken, deleteTokens } = require('~/models');
|
||||
const { setCachedTools, getCachedTools } = require('~/server/services/Config');
|
||||
const { updateMCPUserTools } = require('~/server/services/Config/mcpToolsCache');
|
||||
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||
const { CacheKeys, Constants } = require('librechat-data-provider');
|
||||
const { getMCPManager, getFlowStateManager } = require('~/config');
|
||||
const { reinitMCPServer } = require('~/server/services/Tools/mcp');
|
||||
const { requireJwtAuth } = require('~/server/middleware');
|
||||
const { findPluginAuthsByKeys } = require('~/models');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
const router = Router();
|
||||
@@ -142,33 +144,12 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
`[MCP OAuth] Successfully reconnected ${serverName} for user ${flowState.userId}`,
|
||||
);
|
||||
|
||||
const userTools = (await getCachedTools({ userId: flowState.userId })) || {};
|
||||
|
||||
const mcpDelimiter = Constants.mcp_delimiter;
|
||||
for (const key of Object.keys(userTools)) {
|
||||
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
|
||||
delete userTools[key];
|
||||
}
|
||||
}
|
||||
|
||||
const tools = await userConnection.fetchTools();
|
||||
for (const tool of tools) {
|
||||
const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`;
|
||||
userTools[name] = {
|
||||
type: 'function',
|
||||
['function']: {
|
||||
name,
|
||||
description: tool.description,
|
||||
parameters: tool.inputSchema,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await setCachedTools(userTools, { userId: flowState.userId });
|
||||
|
||||
logger.debug(
|
||||
`[MCP OAuth] Cached ${tools.length} tools for ${serverName} user ${flowState.userId}`,
|
||||
);
|
||||
await updateMCPUserTools({
|
||||
userId: flowState.userId,
|
||||
serverName,
|
||||
tools,
|
||||
});
|
||||
} else {
|
||||
logger.debug(`[MCP OAuth] System-level OAuth completed for ${serverName}`);
|
||||
}
|
||||
@@ -323,124 +304,39 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||
const flowManager = getFlowStateManager(flowsCache);
|
||||
|
||||
await mcpManager.disconnectUserConnection(user.id, serverName);
|
||||
logger.info(
|
||||
`[MCP Reinitialize] Disconnected existing user connection for server: ${serverName}`,
|
||||
);
|
||||
|
||||
let customUserVars = {};
|
||||
/** @type {Record<string, Record<string, string>> | undefined} */
|
||||
let userMCPAuthMap;
|
||||
if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') {
|
||||
for (const varName of Object.keys(serverConfig.customUserVars)) {
|
||||
try {
|
||||
const value = await getUserPluginAuthValue(user.id, varName, false);
|
||||
customUserVars[varName] = value;
|
||||
} catch (err) {
|
||||
logger.error(`[MCP Reinitialize] Error fetching ${varName} for user ${user.id}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let userConnection = null;
|
||||
let oauthRequired = false;
|
||||
let oauthUrl = null;
|
||||
|
||||
try {
|
||||
userConnection = await mcpManager.getUserConnection({
|
||||
user,
|
||||
serverName,
|
||||
flowManager,
|
||||
customUserVars,
|
||||
tokenMethods: {
|
||||
findToken,
|
||||
updateToken,
|
||||
createToken,
|
||||
deleteTokens,
|
||||
},
|
||||
returnOnOAuth: true,
|
||||
oauthStart: async (authURL) => {
|
||||
logger.info(`[MCP Reinitialize] OAuth URL received: ${authURL}`);
|
||||
oauthUrl = authURL;
|
||||
oauthRequired = true;
|
||||
},
|
||||
userMCPAuthMap = await getUserMCPAuthMap({
|
||||
userId: user.id,
|
||||
servers: [serverName],
|
||||
findPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
logger.info(`[MCP Reinitialize] Successfully established connection for ${serverName}`);
|
||||
} catch (err) {
|
||||
logger.info(`[MCP Reinitialize] getUserConnection threw error: ${err.message}`);
|
||||
logger.info(
|
||||
`[MCP Reinitialize] OAuth state - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
||||
);
|
||||
|
||||
const isOAuthError =
|
||||
err.message?.includes('OAuth') ||
|
||||
err.message?.includes('authentication') ||
|
||||
err.message?.includes('401');
|
||||
|
||||
const isOAuthFlowInitiated = err.message === 'OAuth flow initiated - return early';
|
||||
|
||||
if (isOAuthError || oauthRequired || isOAuthFlowInitiated) {
|
||||
logger.info(
|
||||
`[MCP Reinitialize] OAuth required for ${serverName} (isOAuthError: ${isOAuthError}, oauthRequired: ${oauthRequired}, isOAuthFlowInitiated: ${isOAuthFlowInitiated})`,
|
||||
);
|
||||
oauthRequired = true;
|
||||
} else {
|
||||
logger.error(
|
||||
`[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`,
|
||||
err,
|
||||
);
|
||||
return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' });
|
||||
}
|
||||
}
|
||||
|
||||
if (userConnection && !oauthRequired) {
|
||||
const userTools = (await getCachedTools({ userId: user.id })) || {};
|
||||
const result = await reinitMCPServer({
|
||||
req,
|
||||
serverName,
|
||||
userMCPAuthMap,
|
||||
});
|
||||
|
||||
const mcpDelimiter = Constants.mcp_delimiter;
|
||||
for (const key of Object.keys(userTools)) {
|
||||
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
|
||||
delete userTools[key];
|
||||
}
|
||||
}
|
||||
|
||||
const tools = await userConnection.fetchTools();
|
||||
for (const tool of tools) {
|
||||
const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`;
|
||||
userTools[name] = {
|
||||
type: 'function',
|
||||
['function']: {
|
||||
name,
|
||||
description: tool.description,
|
||||
parameters: tool.inputSchema,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await setCachedTools(userTools, { userId: user.id });
|
||||
if (!result) {
|
||||
return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' });
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`[MCP Reinitialize] Sending response for ${serverName} - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
||||
);
|
||||
|
||||
const getResponseMessage = () => {
|
||||
if (oauthRequired) {
|
||||
return `MCP server '${serverName}' ready for OAuth authentication`;
|
||||
}
|
||||
if (userConnection) {
|
||||
return `MCP server '${serverName}' reinitialized successfully`;
|
||||
}
|
||||
return `Failed to reinitialize MCP server '${serverName}'`;
|
||||
};
|
||||
const { success, message, oauthRequired, oauthUrl } = result;
|
||||
|
||||
res.json({
|
||||
success: Boolean((userConnection && !oauthRequired) || (oauthRequired && oauthUrl)),
|
||||
message: getResponseMessage(),
|
||||
success,
|
||||
message,
|
||||
oauthUrl,
|
||||
serverName,
|
||||
oauthRequired,
|
||||
oauthUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[MCP Reinitialize] Unexpected error', error);
|
||||
|
||||
@@ -26,7 +26,7 @@ const ToolCacheKeys = {
|
||||
* @param {string[]} [options.roleIds] - Role IDs for role-based tools
|
||||
* @param {string[]} [options.groupIds] - Group IDs for group-based tools
|
||||
* @param {boolean} [options.includeGlobal=true] - Whether to include global tools
|
||||
* @returns {Promise<Object|null>} The available tools object or null if not cached
|
||||
* @returns {Promise<LCAvailableTools|null>} The available tools object or null if not cached
|
||||
*/
|
||||
async function getCachedTools(options = {}) {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
@@ -41,13 +41,13 @@ async function getCachedTools(options = {}) {
|
||||
// Future implementation will merge tools from multiple sources
|
||||
// based on user permissions, roles, and groups
|
||||
if (userId) {
|
||||
// Check if we have pre-computed effective tools for this user
|
||||
/** @type {LCAvailableTools | null} Check if we have pre-computed effective tools for this user */
|
||||
const effectiveTools = await cache.get(ToolCacheKeys.EFFECTIVE(userId));
|
||||
if (effectiveTools) {
|
||||
return effectiveTools;
|
||||
}
|
||||
|
||||
// Otherwise, compute from individual sources
|
||||
/** @type {LCAvailableTools | null} Otherwise, compute from individual sources */
|
||||
const toolSources = [];
|
||||
|
||||
if (includeGlobal) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { isEnabled, getUserMCPAuthMap } = require('@librechat/api');
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { CacheKeys, EModelEndpoint } = require('librechat-data-provider');
|
||||
const { normalizeEndpointName } = require('~/server/utils');
|
||||
const loadCustomConfig = require('./loadCustomConfig');
|
||||
@@ -53,31 +52,6 @@ const getCustomEndpointConfig = async (endpoint) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {string} params.userId
|
||||
* @param {GenericTool[]} [params.tools]
|
||||
* @param {import('@librechat/data-schemas').PluginAuthMethods['findPluginAuthsByKeys']} params.findPluginAuthsByKeys
|
||||
* @returns {Promise<Record<string, Record<string, string>> | undefined>}
|
||||
*/
|
||||
async function getMCPAuthMap({ userId, tools, findPluginAuthsByKeys }) {
|
||||
try {
|
||||
if (!tools || tools.length === 0) {
|
||||
return;
|
||||
}
|
||||
return await getUserMCPAuthMap({
|
||||
tools,
|
||||
userId,
|
||||
findPluginAuthsByKeys,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[api/server/controllers/agents/client.js #chatCompletion] Error getting custom user vars for agent`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
@@ -88,7 +62,6 @@ async function hasCustomUserVars() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMCPAuthMap,
|
||||
getCustomConfig,
|
||||
getBalanceConfig,
|
||||
hasCustomUserVars,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { config } = require('./EndpointService');
|
||||
const getCachedTools = require('./getCachedTools');
|
||||
const getCustomConfig = require('./getCustomConfig');
|
||||
const mcpToolsCache = require('./mcpToolsCache');
|
||||
const loadCustomConfig = require('./loadCustomConfig');
|
||||
const loadConfigModels = require('./loadConfigModels');
|
||||
const loadDefaultModels = require('./loadDefaultModels');
|
||||
@@ -17,5 +18,6 @@ module.exports = {
|
||||
loadAsyncEndpoints,
|
||||
...getCachedTools,
|
||||
...getCustomConfig,
|
||||
...mcpToolsCache,
|
||||
...getEndpointsConfig,
|
||||
};
|
||||
|
||||
@@ -76,10 +76,11 @@ async function loadConfigModels(req) {
|
||||
fetchPromisesMap[uniqueKey] =
|
||||
fetchPromisesMap[uniqueKey] ||
|
||||
fetchModels({
|
||||
user: req.user.id,
|
||||
baseURL: BASE_URL,
|
||||
apiKey: API_KEY,
|
||||
name,
|
||||
apiKey: API_KEY,
|
||||
baseURL: BASE_URL,
|
||||
user: req.user.id,
|
||||
direct: endpoint.directEndpoint,
|
||||
userIdQuery: models.userIdQuery,
|
||||
});
|
||||
uniqueKeyToEndpointsMap[uniqueKey] = uniqueKeyToEndpointsMap[uniqueKey] || [];
|
||||
|
||||
143
api/server/services/Config/mcpToolsCache.js
Normal file
143
api/server/services/Config/mcpToolsCache.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys, Constants } = require('librechat-data-provider');
|
||||
const { getCachedTools, setCachedTools } = require('./getCachedTools');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
/**
|
||||
* Updates MCP tools in the cache for a specific server and user
|
||||
* @param {Object} params - Parameters for updating MCP tools
|
||||
* @param {string} params.userId - User ID
|
||||
* @param {string} params.serverName - MCP server name
|
||||
* @param {Array} params.tools - Array of tool objects from MCP server
|
||||
* @returns {Promise<LCAvailableTools>}
|
||||
*/
|
||||
async function updateMCPUserTools({ userId, serverName, tools }) {
|
||||
try {
|
||||
const userTools = await getCachedTools({ userId });
|
||||
|
||||
const mcpDelimiter = Constants.mcp_delimiter;
|
||||
for (const key of Object.keys(userTools)) {
|
||||
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
|
||||
delete userTools[key];
|
||||
}
|
||||
}
|
||||
|
||||
for (const tool of tools) {
|
||||
const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`;
|
||||
userTools[name] = {
|
||||
type: 'function',
|
||||
['function']: {
|
||||
name,
|
||||
description: tool.description,
|
||||
parameters: tool.inputSchema,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await setCachedTools(userTools, { userId });
|
||||
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
await cache.delete(CacheKeys.TOOLS);
|
||||
logger.debug(`[MCP Cache] Updated ${tools.length} tools for ${serverName} user ${userId}`);
|
||||
return userTools;
|
||||
} catch (error) {
|
||||
logger.error(`[MCP Cache] Failed to update tools for ${serverName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges app-level tools with global tools
|
||||
* @param {import('@librechat/api').LCAvailableTools} appTools
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function mergeAppTools(appTools) {
|
||||
try {
|
||||
const count = Object.keys(appTools).length;
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
const cachedTools = await getCachedTools({ includeGlobal: true });
|
||||
const mergedTools = { ...cachedTools, ...appTools };
|
||||
await setCachedTools(mergedTools, { isGlobal: true });
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
await cache.delete(CacheKeys.TOOLS);
|
||||
logger.debug(`Merged ${count} app-level tools`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to merge app-level tools:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges user-level tools with global tools
|
||||
* @param {object} params
|
||||
* @param {string} params.userId
|
||||
* @param {Record<string, FunctionTool>} params.cachedUserTools
|
||||
* @param {import('@librechat/api').LCAvailableTools} params.userTools
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function mergeUserTools({ userId, cachedUserTools, userTools }) {
|
||||
try {
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const count = Object.keys(userTools).length;
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
const cachedTools = cachedUserTools ?? (await getCachedTools({ userId }));
|
||||
const mergedTools = { ...cachedTools, ...userTools };
|
||||
await setCachedTools(mergedTools, { userId });
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
await cache.delete(CacheKeys.TOOLS);
|
||||
logger.debug(`Merged ${count} user-level tools`);
|
||||
} catch (error) {
|
||||
logger.error('Failed to merge user-level tools:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all MCP tools for a specific server
|
||||
* @param {Object} params - Parameters for clearing MCP tools
|
||||
* @param {string} [params.userId] - User ID (if clearing user-specific tools)
|
||||
* @param {string} params.serverName - MCP server name
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function clearMCPServerTools({ userId, serverName }) {
|
||||
try {
|
||||
const tools = await getCachedTools({ userId, includeGlobal: !userId });
|
||||
|
||||
// Remove all tools for this server
|
||||
const mcpDelimiter = Constants.mcp_delimiter;
|
||||
let removedCount = 0;
|
||||
for (const key of Object.keys(tools)) {
|
||||
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
|
||||
delete tools[key];
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
await setCachedTools(tools, userId ? { userId } : { isGlobal: true });
|
||||
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
await cache.delete(CacheKeys.TOOLS);
|
||||
|
||||
logger.debug(
|
||||
`[MCP Cache] Removed ${removedCount} tools for ${serverName}${userId ? ` user ${userId}` : ' (global)'}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[MCP Cache] Failed to clear tools for ${serverName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mergeAppTools,
|
||||
mergeUserTools,
|
||||
updateMCPUserTools,
|
||||
clearMCPServerTools,
|
||||
};
|
||||
@@ -30,7 +30,13 @@ const { getModelMaxTokens } = require('~/utils');
|
||||
* @param {TEndpointOption} [params.endpointOption]
|
||||
* @param {Set<string>} [params.allowedProviders]
|
||||
* @param {boolean} [params.isInitialAgent]
|
||||
* @returns {Promise<Agent & { tools: StructuredTool[], attachments: Array<MongoFile>, toolContextMap: Record<string, unknown>, maxContextTokens: number }>}
|
||||
* @returns {Promise<Agent & {
|
||||
* tools: StructuredTool[],
|
||||
* attachments: Array<MongoFile>,
|
||||
* toolContextMap: Record<string, unknown>,
|
||||
* maxContextTokens: number,
|
||||
* userMCPAuthMap?: Record<string, Record<string, string>>
|
||||
* }>}
|
||||
*/
|
||||
const initializeAgent = async ({
|
||||
req,
|
||||
@@ -91,16 +97,19 @@ const initializeAgent = async ({
|
||||
});
|
||||
|
||||
const provider = agent.provider;
|
||||
const { tools: structuredTools, toolContextMap } =
|
||||
(await loadTools?.({
|
||||
req,
|
||||
res,
|
||||
provider,
|
||||
agentId: agent.id,
|
||||
tools: agent.tools,
|
||||
model: agent.model,
|
||||
tool_resources,
|
||||
})) ?? {};
|
||||
const {
|
||||
tools: structuredTools,
|
||||
toolContextMap,
|
||||
userMCPAuthMap,
|
||||
} = (await loadTools?.({
|
||||
req,
|
||||
res,
|
||||
provider,
|
||||
agentId: agent.id,
|
||||
tools: agent.tools,
|
||||
model: agent.model,
|
||||
tool_resources,
|
||||
})) ?? {};
|
||||
|
||||
agent.endpoint = provider;
|
||||
const { getOptions, overrideProvider } = await getProviderConfig(provider);
|
||||
@@ -189,6 +198,7 @@ const initializeAgent = async ({
|
||||
tools,
|
||||
attachments,
|
||||
resendFiles,
|
||||
userMCPAuthMap,
|
||||
toolContextMap,
|
||||
useLegacyContent: !!options.useLegacyContent,
|
||||
maxContextTokens: Math.round((agentMaxContextTokens - maxTokens) * 0.9),
|
||||
|
||||
@@ -19,7 +19,10 @@ const AgentClient = require('~/server/controllers/agents/client');
|
||||
const { getAgent } = require('~/models/Agent');
|
||||
const { logViolation } = require('~/cache');
|
||||
|
||||
function createToolLoader() {
|
||||
/**
|
||||
* @param {AbortSignal} signal
|
||||
*/
|
||||
function createToolLoader(signal) {
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServerRequest} params.req
|
||||
@@ -29,7 +32,11 @@ function createToolLoader() {
|
||||
* @param {string} params.provider
|
||||
* @param {string} params.model
|
||||
* @param {AgentToolResources} params.tool_resources
|
||||
* @returns {Promise<{ tools: StructuredTool[], toolContextMap: Record<string, unknown> } | undefined>}
|
||||
* @returns {Promise<{
|
||||
* tools: StructuredTool[],
|
||||
* toolContextMap: Record<string, unknown>,
|
||||
* userMCPAuthMap?: Record<string, Record<string, string>>
|
||||
* } | undefined>}
|
||||
*/
|
||||
return async function loadTools({ req, res, agentId, tools, provider, model, tool_resources }) {
|
||||
const agent = { id: agentId, tools, provider, model };
|
||||
@@ -38,6 +45,7 @@ function createToolLoader() {
|
||||
req,
|
||||
res,
|
||||
agent,
|
||||
signal,
|
||||
tool_resources,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -46,7 +54,7 @@ function createToolLoader() {
|
||||
};
|
||||
}
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
const initializeClient = async ({ req, res, signal, endpointOption }) => {
|
||||
if (!endpointOption) {
|
||||
throw new Error('Endpoint option not provided');
|
||||
}
|
||||
@@ -92,7 +100,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
/** @type {Set<string>} */
|
||||
const allowedProviders = new Set(req?.app?.locals?.[EModelEndpoint.agents]?.allowedProviders);
|
||||
|
||||
const loadTools = createToolLoader();
|
||||
const loadTools = createToolLoader(signal);
|
||||
/** @type {Array<MongoFile>} */
|
||||
const requestFiles = req.body.files ?? [];
|
||||
/** @type {string} */
|
||||
@@ -111,6 +119,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
});
|
||||
|
||||
const agent_ids = primaryConfig.agent_ids;
|
||||
let userMCPAuthMap = primaryConfig.userMCPAuthMap;
|
||||
if (agent_ids?.length) {
|
||||
for (const agentId of agent_ids) {
|
||||
const agent = await getAgent({ id: agentId });
|
||||
@@ -140,6 +149,11 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
endpointOption,
|
||||
allowedProviders,
|
||||
});
|
||||
if (userMCPAuthMap != null) {
|
||||
Object.assign(userMCPAuthMap, config.userMCPAuthMap ?? {});
|
||||
} else {
|
||||
userMCPAuthMap = config.userMCPAuthMap;
|
||||
}
|
||||
agentConfigs.set(agentId, config);
|
||||
}
|
||||
}
|
||||
@@ -188,7 +202,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
|
||||
: EModelEndpoint.agents,
|
||||
});
|
||||
|
||||
return { client };
|
||||
return { client, userMCPAuthMap };
|
||||
};
|
||||
|
||||
module.exports = { initializeClient };
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
const getLogStores = require('~/cache/getLogStores');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { saveConvo } = require('~/models');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
/**
|
||||
* Add title to conversation in a way that avoids memory retention
|
||||
|
||||
@@ -39,8 +39,9 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
|
||||
if (optionsOnly) {
|
||||
clientOptions = Object.assign(
|
||||
{
|
||||
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
|
||||
proxy: PROXY ?? null,
|
||||
userId: req.user.id,
|
||||
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
|
||||
modelOptions: endpointOption?.model_parameters ?? {},
|
||||
},
|
||||
clientOptions,
|
||||
|
||||
@@ -15,6 +15,7 @@ const { checkPromptCacheSupport, getClaudeHeaders, configureReasoning } = requir
|
||||
* @param {number} [options.modelOptions.topK] - Controls the number of top tokens to consider.
|
||||
* @param {string[]} [options.modelOptions.stop] - Sequences where the API will stop generating further tokens.
|
||||
* @param {boolean} [options.modelOptions.stream] - Whether to stream the response.
|
||||
* @param {string} options.userId - The user ID for tracking and personalization.
|
||||
* @param {string} [options.proxy] - Proxy server URL.
|
||||
* @param {string} [options.reverseProxyUrl] - URL for a reverse proxy, if used.
|
||||
*
|
||||
@@ -47,6 +48,11 @@ function getLLMConfig(apiKey, options = {}) {
|
||||
maxTokens:
|
||||
mergedOptions.maxOutputTokens || anthropicSettings.maxOutputTokens.reset(mergedOptions.model),
|
||||
clientOptions: {},
|
||||
invocationKwargs: {
|
||||
metadata: {
|
||||
user_id: options.userId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
requestOptions = configureReasoning(requestOptions, systemOptions);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
const { z } = require('zod');
|
||||
const { tool } = require('@langchain/core/tools');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { Constants: AgentConstants, Providers, GraphEvents } = require('@librechat/agents');
|
||||
const {
|
||||
Providers,
|
||||
StepTypes,
|
||||
GraphEvents,
|
||||
Constants: AgentConstants,
|
||||
} = require('@librechat/agents');
|
||||
const {
|
||||
sendEvent,
|
||||
MCPOAuthHandler,
|
||||
@@ -11,14 +16,14 @@ const {
|
||||
const {
|
||||
Time,
|
||||
CacheKeys,
|
||||
StepTypes,
|
||||
Constants,
|
||||
ContentTypes,
|
||||
isAssistantsEndpoint,
|
||||
} = require('librechat-data-provider');
|
||||
const { getCachedTools, loadCustomConfig } = require('./Config');
|
||||
const { findToken, createToken, updateToken } = require('~/models');
|
||||
const { getMCPManager, getFlowStateManager } = require('~/config');
|
||||
const { getCachedTools, loadCustomConfig } = require('./Config');
|
||||
const { reinitMCPServer } = require('./Tools/mcp');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
/**
|
||||
@@ -26,16 +31,13 @@ const { getLogStores } = require('~/cache');
|
||||
* @param {ServerResponse} params.res - The Express response object for sending events.
|
||||
* @param {string} params.stepId - The ID of the step in the flow.
|
||||
* @param {ToolCallChunk} params.toolCall - The tool call object containing tool information.
|
||||
* @param {string} params.loginFlowId - The ID of the login flow.
|
||||
* @param {FlowStateManager<any>} params.flowManager - The flow manager instance.
|
||||
*/
|
||||
function createOAuthStart({ res, stepId, toolCall, loginFlowId, flowManager, signal }) {
|
||||
function createRunStepDeltaEmitter({ res, stepId, toolCall }) {
|
||||
/**
|
||||
* Creates a function to handle OAuth login requests.
|
||||
* @param {string} authURL - The URL to redirect the user for OAuth authentication.
|
||||
* @returns {Promise<boolean>} Returns true to indicate the event was sent successfully.
|
||||
* @returns {void}
|
||||
*/
|
||||
return async function (authURL) {
|
||||
return function (authURL) {
|
||||
/** @type {{ id: string; delta: AgentToolCallDelta }} */
|
||||
const data = {
|
||||
id: stepId,
|
||||
@@ -46,17 +48,54 @@ function createOAuthStart({ res, stepId, toolCall, loginFlowId, flowManager, sig
|
||||
expires_at: Date.now() + Time.TWO_MINUTES,
|
||||
},
|
||||
};
|
||||
/** Used to ensure the handler (use of `sendEvent`) is only invoked once */
|
||||
await flowManager.createFlowWithHandler(
|
||||
loginFlowId,
|
||||
'oauth_login',
|
||||
async () => {
|
||||
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
|
||||
logger.debug('Sent OAuth login request to client');
|
||||
return true;
|
||||
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServerResponse} params.res - The Express response object for sending events.
|
||||
* @param {string} params.runId - The Run ID, i.e. message ID
|
||||
* @param {string} params.stepId - The ID of the step in the flow.
|
||||
* @param {ToolCallChunk} params.toolCall - The tool call object containing tool information.
|
||||
* @param {number} [params.index]
|
||||
*/
|
||||
function createRunStepEmitter({ res, runId, stepId, toolCall, index }) {
|
||||
return function () {
|
||||
/** @type {import('@librechat/agents').RunStep} */
|
||||
const data = {
|
||||
runId: runId ?? Constants.USE_PRELIM_RESPONSE_MESSAGE_ID,
|
||||
id: stepId,
|
||||
type: StepTypes.TOOL_CALLS,
|
||||
index: index ?? 0,
|
||||
stepDetails: {
|
||||
type: StepTypes.TOOL_CALLS,
|
||||
tool_calls: [toolCall],
|
||||
},
|
||||
signal,
|
||||
);
|
||||
};
|
||||
sendEvent(res, { event: GraphEvents.ON_RUN_STEP, data });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function used to ensure the flow handler is only invoked once
|
||||
* @param {object} params
|
||||
* @param {string} params.flowId - The ID of the login flow.
|
||||
* @param {FlowStateManager<any>} params.flowManager - The flow manager instance.
|
||||
* @param {(authURL: string) => void} [params.callback]
|
||||
*/
|
||||
function createOAuthStart({ flowId, flowManager, callback }) {
|
||||
/**
|
||||
* Creates a function to handle OAuth login requests.
|
||||
* @param {string} authURL - The URL to redirect the user for OAuth authentication.
|
||||
* @returns {Promise<boolean>} Returns true to indicate the event was sent successfully.
|
||||
*/
|
||||
return async function (authURL) {
|
||||
await flowManager.createFlowWithHandler(flowId, 'oauth_login', async () => {
|
||||
callback?.(authURL);
|
||||
logger.debug('Sent OAuth login request to client');
|
||||
return true;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,23 +138,166 @@ function createAbortHandler({ userId, serverName, toolName, flowManager }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a general tool for an entire action set.
|
||||
* @param {Object} params
|
||||
* @param {() => void} params.runStepEmitter
|
||||
* @param {(authURL: string) => void} params.runStepDeltaEmitter
|
||||
* @returns {(authURL: string) => void}
|
||||
*/
|
||||
function createOAuthCallback({ runStepEmitter, runStepDeltaEmitter }) {
|
||||
return function (authURL) {
|
||||
runStepEmitter();
|
||||
runStepDeltaEmitter(authURL);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {ServerRequest} params.req - The Express request object, containing user/request info.
|
||||
* @param {ServerResponse} params.res - The Express response object for sending events.
|
||||
* @param {string} params.serverName
|
||||
* @param {AbortSignal} params.signal
|
||||
* @param {string} params.model
|
||||
* @param {number} [params.index]
|
||||
* @param {Record<string, Record<string, string>>} [params.userMCPAuthMap]
|
||||
* @returns { Promise<Array<typeof tool | { _call: (toolInput: Object | string) => unknown}>> } An object with `_call` method to execute the tool input.
|
||||
*/
|
||||
async function reconnectServer({ req, res, index, signal, serverName, userMCPAuthMap }) {
|
||||
const runId = Constants.USE_PRELIM_RESPONSE_MESSAGE_ID;
|
||||
const flowId = `${req.user?.id}:${serverName}:${Date.now()}`;
|
||||
const flowManager = getFlowStateManager(getLogStores(CacheKeys.FLOWS));
|
||||
const stepId = 'step_oauth_login_' + serverName;
|
||||
const toolCall = {
|
||||
id: flowId,
|
||||
name: serverName,
|
||||
type: 'tool_call_chunk',
|
||||
};
|
||||
|
||||
const runStepEmitter = createRunStepEmitter({
|
||||
res,
|
||||
index,
|
||||
runId,
|
||||
stepId,
|
||||
toolCall,
|
||||
});
|
||||
const runStepDeltaEmitter = createRunStepDeltaEmitter({
|
||||
res,
|
||||
stepId,
|
||||
toolCall,
|
||||
});
|
||||
const callback = createOAuthCallback({ runStepEmitter, runStepDeltaEmitter });
|
||||
const oauthStart = createOAuthStart({
|
||||
res,
|
||||
flowId,
|
||||
callback,
|
||||
flowManager,
|
||||
});
|
||||
return await reinitMCPServer({
|
||||
req,
|
||||
signal,
|
||||
serverName,
|
||||
oauthStart,
|
||||
flowManager,
|
||||
userMCPAuthMap,
|
||||
forceNew: true,
|
||||
returnOnOAuth: false,
|
||||
connectionTimeout: Time.TWO_MINUTES,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all tools from the specified MCP Server via `toolKey`.
|
||||
*
|
||||
* @param {Object} params - The parameters for loading action sets.
|
||||
* This function assumes tools could not be aggregated from the cache of tool definitions,
|
||||
* i.e. `availableTools`, and will reinitialize the MCP server to ensure all tools are generated.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {ServerRequest} params.req - The Express request object, containing user/request info.
|
||||
* @param {ServerResponse} params.res - The Express response object for sending events.
|
||||
* @param {string} params.serverName
|
||||
* @param {string} params.model
|
||||
* @param {Providers | EModelEndpoint} params.provider - The provider for the tool.
|
||||
* @param {number} [params.index]
|
||||
* @param {AbortSignal} [params.signal]
|
||||
* @param {Record<string, Record<string, string>>} [params.userMCPAuthMap]
|
||||
* @returns { Promise<Array<typeof tool | { _call: (toolInput: Object | string) => unknown}>> } An object with `_call` method to execute the tool input.
|
||||
*/
|
||||
async function createMCPTools({ req, res, index, signal, serverName, provider, userMCPAuthMap }) {
|
||||
const result = await reconnectServer({ req, res, index, signal, serverName, userMCPAuthMap });
|
||||
if (!result || !result.tools) {
|
||||
logger.warn(`[MCP][${serverName}] Failed to reinitialize MCP server.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const serverTools = [];
|
||||
for (const tool of result.tools) {
|
||||
const toolInstance = await createMCPTool({
|
||||
req,
|
||||
res,
|
||||
provider,
|
||||
userMCPAuthMap,
|
||||
availableTools: result.availableTools,
|
||||
toolKey: `${tool.name}${Constants.mcp_delimiter}${serverName}`,
|
||||
});
|
||||
if (toolInstance) {
|
||||
serverTools.push(toolInstance);
|
||||
}
|
||||
}
|
||||
|
||||
return serverTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single tool from the specified MCP Server via `toolKey`.
|
||||
* @param {Object} params
|
||||
* @param {ServerRequest} params.req - The Express request object, containing user/request info.
|
||||
* @param {ServerResponse} params.res - The Express response object for sending events.
|
||||
* @param {string} params.toolKey - The toolKey for the tool.
|
||||
* @param {import('@librechat/agents').Providers | EModelEndpoint} params.provider - The provider for the tool.
|
||||
* @param {string} params.model - The model for the tool.
|
||||
* @param {number} [params.index]
|
||||
* @param {AbortSignal} [params.signal]
|
||||
* @param {Providers | EModelEndpoint} params.provider - The provider for the tool.
|
||||
* @param {LCAvailableTools} [params.availableTools]
|
||||
* @param {Record<string, Record<string, string>>} [params.userMCPAuthMap]
|
||||
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
|
||||
*/
|
||||
async function createMCPTool({ req, res, toolKey, provider: _provider }) {
|
||||
const availableTools = await getCachedTools({ userId: req.user?.id, includeGlobal: true });
|
||||
const toolDefinition = availableTools?.[toolKey]?.function;
|
||||
async function createMCPTool({
|
||||
req,
|
||||
res,
|
||||
index,
|
||||
signal,
|
||||
toolKey,
|
||||
provider,
|
||||
userMCPAuthMap,
|
||||
availableTools: tools,
|
||||
}) {
|
||||
const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter);
|
||||
const availableTools =
|
||||
tools ?? (await getCachedTools({ userId: req.user?.id, includeGlobal: true }));
|
||||
/** @type {LCTool | undefined} */
|
||||
let toolDefinition = availableTools?.[toolKey]?.function;
|
||||
if (!toolDefinition) {
|
||||
logger.error(`Tool ${toolKey} not found in available tools`);
|
||||
return null;
|
||||
logger.warn(
|
||||
`[MCP][${serverName}][${toolName}] Requested tool not found in available tools, re-initializing MCP server.`,
|
||||
);
|
||||
const result = await reconnectServer({ req, res, index, signal, serverName, userMCPAuthMap });
|
||||
toolDefinition = result?.availableTools?.[toolKey]?.function;
|
||||
}
|
||||
|
||||
if (!toolDefinition) {
|
||||
logger.warn(`[MCP][${serverName}][${toolName}] Tool definition not found, cannot create tool.`);
|
||||
return;
|
||||
}
|
||||
|
||||
return createToolInstance({
|
||||
res,
|
||||
provider,
|
||||
toolName,
|
||||
serverName,
|
||||
toolDefinition,
|
||||
});
|
||||
}
|
||||
|
||||
function createToolInstance({ res, toolName, serverName, toolDefinition, provider: _provider }) {
|
||||
/** @type {LCTool} */
|
||||
const { description, parameters } = toolDefinition;
|
||||
const isGoogle = _provider === Providers.VERTEXAI || _provider === Providers.GOOGLE;
|
||||
@@ -128,16 +310,8 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) {
|
||||
schema = z.object({ input: z.string().optional() });
|
||||
}
|
||||
|
||||
const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter);
|
||||
const normalizedToolKey = `${toolName}${Constants.mcp_delimiter}${normalizeServerName(serverName)}`;
|
||||
|
||||
if (!req.user?.id) {
|
||||
logger.error(
|
||||
`[MCP][${serverName}][${toolName}] User ID not found on request. Cannot create tool.`,
|
||||
);
|
||||
throw new Error(`User ID not found on request. Cannot create tool for ${toolKey}.`);
|
||||
}
|
||||
|
||||
/** @type {(toolArguments: Object | string, config?: GraphRunnableConfig) => Promise<unknown>} */
|
||||
const _call = async (toolArguments, config) => {
|
||||
const userId = config?.configurable?.user?.id || config?.configurable?.user_id;
|
||||
@@ -154,14 +328,16 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) {
|
||||
const provider = (config?.metadata?.provider || _provider)?.toLowerCase();
|
||||
|
||||
const { args: _args, stepId, ...toolCall } = config.toolCall ?? {};
|
||||
const loginFlowId = `${serverName}:oauth_login:${config.metadata.thread_id}:${config.metadata.run_id}`;
|
||||
const oauthStart = createOAuthStart({
|
||||
const flowId = `${serverName}:oauth_login:${config.metadata.thread_id}:${config.metadata.run_id}`;
|
||||
const runStepDeltaEmitter = createRunStepDeltaEmitter({
|
||||
res,
|
||||
stepId,
|
||||
toolCall,
|
||||
loginFlowId,
|
||||
});
|
||||
const oauthStart = createOAuthStart({
|
||||
flowId,
|
||||
flowManager,
|
||||
signal: derivedSignal,
|
||||
callback: runStepDeltaEmitter,
|
||||
});
|
||||
const oauthEnd = createOAuthEnd({
|
||||
res,
|
||||
@@ -207,7 +383,7 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) {
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[MCP][User: ${userId}][${serverName}] Error calling "${toolName}" MCP tool:`,
|
||||
`[MCP][${serverName}][${toolName}][User: ${userId}] Error calling MCP tool:`,
|
||||
error,
|
||||
);
|
||||
|
||||
@@ -220,12 +396,12 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) {
|
||||
|
||||
if (isOAuthError) {
|
||||
throw new Error(
|
||||
`OAuth authentication required for ${serverName}. Please check the server logs for the authentication URL.`,
|
||||
`[MCP][${serverName}][${toolName}] OAuth authentication required. Please check the server logs for the authentication URL.`,
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`"${toolKey}" tool call failed${error?.message ? `: ${error?.message}` : '.'}`,
|
||||
`[MCP][${serverName}][${toolName}] tool call failed${error?.message ? `: ${error?.message}` : '.'}`,
|
||||
);
|
||||
} finally {
|
||||
// Clean up abort handler to prevent memory leaks
|
||||
@@ -380,6 +556,7 @@ async function getServerConnectionStatus(
|
||||
|
||||
module.exports = {
|
||||
createMCPTool,
|
||||
createMCPTools,
|
||||
getMCPSetupData,
|
||||
checkOAuthFlowStatus,
|
||||
getServerConnectionStatus,
|
||||
|
||||
@@ -34,6 +34,7 @@ const { openAIApiKey, userProvidedOpenAI } = require('./Config/EndpointService')
|
||||
* @param {string} params.apiKey - The API key for authentication with the API.
|
||||
* @param {string} params.baseURL - The base path URL for the API.
|
||||
* @param {string} [params.name='OpenAI'] - The name of the API; defaults to 'OpenAI'.
|
||||
* @param {boolean} [params.direct=false] - Whether `directEndpoint` was configured
|
||||
* @param {boolean} [params.azure=false] - Whether to fetch models from Azure.
|
||||
* @param {boolean} [params.userIdQuery=false] - Whether to send the user ID as a query parameter.
|
||||
* @param {boolean} [params.createTokenConfig=true] - Whether to create a token configuration from the API response.
|
||||
@@ -44,14 +45,16 @@ const { openAIApiKey, userProvidedOpenAI } = require('./Config/EndpointService')
|
||||
const fetchModels = async ({
|
||||
user,
|
||||
apiKey,
|
||||
baseURL,
|
||||
baseURL: _baseURL,
|
||||
name = EModelEndpoint.openAI,
|
||||
direct,
|
||||
azure = false,
|
||||
userIdQuery = false,
|
||||
createTokenConfig = true,
|
||||
tokenKey,
|
||||
}) => {
|
||||
let models = [];
|
||||
const baseURL = direct ? extractBaseURL(_baseURL) : _baseURL;
|
||||
|
||||
if (!baseURL && !azure) {
|
||||
return models;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { sleep } = require('@librechat/agents');
|
||||
const { getToolkitKey } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { zodToJsonSchema } = require('zod-to-json-schema');
|
||||
const { getToolkitKey, getUserMCPAuthMap } = require('@librechat/api');
|
||||
const { Calculator } = require('@langchain/community/tools/calculator');
|
||||
const { tool: toolFn, Tool, DynamicStructuredTool } = require('@langchain/core/tools');
|
||||
const {
|
||||
@@ -33,12 +33,17 @@ const {
|
||||
toolkits,
|
||||
} = require('~/app/clients/tools');
|
||||
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
|
||||
const { getEndpointsConfig, getCachedTools } = require('~/server/services/Config');
|
||||
const {
|
||||
getEndpointsConfig,
|
||||
hasCustomUserVars,
|
||||
getCachedTools,
|
||||
} = require('~/server/services/Config');
|
||||
const { createOnSearchResults } = require('~/server/services/Tools/search');
|
||||
const { isActionDomainAllowed } = require('~/server/services/domains');
|
||||
const { recordUsage } = require('~/server/services/Threads');
|
||||
const { loadTools } = require('~/app/clients/tools/util');
|
||||
const { redactMessage } = require('~/config/parsers');
|
||||
const { findPluginAuthsByKeys } = require('~/models');
|
||||
|
||||
/**
|
||||
* Loads and formats tools from the specified tool directory.
|
||||
@@ -469,11 +474,12 @@ async function processRequiredActions(client, requiredActions) {
|
||||
* @param {Object} params - Run params containing user and request information.
|
||||
* @param {ServerRequest} params.req - The request object.
|
||||
* @param {ServerResponse} params.res - The request object.
|
||||
* @param {AbortSignal} params.signal
|
||||
* @param {Pick<Agent, 'id' | 'provider' | 'model' | 'tools'} params.agent - The agent to load tools for.
|
||||
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
|
||||
* @returns {Promise<{ tools?: StructuredTool[] }>} The agent tools.
|
||||
* @returns {Promise<{ tools?: StructuredTool[]; userMCPAuthMap?: Record<string, Record<string, string>> }>} The agent tools.
|
||||
*/
|
||||
async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) {
|
||||
async function loadAgentTools({ req, res, agent, signal, tool_resources, openAIApiKey }) {
|
||||
if (!agent.tools || agent.tools.length === 0) {
|
||||
return {};
|
||||
} else if (agent.tools && agent.tools.length === 1 && agent.tools[0] === AgentCapabilities.ocr) {
|
||||
@@ -523,8 +529,20 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey })
|
||||
webSearchCallbacks = createOnSearchResults(res);
|
||||
}
|
||||
|
||||
/** @type {Record<string, Record<string, string>>} */
|
||||
let userMCPAuthMap;
|
||||
if (await hasCustomUserVars()) {
|
||||
userMCPAuthMap = await getUserMCPAuthMap({
|
||||
tools: agent.tools,
|
||||
userId: req.user.id,
|
||||
findPluginAuthsByKeys,
|
||||
});
|
||||
}
|
||||
|
||||
const { loadedTools, toolContextMap } = await loadTools({
|
||||
agent,
|
||||
signal,
|
||||
userMCPAuthMap,
|
||||
functions: true,
|
||||
user: req.user.id,
|
||||
tools: _agentTools,
|
||||
@@ -588,6 +606,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey })
|
||||
if (!checkCapability(AgentCapabilities.actions)) {
|
||||
return {
|
||||
tools: agentTools,
|
||||
userMCPAuthMap,
|
||||
toolContextMap,
|
||||
};
|
||||
}
|
||||
@@ -599,6 +618,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey })
|
||||
}
|
||||
return {
|
||||
tools: agentTools,
|
||||
userMCPAuthMap,
|
||||
toolContextMap,
|
||||
};
|
||||
}
|
||||
@@ -707,6 +727,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey })
|
||||
return {
|
||||
tools: agentTools,
|
||||
toolContextMap,
|
||||
userMCPAuthMap,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
142
api/server/services/Tools/mcp.js
Normal file
142
api/server/services/Tools/mcp.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys, Constants } = require('librechat-data-provider');
|
||||
const { findToken, createToken, updateToken, deleteTokens } = require('~/models');
|
||||
const { getMCPManager, getFlowStateManager } = require('~/config');
|
||||
const { updateMCPUserTools } = require('~/server/services/Config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {ServerRequest} params.req
|
||||
* @param {string} params.serverName - The name of the MCP server
|
||||
* @param {boolean} params.returnOnOAuth - Whether to initiate OAuth and return, or wait for OAuth flow to finish
|
||||
* @param {AbortSignal} [params.signal] - The abort signal to handle cancellation.
|
||||
* @param {boolean} [params.forceNew]
|
||||
* @param {number} [params.connectionTimeout]
|
||||
* @param {FlowStateManager<any>} [params.flowManager]
|
||||
* @param {(authURL: string) => Promise<boolean>} [params.oauthStart]
|
||||
* @param {Record<string, Record<string, string>>} [params.userMCPAuthMap]
|
||||
*/
|
||||
async function reinitMCPServer({
|
||||
req,
|
||||
signal,
|
||||
forceNew,
|
||||
serverName,
|
||||
userMCPAuthMap,
|
||||
connectionTimeout,
|
||||
returnOnOAuth = true,
|
||||
oauthStart: _oauthStart,
|
||||
flowManager: _flowManager,
|
||||
}) {
|
||||
/** @type {MCPConnection | null} */
|
||||
let userConnection = null;
|
||||
/** @type {LCAvailableTools | null} */
|
||||
let availableTools = null;
|
||||
/** @type {ReturnType<MCPConnection['fetchTools']> | null} */
|
||||
let tools = null;
|
||||
let oauthRequired = false;
|
||||
let oauthUrl = null;
|
||||
try {
|
||||
const customUserVars = userMCPAuthMap?.[`${Constants.mcp_prefix}${serverName}`];
|
||||
const flowManager = _flowManager ?? getFlowStateManager(getLogStores(CacheKeys.FLOWS));
|
||||
const mcpManager = getMCPManager();
|
||||
|
||||
const oauthStart =
|
||||
_oauthStart ??
|
||||
(async (authURL) => {
|
||||
logger.info(`[MCP Reinitialize] OAuth URL received: ${authURL}`);
|
||||
oauthUrl = authURL;
|
||||
oauthRequired = true;
|
||||
});
|
||||
|
||||
try {
|
||||
userConnection = await mcpManager.getUserConnection({
|
||||
user: req.user,
|
||||
signal,
|
||||
forceNew,
|
||||
oauthStart,
|
||||
serverName,
|
||||
flowManager,
|
||||
returnOnOAuth,
|
||||
customUserVars,
|
||||
connectionTimeout,
|
||||
tokenMethods: {
|
||||
findToken,
|
||||
updateToken,
|
||||
createToken,
|
||||
deleteTokens,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`[MCP Reinitialize] Successfully established connection for ${serverName}`);
|
||||
} catch (err) {
|
||||
logger.info(`[MCP Reinitialize] getUserConnection threw error: ${err.message}`);
|
||||
logger.info(
|
||||
`[MCP Reinitialize] OAuth state - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
||||
);
|
||||
|
||||
const isOAuthError =
|
||||
err.message?.includes('OAuth') ||
|
||||
err.message?.includes('authentication') ||
|
||||
err.message?.includes('401');
|
||||
|
||||
const isOAuthFlowInitiated = err.message === 'OAuth flow initiated - return early';
|
||||
|
||||
if (isOAuthError || oauthRequired || isOAuthFlowInitiated) {
|
||||
logger.info(
|
||||
`[MCP Reinitialize] OAuth required for ${serverName} (isOAuthError: ${isOAuthError}, oauthRequired: ${oauthRequired}, isOAuthFlowInitiated: ${isOAuthFlowInitiated})`,
|
||||
);
|
||||
oauthRequired = true;
|
||||
} else {
|
||||
logger.error(
|
||||
`[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (userConnection && !oauthRequired) {
|
||||
tools = await userConnection.fetchTools();
|
||||
availableTools = await updateMCPUserTools({
|
||||
userId: req.user.id,
|
||||
serverName,
|
||||
tools,
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`[MCP Reinitialize] Sending response for ${serverName} - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
||||
);
|
||||
|
||||
const getResponseMessage = () => {
|
||||
if (oauthRequired) {
|
||||
return `MCP server '${serverName}' ready for OAuth authentication`;
|
||||
}
|
||||
if (userConnection) {
|
||||
return `MCP server '${serverName}' reinitialized successfully`;
|
||||
}
|
||||
return `Failed to reinitialize MCP server '${serverName}'`;
|
||||
};
|
||||
|
||||
const result = {
|
||||
availableTools,
|
||||
success: Boolean((userConnection && !oauthRequired) || (oauthRequired && oauthUrl)),
|
||||
message: getResponseMessage(),
|
||||
oauthRequired,
|
||||
serverName,
|
||||
oauthUrl,
|
||||
tools,
|
||||
};
|
||||
logger.debug(`[MCP Reinitialize] Response for ${serverName}:`, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[MCP Reinitialize] Error loading MCP Tools, servers may still be initializing:',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
reinitMCPServer,
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { getCachedTools, setCachedTools } = require('./Config');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
const { createMCPManager } = require('~/config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
const { mergeAppTools } = require('./Config');
|
||||
|
||||
/**
|
||||
* Initialize MCP servers
|
||||
@@ -18,21 +16,12 @@ async function initializeMCPs(app) {
|
||||
|
||||
try {
|
||||
delete app.locals.mcpConfig;
|
||||
const cachedTools = await getCachedTools();
|
||||
const mcpTools = mcpManager.getAppToolFunctions() || {};
|
||||
await mergeAppTools(mcpTools);
|
||||
|
||||
if (!cachedTools) {
|
||||
logger.warn('No available tools found in cache during MCP initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
const mcpTools = mcpManager.getAppToolFunctions() ?? {};
|
||||
await setCachedTools({ ...cachedTools, ...mcpTools }, { isGlobal: true });
|
||||
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
await cache.delete(CacheKeys.TOOLS);
|
||||
logger.debug('Cleared tools array cache after MCP initialization');
|
||||
|
||||
logger.info('MCP servers initialized successfully');
|
||||
logger.info(
|
||||
`MCP servers initialized successfully. Added ${Object.keys(mcpTools).length} MCP tools.`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize MCP servers:', error);
|
||||
}
|
||||
|
||||
@@ -138,9 +138,6 @@ async function loadDefaultInterface(config, configDefaults) {
|
||||
defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.USE],
|
||||
defaults.prompts,
|
||||
),
|
||||
[Permissions.SHARED_GLOBAL]:
|
||||
defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARED_GLOBAL],
|
||||
[Permissions.CREATE]: defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.CREATE],
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
@@ -155,9 +152,6 @@ async function loadDefaultInterface(config, configDefaults) {
|
||||
defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.USE],
|
||||
defaults.memories,
|
||||
),
|
||||
[Permissions.CREATE]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.CREATE],
|
||||
[Permissions.UPDATE]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.UPDATE],
|
||||
[Permissions.READ]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.READ],
|
||||
[Permissions.OPT_OUT]: isPersonalizationEnabled,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
@@ -173,9 +167,6 @@ async function loadDefaultInterface(config, configDefaults) {
|
||||
defaultPerms[PermissionTypes.AGENTS]?.[Permissions.USE],
|
||||
defaults.agents,
|
||||
),
|
||||
[Permissions.SHARED_GLOBAL]:
|
||||
defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARED_GLOBAL],
|
||||
[Permissions.CREATE]: defaultPerms[PermissionTypes.AGENTS]?.[Permissions.CREATE],
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
|
||||
@@ -54,22 +54,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForUser = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
|
||||
@@ -87,22 +80,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForAdmin = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
|
||||
@@ -164,22 +150,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForUser = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: false,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: false,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
|
||||
@@ -197,22 +176,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForAdmin = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: false,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: false,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
|
||||
@@ -274,22 +246,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForUser = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
|
||||
@@ -307,22 +272,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForAdmin = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
|
||||
@@ -397,22 +355,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForUser = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
|
||||
@@ -430,22 +381,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForAdmin = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
|
||||
@@ -507,22 +451,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForUser = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
|
||||
@@ -540,22 +477,15 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForAdmin = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
|
||||
@@ -627,9 +557,6 @@ describe('loadDefaultInterface', () => {
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
@@ -650,9 +577,6 @@ describe('loadDefaultInterface', () => {
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
@@ -738,15 +662,10 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForUser = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
}, // Explicitly configured
|
||||
// All other permissions that don't exist in the database
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
@@ -766,15 +685,10 @@ describe('loadDefaultInterface', () => {
|
||||
const expectedPermissionsForAdmin = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
}, // Explicitly configured
|
||||
// All other permissions that don't exist in the database
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
|
||||
@@ -888,43 +802,29 @@ describe('loadDefaultInterface', () => {
|
||||
// Check PROMPTS permissions use role defaults
|
||||
expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
});
|
||||
|
||||
expect(adminCall[1][PermissionTypes.PROMPTS]).toEqual({
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
});
|
||||
|
||||
// Check AGENTS permissions use role defaults
|
||||
expect(userCall[1][PermissionTypes.AGENTS]).toEqual({
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: false,
|
||||
[Permissions.CREATE]: true,
|
||||
});
|
||||
|
||||
expect(adminCall[1][PermissionTypes.AGENTS]).toEqual({
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
});
|
||||
|
||||
// Check MEMORIES permissions use role defaults
|
||||
expect(userCall[1][PermissionTypes.MEMORIES]).toEqual({
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
});
|
||||
|
||||
expect(adminCall[1][PermissionTypes.MEMORIES]).toEqual({
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: true,
|
||||
[Permissions.OPT_OUT]: undefined,
|
||||
});
|
||||
});
|
||||
@@ -1164,12 +1064,6 @@ describe('loadDefaultInterface', () => {
|
||||
// Explicitly configured permissions should be updated
|
||||
expect(userCall[1][PermissionTypes.PROMPTS]).toEqual({
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.SHARED_GLOBAL]:
|
||||
roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PROMPTS][
|
||||
Permissions.SHARED_GLOBAL
|
||||
],
|
||||
[Permissions.CREATE]:
|
||||
roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PROMPTS][Permissions.CREATE],
|
||||
});
|
||||
expect(userCall[1][PermissionTypes.BOOKMARKS]).toEqual({ [Permissions.USE]: true });
|
||||
expect(userCall[1][PermissionTypes.MARKETPLACE]).toEqual({ [Permissions.USE]: true });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
logAgentMigrationWarning,
|
||||
@@ -16,7 +17,8 @@ const { findRoleByIdentifier } = require('~/models');
|
||||
async function checkMigrations() {
|
||||
try {
|
||||
const agentMigrationResult = await checkAgentPermissionsMigration({
|
||||
db: {
|
||||
mongoose,
|
||||
methods: {
|
||||
findRoleByIdentifier,
|
||||
getProjectByName,
|
||||
},
|
||||
@@ -28,7 +30,8 @@ async function checkMigrations() {
|
||||
}
|
||||
try {
|
||||
const promptMigrationResult = await checkPromptPermissionsMigration({
|
||||
db: {
|
||||
mongoose,
|
||||
methods: {
|
||||
findRoleByIdentifier,
|
||||
getProjectByName,
|
||||
},
|
||||
|
||||
@@ -1115,6 +1115,18 @@
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports MCPConnection
|
||||
* @typedef {import('@librechat/api').MCPConnection} MCPConnection
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports LCFunctionTool
|
||||
* @typedef {import('@librechat/api').LCFunctionTool} LCFunctionTool
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @exports FlowStateManager
|
||||
* @typedef {import('@librechat/api').FlowStateManager} FlowStateManager
|
||||
@@ -1825,6 +1837,7 @@
|
||||
* @param {object} opts - Options for the completion
|
||||
* @param {onTokenProgress} opts.onProgress - Callback function to handle token progress
|
||||
* @param {AbortController} opts.abortController - AbortController instance
|
||||
* @param {Record<string, Record<string, string>>} [opts.userMCPAuthMap]
|
||||
* @returns {Promise<string>}
|
||||
* @memberof typedefs
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="description" content="LibreChat - An open source chat application with support for multiple AI models" />
|
||||
<title>LibreChat</title>
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/apple-touch-icon-180x180.png" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.8.0-rc2",
|
||||
"version": "v0.8.0-rc3",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -225,7 +225,8 @@ export type AgentPanelContextType = {
|
||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
agent_id?: string;
|
||||
agentsConfig?: t.TAgentsEndpoint;
|
||||
agentsConfig?: t.TAgentsEndpoint | null;
|
||||
endpointsConfig?: t.TEndpointsConfig | null;
|
||||
};
|
||||
|
||||
export type AgentModelPanelProps = {
|
||||
@@ -336,6 +337,7 @@ export type TAskProps = {
|
||||
|
||||
export type TOptions = {
|
||||
editedMessageId?: string | null;
|
||||
editedContent?: t.TEditedContent;
|
||||
editedText?: string | null;
|
||||
isRegenerate?: boolean;
|
||||
isContinued?: boolean;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Label } from '@librechat/client';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { useLocalize, TranslationKeys, useAgentCategories } from '~/hooks';
|
||||
import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: t.Agent; // The agent data to display
|
||||
@@ -15,6 +15,21 @@ interface AgentCardProps {
|
||||
*/
|
||||
const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' }) => {
|
||||
const localize = useLocalize();
|
||||
const { categories } = useAgentCategories();
|
||||
|
||||
const categoryLabel = useMemo(() => {
|
||||
if (!agent.category) return '';
|
||||
|
||||
const category = categories.find((cat) => cat.value === agent.category);
|
||||
if (category) {
|
||||
if (category.label && category.label.startsWith('com_')) {
|
||||
return localize(category.label as TranslationKeys);
|
||||
}
|
||||
return category.label;
|
||||
}
|
||||
|
||||
return agent.category.charAt(0).toUpperCase() + agent.category.slice(1);
|
||||
}, [agent.category, categories, localize]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -49,9 +64,7 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
|
||||
{/* Category tag */}
|
||||
{agent.category && (
|
||||
<div className="inline-flex items-center rounded-md border-border-xheavy bg-surface-active-alt px-2 py-1 text-xs font-medium">
|
||||
<Label className="line-clamp-1 font-normal">
|
||||
{agent.category.charAt(0).toUpperCase() + agent.category.slice(1)}
|
||||
</Label>
|
||||
<Label className="line-clamp-1 font-normal">{categoryLabel}</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useAgentCategories } from '~/hooks/Agents';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface AgentCategoryDisplayProps {
|
||||
category?: string;
|
||||
className?: string;
|
||||
showIcon?: boolean;
|
||||
iconClassName?: string;
|
||||
showEmptyFallback?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to display an agent category with proper translation
|
||||
*
|
||||
* @param category - The category value (e.g., "general", "hr", etc.)
|
||||
* @param className - Optional className for the container
|
||||
* @param showIcon - Whether to show the category icon
|
||||
* @param iconClassName - Optional className for the icon
|
||||
* @param showEmptyFallback - Whether to show a fallback for empty categories
|
||||
*/
|
||||
const AgentCategoryDisplay: React.FC<AgentCategoryDisplayProps> = ({
|
||||
category,
|
||||
className = '',
|
||||
showIcon = true,
|
||||
iconClassName = 'h-4 w-4 mr-2',
|
||||
showEmptyFallback = false,
|
||||
}) => {
|
||||
const { categories, emptyCategory } = useAgentCategories();
|
||||
|
||||
// Find the category in our processed categories list
|
||||
const categoryItem = categories.find((c) => c.value === category);
|
||||
|
||||
// Handle empty string case differently than undefined/null
|
||||
if (category === '') {
|
||||
if (!showEmptyFallback) {
|
||||
return null;
|
||||
}
|
||||
// Show the empty category placeholder
|
||||
return (
|
||||
<div className={cn('flex items-center text-gray-400', className)}>
|
||||
<span>{emptyCategory.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// No category or unknown category
|
||||
if (!category || !categoryItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{showIcon && categoryItem.icon && (
|
||||
<span className={cn('flex-shrink-0', iconClassName)}>{categoryItem.icon}</span>
|
||||
)}
|
||||
<span>{categoryItem.label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentCategoryDisplay;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { useMediaQuery } from '@librechat/client';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { useLocalize, TranslationKeys } from '~/hooks';
|
||||
import { SmartLoader } from './SmartLoader';
|
||||
import { useLocalize } from '~/hooks/';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
/**
|
||||
@@ -36,14 +36,17 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
||||
const localize = useLocalize();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
// Helper function to get category display name from database data
|
||||
/** Helper function to get category display name from database data */
|
||||
const getCategoryDisplayName = (category: t.TCategory) => {
|
||||
// Special cases for system categories
|
||||
if (category.value === 'promoted') {
|
||||
return localize('com_agents_top_picks');
|
||||
}
|
||||
if (category.value === 'all') {
|
||||
return 'All';
|
||||
return localize('com_agents_all_category');
|
||||
}
|
||||
if (category.label && category.label.startsWith('com_')) {
|
||||
return localize(category.label as TranslationKeys);
|
||||
}
|
||||
// Use database label or fallback to capitalized value
|
||||
return category.label || category.value.charAt(0).toUpperCase() + category.value.slice(1);
|
||||
@@ -158,7 +161,11 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
||||
aria-selected={activeTab === category.value}
|
||||
aria-controls={`tabpanel-${category.value}`}
|
||||
tabIndex={activeTab === category.value ? 0 : -1}
|
||||
aria-label={`${getCategoryDisplayName(category)} tab (${index + 1} of ${categories.length})`}
|
||||
aria-label={localize('com_agents_category_tab_label', {
|
||||
category: getCategoryDisplayName(category),
|
||||
position: index + 1,
|
||||
total: categories.length,
|
||||
})}
|
||||
>
|
||||
{getCategoryDisplayName(category)}
|
||||
{/* Underline for active tab */}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { TooltipAnchor, Button, NewChatIcon, useMediaQuery } from '@librechat/cl
|
||||
import { PermissionTypes, Permissions, QueryKeys, Constants } from 'librechat-data-provider';
|
||||
import type t from 'librechat-data-provider';
|
||||
import type { ContextType } from '~/common';
|
||||
import { useDocumentTitle, useHasAccess, useLocalize, TranslationKeys } from '~/hooks';
|
||||
import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider';
|
||||
import { useDocumentTitle, useHasAccess, useLocalize } from '~/hooks';
|
||||
import MarketplaceAdminSettings from './MarketplaceAdminSettings';
|
||||
import { SidePanelProvider, useChatContext } from '~/Providers';
|
||||
import { MarketplaceProvider } from './MarketplaceContext';
|
||||
@@ -381,8 +381,8 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
||||
}
|
||||
if (displayCategory === 'all') {
|
||||
return {
|
||||
name: 'All Agents',
|
||||
description: 'Browse all shared agents across all categories',
|
||||
name: localize('com_agents_all'),
|
||||
description: localize('com_agents_all_description'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -392,8 +392,12 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
||||
);
|
||||
if (categoryData) {
|
||||
return {
|
||||
name: categoryData.label,
|
||||
description: categoryData.description || '',
|
||||
name: categoryData.label?.startsWith('com_')
|
||||
? localize(categoryData.label as TranslationKeys)
|
||||
: categoryData.label,
|
||||
description: categoryData.description?.startsWith('com_')
|
||||
? localize(categoryData.description as TranslationKeys)
|
||||
: categoryData.description || '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -455,8 +459,8 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
||||
}
|
||||
if (nextCategory === 'all') {
|
||||
return {
|
||||
name: 'All Agents',
|
||||
description: 'Browse all shared agents across all categories',
|
||||
name: localize('com_agents_all'),
|
||||
description: localize('com_agents_all_description'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -466,8 +470,16 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
||||
);
|
||||
if (categoryData) {
|
||||
return {
|
||||
name: categoryData.label,
|
||||
description: categoryData.description || '',
|
||||
name: categoryData.label?.startsWith('com_')
|
||||
? localize(categoryData.label as TranslationKeys)
|
||||
: categoryData.label,
|
||||
description: categoryData.description?.startsWith('com_')
|
||||
? localize(
|
||||
categoryData.description as Parameters<
|
||||
typeof localize
|
||||
>[0],
|
||||
)
|
||||
: categoryData.description || '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ const mockLocalize = jest.fn((key: string, options?: any) => {
|
||||
com_agents_search_empty_heading: 'No search results',
|
||||
com_agents_created_by: 'by',
|
||||
com_agents_top_picks: 'Top Picks',
|
||||
com_agents_all_category: 'All',
|
||||
// ErrorDisplay translations
|
||||
com_agents_error_suggestion_generic: 'Try refreshing the page or check your network connection',
|
||||
com_agents_error_network_title: 'Network Error',
|
||||
@@ -199,7 +200,7 @@ describe('Accessibility Improvements', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const promotedTab = screen.getByRole('tab', { name: /Top Picks tab/ });
|
||||
const promotedTab = screen.getByRole('tab', { name: /Top Picks category/ });
|
||||
|
||||
// Test arrow key navigation
|
||||
fireEvent.keyDown(promotedTab, { key: 'ArrowRight' });
|
||||
@@ -226,8 +227,8 @@ describe('Accessibility Improvements', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const promotedTab = screen.getByRole('tab', { name: /Top Picks tab/ });
|
||||
const allTab = screen.getByRole('tab', { name: /All tab/ });
|
||||
const promotedTab = screen.getByRole('tab', { name: /Top Picks category/ });
|
||||
const allTab = screen.getByRole('tab', { name: /All category/ });
|
||||
|
||||
// Active tab should be focusable
|
||||
expect(promotedTab).toHaveAttribute('tabIndex', '0');
|
||||
|
||||
@@ -8,10 +8,42 @@ import type t from 'librechat-data-provider';
|
||||
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
|
||||
const mockTranslations: Record<string, string> = {
|
||||
com_agents_created_by: 'Created by',
|
||||
com_agents_agent_card_label: '{{name}} agent. {{description}}',
|
||||
com_agents_category_general: 'General',
|
||||
com_agents_category_hr: 'Human Resources',
|
||||
};
|
||||
return mockTranslations[key] || key;
|
||||
});
|
||||
|
||||
// Mock useAgentCategories hook
|
||||
jest.mock('~/hooks', () => ({
|
||||
useLocalize: () => (key: string, values?: Record<string, string>) => {
|
||||
const mockTranslations: Record<string, string> = {
|
||||
com_agents_created_by: 'Created by',
|
||||
com_agents_agent_card_label: '{{name}} agent. {{description}}',
|
||||
com_agents_category_general: 'General',
|
||||
com_agents_category_hr: 'Human Resources',
|
||||
};
|
||||
let translation = mockTranslations[key] || key;
|
||||
|
||||
// Replace placeholders with actual values
|
||||
if (values) {
|
||||
Object.entries(values).forEach(([placeholder, value]) => {
|
||||
translation = translation.replace(new RegExp(`{{${placeholder}}}`, 'g'), value);
|
||||
});
|
||||
}
|
||||
|
||||
return translation;
|
||||
},
|
||||
useAgentCategories: () => ({
|
||||
categories: [
|
||||
{ value: 'general', label: 'com_agents_category_general' },
|
||||
{ value: 'hr', label: 'com_agents_category_hr' },
|
||||
{ value: 'custom', label: 'Custom Category' }, // Non-localized custom category
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('AgentCard', () => {
|
||||
const mockAgent: t.Agent = {
|
||||
id: '1',
|
||||
@@ -200,6 +232,49 @@ describe('AgentCard', () => {
|
||||
|
||||
const card = screen.getByRole('button');
|
||||
expect(card).toHaveAttribute('tabIndex', '0');
|
||||
expect(card).toHaveAttribute('aria-label', 'com_agents_agent_card_label');
|
||||
expect(card).toHaveAttribute(
|
||||
'aria-label',
|
||||
'Test Agent agent. A test agent for testing purposes',
|
||||
);
|
||||
});
|
||||
|
||||
it('displays localized category label', () => {
|
||||
const agentWithCategory = {
|
||||
...mockAgent,
|
||||
category: 'general',
|
||||
};
|
||||
|
||||
render(<AgentCard agent={agentWithCategory} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('General')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays custom category label', () => {
|
||||
const agentWithCustomCategory = {
|
||||
...mockAgent,
|
||||
category: 'custom',
|
||||
};
|
||||
|
||||
render(<AgentCard agent={agentWithCustomCategory} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('Custom Category')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays capitalized fallback for unknown category', () => {
|
||||
const agentWithUnknownCategory = {
|
||||
...mockAgent,
|
||||
category: 'unknown',
|
||||
};
|
||||
|
||||
render(<AgentCard agent={agentWithUnknownCategory} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('Unknown')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display category tag when category is not provided', () => {
|
||||
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.queryByText('General')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Unknown')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import AgentCategoryDisplay from '../AgentCategoryDisplay';
|
||||
|
||||
// Mock the useAgentCategories hook
|
||||
jest.mock('~/hooks/Agents', () => ({
|
||||
useAgentCategories: () => ({
|
||||
categories: [
|
||||
{
|
||||
value: 'general',
|
||||
label: 'General',
|
||||
icon: <span data-testid="icon-general">{''}</span>,
|
||||
className: 'w-full',
|
||||
},
|
||||
{
|
||||
value: 'hr',
|
||||
label: 'HR',
|
||||
icon: <span data-testid="icon-hr">{''}</span>,
|
||||
className: 'w-full',
|
||||
},
|
||||
{
|
||||
value: 'rd',
|
||||
label: 'R&D',
|
||||
icon: <span data-testid="icon-rd">{''}</span>,
|
||||
className: 'w-full',
|
||||
},
|
||||
{
|
||||
value: 'finance',
|
||||
label: 'Finance',
|
||||
icon: <span data-testid="icon-finance">{''}</span>,
|
||||
className: 'w-full',
|
||||
},
|
||||
],
|
||||
emptyCategory: {
|
||||
value: '',
|
||||
label: 'General',
|
||||
className: 'w-full',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('AgentCategoryDisplay', () => {
|
||||
it('should display the proper label for a category', () => {
|
||||
render(<AgentCategoryDisplay category="rd" />);
|
||||
expect(screen.getByText('R&D')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the icon when showIcon is true', () => {
|
||||
render(<AgentCategoryDisplay category="finance" showIcon={true} />);
|
||||
expect(screen.getByTestId('icon-finance')).toBeInTheDocument();
|
||||
expect(screen.getByText('Finance')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display the icon when showIcon is false', () => {
|
||||
render(<AgentCategoryDisplay category="hr" showIcon={false} />);
|
||||
expect(screen.queryByTestId('icon-hr')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('HR')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom classnames', () => {
|
||||
render(<AgentCategoryDisplay category="general" className="test-class" />);
|
||||
expect(screen.getByText('General').parentElement).toHaveClass('test-class');
|
||||
});
|
||||
|
||||
it('should not render anything for unknown categories', () => {
|
||||
const { container } = render(<AgentCategoryDisplay category="unknown" />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render anything when no category is provided', () => {
|
||||
const { container } = render(<AgentCategoryDisplay />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should not render anything for empty category when showEmptyFallback is false', () => {
|
||||
const { container } = render(<AgentCategoryDisplay category="" />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('should render empty category placeholder when showEmptyFallback is true', () => {
|
||||
render(<AgentCategoryDisplay category="" showEmptyFallback={true} />);
|
||||
expect(screen.getByText('General')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should apply custom iconClassName to the icon', () => {
|
||||
render(<AgentCategoryDisplay category="general" iconClassName="custom-icon-class" />);
|
||||
const iconElement = screen.getByTestId('icon-general').parentElement;
|
||||
expect(iconElement).toHaveClass('custom-icon-class');
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,8 @@ import type t from 'librechat-data-provider';
|
||||
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
|
||||
const mockTranslations: Record<string, string> = {
|
||||
com_agents_top_picks: 'Top Picks',
|
||||
com_agents_all: 'All',
|
||||
com_agents_all: 'All Agents',
|
||||
com_agents_all_category: 'All',
|
||||
com_ui_no_categories: 'No categories available',
|
||||
com_agents_category_tabs_label: 'Agent Categories',
|
||||
com_ui_agent_category_general: 'General',
|
||||
|
||||
@@ -89,7 +89,7 @@ const BookmarkEditDialog = ({
|
||||
<OGDialog open={open} onOpenChange={setOpen} triggerRef={triggerRef}>
|
||||
{children}
|
||||
<OGDialogTemplate
|
||||
title="Bookmark"
|
||||
title={bookmark ? localize('com_ui_bookmarks_edit') : localize('com_ui_bookmarks_new')}
|
||||
showCloseButton={false}
|
||||
className="w-11/12 md:max-w-2xl"
|
||||
main={
|
||||
|
||||
@@ -38,6 +38,8 @@ const BookmarkForm = ({
|
||||
control,
|
||||
formState: { errors },
|
||||
} = useForm<TConversationTagRequest>({
|
||||
mode: 'onBlur',
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: {
|
||||
tag: bookmark?.tag ?? '',
|
||||
description: bookmark?.description ?? '',
|
||||
@@ -98,23 +100,30 @@ const BookmarkForm = ({
|
||||
<Input
|
||||
type="text"
|
||||
id="bookmark-tag"
|
||||
aria-label="Bookmark"
|
||||
aria-label={
|
||||
bookmark ? localize('com_ui_bookmarks_edit') : localize('com_ui_bookmarks_new')
|
||||
}
|
||||
{...register('tag', {
|
||||
required: 'tag is required',
|
||||
required: localize('com_ui_field_required'),
|
||||
maxLength: {
|
||||
value: 128,
|
||||
message: localize('com_auth_password_max_length'),
|
||||
message: localize('com_ui_field_max_length', {
|
||||
field: localize('com_ui_bookmarks_title'),
|
||||
length: 128,
|
||||
}),
|
||||
},
|
||||
validate: (value) => {
|
||||
return (
|
||||
value === bookmark?.tag ||
|
||||
bookmarks.every((bookmark) => bookmark.tag !== value) ||
|
||||
'tag must be unique'
|
||||
localize('com_ui_bookmarks_tag_exists')
|
||||
);
|
||||
},
|
||||
})}
|
||||
aria-invalid={!!errors.tag}
|
||||
placeholder="Bookmark"
|
||||
placeholder={
|
||||
bookmark ? localize('com_ui_bookmarks_edit') : localize('com_ui_bookmarks_new')
|
||||
}
|
||||
/>
|
||||
{errors.tag && <span className="text-sm text-red-500">{errors.tag.message}</span>}
|
||||
</div>
|
||||
@@ -127,7 +136,10 @@ const BookmarkForm = ({
|
||||
{...register('description', {
|
||||
maxLength: {
|
||||
value: 1048,
|
||||
message: 'Maximum 1048 characters',
|
||||
message: localize('com_ui_field_max_length', {
|
||||
field: localize('com_ui_bookmarks_description'),
|
||||
length: 1048,
|
||||
}),
|
||||
},
|
||||
})}
|
||||
id="bookmark-description"
|
||||
|
||||
@@ -94,6 +94,12 @@ const ContentParts = memo(
|
||||
return null;
|
||||
}
|
||||
|
||||
const isToolCall =
|
||||
part.type === ContentTypes.TOOL_CALL || part['tool_call_ids'] != null;
|
||||
if (isToolCall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EditTextPart
|
||||
index={idx}
|
||||
|
||||
@@ -62,17 +62,26 @@ const EditTextPart = ({
|
||||
const messages = getMessages();
|
||||
const parentMessage = messages?.find((msg) => msg.messageId === message?.parentMessageId);
|
||||
|
||||
const editedContent =
|
||||
part.type === ContentTypes.THINK
|
||||
? {
|
||||
index,
|
||||
type: ContentTypes.THINK as const,
|
||||
[ContentTypes.THINK]: data.text,
|
||||
}
|
||||
: {
|
||||
index,
|
||||
type: ContentTypes.TEXT as const,
|
||||
[ContentTypes.TEXT]: data.text,
|
||||
};
|
||||
|
||||
if (!parentMessage) {
|
||||
return;
|
||||
}
|
||||
ask(
|
||||
{ ...parentMessage },
|
||||
{
|
||||
editedContent: {
|
||||
index,
|
||||
text: data.text,
|
||||
type: part.type,
|
||||
},
|
||||
editedContent,
|
||||
editedMessageId: messageId,
|
||||
isRegenerate: true,
|
||||
isEdited: true,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import { UserIcon } from '@librechat/client';
|
||||
import { UserIcon, useAvatar } from '@librechat/client';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
import type { IconProps } from '~/common';
|
||||
import MessageEndpointIcon from './MessageEndpointIcon';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useAvatar from '~/hooks/Messages/useAvatar';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ import { useState, memo } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import * as Select from '@ariakit/react/select';
|
||||
import { FileText, LogOut } from 'lucide-react';
|
||||
import { LinkIcon, GearIcon, DropdownMenuSeparator, UserIcon } from '@librechat/client';
|
||||
import { LinkIcon, GearIcon, DropdownMenuSeparator, Avatar } from '@librechat/client';
|
||||
import { useGetStartupConfig, useGetUserBalance } from '~/data-provider';
|
||||
import FilesView from '~/components/Chat/Input/Files/FilesView';
|
||||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import useAvatar from '~/hooks/Messages/useAvatar';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import Settings from './Settings';
|
||||
import store from '~/store';
|
||||
@@ -21,9 +20,6 @@ function AccountSettings() {
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showFiles, setShowFiles] = useRecoilState(store.showFiles);
|
||||
|
||||
const avatarSrc = useAvatar(user);
|
||||
const avatarSeed = user?.avatar || user?.name || user?.username || '';
|
||||
|
||||
return (
|
||||
<Select.SelectProvider>
|
||||
<Select.Select
|
||||
@@ -33,26 +29,7 @@ function AccountSettings() {
|
||||
>
|
||||
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
|
||||
<div className="relative flex">
|
||||
{avatarSeed.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgb(121, 137, 255)',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
|
||||
}}
|
||||
className="relative flex items-center justify-center rounded-full p-1 text-text-primary"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<UserIcon />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
className="rounded-full"
|
||||
src={(user?.avatar ?? '') || avatarSrc}
|
||||
alt={`${user?.name || user?.username || user?.email || ''}'s avatar`}
|
||||
/>
|
||||
)}
|
||||
<Avatar user={user} size={32} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -77,6 +77,7 @@ export const LangSelector = ({
|
||||
{ value: 'zh-Hans', label: localize('com_nav_lang_chinese') },
|
||||
{ value: 'zh-Hant', label: localize('com_nav_lang_traditional_chinese') },
|
||||
{ value: 'ar-EG', label: localize('com_nav_lang_arabic') },
|
||||
{ value: 'bs', label: localize('com_nav_lang_bosnian') },
|
||||
{ value: 'da-DK', label: localize('com_nav_lang_danish') },
|
||||
{ value: 'de-DE', label: localize('com_nav_lang_german') },
|
||||
{ value: 'es-ES', label: localize('com_nav_lang_spanish') },
|
||||
@@ -88,6 +89,7 @@ export const LangSelector = ({
|
||||
{ value: 'hu-HU', label: localize('com_nav_lang_hungarian') },
|
||||
{ value: 'hy-AM', label: localize('com_nav_lang_armenian') },
|
||||
{ value: 'it-IT', label: localize('com_nav_lang_italian') },
|
||||
{ value: 'nb', label: localize('com_nav_lang_norwegian_bokmal') },
|
||||
{ value: 'pl-PL', label: localize('com_nav_lang_polish') },
|
||||
{ value: 'pt-BR', label: localize('com_nav_lang_brazilian_portuguese') },
|
||||
{ value: 'pt-PT', label: localize('com_nav_lang_portuguese') },
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function EmptyPromptPreview() {
|
||||
const localize = useLocalize();
|
||||
|
||||
return (
|
||||
<div className="h-full w-full content-center text-center font-bold dark:text-gray-200">
|
||||
Select or Create a Prompt
|
||||
<div className="h-full w-full content-center text-center font-bold text-text-secondary">
|
||||
{localize('com_ui_select_or_create_prompt')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ControlCombobox } from '@librechat/client';
|
||||
import {
|
||||
useWatch,
|
||||
@@ -9,7 +8,7 @@ import {
|
||||
useFormContext,
|
||||
ControllerRenderProps,
|
||||
} from 'react-hook-form';
|
||||
import { useAgentCategories } from '~/hooks/Agents';
|
||||
import { TranslationKeys, useLocalize, useAgentCategories } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
/**
|
||||
@@ -35,22 +34,25 @@ const useCategorySync = (agent_id: string | null) => {
|
||||
* A component for selecting agent categories with form validation
|
||||
*/
|
||||
const AgentCategorySelector: React.FC<{ className?: string }> = ({ className }) => {
|
||||
const { t } = useTranslation();
|
||||
const localize = useLocalize();
|
||||
const formContext = useFormContext();
|
||||
const { categories } = useAgentCategories();
|
||||
|
||||
// Always call useWatch
|
||||
const agent_id = useWatch({
|
||||
name: 'id',
|
||||
control: formContext.control,
|
||||
});
|
||||
|
||||
// Use custom hook for category sync
|
||||
const { syncCategory } = useCategorySync(agent_id);
|
||||
const getCategoryLabel = (category: { label: string; value: string }) => {
|
||||
if (category.label && category.label.startsWith('com_')) {
|
||||
return localize(category.label as TranslationKeys);
|
||||
}
|
||||
return category.label;
|
||||
};
|
||||
|
||||
// Transform categories to the format expected by ControlCombobox
|
||||
const comboboxItems = categories.map((category) => ({
|
||||
label: category.label,
|
||||
label: getCategoryLabel(category),
|
||||
value: category.value,
|
||||
}));
|
||||
|
||||
@@ -59,8 +61,8 @@ const AgentCategorySelector: React.FC<{ className?: string }> = ({ className })
|
||||
return categoryItem?.label || comboboxItems.find((c) => c.value === 'general')?.label;
|
||||
};
|
||||
|
||||
const searchPlaceholder = t('com_ui_search_agent_category', 'Search categories...');
|
||||
const ariaLabel = t('com_ui_agent_category_selector_aria', "Agent's category selector");
|
||||
const searchPlaceholder = localize('com_ui_search_agent_category');
|
||||
const ariaLabel = localize('com_ui_agent_category_selector_aria');
|
||||
|
||||
return (
|
||||
<Controller
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
|
||||
import type { TAgentsEndpoint, TEndpointsConfig, TConfig } from 'librechat-data-provider';
|
||||
import type { TAgentsEndpoint, TEndpointsConfig } from 'librechat-data-provider';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
|
||||
interface UseGetAgentsConfigOptions {
|
||||
@@ -20,11 +20,12 @@ export default function useGetAgentsConfig(options?: UseGetAgentsConfigOptions):
|
||||
const endpointsConfig = providedConfig || queriedConfig;
|
||||
|
||||
const agentsConfig = useMemo<TAgentsEndpoint | null>(() => {
|
||||
const config = endpointsConfig?.[EModelEndpoint.agents] ?? null;
|
||||
const config: TAgentsEndpoint | null =
|
||||
(endpointsConfig?.[EModelEndpoint.agents] as TAgentsEndpoint | null) ?? null;
|
||||
if (!config) return null;
|
||||
|
||||
return {
|
||||
...(config as TConfig),
|
||||
...config,
|
||||
capabilities: Array.isArray(config.capabilities)
|
||||
? config.capabilities.map((cap) => cap as unknown as AgentCapabilities)
|
||||
: ([] as AgentCapabilities[]),
|
||||
|
||||
@@ -230,15 +230,19 @@ export default function useChatFunctions({
|
||||
|
||||
const responseMessageId =
|
||||
editedMessageId ??
|
||||
(latestMessage?.messageId && isRegenerate ? latestMessage?.messageId + '_' : null) ??
|
||||
(latestMessage?.messageId && isRegenerate
|
||||
? latestMessage.messageId.replace(/_+$/, '') + '_'
|
||||
: null) ??
|
||||
null;
|
||||
const initialResponseId =
|
||||
responseMessageId ?? `${isRegenerate ? messageId : intermediateId}`.replace(/_+$/, '') + '_';
|
||||
|
||||
const initialResponse: TMessage = {
|
||||
sender: responseSender,
|
||||
text: '',
|
||||
endpoint: endpoint ?? '',
|
||||
parentMessageId: isRegenerate ? messageId : intermediateId,
|
||||
messageId: responseMessageId ?? `${isRegenerate ? messageId : intermediateId}_`,
|
||||
messageId: initialResponseId,
|
||||
thread_id,
|
||||
conversationId,
|
||||
unfinished: false,
|
||||
@@ -267,13 +271,13 @@ export default function useChatFunctions({
|
||||
|
||||
if (editedContent && latestMessage?.content) {
|
||||
initialResponse.content = cloneDeep(latestMessage.content);
|
||||
const { index, text, type } = editedContent;
|
||||
const { index, type, ...part } = editedContent;
|
||||
if (initialResponse.content && index >= 0 && index < initialResponse.content.length) {
|
||||
const contentPart = initialResponse.content[index];
|
||||
if (type === ContentTypes.THINK && contentPart.type === ContentTypes.THINK) {
|
||||
contentPart[ContentTypes.THINK] = text;
|
||||
contentPart[ContentTypes.THINK] = part[ContentTypes.THINK];
|
||||
} else if (type === ContentTypes.TEXT && contentPart.type === ContentTypes.TEXT) {
|
||||
contentPart[ContentTypes.TEXT] = text;
|
||||
contentPart[ContentTypes.TEXT] = part[ContentTypes.TEXT];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { QueryKeys } from 'librechat-data-provider';
|
||||
import type { ConversationListResponse } from 'librechat-data-provider';
|
||||
import type { InfiniteData } from '@tanstack/react-query';
|
||||
import type t from 'librechat-data-provider';
|
||||
import { updateConvoFieldsInfinite } from '~/utils/convos';
|
||||
|
||||
const useUpdateTagsInConvo = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -53,30 +52,31 @@ const useUpdateTagsInConvo = () => {
|
||||
QueryKeys.allConversations,
|
||||
]);
|
||||
|
||||
const conversationIdsWithTag = [] as string[];
|
||||
|
||||
// update tag to newTag in all conversations
|
||||
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
|
||||
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
|
||||
const page = newData.pages[pageIndex];
|
||||
page.conversations = page.conversations.map((conversation) => {
|
||||
if (
|
||||
conversation.conversationId &&
|
||||
'tags' in conversation &&
|
||||
Array.isArray((conversation as { tags?: string[] }).tags) &&
|
||||
(conversation as { tags?: string[] }).tags?.includes(tag)
|
||||
) {
|
||||
(conversation as { tags: string[] }).tags = (conversation as { tags: string[] }).tags.map(
|
||||
(t: string) => (t === tag ? newTag : t),
|
||||
);
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
if (data) {
|
||||
const newData = JSON.parse(JSON.stringify(data)) as InfiniteData<ConversationListResponse>;
|
||||
for (let pageIndex = 0; pageIndex < newData.pages.length; pageIndex++) {
|
||||
const page = newData.pages[pageIndex];
|
||||
page.conversations = page.conversations.map((conversation) => {
|
||||
if (
|
||||
conversation.conversationId &&
|
||||
'tags' in conversation &&
|
||||
Array.isArray((conversation as { tags?: string[] }).tags) &&
|
||||
(conversation as { tags?: string[] }).tags?.includes(tag)
|
||||
) {
|
||||
(conversation as { tags: string[] }).tags = (
|
||||
conversation as { tags: string[] }
|
||||
).tags.map((t: string) => (t === tag ? newTag : t));
|
||||
}
|
||||
return conversation;
|
||||
});
|
||||
}
|
||||
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
|
||||
[QueryKeys.allConversations],
|
||||
newData,
|
||||
);
|
||||
}
|
||||
queryClient.setQueryData<InfiniteData<ConversationListResponse>>(
|
||||
[QueryKeys.allConversations],
|
||||
newData,
|
||||
);
|
||||
|
||||
const conversationIdsWithTag = [] as string[];
|
||||
|
||||
// update the tag to newTag from the cache of each conversation
|
||||
for (let i = 0; i < conversationIdsWithTag.length; i++) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { default as useAvatar } from './useAvatar';
|
||||
export { default as useProgress } from './useProgress';
|
||||
export { default as useAttachments } from './useAttachments';
|
||||
export { default as useSubmitMessage } from './useSubmitMessage';
|
||||
|
||||
@@ -182,7 +182,7 @@ export default function useEventHandlers({
|
||||
const { token } = useAuthContext();
|
||||
|
||||
const contentHandler = useContentHandler({ setMessages, getMessages });
|
||||
const stepHandler = useStepHandler({
|
||||
const { stepHandler, clearStepMaps } = useStepHandler({
|
||||
setMessages,
|
||||
getMessages,
|
||||
announcePolite,
|
||||
@@ -480,12 +480,13 @@ export default function useEventHandlers({
|
||||
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, id], _messages);
|
||||
};
|
||||
|
||||
/** Handle edge case where stream is cancelled before any response, which creates a blank page */
|
||||
if (
|
||||
!conversation.conversationId &&
|
||||
const hasNoResponse =
|
||||
responseMessage?.content?.[0]?.['text']?.value ===
|
||||
submission.initialResponse?.content?.[0]?.['text']?.value
|
||||
) {
|
||||
submission.initialResponse?.content?.[0]?.['text']?.value ||
|
||||
!!responseMessage?.content?.[0]?.['tool_call']?.auth;
|
||||
|
||||
/** Handle edge case where stream is cancelled before any response, which creates a blank page */
|
||||
if (!conversation.conversationId && hasNoResponse) {
|
||||
const currentConvoId =
|
||||
(submissionConvo.conversationId ?? conversation.conversationId) || Constants.NEW_CONVO;
|
||||
if (isNewConvo && submissionConvo.conversationId) {
|
||||
@@ -806,6 +807,7 @@ export default function useEventHandlers({
|
||||
);
|
||||
|
||||
return {
|
||||
clearStepMaps,
|
||||
stepHandler,
|
||||
syncHandler,
|
||||
finalHandler,
|
||||
|
||||
@@ -62,6 +62,7 @@ export default function useSSE(
|
||||
} = chatHelpers;
|
||||
|
||||
const {
|
||||
clearStepMaps,
|
||||
stepHandler,
|
||||
syncHandler,
|
||||
finalHandler,
|
||||
@@ -101,6 +102,7 @@ export default function useSSE(
|
||||
payload = removeNullishValues(payload) as TPayload;
|
||||
|
||||
let textIndex = null;
|
||||
clearStepMaps();
|
||||
|
||||
const sse = new SSE(payloadData.server, {
|
||||
payload: JSON.stringify(payload),
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { StepTypes, ContentTypes, ToolCallTypes, getNonEmptyValue } from 'librechat-data-provider';
|
||||
import {
|
||||
Constants,
|
||||
StepTypes,
|
||||
ContentTypes,
|
||||
ToolCallTypes,
|
||||
getNonEmptyValue,
|
||||
} from 'librechat-data-provider';
|
||||
import type {
|
||||
Agents,
|
||||
TMessage,
|
||||
@@ -178,11 +184,12 @@ export default function useStepHandler({
|
||||
return { ...message, content: updatedContent as TMessageContentParts[] };
|
||||
};
|
||||
|
||||
return useCallback(
|
||||
const stepHandler = useCallback(
|
||||
({ event, data }: TStepEvent, submission: EventSubmission) => {
|
||||
const messages = getMessages() || [];
|
||||
const { userMessage } = submission;
|
||||
setIsSubmitting(true);
|
||||
let parentMessageId = userMessage.messageId;
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastAnnouncementTimeRef.current > MESSAGE_UPDATE_INTERVAL) {
|
||||
@@ -197,7 +204,11 @@ export default function useStepHandler({
|
||||
|
||||
if (event === 'on_run_step') {
|
||||
const runStep = data as Agents.RunStep;
|
||||
const responseMessageId = runStep.runId ?? '';
|
||||
let responseMessageId = runStep.runId ?? '';
|
||||
if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) {
|
||||
responseMessageId = submission?.initialResponse?.messageId ?? '';
|
||||
parentMessageId = submission?.initialResponse?.parentMessageId ?? '';
|
||||
}
|
||||
if (!responseMessageId) {
|
||||
console.warn('No message id found in run step event');
|
||||
return;
|
||||
@@ -211,7 +222,7 @@ export default function useStepHandler({
|
||||
|
||||
response = {
|
||||
...responseMessage,
|
||||
parentMessageId: userMessage.messageId,
|
||||
parentMessageId,
|
||||
conversationId: userMessage.conversationId,
|
||||
messageId: responseMessageId,
|
||||
content: initialContent,
|
||||
@@ -246,14 +257,18 @@ export default function useStepHandler({
|
||||
|
||||
messageMap.current.set(responseMessageId, updatedResponse);
|
||||
const updatedMessages = messages.map((msg) =>
|
||||
msg.messageId === runStep.runId ? updatedResponse : msg,
|
||||
msg.messageId === responseMessageId ? updatedResponse : msg,
|
||||
);
|
||||
|
||||
setMessages(updatedMessages);
|
||||
}
|
||||
} else if (event === 'on_agent_update') {
|
||||
const { agent_update } = data as Agents.AgentUpdate;
|
||||
const responseMessageId = agent_update.runId || '';
|
||||
let responseMessageId = agent_update.runId || '';
|
||||
if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) {
|
||||
responseMessageId = submission?.initialResponse?.messageId ?? '';
|
||||
parentMessageId = submission?.initialResponse?.parentMessageId ?? '';
|
||||
}
|
||||
if (!responseMessageId) {
|
||||
console.warn('No message id found in agent update event');
|
||||
return;
|
||||
@@ -271,7 +286,11 @@ export default function useStepHandler({
|
||||
} else if (event === 'on_message_delta') {
|
||||
const messageDelta = data as Agents.MessageDeltaEvent;
|
||||
const runStep = stepMap.current.get(messageDelta.id);
|
||||
const responseMessageId = runStep?.runId ?? '';
|
||||
let responseMessageId = runStep?.runId ?? '';
|
||||
if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) {
|
||||
responseMessageId = submission?.initialResponse?.messageId ?? '';
|
||||
parentMessageId = submission?.initialResponse?.parentMessageId ?? '';
|
||||
}
|
||||
|
||||
if (!runStep || !responseMessageId) {
|
||||
console.warn('No run step or runId found for message delta event');
|
||||
@@ -299,7 +318,11 @@ export default function useStepHandler({
|
||||
} else if (event === 'on_reasoning_delta') {
|
||||
const reasoningDelta = data as Agents.ReasoningDeltaEvent;
|
||||
const runStep = stepMap.current.get(reasoningDelta.id);
|
||||
const responseMessageId = runStep?.runId ?? '';
|
||||
let responseMessageId = runStep?.runId ?? '';
|
||||
if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) {
|
||||
responseMessageId = submission?.initialResponse?.messageId ?? '';
|
||||
parentMessageId = submission?.initialResponse?.parentMessageId ?? '';
|
||||
}
|
||||
|
||||
if (!runStep || !responseMessageId) {
|
||||
console.warn('No run step or runId found for reasoning delta event');
|
||||
@@ -327,7 +350,11 @@ export default function useStepHandler({
|
||||
} else if (event === 'on_run_step_delta') {
|
||||
const runStepDelta = data as Agents.RunStepDeltaEvent;
|
||||
const runStep = stepMap.current.get(runStepDelta.id);
|
||||
const responseMessageId = runStep?.runId ?? '';
|
||||
let responseMessageId = runStep?.runId ?? '';
|
||||
if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) {
|
||||
responseMessageId = submission?.initialResponse?.messageId ?? '';
|
||||
parentMessageId = submission?.initialResponse?.parentMessageId ?? '';
|
||||
}
|
||||
|
||||
if (!runStep || !responseMessageId) {
|
||||
console.warn('No run step or runId found for run step delta event');
|
||||
@@ -366,7 +393,7 @@ export default function useStepHandler({
|
||||
|
||||
messageMap.current.set(responseMessageId, updatedResponse);
|
||||
const updatedMessages = messages.map((msg) =>
|
||||
msg.messageId === runStep.runId ? updatedResponse : msg,
|
||||
msg.messageId === responseMessageId ? updatedResponse : msg,
|
||||
);
|
||||
|
||||
setMessages(updatedMessages);
|
||||
@@ -377,7 +404,11 @@ export default function useStepHandler({
|
||||
const { id: stepId } = result;
|
||||
|
||||
const runStep = stepMap.current.get(stepId);
|
||||
const responseMessageId = runStep?.runId ?? '';
|
||||
let responseMessageId = runStep?.runId ?? '';
|
||||
if (responseMessageId === Constants.USE_PRELIM_RESPONSE_MESSAGE_ID) {
|
||||
responseMessageId = submission?.initialResponse?.messageId ?? '';
|
||||
parentMessageId = submission?.initialResponse?.parentMessageId ?? '';
|
||||
}
|
||||
|
||||
if (!runStep || !responseMessageId) {
|
||||
console.warn('No run step or runId found for completed tool call event');
|
||||
@@ -399,7 +430,7 @@ export default function useStepHandler({
|
||||
|
||||
messageMap.current.set(responseMessageId, updatedResponse);
|
||||
const updatedMessages = messages.map((msg) =>
|
||||
msg.messageId === runStep.runId ? updatedResponse : msg,
|
||||
msg.messageId === responseMessageId ? updatedResponse : msg,
|
||||
);
|
||||
|
||||
setMessages(updatedMessages);
|
||||
@@ -414,4 +445,11 @@ export default function useStepHandler({
|
||||
},
|
||||
[getMessages, setIsSubmitting, lastAnnouncementTimeRef, announcePolite, setMessages],
|
||||
);
|
||||
|
||||
const clearStepMaps = useCallback(() => {
|
||||
toolCallIdMap.current.clear();
|
||||
messageMap.current.clear();
|
||||
stepMap.current.clear();
|
||||
}, []);
|
||||
return { stepHandler, clearStepMaps };
|
||||
}
|
||||
|
||||
1
client/src/locales/bs/translation.json
Normal file
1
client/src/locales/bs/translation.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -6,8 +6,24 @@
|
||||
"com_a11y_start": "The AI has started their reply.",
|
||||
"com_agents_agent_card_label": "{{name}} agent. {{description}}",
|
||||
"com_agents_all": "All Agents",
|
||||
"com_agents_all_category": "All",
|
||||
"com_agents_all_description": "Browse all shared agents across all categories",
|
||||
"com_agents_by_librechat": "by LibreChat",
|
||||
"com_agents_category_aftersales": "After Sales",
|
||||
"com_agents_category_aftersales_description": "Agents specialized in post-sale support, maintenance, and customer service",
|
||||
"com_agents_category_empty": "No agents found in the {{category}} category",
|
||||
"com_agents_category_finance": "Finance",
|
||||
"com_agents_category_finance_description": "Agents specialized in financial analysis, budgeting, and accounting",
|
||||
"com_agents_category_general": "General",
|
||||
"com_agents_category_general_description": "General purpose agents for common tasks and inquiries",
|
||||
"com_agents_category_hr": "Human Resources",
|
||||
"com_agents_category_hr_description": "Agents specialized in HR processes, policies, and employee support",
|
||||
"com_agents_category_it": "IT",
|
||||
"com_agents_category_it_description": "Agents for IT support, technical troubleshooting, and system administration",
|
||||
"com_agents_category_rd": "Research & Development",
|
||||
"com_agents_category_rd_description": "Agents focused on R&D processes, innovation, and technical research",
|
||||
"com_agents_category_sales": "Sales",
|
||||
"com_agents_category_sales_description": "Agents focused on sales processes, customer relations",
|
||||
"com_agents_category_tab_label": "{{category}} category, {{position}} of {{total}}",
|
||||
"com_agents_category_tabs_label": "Agent Categories",
|
||||
"com_agents_clear_search": "Clear search",
|
||||
@@ -478,6 +494,7 @@
|
||||
"com_nav_lang_arabic": "العربية",
|
||||
"com_nav_lang_armenian": "Հայերեն",
|
||||
"com_nav_lang_auto": "Auto detect",
|
||||
"com_nav_lang_bosnian": "Босански",
|
||||
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
|
||||
"com_nav_lang_catalan": "Català",
|
||||
"com_nav_lang_chinese": "中文",
|
||||
@@ -497,6 +514,7 @@
|
||||
"com_nav_lang_japanese": "日本語",
|
||||
"com_nav_lang_korean": "한국어",
|
||||
"com_nav_lang_latvian": "Latviski",
|
||||
"com_nav_lang_norwegian_bokmal": "Norsk Bokmål",
|
||||
"com_nav_lang_persian": "فارسی",
|
||||
"com_nav_lang_polish": "Polski",
|
||||
"com_nav_lang_portuguese": "Português",
|
||||
@@ -722,6 +740,7 @@
|
||||
"com_ui_bookmarks_edit": "Edit Bookmark",
|
||||
"com_ui_bookmarks_filter": "Filter bookmarks...",
|
||||
"com_ui_bookmarks_new": "New Bookmark",
|
||||
"com_ui_bookmarks_tag_exists": "A bookmark with this title already exists",
|
||||
"com_ui_bookmarks_title": "Title",
|
||||
"com_ui_bookmarks_update_error": "There was an error updating the bookmark",
|
||||
"com_ui_bookmarks_update_success": "Bookmark updated successfully",
|
||||
@@ -866,6 +885,7 @@
|
||||
"com_ui_feedback_tag_not_matched": "Didn't match my request",
|
||||
"com_ui_feedback_tag_other": "Other issue",
|
||||
"com_ui_feedback_tag_unjustified_refusal": "Refused without reason",
|
||||
"com_ui_field_max_length": "{{field}} must be less than {{length}} characters",
|
||||
"com_ui_field_required": "This field is required",
|
||||
"com_ui_file_size": "File Size",
|
||||
"com_ui_files": "Files",
|
||||
@@ -994,6 +1014,7 @@
|
||||
"com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one",
|
||||
"com_ui_no_categories": "No categories available",
|
||||
"com_ui_no_category": "No category",
|
||||
"com_ui_no_changes": "No changes were made",
|
||||
"com_ui_no_data": "something needs to go here. was empty",
|
||||
"com_ui_no_individual_access": "No individual users or groups have access to this agent",
|
||||
"com_ui_no_personalization_available": "No personalization options are currently available",
|
||||
@@ -1109,6 +1130,7 @@
|
||||
"com_ui_select_file": "Select a file",
|
||||
"com_ui_select_model": "Select a model",
|
||||
"com_ui_select_options": "Select options...",
|
||||
"com_ui_select_or_create_prompt": "Select or Create a Prompt",
|
||||
"com_ui_select_provider": "Select a provider",
|
||||
"com_ui_select_provider_first": "Select a provider first",
|
||||
"com_ui_select_region": "Select a region",
|
||||
|
||||
@@ -37,12 +37,15 @@ import translationZh_Hans from './zh-Hans/translation.json';
|
||||
import translationZh_Hant from './zh-Hant/translation.json';
|
||||
import translationBo from './bo/translation.json';
|
||||
import translationUk from './uk/translation.json';
|
||||
import translationBs from './bs/translation.json';
|
||||
import translationNb from './nb/translation.json';
|
||||
|
||||
export const defaultNS = 'translation';
|
||||
|
||||
export const resources = {
|
||||
en: { translation: translationEn },
|
||||
ar: { translation: translationAr },
|
||||
bs: { translation: translationBs },
|
||||
ca: { translation: translationCa },
|
||||
cs: { translation: translationCs },
|
||||
'zh-Hans': { translation: translationZh_Hans },
|
||||
@@ -54,6 +57,7 @@ export const resources = {
|
||||
fa: { translation: translationFa },
|
||||
fr: { translation: translationFr },
|
||||
it: { translation: translationIt },
|
||||
nb: { translation: translationNb },
|
||||
pl: { translation: translationPl },
|
||||
'pt-BR': { translation: translationPt_BR },
|
||||
'pt-PT': { translation: translationPt_PT },
|
||||
|
||||
@@ -4,31 +4,83 @@
|
||||
"com_a11y_ai_composing": "Mākslīgais intelekts joprojām veido.",
|
||||
"com_a11y_end": "Mākslīgais intelekts ir pabeidzis atbildi.",
|
||||
"com_a11y_start": "Mākslīgais intelekts ir sācis savu atbildi.",
|
||||
"com_agents_agent_card_label": "{{name}} aģents. {{description}}",
|
||||
"com_agents_all": "Visi aģenti",
|
||||
"com_agents_by_librechat": "no LibreChat",
|
||||
"com_agents_category_empty": "Nav atrasts neviens aģents {{category}} kategorijā",
|
||||
"com_agents_category_tab_label": "{{category}} kategorija, {{position}} no {{total}}",
|
||||
"com_agents_category_tabs_label": "Aģentu kategorijas",
|
||||
"com_agents_clear_search": "Notīrīt meklēšanu",
|
||||
"com_agents_code_interpreter": "Ja šī opcija ir iespējota, jūsu aģents var izmantot LibreChat koda interpretētāja API, lai droši palaistu ģenerēto kodu, tostarp failu apstrādi. Nepieciešama derīga API atslēga.",
|
||||
"com_agents_code_interpreter_title": "Koda interpretētāja API",
|
||||
"com_agents_contact": "Sazinieties ar",
|
||||
"com_agents_copy_link": "Kopēt saiti",
|
||||
"com_agents_create_error": "Izveidojot jūsu aģentu, radās kļūda.",
|
||||
"com_agents_created_by": "izveidojis",
|
||||
"com_agents_description_placeholder": "Pēc izvēles: aprakstiet savu aģentu šeit",
|
||||
"com_agents_empty_state_heading": "Nav atrasts neviens aģents",
|
||||
"com_agents_enable_file_search": "Iespējot failu meklēšanu",
|
||||
"com_agents_error_bad_request_message": "Pieprasījumu nevarēja apstrādāt.",
|
||||
"com_agents_error_bad_request_suggestion": "Lūdzu, pārbaudiet ievadītos datus un mēģiniet vēlreiz.",
|
||||
"com_agents_error_category_title": "Kategorija Kļūda",
|
||||
"com_agents_error_generic": "Satura ielādēšanas laikā radās problēma.",
|
||||
"com_agents_error_invalid_request": "Nepareizs pieprasījums",
|
||||
"com_agents_error_loading": "Kļūda aģentu ielādēšanā",
|
||||
"com_agents_error_network_message": "Nevar izveidot savienojumu ar serveri.",
|
||||
"com_agents_error_network_suggestion": "Pārbaudiet interneta savienojumu un mēģiniet vēlreiz.",
|
||||
"com_agents_error_network_title": "Savienojuma problēmas",
|
||||
"com_agents_error_not_found_message": "Pieprasīto saturu neizdevās atrast.",
|
||||
"com_agents_error_not_found_suggestion": "Mēģiniet pārlūkot citas iespējas vai atgriezieties katalogā.",
|
||||
"com_agents_error_not_found_title": "Nav atrasts",
|
||||
"com_agents_error_retry": "Mēģiniet vēlreiz",
|
||||
"com_agents_error_search_title": "Meklēšanas kļūda",
|
||||
"com_agents_error_searching": "Kļūda meklējot aģentus",
|
||||
"com_agents_error_server_message": "Serveris uz laiku nav pieejams.",
|
||||
"com_agents_error_server_suggestion": "Lūdzu, mēģiniet vēlreiz pēc dažām minūtēm.",
|
||||
"com_agents_error_server_title": "Servera kļūda",
|
||||
"com_agents_error_suggestion_generic": "Lūdzu, mēģiniet atsvaidzināt lapu vai mēģiniet vēlreiz vēlāk.",
|
||||
"com_agents_error_timeout_message": "Pieprasījuma izpildei bija nepieciešams pārāk ilgs laiks.",
|
||||
"com_agents_error_timeout_suggestion": "Lūdzu, pārbaudiet interneta savienojumu un mēģiniet vēlreiz.",
|
||||
"com_agents_error_timeout_title": "Savienojumu neizdevās izveidot",
|
||||
"com_agents_error_title": "Kaut kas nogāja greizi",
|
||||
"com_agents_file_context": "Failu konteksts (OCR)",
|
||||
"com_agents_file_context_disabled": "Pirms failu augšupielādes failu kontekstam ir jāizveido aģents.",
|
||||
"com_agents_file_context_info": "Faili, kas augšupielādēti kā “Konteksts”, tiek apstrādāti, izmantojot OCR, lai iegūtu tekstu, kas pēc tam tiek pievienots aģenta norādījumiem. Ideāli piemērots dokumentiem, attēliem ar tekstu vai PDF failiem, kuriem nepieciešams pilns faila teksta saturs.",
|
||||
"com_agents_file_search_disabled": "Pirms failu augšupielādes failu meklēšanai ir jāizveido aģents.",
|
||||
"com_agents_file_search_info": "Kad šī opcija ir iespējota, aģents tiks informēts par precīziem tālāk norādītajiem failu nosaukumiem, ļaujot tam izgūt atbilstošu kontekstu no šiem failiem.",
|
||||
"com_agents_grid_announcement": "Rādu {{count}} aģentus {{category}} kategorijā",
|
||||
"com_agents_instructions_placeholder": "Sistēmas instrukcijas, ko izmantos aģents",
|
||||
"com_agents_link_copied": "Saite nokopēta",
|
||||
"com_agents_link_copy_failed": "Neizdevās nokopēt saiti",
|
||||
"com_agents_load_more_label": "Ielādēt vairāk aģentus no {{category}} kategorijas",
|
||||
"com_agents_loading": "Notiek ielāde...",
|
||||
"com_agents_marketplace": "Aģentu katalogs",
|
||||
"com_agents_marketplace_subtitle": "Atklājiet un izmantojiet jaudīgus mākslīgā intelekta aģentus, lai uzlabotu darba plūsmu un produktivitāti.",
|
||||
"com_agents_mcp_description_placeholder": "Dažos vārdos paskaidrojiet, ko tas dara.",
|
||||
"com_agents_mcp_icon_size": "Minimālais izmērs 128 x 128 px",
|
||||
"com_agents_mcp_info": "MCP serveru pievienošana savam aģentam, lai tas varētu veikt uzdevumus un mijiedarboties ar ārējiem pakalpojumiem.",
|
||||
"com_agents_mcp_name_placeholder": "Pielāgotais rīks",
|
||||
"com_agents_mcp_trust_subtext": "LibreChat nav validējis neoficiālus savienotājus",
|
||||
"com_agents_mcps_disabled": "Pirms MCP pievienošanas ir jāizveido aģents.",
|
||||
"com_agents_missing_name": "Pirms aģenta izveidošanas ievadiet aģenta nosaukumu.",
|
||||
"com_agents_missing_provider_model": "Pirms aģenta izveides izvēlieties pakalpojumu sniedzēju un modeli.",
|
||||
"com_agents_name_placeholder": "Pēc izvēles: aģenta nosaukums",
|
||||
"com_agents_no_access": "Jums nav piekļuves šī aģenta rediģēšanai.",
|
||||
"com_agents_no_agent_id_error": "Nav atrasts aģenta ID. Lūdzu, pārliecinieties, ka aģents ir izveidots.",
|
||||
"com_agents_no_more_results": "Jūs esat sasniedzis rezultātu beigas",
|
||||
"com_agents_not_available": "Aģents nav pieejams",
|
||||
"com_agents_recommended": "Mūsu rekomendētie aģenti",
|
||||
"com_agents_results_for": "Rezultāti par '{{query}}'",
|
||||
"com_agents_search_aria": "Meklēt aģentus",
|
||||
"com_agents_search_empty_heading": "Nav meklēšanas rezultātu",
|
||||
"com_agents_search_info": "Ja šī opcija ir iespējota, jūsu aģents var meklēt jaunāko informāciju tīmeklī. Nepieciešama derīga API atslēga.",
|
||||
"com_agents_search_instructions": "Rakstiet lai meklētu aģentus pēc vārda vai apraksta",
|
||||
"com_agents_search_name": "Meklēt aģentus pēc nosaukuma",
|
||||
"com_agents_search_no_results": "Nav atrasts neviens aģents \"{{query}}\"",
|
||||
"com_agents_search_placeholder": "Meklēt aģentus...",
|
||||
"com_agents_see_more": "Skatīt vairāk",
|
||||
"com_agents_start_chat": "Sākt sarunu",
|
||||
"com_agents_top_picks": "Labākās izvēles",
|
||||
"com_agents_update_error": "Jūsu aģenta atjaunināšanā ir kļūda.",
|
||||
"com_assistants_action_attempt": "Asistents vēlas runāt ar {{0}}",
|
||||
"com_assistants_actions": "Darbības",
|
||||
@@ -308,10 +360,21 @@
|
||||
"com_error_moderation": "Šķiet, ka mūsu moderācijas sistēma ir atzīmējusi iesniegto saturu kā neatbilstošu mūsu vadlīnijām. Mēs nevaram turpināt darbu ar šo konkrēto tēmu. Ja jums ir vēl kādi jautājumi vai tēmas, kuras vēlaties izpētīt, lūdzu, rediģējiet savu ziņu vai izveidojiet jaunu sarunu.",
|
||||
"com_error_no_base_url": "Nav atrasts bāzes URL. Lūdzu, norādiet to un mēģiniet vēlreiz.",
|
||||
"com_error_no_user_key": "Atslēga nav atrasta. Lūdzu, norādiet atslēgu un mēģiniet vēlreiz.",
|
||||
"com_file_pages": "Lapas: {{pages}}",
|
||||
"com_file_source": "Fails",
|
||||
"com_file_unknown": "Nezināms fails",
|
||||
"com_files_download_failed": "{{0}} failu lejuplāde neizdevās",
|
||||
"com_files_download_percent_complete": "{{0}}% pabeigts",
|
||||
"com_files_download_progress": "{{0}} no {{1}} failiem",
|
||||
"com_files_downloading": "Notiek failu lejuplādēšana",
|
||||
"com_files_filter": "Filtrēt failus...",
|
||||
"com_files_no_results": "Nav rezultātu.",
|
||||
"com_files_number_selected": "{{0}} no {{1}} atlasīti faili",
|
||||
"com_files_preparing_download": "Sagatavošanās lejupielādei...",
|
||||
"com_files_sharepoint_picker_title": "Izvēlieties failus",
|
||||
"com_files_table": "kaut kam šeit ir jānotiek. bija tukšs",
|
||||
"com_files_upload_local_machine": "No lokālā datora",
|
||||
"com_files_upload_sharepoint": "No SharePoint",
|
||||
"com_generated_files": "Ģenerētie faili:",
|
||||
"com_hide_examples": "Slēpt piemērus",
|
||||
"com_info_heic_converting": "Konvertēju HEIC attēlu uz JPEG...",
|
||||
@@ -347,7 +410,7 @@
|
||||
"com_nav_balance_month": "mēnesis",
|
||||
"com_nav_balance_months": "mēneši",
|
||||
"com_nav_balance_next_refill": "Nākamā bilances papildināšana:",
|
||||
"com_nav_balance_next_refill_info": "Nākamā bilances papildināšana notiks automātiski tikai tad, ja būs izpildīti abi nosacījumi: kopš pēdējās bilances papildināšanas ir pagājis norādītais laika atjaunošanas biežums un uzaicinājuma nosūtīšana izraisītu jūsu atlikuma samazināšanos zem nulles.",
|
||||
"com_nav_balance_next_refill_info": "Nākamā bilances papildināšana notiks automātiski tikai tad, ja būs izpildīti abi nosacījumi: kopš pēdējās bilances papildināšanas ir pagājis norādītais laika atjaunošanas biežums un vaicājuma nosūtīšana izraisītu jūsu atlikuma samazināšanos zem nulles.",
|
||||
"com_nav_balance_refill_amount": "Bilances papildināšanas apjoms:",
|
||||
"com_nav_balance_second": "otrais",
|
||||
"com_nav_balance_seconds": "sekundes",
|
||||
@@ -415,6 +478,7 @@
|
||||
"com_nav_lang_arabic": "العربية",
|
||||
"com_nav_lang_armenian": "Հայերեն",
|
||||
"com_nav_lang_auto": "Automātiska noteikšana",
|
||||
"com_nav_lang_bosnian": "Босански",
|
||||
"com_nav_lang_brazilian_portuguese": "Brazīlijas portugāļu",
|
||||
"com_nav_lang_catalan": "Katalā",
|
||||
"com_nav_lang_chinese": "中文",
|
||||
@@ -434,6 +498,7 @@
|
||||
"com_nav_lang_japanese": "日本語",
|
||||
"com_nav_lang_korean": "한국어",
|
||||
"com_nav_lang_latvian": "Latviešu",
|
||||
"com_nav_lang_norwegian_bokmal": "Norsk Bokmål",
|
||||
"com_nav_lang_persian": "فارسی",
|
||||
"com_nav_lang_polish": "Poļu",
|
||||
"com_nav_lang_portuguese": "Portugāļu",
|
||||
@@ -515,9 +580,21 @@
|
||||
"com_sidepanel_manage_files": "Pārvaldīt failus",
|
||||
"com_sidepanel_mcp_no_servers_with_vars": "Nav MCP serveru ar konfigurējamiem mainīgajiem.",
|
||||
"com_sidepanel_parameters": "Parametri",
|
||||
"com_sources_agent_file": "Avota dokuments",
|
||||
"com_sources_agent_files": "Aģentu faili",
|
||||
"com_sources_download_aria_label": "Lejupielādēt {{filename}}{{status}}",
|
||||
"com_sources_download_failed": "Lejupielādē neizdevās",
|
||||
"com_sources_download_local_unavailable": "Neizdevās lejupielādēt: Faili nav saglabāti",
|
||||
"com_sources_downloading_status": " (lejupielādē...)",
|
||||
"com_sources_error_fallback": "Nevar ielādēt avotus",
|
||||
"com_sources_image_alt": "Meklēšanas rezultāta attēls",
|
||||
"com_sources_more_files": "+{{count}} faili",
|
||||
"com_sources_more_sources": "+{{count}} avoti",
|
||||
"com_sources_pages": "Lapas",
|
||||
"com_sources_region_label": "Meklēšanas rezultāti un avoti",
|
||||
"com_sources_reload_page": "Atkārtoti ielādēt lapu",
|
||||
"com_sources_tab_all": "Visi",
|
||||
"com_sources_tab_files": "Faili",
|
||||
"com_sources_tab_images": "Attēli",
|
||||
"com_sources_tab_news": "Ziņas",
|
||||
"com_sources_title": "Avoti",
|
||||
@@ -546,6 +623,14 @@
|
||||
"com_ui_advanced": "Advancēts",
|
||||
"com_ui_advanced_settings": "Advancētie iestatījumi",
|
||||
"com_ui_agent": "Aģents",
|
||||
"com_ui_agent_category_aftersales": "Pēcpārdošanas pakalpojumi",
|
||||
"com_ui_agent_category_finance": "Finanses",
|
||||
"com_ui_agent_category_general": "Vispārīgi",
|
||||
"com_ui_agent_category_hr": "Personāla daļa",
|
||||
"com_ui_agent_category_it": "IT",
|
||||
"com_ui_agent_category_rd": "Pētniecība un attīstība",
|
||||
"com_ui_agent_category_sales": "Pārdošana",
|
||||
"com_ui_agent_category_selector_aria": "Aģenta kategorijas atlasītājs",
|
||||
"com_ui_agent_chain": "Aģentu ķēde (aģentu maisījums)",
|
||||
"com_ui_agent_chain_info": "Ļauj izveidot aģentu secību ķēdes. Katrs aģents var piekļūt iepriekšējo ķēdē esošo aģentu izvades datiem. Balstīts uz \"Aģentu sajaukuma\" arhitektūru, kurā aģenti izmanto iepriekšējos izvades datus kā palīginformāciju.",
|
||||
"com_ui_agent_chain_max": "Jūs esat sasniedzis maksimālo skaitu {{0}} aģentu.",
|
||||
@@ -553,8 +638,10 @@
|
||||
"com_ui_agent_deleted": "Aģents veiksmīgi izdzēsts",
|
||||
"com_ui_agent_duplicate_error": "Dublējot aģentu, radās kļūda.",
|
||||
"com_ui_agent_duplicated": "Aģents veiksmīgi dublēts",
|
||||
"com_ui_agent_name_is_required": "Obligāti jānorāda aģenta nosaukums",
|
||||
"com_ui_agent_recursion_limit": "Maksimālais aģenta soļu skaits",
|
||||
"com_ui_agent_recursion_limit_info": "Ierobežo, cik soļus aģents var veikt vienā izpildes reizē, pirms sniedz galīgo atbildi. Noklusējuma vērtība ir 25 soļi. Solis ir vai nu AI API pieprasījums, vai rīka lietošanas kārta. Piemēram, pamata rīka mijiedarbība ietver 3 soļus: sākotnējo pieprasījumu, rīka lietošanu un turpmāko pieprasījumu.",
|
||||
"com_ui_agent_url_copied": "Aģenta URL kopēts starpliktuvē",
|
||||
"com_ui_agent_var": "{{0}} aģents",
|
||||
"com_ui_agent_version": "Versija",
|
||||
"com_ui_agent_version_active": "Aktīvā versija",
|
||||
@@ -571,6 +658,7 @@
|
||||
"com_ui_agent_version_unknown_date": "Nezināms datums",
|
||||
"com_ui_agents": "Aģenti",
|
||||
"com_ui_agents_allow_create": "Atļaut aģentu izveidi",
|
||||
"com_ui_agents_allow_share": "Atļaut aģentu koplietošanu",
|
||||
"com_ui_agents_allow_use": "Atļaut aģentu izmantošanu",
|
||||
"com_ui_all": "visi",
|
||||
"com_ui_all_proper": "Visi",
|
||||
@@ -591,6 +679,7 @@
|
||||
"com_ui_assistant_deleted": "Asistents ir veiksmīgi izdzēsts.",
|
||||
"com_ui_assistants": "Asistenti",
|
||||
"com_ui_assistants_output": "Asistentu izvade",
|
||||
"com_ui_at_least_one_owner_required": "Nepieciešams vismaz viens īpašnieks",
|
||||
"com_ui_attach_error": "Nevar pievienot failu. Izveidojiet vai atlasiet sarunu vai mēģiniet atsvaidzināt lapu.",
|
||||
"com_ui_attach_error_openai": "Nevar pievienot asistenta failus citiem galapunktiem",
|
||||
"com_ui_attach_error_size": "Galapunkta faila lieluma ierobežojums ir pārsniegts:",
|
||||
@@ -607,12 +696,16 @@
|
||||
"com_ui_available_tools": "Pieejamie rīki",
|
||||
"com_ui_avatar": "Avatars",
|
||||
"com_ui_azure": "Azure",
|
||||
"com_ui_azure_ad": "Azure Entra ID",
|
||||
"com_ui_back": "Atpakaļ",
|
||||
"com_ui_back_to_chat": "Atpakaļ uz sarunu",
|
||||
"com_ui_back_to_prompts": "Atpakaļ pie uzvednēm",
|
||||
"com_ui_backup_code_number": "Kods #{{number}}",
|
||||
"com_ui_backup_codes": "Rezerves kodi",
|
||||
"com_ui_backup_codes_regenerate_error": "Atkārtoti ģenerējot rezerves kodus, radās kļūda.",
|
||||
"com_ui_backup_codes_regenerated": "Rezerves kodi ir veiksmīgi atjaunoti",
|
||||
"com_ui_backup_codes_security_info": "Drošības apsvērumu dēļ rezerves kodi tiek parādīti tikai vienu reizi. Lūdzu, saglabājiet tos drošā vietā.",
|
||||
"com_ui_backup_codes_status": "Rezerves kodu statuss",
|
||||
"com_ui_basic": "Pamata",
|
||||
"com_ui_basic_auth_header": "Pamata autorizācijas galvene",
|
||||
"com_ui_bearer": "Nesējs",
|
||||
@@ -669,6 +762,7 @@
|
||||
"com_ui_copy_code": "Kopēt kodu",
|
||||
"com_ui_copy_link": "Kopēt saiti",
|
||||
"com_ui_copy_to_clipboard": "Kopēt starpliktuvē",
|
||||
"com_ui_copy_url_to_clipboard": "URL kopēšana uz starpliktuvi",
|
||||
"com_ui_create": "Izveidot",
|
||||
"com_ui_create_link": "Izveidot saiti",
|
||||
"com_ui_create_memory": "Izveidot atmiņu",
|
||||
@@ -754,6 +848,7 @@
|
||||
"com_ui_error_connection": "Kļūda, izveidojot savienojumu ar serveri, mēģiniet atsvaidzināt lapu.",
|
||||
"com_ui_error_save_admin_settings": "Saglabājot administratora iestatījumus, radās kļūda.",
|
||||
"com_ui_error_updating_preferences": "Kļūda, atjauninot preferences",
|
||||
"com_ui_everyone_permission_level": "Visu lietotāju atļaujas līmenis",
|
||||
"com_ui_examples": "Piemēri",
|
||||
"com_ui_expand_chat": "Izvērst sarunu",
|
||||
"com_ui_export_convo_modal": "Eksportēt sarunas modālo logu",
|
||||
@@ -818,6 +913,7 @@
|
||||
"com_ui_good_afternoon": "Labdien",
|
||||
"com_ui_good_evening": "Labvakar",
|
||||
"com_ui_good_morning": "Labrīt",
|
||||
"com_ui_group": "Grupa",
|
||||
"com_ui_happy_birthday": "Man šodien ir pirmā dzimšanas diena!",
|
||||
"com_ui_hide_image_details": "Slēpt attēla detaļas",
|
||||
"com_ui_hide_password": "Paslēpt paroli",
|
||||
@@ -851,6 +947,8 @@
|
||||
"com_ui_logo": "{{0}} Logotips",
|
||||
"com_ui_low": "Zems",
|
||||
"com_ui_manage": "Pārvaldīt",
|
||||
"com_ui_marketplace": "Katalogs",
|
||||
"com_ui_marketplace_allow_use": "Atļaut izmantot katalogu",
|
||||
"com_ui_max_tags": "Maksimālais atļautais skaits ir {{0}}, izmantojot jaunākās vērtības.",
|
||||
"com_ui_mcp_authenticated_success": "MCP serveris '{{0}}' veiksmīgi autentificēts",
|
||||
"com_ui_mcp_enter_var": "Ievadiet vērtību {{0}}",
|
||||
@@ -898,10 +996,13 @@
|
||||
"com_ui_next": "Nākamais",
|
||||
"com_ui_no": "Nē",
|
||||
"com_ui_no_bookmarks": "Šķiet, ka jums vēl nav grāmatzīmju. Noklikšķiniet uz sarunas un pievienojiet jaunu.",
|
||||
"com_ui_no_categories": "Nav pieejamas nevienas kategorijas",
|
||||
"com_ui_no_category": "Nav kategorijas",
|
||||
"com_ui_no_data": "kaut kam šeit ir jānotiek. bija tukšs",
|
||||
"com_ui_no_individual_access": "Aatsevišķiem lietotājiem vai grupām nav pieejas pie šī aģenta",
|
||||
"com_ui_no_personalization_available": "Pašlaik nav pieejamas personalizācijas opcijas",
|
||||
"com_ui_no_read_access": "Jums nav atļaujas skatīt atmiņas",
|
||||
"com_ui_no_results_found": "Nav atrastu rezultātu",
|
||||
"com_ui_no_terms_content": "Nav noteikumu un nosacījumu satura, ko parādīt",
|
||||
"com_ui_no_valid_items": "kaut kam šeit ir jānotiek. bija tukšs",
|
||||
"com_ui_none": "Neviens",
|
||||
@@ -924,6 +1025,14 @@
|
||||
"com_ui_openai": "OpenAI",
|
||||
"com_ui_optional": "(pēc izvēles)",
|
||||
"com_ui_page": "Lapa",
|
||||
"com_ui_people": "cilvēki",
|
||||
"com_ui_people_picker": "Cilvēku atlasītājs",
|
||||
"com_ui_people_picker_allow_view_groups": "Atļaut skatīt grupas",
|
||||
"com_ui_people_picker_allow_view_roles": "Atļaut lomu skatīšanu",
|
||||
"com_ui_people_picker_allow_view_users": "Atļaut skatīt lietotājus",
|
||||
"com_ui_permissions_failed_load": "Neizdevās ielādēt pieejas tiesības. Lūdzu, mēģiniet vēlreiz.",
|
||||
"com_ui_permissions_failed_update": "Neizdevās atjaunināt pieejas tiesības. Lūdzu, mēģiniet vēlreiz.",
|
||||
"com_ui_permissions_updated_success": "Pieejas tiesības ir veiksmīgi atjauninātas.",
|
||||
"com_ui_preferences_updated": "Preferences veiksmīgi atjauninātas",
|
||||
"com_ui_prev": "Iepriekšējais",
|
||||
"com_ui_preview": "Priekšskatījums",
|
||||
@@ -938,6 +1047,7 @@
|
||||
"com_ui_prompt_update_error": "Atjauninot uzvedni, radās kļūda.",
|
||||
"com_ui_prompts": "Uzvednes",
|
||||
"com_ui_prompts_allow_create": "Atļaut uzvedņu izveidi",
|
||||
"com_ui_prompts_allow_share": "Atļaut kopīgošanas uzvednes",
|
||||
"com_ui_prompts_allow_use": "Atļaut izmantot uzvednes",
|
||||
"com_ui_provider": "Pakalpojumu sniedzējs",
|
||||
"com_ui_quality": "Kvalitāte",
|
||||
@@ -945,12 +1055,14 @@
|
||||
"com_ui_redirecting_to_provider": "Pārvirzu uz {{0}}, lūdzu, uzgaidiet...",
|
||||
"com_ui_reference_saved_memories": "References uz saglabātajām atmiņām",
|
||||
"com_ui_reference_saved_memories_description": "Ļaut asistentam atsaukties uz jūsu saglabātajām atmiņām un izmantot tās atbildot",
|
||||
"com_ui_refresh": "Atsvaidzināt",
|
||||
"com_ui_refresh_link": "Atsvaidzināt saiti",
|
||||
"com_ui_regenerate": "Atjaunot",
|
||||
"com_ui_regenerate_backup": "Atjaunot rezerves kodus",
|
||||
"com_ui_regenerating": "Atjaunojas...",
|
||||
"com_ui_region": "Reģions",
|
||||
"com_ui_reinitialize": "Reinicializēt",
|
||||
"com_ui_remove_user": "Noņemt {{0}}",
|
||||
"com_ui_rename": "Pārdēvēt",
|
||||
"com_ui_rename_conversation": "Pārdēvēt sarunu",
|
||||
"com_ui_rename_failed": "Neizdevās pārdēvēt sarunu",
|
||||
@@ -958,6 +1070,7 @@
|
||||
"com_ui_requires_auth": "Nepieciešama autentifikācija",
|
||||
"com_ui_reset_var": "Atiestatīt {{0}}",
|
||||
"com_ui_reset_zoom": "Atiestatīt tālummaiņu",
|
||||
"com_ui_resource": "resurss",
|
||||
"com_ui_result": "Rezultāts",
|
||||
"com_ui_revoke": "Atsaukt",
|
||||
"com_ui_revoke_info": "Atcelt visus lietotāja sniegtos lietotāja datus",
|
||||
@@ -965,24 +1078,41 @@
|
||||
"com_ui_revoke_key_endpoint": "Atsaukt atslēgu priekš {{0}}",
|
||||
"com_ui_revoke_keys": "Atsaukt atslēgas",
|
||||
"com_ui_revoke_keys_confirm": "Vai tiešām vēlaties atsaukt visas atslēgas?",
|
||||
"com_ui_role": "Loma",
|
||||
"com_ui_role_editor": "Redaktors",
|
||||
"com_ui_role_editor_desc": "Var skatīt un rediģēt aģentu",
|
||||
"com_ui_role_manager": "Vadītājs",
|
||||
"com_ui_role_manager_desc": "Var skatīt, rediģēt un dzēst aģentu",
|
||||
"com_ui_role_owner": "Īpašnieks",
|
||||
"com_ui_role_owner_desc": "Pilnīga kontrole pār aģentu, tostarp tā koplietošana",
|
||||
"com_ui_role_select": "Loma",
|
||||
"com_ui_role_viewer": "Skatītājs",
|
||||
"com_ui_role_viewer_desc": "Var skatīt un izmantot aģentu, bet nevar to rediģēt",
|
||||
"com_ui_roleplay": "Lomu spēle",
|
||||
"com_ui_run_code": "Palaišanas kods",
|
||||
"com_ui_run_code_error": "Radās kļūda, izpildot kodu",
|
||||
"com_ui_save": "Saglabāt",
|
||||
"com_ui_save_badge_changes": "Vai saglabāt emblēmas izmaiņas?",
|
||||
"com_ui_save_changes": "Saglabāt izmaiņas",
|
||||
"com_ui_save_submit": "Saglabāt un iesniegt",
|
||||
"com_ui_saved": "Saglabāts!",
|
||||
"com_ui_saving": "Saglabā...",
|
||||
"com_ui_schema": "Shēma",
|
||||
"com_ui_scope": "Mērogs",
|
||||
"com_ui_search": "Meklēt",
|
||||
"com_ui_search_above_to_add": "Meklēt augstāk, lai pievienotu lietotājus vai grupas",
|
||||
"com_ui_search_above_to_add_all": "Meklēt augstāk, lai pievienotu lietotājus, grupas vai lomas",
|
||||
"com_ui_search_above_to_add_people": "Meklēt augstāk, lai pievienotu personas",
|
||||
"com_ui_search_agent_category": "Meklēt kategorijas...",
|
||||
"com_ui_search_default_placeholder": "Meklēt pēc vārda vai e-pasta adreses (vismaz 2 rakstu zīmes)",
|
||||
"com_ui_search_people_placeholder": "Meklēt personas vai grupas pēc vārda vai e-pasta adreses",
|
||||
"com_ui_seconds": "sekundes",
|
||||
"com_ui_secret_key": "Slepenā atslēga",
|
||||
"com_ui_select": "Atlasīt",
|
||||
"com_ui_select_all": "Atlasīt visu",
|
||||
"com_ui_select_file": "Atlasiet failu",
|
||||
"com_ui_select_model": "Izvēlieties modeli",
|
||||
"com_ui_select_options": "Izvēlieties opcijas...",
|
||||
"com_ui_select_provider": "Izvēlieties pakalpojumu sniedzēju",
|
||||
"com_ui_select_provider_first": "Vispirms izvēlieties pakalpojumu sniedzēju",
|
||||
"com_ui_select_region": "Izvēlieties reģionu",
|
||||
@@ -995,6 +1125,8 @@
|
||||
"com_ui_share_create_message": "Jūsu vārds un visas ziņas, ko pievienojat pēc kopīgošanas, paliek privātas.",
|
||||
"com_ui_share_delete_error": "Dzēšot koplietoto saiti, radās kļūda.",
|
||||
"com_ui_share_error": "Kopīgojot sarunas saiti, radās kļūda.",
|
||||
"com_ui_share_everyone": "Koplietot ar visiem",
|
||||
"com_ui_share_everyone_description_var": "Šis {{resource}} būs pieejams ikvienam. Lūdzu, pārliecinieties, ka {{resource}} patiesībā ir paredzēts koplietošanai ar visiem. Esiet uzmanīgi ar saviem datiem.",
|
||||
"com_ui_share_link_to_chat": "Kopīgot saiti sarunai",
|
||||
"com_ui_share_update_message": "Jūsu vārds, pielāgotie norādījumi un visas ziņas, ko pievienojat pēc kopīgošanas, paliek privātas.",
|
||||
"com_ui_share_var": "Kopīgot {{0}}",
|
||||
@@ -1021,6 +1153,13 @@
|
||||
"com_ui_stop": "Apstāties",
|
||||
"com_ui_storage": "Uzglabāšana",
|
||||
"com_ui_submit": "Iesniegt",
|
||||
"com_ui_support_contact": "Atbalsta kontaktinformācija",
|
||||
"com_ui_support_contact_email": "E-pasts",
|
||||
"com_ui_support_contact_email_invalid": "Lūdzu, ievadiet derīgu e-pasta adresi",
|
||||
"com_ui_support_contact_email_placeholder": "atbalsts@piemers.com",
|
||||
"com_ui_support_contact_name": "Vārds",
|
||||
"com_ui_support_contact_name_min_length": "Vārdam jābūt vismaz {{minLength}} rakstu zīmēm",
|
||||
"com_ui_support_contact_name_placeholder": "Atbalsta kontaktpersonas vārds",
|
||||
"com_ui_teach_or_explain": "Mācīšanās",
|
||||
"com_ui_temporary": "Pagaidu saruna",
|
||||
"com_ui_terms_and_conditions": "Noteikumi un nosacījumi",
|
||||
@@ -1037,6 +1176,7 @@
|
||||
"com_ui_tools": "Rīki",
|
||||
"com_ui_travel": "Ceļošana",
|
||||
"com_ui_trust_app": "Es uzticos šai lietotnei",
|
||||
"com_ui_try_adjusting_search": "Mēģiniet pielāgot meklēšanas vaicājumus",
|
||||
"com_ui_unarchive": "Atarhivēt",
|
||||
"com_ui_unarchive_error": "Neizdevās atarhivēt sarunu",
|
||||
"com_ui_unknown": "Nezināms",
|
||||
@@ -1046,6 +1186,7 @@
|
||||
"com_ui_update_mcp_error": "Izveidojot vai atjauninot MCP, radās kļūda.",
|
||||
"com_ui_update_mcp_success": "Veiksmīgi izveidots vai atjaunināts MCP",
|
||||
"com_ui_upload": "Augšupielādēt",
|
||||
"com_ui_upload_agent_avatar": "Aģenta avatars veiksmīgi atjaunināts",
|
||||
"com_ui_upload_code_files": "Augšupielādēt koda interpretētājam",
|
||||
"com_ui_upload_delay": "Augšupielāde \"{{0}}\" aizņem vairāk laika nekā paredzēts. Lūdzu, uzgaidiet, kamēr faila indeksēšana ir pabeigta izguvei.",
|
||||
"com_ui_upload_error": "Augšupielādējot failu, radās kļūda.",
|
||||
@@ -1065,6 +1206,8 @@
|
||||
"com_ui_use_memory": "Izmantot atmiņu",
|
||||
"com_ui_use_micrphone": "Izmantot mikrofonu",
|
||||
"com_ui_used": "Lietots",
|
||||
"com_ui_user": "Lietotājs",
|
||||
"com_ui_user_group_permissions": "Lietotāju un grupu atļaujas",
|
||||
"com_ui_value": "Vērtība",
|
||||
"com_ui_variables": "Mainīgie",
|
||||
"com_ui_variables_info": "Mainīgo veidošanai tekstā izmantot dubultās iekavas, piemēram, `{{example variable}}`, lai vēlāk aizpildītu, izmantojot uzvedni.",
|
||||
|
||||
1
client/src/locales/nb/translation.json
Normal file
1
client/src/locales/nb/translation.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -4,6 +4,7 @@
|
||||
"com_a11y_ai_composing": "De AI is nog bezig met het formuleren van een antwoord.",
|
||||
"com_a11y_end": "De AI is klaar met het antwoord.",
|
||||
"com_a11y_start": "De AI is begonnen met antwoorden.",
|
||||
"com_agents_all": "Alle Agents",
|
||||
"com_agents_by_librechat": "door LibreChat",
|
||||
"com_agents_code_interpreter": "Indien ingeschakeld, kan je agent de LibreChat Code Interpreter API gebruiken om gegenereerde code, inclusief het verwerken van bestanden, veilig uit te voeren. Vereist een geldige API-sleutel.",
|
||||
"com_agents_code_interpreter_title": "Code Interpreter API",
|
||||
|
||||
@@ -478,6 +478,7 @@
|
||||
"com_nav_lang_arabic": "العربية",
|
||||
"com_nav_lang_armenian": "Հայերեն",
|
||||
"com_nav_lang_auto": "自动检测语言",
|
||||
"com_nav_lang_bosnian": "Босански",
|
||||
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
|
||||
"com_nav_lang_catalan": "Català",
|
||||
"com_nav_lang_chinese": "中文",
|
||||
@@ -497,6 +498,7 @@
|
||||
"com_nav_lang_japanese": "日本語",
|
||||
"com_nav_lang_korean": "한국어",
|
||||
"com_nav_lang_latvian": "Latviski",
|
||||
"com_nav_lang_norwegian_bokmal": "Norsk Bokmål",
|
||||
"com_nav_lang_persian": "فارسی",
|
||||
"com_nav_lang_polish": "Polski",
|
||||
"com_nav_lang_portuguese": "Português",
|
||||
|
||||
@@ -4,12 +4,24 @@
|
||||
"com_a11y_ai_composing": "AI 仍在撰寫中",
|
||||
"com_a11y_end": "AI 已完成回覆",
|
||||
"com_a11y_start": "AI 已開始回覆。",
|
||||
"com_agents_agent_card_label": "{{name}} agent。{{description}}",
|
||||
"com_agents_all": "全部 Agent",
|
||||
"com_agents_by_librechat": "由 LibreChat 提供",
|
||||
"com_agents_category_empty": "在 {{category}} 類別中找不到 agent",
|
||||
"com_agents_category_tab_label": "{{category}} 類別,{{position}} / {{total}}",
|
||||
"com_agents_category_tabs_label": "Agent 類別",
|
||||
"com_agents_clear_search": "清除搜尋",
|
||||
"com_agents_code_interpreter": "啟用後,您的代理可以安全地使用 LibreChat 程式碼解譯器 API 來執行產生的程式碼,包括檔案處理功能。需要有效的 API 金鑰。",
|
||||
"com_agents_code_interpreter_title": "程式碼解譯器 API",
|
||||
"com_agents_contact": "關聯",
|
||||
"com_agents_copy_link": "複製連結",
|
||||
"com_agents_create_error": "建立您的代理時發生錯誤。",
|
||||
"com_agents_created_by": "來自",
|
||||
"com_agents_description_placeholder": "選填:在此描述您的代理程式",
|
||||
"com_agents_empty_state_heading": "未找到 agent",
|
||||
"com_agents_enable_file_search": "啟用檔案搜尋",
|
||||
"com_agents_error_bad_request_message": "無法處理該請求",
|
||||
"com_agents_error_bad_request_suggestion": "請檢查您的輸入並再試一次。",
|
||||
"com_agents_file_context": "文件內容 (OCR)",
|
||||
"com_agents_file_context_disabled": "在為檔案上下文上傳檔案之前,必須先建立 Agent。",
|
||||
"com_agents_file_context_info": "以「Context」標記上傳的檔案會使用 OCR 擷取文字,擷取後的文字會被加入到 Agent 的指示中。適合用於文件、含文字的圖片或需要取得檔案完整文字內容的 PDF。",
|
||||
@@ -229,7 +241,7 @@
|
||||
"com_endpoint_openai_max_tokens": "可選的 `max_tokens` 欄位,代表在對話完成中可以生成的最大 token 數。\n\n輸入 token 和生成 token 的總長度受限於模型的上下文長度。如果此數字超過最大上下文 token 數,您可能會遇到錯誤。",
|
||||
"com_endpoint_openai_pres": "數值範圍介於 -2.0 和 2.0 之間。正值會根據該 token 是否在目前的文字中出現來進行懲罰,增加模型談及新主題的可能性。",
|
||||
"com_endpoint_openai_prompt_prefix_placeholder": "在系統訊息中設定自訂提示。",
|
||||
"com_endpoint_openai_reasoning_effort": "僅適用於推理模型:限制推理的投入。降低推理投入可以使回應更快,且在回應中使用較少的推理 token。「Minimal」會產生非常少的推理 token,以達到最快的首次 token 產生時間,特別適合程式碼與指令遵循。",
|
||||
"com_endpoint_openai_reasoning_effort": "僅適用於推理模型:限制推理的投入。降低推理投入可以使回應更快,且在回應中使用較少的推理 token。最小值會產生非常少的推理 token,以達到最快的首次 token 產生時間,特別適合程式碼與指令遵循。",
|
||||
"com_endpoint_openai_reasoning_summary": "僅限 Responses API:模型執行推理的摘要。這有助於除錯並理解模型的推理過程。可設定為 無、自動、簡潔或詳細。",
|
||||
"com_endpoint_openai_resend": "重新傳送之前所有附加的圖片。注意:這可能會大幅增加 token 成本,如果附加了太多圖片,您可能會遇到錯誤。",
|
||||
"com_endpoint_openai_resend_files": "重新傳送之前附加的所有檔案。注意:這將增加 token 成本,如果附件過多,您可能會遇到錯誤。",
|
||||
@@ -238,7 +250,7 @@
|
||||
"com_endpoint_openai_topp": "與溫度取樣的替代方法,稱為核心取樣,其中模型考慮 top_p 機率質量的 token 結果。所以 0.1 表示只考慮佔 top 10% 機率質量的 token。我們建議修改這個或溫度,但不建議兩者都修改。",
|
||||
"com_endpoint_openai_use_responses_api": "使用 Responses API 取代 Chat Completions,Responses API 包含來自 OpenAI 的延伸功能。使用 o1-pro、o3-pro 或要啟用推理摘要時,必須使用 Responses API。",
|
||||
"com_endpoint_openai_use_web_search": "啟用 OpenAI 內建的網路搜尋功能,使模型能夠在網路上搜尋最新資訊,從而提供更準確、即時的回應。",
|
||||
"com_endpoint_openai_verbosity": "限制模型回應的冗長程度。較低的值會得到較簡潔的回應,較高的值會得到較冗長的回應。目前支援的值為 low、medium 與 high。",
|
||||
"com_endpoint_openai_verbosity": "限制模型回應的詳盡程度。數值較低會產生較精簡的回應,數值較高會產生較詳盡的回應。目前支援的值為低/中/高。",
|
||||
"com_endpoint_output": "輸出",
|
||||
"com_endpoint_plug_image_detail": "影像詳細資訊",
|
||||
"com_endpoint_plug_resend_files": "重新傳送檔案",
|
||||
@@ -280,6 +292,7 @@
|
||||
"com_endpoint_top_k": "Top K",
|
||||
"com_endpoint_top_p": "Top P",
|
||||
"com_endpoint_use_active_assistant": "使用活躍助理",
|
||||
"com_endpoint_verbosity": "詳盡程度",
|
||||
"com_error_expired_user_key": "提供給 {{0}} 的金鑰已於 {{1}} 到期。請提供一個新的金鑰並重試。",
|
||||
"com_error_files_dupe": "偵測到重複的檔案。",
|
||||
"com_error_files_empty": "不允許空白檔案。",
|
||||
@@ -318,6 +331,7 @@
|
||||
"com_nav_balance": "餘額",
|
||||
"com_nav_balance_day": "日",
|
||||
"com_nav_balance_days": "日",
|
||||
"com_nav_balance_every": "每",
|
||||
"com_nav_balance_hour": "小時",
|
||||
"com_nav_balance_hours": "小時",
|
||||
"com_nav_balance_interval": "間隔",
|
||||
@@ -386,9 +400,14 @@
|
||||
"com_nav_info_save_draft": "啟用後,您在聊天表單中輸入的文字和附件將自動儲存為本地草稿。即使重新載入頁面或切換至其他對話,這些草稿仍會保留。草稿僅儲存在您的裝置上,並會在訊息送出後自動刪除。",
|
||||
"com_nav_info_user_name_display": "啟用時,每則您發送的訊息上方都會顯示您的使用者名稱。停用時,您的訊息上方只會顯示「您」。",
|
||||
"com_nav_lang_arabic": "العربية",
|
||||
"com_nav_lang_armenian": "Հայերեն",
|
||||
"com_nav_lang_auto": "自動偵測",
|
||||
"com_nav_lang_bosnian": "Босански",
|
||||
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
|
||||
"com_nav_lang_catalan": "Català",
|
||||
"com_nav_lang_chinese": "中文",
|
||||
"com_nav_lang_czech": "Čeština",
|
||||
"com_nav_lang_danish": "Dansk",
|
||||
"com_nav_lang_dutch": "Nederlands",
|
||||
"com_nav_lang_english": "English",
|
||||
"com_nav_lang_estonian": "Eesti keel",
|
||||
@@ -397,18 +416,25 @@
|
||||
"com_nav_lang_georgian": "ქართული",
|
||||
"com_nav_lang_german": "Deutsch",
|
||||
"com_nav_lang_hebrew": "עברית",
|
||||
"com_nav_lang_hungarian": "Magyar",
|
||||
"com_nav_lang_indonesia": "Indonesia",
|
||||
"com_nav_lang_italian": "Italiano",
|
||||
"com_nav_lang_japanese": "日本語",
|
||||
"com_nav_lang_korean": "한국어",
|
||||
"com_nav_lang_latvian": "Latviski",
|
||||
"com_nav_lang_norwegian_bokmal": "Norsk Bokmål",
|
||||
"com_nav_lang_persian": "فارسی",
|
||||
"com_nav_lang_polish": "Polski",
|
||||
"com_nav_lang_portuguese": "Português",
|
||||
"com_nav_lang_russian": "Русский",
|
||||
"com_nav_lang_spanish": "Español",
|
||||
"com_nav_lang_swedish": "Svenska",
|
||||
"com_nav_lang_thai": "ไทย",
|
||||
"com_nav_lang_tibetan": "བོད་སྐད་",
|
||||
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||
"com_nav_lang_turkish": "Türkçe",
|
||||
"com_nav_lang_ukrainian": "Українська",
|
||||
"com_nav_lang_uyghur": "Uyƣur tili",
|
||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||
"com_nav_language": "語言",
|
||||
"com_nav_latex_parsing": "解析訊息中的 LaTeX 內容(可能影響效能)",
|
||||
@@ -531,6 +557,7 @@
|
||||
"com_ui_attachment": "附件",
|
||||
"com_ui_auth_type": "認證類型",
|
||||
"com_ui_authentication": "驗證",
|
||||
"com_ui_auto": "自動",
|
||||
"com_ui_avatar": "大頭照",
|
||||
"com_ui_back_to_chat": "返回對話",
|
||||
"com_ui_back_to_prompts": "返回提示",
|
||||
@@ -562,6 +589,7 @@
|
||||
"com_ui_collapse_chat": "收合對話",
|
||||
"com_ui_command_placeholder": "選填:輸入指令,若未填寫將使用名稱",
|
||||
"com_ui_command_usage_placeholder": "透過指令或名稱選擇提示",
|
||||
"com_ui_concise": "簡潔",
|
||||
"com_ui_confirm_action": "確認操作",
|
||||
"com_ui_context": "情境",
|
||||
"com_ui_continue": "繼續",
|
||||
@@ -613,6 +641,7 @@
|
||||
"com_ui_description": "描述",
|
||||
"com_ui_description_placeholder": "選填:輸入要顯示的提示描述",
|
||||
"com_ui_deselect_all": "全部取消選取",
|
||||
"com_ui_detailed": "詳細",
|
||||
"com_ui_download_error": "下載檔案時發生錯誤。該檔案可能已被刪除。",
|
||||
"com_ui_dropdown_variables": "下拉式變數:",
|
||||
"com_ui_dropdown_variables_info": "為您的提示建立自訂下拉選單:`{{variable_name:選項1|選項2|選項3}}`",
|
||||
@@ -621,6 +650,7 @@
|
||||
"com_ui_duplication_processing": "正在複製對話...",
|
||||
"com_ui_duplication_success": "已成功複製對話",
|
||||
"com_ui_edit": "編輯",
|
||||
"com_ui_empty_category": "-",
|
||||
"com_ui_endpoint": "端點",
|
||||
"com_ui_endpoint_menu": "語言模型端點選單",
|
||||
"com_ui_enter": "輸入",
|
||||
@@ -658,7 +688,10 @@
|
||||
"com_ui_generate_qrcode": "產生 QR 碼",
|
||||
"com_ui_generating": "產生中...",
|
||||
"com_ui_go_to_conversation": "前往對話",
|
||||
"com_ui_good_evening": "晚安",
|
||||
"com_ui_good_morning": "早安",
|
||||
"com_ui_happy_birthday": "這是我的第一個生日!",
|
||||
"com_ui_high": "高",
|
||||
"com_ui_host": "主機",
|
||||
"com_ui_image_gen": "影像生成",
|
||||
"com_ui_import_conversation_error": "匯入對話時發生錯誤",
|
||||
@@ -668,17 +701,20 @@
|
||||
"com_ui_include_shadcnui": "包含 shadcn/ui 元件說明",
|
||||
"com_ui_input": "輸入",
|
||||
"com_ui_instructions": "說明",
|
||||
"com_ui_key": "鍵",
|
||||
"com_ui_latest_footer": "讓每個人都能使用 AI",
|
||||
"com_ui_librechat_code_api_key": "取得你的 LibreChat 程式碼解譯器 API 金鑰",
|
||||
"com_ui_librechat_code_api_subtitle": "安全性高。多語言支援。檔案輸入/輸出。",
|
||||
"com_ui_librechat_code_api_title": "執行 AI 程式碼",
|
||||
"com_ui_locked": "已鎖定",
|
||||
"com_ui_logo": "{{0}} 標誌",
|
||||
"com_ui_low": "低",
|
||||
"com_ui_manage": "管理",
|
||||
"com_ui_max_tags": "允許的最大數量為 {{0}},已使用最新值。",
|
||||
"com_ui_mcp_enter_var": "請輸入 {{0}} 的值",
|
||||
"com_ui_mcp_server_not_found": "找不到伺服器。",
|
||||
"com_ui_mcp_url": "MCP 伺服器",
|
||||
"com_ui_medium": "中",
|
||||
"com_ui_memories": "記憶",
|
||||
"com_ui_memories_allow_create": "允許建立記憶",
|
||||
"com_ui_memories_allow_opt_out": "允許用戶選擇不使用記憶功能",
|
||||
@@ -700,21 +736,25 @@
|
||||
"com_ui_memory_would_exceed": "無法儲存——會超出限制 {{tokens}} 個 token。請刪除現有記憶以釋放空間。",
|
||||
"com_ui_mention": "提及端點、助理或預設設定以快速切換",
|
||||
"com_ui_min_tags": "無法再移除更多值,至少需要 {{0}} 個。",
|
||||
"com_ui_minimal": "最小值",
|
||||
"com_ui_model": "模型",
|
||||
"com_ui_model_parameters": "模型參數",
|
||||
"com_ui_more_info": "更多資訊",
|
||||
"com_ui_my_prompts": "我的提示",
|
||||
"com_ui_name": "名稱",
|
||||
"com_ui_new": "新",
|
||||
"com_ui_new_chat": "新對話",
|
||||
"com_ui_next": "下一個",
|
||||
"com_ui_no": "否",
|
||||
"com_ui_no_bookmarks": "看來您還沒有任何書籤。請點選對話並新增一個書籤",
|
||||
"com_ui_no_category": "無分類",
|
||||
"com_ui_no_terms_content": "沒有條款和條件內容顯示",
|
||||
"com_ui_none": "無",
|
||||
"com_ui_nothing_found": "找不到任何內容",
|
||||
"com_ui_of": "的",
|
||||
"com_ui_off": "關閉",
|
||||
"com_ui_on": "開啟",
|
||||
"com_ui_openai": "OpenAI",
|
||||
"com_ui_page": "頁面",
|
||||
"com_ui_prev": "上一個",
|
||||
"com_ui_preview": "預覽",
|
||||
@@ -763,6 +803,7 @@
|
||||
"com_ui_select_search_plugin": "依名稱搜尋外掛程式",
|
||||
"com_ui_select_search_provider": "依名稱搜尋供應商",
|
||||
"com_ui_select_search_region": "依名稱搜尋區域",
|
||||
"com_ui_set": "設置",
|
||||
"com_ui_share": "分享",
|
||||
"com_ui_share_create_message": "您的姓名以及您在共享後新增的任何訊息都會保密。",
|
||||
"com_ui_share_delete_error": "刪除共享連結時發生錯誤。",
|
||||
@@ -780,8 +821,11 @@
|
||||
"com_ui_stop": "停止",
|
||||
"com_ui_storage": "儲存空間",
|
||||
"com_ui_submit": "送出",
|
||||
"com_ui_support_contact_email": "Email",
|
||||
"com_ui_terms_and_conditions": "條款和條件",
|
||||
"com_ui_terms_of_service": "服務條款",
|
||||
"com_ui_thoughts": "思考過程",
|
||||
"com_ui_token": "token",
|
||||
"com_ui_tools": "工具",
|
||||
"com_ui_unarchive": "取消封存",
|
||||
"com_ui_unarchive_error": "取消封存對話時發生錯誤",
|
||||
@@ -825,6 +869,7 @@
|
||||
"com_ui_web_search_searxng_instance_url": "SearXNG Instance URL",
|
||||
"com_ui_web_searching": "正在搜尋網路",
|
||||
"com_ui_web_searching_again": "正在重新搜尋網路",
|
||||
"com_ui_weekend_morning": "週末愉快",
|
||||
"com_ui_yes": "是",
|
||||
"com_ui_zoom": "縮放",
|
||||
"com_user_message": "您"
|
||||
|
||||
@@ -207,4 +207,36 @@ y$ which spans lines`;
|
||||
const expected = 'Set $$\\{x | x > \\$0\\}$$ for positive prices';
|
||||
expect(preprocessLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('does not convert when closing dollar is preceded by backtick', () => {
|
||||
const content = 'The error "invalid $lookup namespace" occurs when using `$lookup` operator';
|
||||
const expected = 'The error "invalid $lookup namespace" occurs when using `$lookup` operator';
|
||||
expect(preprocessLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('handles mixed backtick and non-backtick cases', () => {
|
||||
const content = 'Use $x + y$ in math but `$lookup` in code';
|
||||
const expected = 'Use $$x + y$$ in math but `$lookup` in code';
|
||||
expect(preprocessLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('escapes currency amounts without commas', () => {
|
||||
const content =
|
||||
'The total amount invested is $1157.90 (existing amount) + $500 (new investment) = $1657.90.';
|
||||
const expected =
|
||||
'The total amount invested is \\$1157.90 (existing amount) + \\$500 (new investment) = \\$1657.90.';
|
||||
expect(preprocessLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('handles large currency amounts', () => {
|
||||
const content = 'You can win $1000000 or even $9999999.99!';
|
||||
const expected = 'You can win \\$1000000 or even \\$9999999.99!';
|
||||
expect(preprocessLaTeX(content)).toBe(expected);
|
||||
});
|
||||
|
||||
test('escapes currency with many decimal places', () => {
|
||||
const content = 'Bitcoin: $0.00001234, Gas: $3.999, Rate: $1.234567890';
|
||||
const expected = 'Bitcoin: \\$0.00001234, Gas: \\$3.999, Rate: \\$1.234567890';
|
||||
expect(preprocessLaTeX(content)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,8 @@ const MHCHEM_CE_REGEX = /\$\\ce\{/g;
|
||||
const MHCHEM_PU_REGEX = /\$\\pu\{/g;
|
||||
const MHCHEM_CE_ESCAPED_REGEX = /\$\\\\ce\{[^}]*\}\$/g;
|
||||
const MHCHEM_PU_ESCAPED_REGEX = /\$\\\\pu\{[^}]*\}\$/g;
|
||||
const CURRENCY_REGEX =
|
||||
/(?<![\\$])\$(?!\$)(?=\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?(?:\s|$|[^a-zA-Z\d]))/g;
|
||||
const SINGLE_DOLLAR_REGEX = /(?<!\\)\$(?!\$)((?:[^$\n]|\\[$])+?)(?<!\\)\$(?!\$)/g;
|
||||
const CURRENCY_REGEX = /(?<![\\$])\$(?!\$)(?=\d+(?:,\d{3})*(?:\.\d+)?(?:\s|$|[^a-zA-Z\d]))/g;
|
||||
const SINGLE_DOLLAR_REGEX = /(?<!\\)\$(?!\$)((?:[^$\n]|\\[$])+?)(?<!\\)(?<!`)\$(?!\$)/g;
|
||||
|
||||
/**
|
||||
* Escapes mhchem package notation in LaTeX by converting single dollar delimiters to double dollars
|
||||
|
||||
@@ -16,6 +16,38 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
|
||||
|
||||
logger.info('Starting Enhanced Agent Permissions Migration', { dryRun, batchSize });
|
||||
|
||||
/** Ensurse `aclentries` collection exists for DocumentDB compatibility
|
||||
* @param {import('mongoose').mongo.Db} db
|
||||
* @param {string} collectionName
|
||||
*/
|
||||
async function ensureCollectionExists(db, collectionName) {
|
||||
try {
|
||||
const collections = await db.listCollections({ name: collectionName }).toArray();
|
||||
if (collections.length === 0) {
|
||||
await db.createCollection(collectionName);
|
||||
logger.info(`Created collection: ${collectionName}`);
|
||||
} else {
|
||||
logger.info(`Collection already exists: ${collectionName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`'Failed to check/create "${collectionName}" collection:`, error);
|
||||
// If listCollections fails, try alternative approach
|
||||
try {
|
||||
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
|
||||
await db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
|
||||
} catch (createError) {
|
||||
logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
/** @type {import('mongoose').mongo.Db | undefined} */
|
||||
const db = mongoose.connection.db;
|
||||
if (db) {
|
||||
await ensureCollectionExists(db, 'aclentries');
|
||||
}
|
||||
|
||||
// Verify required roles exist
|
||||
const ownerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
|
||||
const viewerRole = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
|
||||
@@ -100,12 +132,19 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Agent categorization:', {
|
||||
globalEditAccess: categories.globalEditAccess.length,
|
||||
globalViewAccess: categories.globalViewAccess.length,
|
||||
privateAgents: categories.privateAgents.length,
|
||||
total: agentsToMigrate.length,
|
||||
});
|
||||
logger.info(
|
||||
'Agent categorization:\n' +
|
||||
JSON.stringify(
|
||||
{
|
||||
globalEditAccess: categories.globalEditAccess.length,
|
||||
globalViewAccess: categories.globalViewAccess.length,
|
||||
privateAgents: categories.privateAgents.length,
|
||||
total: agentsToMigrate.length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
return {
|
||||
|
||||
@@ -16,6 +16,38 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
|
||||
|
||||
logger.info('Starting PromptGroup Permissions Migration', { dryRun, batchSize });
|
||||
|
||||
/** Ensurse `aclentries` collection exists for DocumentDB compatibility
|
||||
* @param {import('mongoose').mongo.Db} db
|
||||
* @param {string} collectionName
|
||||
*/
|
||||
async function ensureCollectionExists(db, collectionName) {
|
||||
try {
|
||||
const collections = await db.listCollections({ name: collectionName }).toArray();
|
||||
if (collections.length === 0) {
|
||||
await db.createCollection(collectionName);
|
||||
logger.info(`Created collection: ${collectionName}`);
|
||||
} else {
|
||||
logger.info(`Collection already exists: ${collectionName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`'Failed to check/create "${collectionName}" collection:`, error);
|
||||
// If listCollections fails, try alternative approach
|
||||
try {
|
||||
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
|
||||
await db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
|
||||
} catch (createError) {
|
||||
logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
/** @type {import('mongoose').mongo.Db | undefined} */
|
||||
const db = mongoose.connection.db;
|
||||
if (db) {
|
||||
await ensureCollectionExists(db, 'aclentries');
|
||||
}
|
||||
|
||||
// Verify required roles exist
|
||||
const ownerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER);
|
||||
const viewerRole = await findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER);
|
||||
@@ -91,11 +123,18 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('PromptGroup categorization:', {
|
||||
globalViewAccess: categories.globalViewAccess.length,
|
||||
privateGroups: categories.privateGroups.length,
|
||||
total: promptGroupsToMigrate.length,
|
||||
});
|
||||
logger.info(
|
||||
'PromptGroup categorization:\n' +
|
||||
JSON.stringify(
|
||||
{
|
||||
globalViewAccess: categories.globalViewAccess.length,
|
||||
privateGroups: categories.privateGroups.length,
|
||||
total: promptGroupsToMigrate.length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// v0.8.0-rc2
|
||||
// v0.8.0-rc3
|
||||
// See .env.test.example for an example of the '.env.test' file.
|
||||
require('dotenv').config({ path: './e2e/.env.test' });
|
||||
|
||||
@@ -23,7 +23,7 @@ version: 1.8.10
|
||||
# It is recommended to use it with quotes.
|
||||
|
||||
# renovate: image=ghcr.io/danny-avila/librechat
|
||||
appVersion: "v0.8.0-rc2"
|
||||
appVersion: "v0.8.0-rc3"
|
||||
|
||||
home: https://www.librechat.ai
|
||||
|
||||
|
||||
498
package-lock.json
generated
498
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "LibreChat",
|
||||
"version": "v0.8.0-rc2",
|
||||
"version": "v0.8.0-rc3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "LibreChat",
|
||||
"version": "v0.8.0-rc2",
|
||||
"version": "v0.8.0-rc3",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"api",
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"api": {
|
||||
"name": "@librechat/backend",
|
||||
"version": "v0.8.0-rc2",
|
||||
"version": "v0.8.0-rc3",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0",
|
||||
@@ -2623,7 +2623,7 @@
|
||||
},
|
||||
"client": {
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.8.0-rc2",
|
||||
"version": "v0.8.0-rc3",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.15",
|
||||
@@ -17654,6 +17654,454 @@
|
||||
"kuler": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/adventurer": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.2.4.tgz",
|
||||
"integrity": "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/adventurer-neutral": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.2.4.tgz",
|
||||
"integrity": "sha512-I9IrB4ZYbUHSOUpWoUbfX3vG8FrjcW8htoQ4bEOR7TYOKKE11Mo1nrGMuHZ7GPfwN0CQeK1YVJhWqLTmtYn7Pg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/avataaars": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.2.4.tgz",
|
||||
"integrity": "sha512-QKNBtA/1QGEzR+JjS4XQyrFHYGbzdOp0oa6gjhGhUDrMegDFS8uyjdRfDQsFTebVkyLWjgBQKZEiDqKqHptB6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/avataaars-neutral": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.2.4.tgz",
|
||||
"integrity": "sha512-HtBvA7elRv50QTOOsBdtYB1GVimCpGEDlDgWsu1snL5Z3d1+3dIESoXQd3mXVvKTVT8Z9ciA4TEaF09WfxDjAA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/big-ears": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.2.4.tgz",
|
||||
"integrity": "sha512-U33tbh7Io6wG6ViUMN5fkWPER7hPKMaPPaYgafaYQlCT4E7QPKF2u8X1XGag3jCKm0uf4SLXfuZ8v+YONcHmNQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/big-ears-neutral": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.2.4.tgz",
|
||||
"integrity": "sha512-pPjYu80zMFl43A9sa5+tAKPkhp4n9nd7eN878IOrA1HAowh/XePh5JN8PTkNFS9eM+rnN9m8WX08XYFe30kLYw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/big-smile": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.2.4.tgz",
|
||||
"integrity": "sha512-zeEfXOOXy7j9tfkPLzfQdLBPyQsctBetTdEfKRArc1k3RUliNPxfJG9j88+cXQC6GXrVW2pcT2X50NSPtugCFQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/bottts": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.2.4.tgz",
|
||||
"integrity": "sha512-4CTqrnVg+NQm6lZ4UuCJish8gGWe8EqSJrzvHQRO5TEyAKjYxbTdVqejpkycG1xkawha4FfxsYgtlSx7UwoVMw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/bottts-neutral": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.2.4.tgz",
|
||||
"integrity": "sha512-eMVdofdD/udHsKIaeWEXShDRtiwk7vp4FjY7l0f79vIzfhkIsXKEhPcnvHKOl/yoArlDVS3Uhgjj0crWTO9RJA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/collection": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.2.4.tgz",
|
||||
"integrity": "sha512-I1wCUp0yu5qSIeMQHmDYXQIXKkKjcja/SYBxppPkYFXpR2alxb0k9/swFDdMbkY6a1c9AT1kI1y+Pg6ywQ2rTA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dicebear/adventurer": "9.2.4",
|
||||
"@dicebear/adventurer-neutral": "9.2.4",
|
||||
"@dicebear/avataaars": "9.2.4",
|
||||
"@dicebear/avataaars-neutral": "9.2.4",
|
||||
"@dicebear/big-ears": "9.2.4",
|
||||
"@dicebear/big-ears-neutral": "9.2.4",
|
||||
"@dicebear/big-smile": "9.2.4",
|
||||
"@dicebear/bottts": "9.2.4",
|
||||
"@dicebear/bottts-neutral": "9.2.4",
|
||||
"@dicebear/croodles": "9.2.4",
|
||||
"@dicebear/croodles-neutral": "9.2.4",
|
||||
"@dicebear/dylan": "9.2.4",
|
||||
"@dicebear/fun-emoji": "9.2.4",
|
||||
"@dicebear/glass": "9.2.4",
|
||||
"@dicebear/icons": "9.2.4",
|
||||
"@dicebear/identicon": "9.2.4",
|
||||
"@dicebear/initials": "9.2.4",
|
||||
"@dicebear/lorelei": "9.2.4",
|
||||
"@dicebear/lorelei-neutral": "9.2.4",
|
||||
"@dicebear/micah": "9.2.4",
|
||||
"@dicebear/miniavs": "9.2.4",
|
||||
"@dicebear/notionists": "9.2.4",
|
||||
"@dicebear/notionists-neutral": "9.2.4",
|
||||
"@dicebear/open-peeps": "9.2.4",
|
||||
"@dicebear/personas": "9.2.4",
|
||||
"@dicebear/pixel-art": "9.2.4",
|
||||
"@dicebear/pixel-art-neutral": "9.2.4",
|
||||
"@dicebear/rings": "9.2.4",
|
||||
"@dicebear/shapes": "9.2.4",
|
||||
"@dicebear/thumbs": "9.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/core": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.2.4.tgz",
|
||||
"integrity": "sha512-hz6zArEcUwkZzGOSJkWICrvqnEZY7BKeiq9rqKzVJIc1tRVv0MkR0FGvIxSvXiK9TTIgKwu656xCWAGAl6oh+w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/croodles": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.2.4.tgz",
|
||||
"integrity": "sha512-CqT0NgVfm+5kd+VnjGY4WECNFeOrj5p7GCPTSEA7tCuN72dMQOX47P9KioD3wbExXYrIlJgOcxNrQeb/FMGc3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/croodles-neutral": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.2.4.tgz",
|
||||
"integrity": "sha512-8vAS9lIEKffSUVx256GSRAlisB8oMX38UcPWw72venO/nitLVsyZ6hZ3V7eBdII0Onrjqw1RDndslQODbVcpTw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/dylan": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.2.4.tgz",
|
||||
"integrity": "sha512-tiih1358djAq0jDDzmW3N3S4C3ynC2yn4hhlTAq/MaUAQtAi47QxdHdFGdxH0HBMZKqA4ThLdVk3yVgN4xsukg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/fun-emoji": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.2.4.tgz",
|
||||
"integrity": "sha512-Od729skczse1HvHekgEFv+mSuJKMC4sl5hENGi/izYNe6DZDqJrrD0trkGT/IVh/SLXUFbq1ZFY9I2LoUGzFZg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/glass": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.2.4.tgz",
|
||||
"integrity": "sha512-5lxbJode1t99eoIIgW0iwZMoZU4jNMJv/6vbsgYUhAslYFX5zP0jVRscksFuo89TTtS7YKqRqZAL3eNhz4bTDw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/icons": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.2.4.tgz",
|
||||
"integrity": "sha512-bRsK1qj8u9Z76xs8XhXlgVr/oHh68tsHTJ/1xtkX9DeTQTSamo2tS26+r231IHu+oW3mePtFnwzdG9LqEPRd4A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/identicon": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.2.4.tgz",
|
||||
"integrity": "sha512-R9nw/E8fbu9HltHOqI9iL/o9i7zM+2QauXWMreQyERc39oGR9qXiwgBxsfYGcIS4C85xPyuL5B3I2RXrLBlJPg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/initials": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.2.4.tgz",
|
||||
"integrity": "sha512-4SzHG5WoQZl1TGcpEZR4bdsSkUVqwNQCOwWSPAoBJa3BNxbVsvL08LF7I97BMgrCoknWZjQHUYt05amwTPTKtg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/lorelei": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.2.4.tgz",
|
||||
"integrity": "sha512-eS4mPYUgDpo89HvyFAx/kgqSSKh8W4zlUA8QJeIUCWTB0WpQmeqkSgIyUJjGDYSrIujWi+zEhhckksM5EwW0Dg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/lorelei-neutral": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.2.4.tgz",
|
||||
"integrity": "sha512-bWq2/GonbcJULtT+B/MGcM2UnA7kBQoH+INw8/oW83WI3GNTZ6qEwe3/W4QnCgtSOhUsuwuiSULguAFyvtkOZQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/micah": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.2.4.tgz",
|
||||
"integrity": "sha512-XNWJ8Mx+pncIV8Ye0XYc/VkMiax8kTxcP3hLTC5vmELQyMSLXzg/9SdpI+W/tCQghtPZRYTT3JdY9oU9IUlP2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/miniavs": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.2.4.tgz",
|
||||
"integrity": "sha512-k7IYTAHE/4jSO6boMBRrNlqPT3bh7PLFM1atfe0nOeCDwmz/qJUBP3HdONajbf3fmo8f2IZYhELrNWTOE7Ox3Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/notionists": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.2.4.tgz",
|
||||
"integrity": "sha512-zcvpAJ93EfC0xQffaPZQuJPShwPhnu9aTcoPsaYGmw0oEDLcv2XYmDhUUdX84QYCn6LtCZH053rHLVazRW+OGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/notionists-neutral": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.2.4.tgz",
|
||||
"integrity": "sha512-fskWzBVxQzJhCKqY24DGZbYHSBaauoRa1DgXM7+7xBuksH7mfbTmZTvnUAsAqJYBkla8IPb4ERKduDWtlWYYjQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/open-peeps": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.2.4.tgz",
|
||||
"integrity": "sha512-s6nwdjXFsplqEI7imlsel4Gt6kFVJm6YIgtZSpry0UdwDoxUUudei5bn957j9lXwVpVUcRjJW+TuEKztYjXkKQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/personas": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.2.4.tgz",
|
||||
"integrity": "sha512-JNim8RfZYwb0MfxW6DLVfvreCFIevQg+V225Xe5tDfbFgbcYEp4OU/KaiqqO2476OBjCw7i7/8USbv2acBhjwA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/pixel-art": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.2.4.tgz",
|
||||
"integrity": "sha512-4Ao45asieswUdlCTBZqcoF/0zHR3OWUWB0Mvhlu9b1Fbc6IlPBiOfx2vsp6bnVGVnMag58tJLecx2omeXdECBQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/pixel-art-neutral": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.2.4.tgz",
|
||||
"integrity": "sha512-ZITPLD1cPN4GjKkhWi80s7e5dcbXy34ijWlvmxbc4eb/V7fZSsyRa9EDUW3QStpo+xrCJLcLR+3RBE5iz0PC/A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/rings": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.2.4.tgz",
|
||||
"integrity": "sha512-teZxELYyV2ogzgb5Mvtn/rHptT0HXo9SjUGS4A52mOwhIdHSGGU71MqA1YUzfae9yJThsw6K7Z9kzuY2LlZZHA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/shapes": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.2.4.tgz",
|
||||
"integrity": "sha512-MhK9ZdFm1wUnH4zWeKPRMZ98UyApolf5OLzhCywfu38tRN6RVbwtBRHc/42ZwoN1JU1JgXr7hzjYucMqISHtbA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dicebear/thumbs": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.2.4.tgz",
|
||||
"integrity": "sha512-EL4sMqv9p2+1Xy3d8e8UxyeKZV2+cgt3X2x2RTRzEOIIhobtkL8u6lJxmJbiGbpVtVALmrt5e7gjmwqpryYDpg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dicebear/core": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.0.tgz",
|
||||
@@ -31411,13 +31859,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cipher-base": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
|
||||
"integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
|
||||
"integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cjs-module-lexer": {
|
||||
@@ -47184,16 +47636,24 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"version": "2.4.12",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
|
||||
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
|
||||
"dev": true,
|
||||
"license": "(MIT AND BSD-3-Clause)",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
"inherits": "^2.0.4",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"to-buffer": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"sha.js": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
@@ -51346,7 +51806,7 @@
|
||||
},
|
||||
"packages/api": {
|
||||
"name": "@librechat/api",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
@@ -51472,7 +51932,7 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@librechat/client",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.2",
|
||||
@@ -51500,6 +51960,8 @@
|
||||
"peerDependencies": {
|
||||
"@ariakit/react": "^0.4.16",
|
||||
"@ariakit/react-core": "^0.4.17",
|
||||
"@dicebear/collection": "^9.2.2",
|
||||
"@dicebear/core": "^9.2.2",
|
||||
"@headlessui/react": "^2.1.2",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.2",
|
||||
@@ -51771,7 +52233,7 @@
|
||||
},
|
||||
"packages/data-provider": {
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.8.003",
|
||||
"version": "0.8.004",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.2",
|
||||
@@ -51876,7 +52338,7 @@
|
||||
},
|
||||
"packages/data-schemas": {
|
||||
"name": "@librechat/data-schemas",
|
||||
"version": "0.0.18",
|
||||
"version": "0.0.20",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "LibreChat",
|
||||
"version": "v0.8.0-rc2",
|
||||
"version": "v0.8.0-rc3",
|
||||
"description": "",
|
||||
"workspaces": [
|
||||
"api",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/api",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"type": "commonjs",
|
||||
"description": "MCP services for LibreChat",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { AccessRoleIds, ResourceType, PrincipalType, Constants } from 'librechat-data-provider';
|
||||
import type { AccessRoleMethods, IAgent } from '@librechat/data-schemas';
|
||||
import type { Model } from 'mongoose';
|
||||
import type { Model, Mongoose, mongo } from 'mongoose';
|
||||
|
||||
const { GLOBAL_PROJECT_NAME } = Constants;
|
||||
|
||||
@@ -17,7 +17,8 @@ export interface MigrationCheckDbMethods {
|
||||
}
|
||||
|
||||
export interface MigrationCheckParams {
|
||||
db: MigrationCheckDbMethods;
|
||||
mongoose: Mongoose;
|
||||
methods: MigrationCheckDbMethods;
|
||||
AgentModel: Model<IAgent>;
|
||||
}
|
||||
|
||||
@@ -46,16 +47,44 @@ export interface MigrationCheckResult {
|
||||
* This performs a dry-run check similar to the migration script
|
||||
*/
|
||||
export async function checkAgentPermissionsMigration({
|
||||
db,
|
||||
methods,
|
||||
mongoose,
|
||||
AgentModel,
|
||||
}: MigrationCheckParams): Promise<MigrationCheckResult> {
|
||||
logger.debug('Checking if agent permissions migration is needed');
|
||||
|
||||
try {
|
||||
/** Ensurse `aclentries` collection exists for DocumentDB compatibility */
|
||||
async function ensureCollectionExists(db: mongo.Db, collectionName: string) {
|
||||
try {
|
||||
const collections = await db.listCollections({ name: collectionName }).toArray();
|
||||
if (collections.length === 0) {
|
||||
await db.createCollection(collectionName);
|
||||
logger.info(`Created collection: ${collectionName}`);
|
||||
} else {
|
||||
logger.debug(`Collection already exists: ${collectionName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`'Failed to check/create "${collectionName}" collection:`, error);
|
||||
// If listCollections fails, try alternative approach
|
||||
try {
|
||||
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
|
||||
await db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
|
||||
} catch (createError) {
|
||||
logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const db = mongoose.connection.db;
|
||||
if (db) {
|
||||
await ensureCollectionExists(db, 'aclentries');
|
||||
}
|
||||
|
||||
// Verify required roles exist
|
||||
const ownerRole = await db.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
|
||||
const viewerRole = await db.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
|
||||
const editorRole = await db.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
|
||||
const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
|
||||
const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
|
||||
const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
|
||||
|
||||
if (!ownerRole || !viewerRole || !editorRole) {
|
||||
logger.warn(
|
||||
@@ -70,7 +99,7 @@ export async function checkAgentPermissionsMigration({
|
||||
}
|
||||
|
||||
// Get global project agent IDs
|
||||
const globalProject = await db.getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']);
|
||||
const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['agentIds']);
|
||||
const globalAgentIds = new Set(globalProject?.agentIds || []);
|
||||
|
||||
// Find agents without ACL entries (no batching for efficiency on startup)
|
||||
|
||||
@@ -6,8 +6,11 @@ import type { AzureOpenAIInput } from '@langchain/openai';
|
||||
import type { OpenAI } from 'openai';
|
||||
import type * as t from '~/types';
|
||||
import { sanitizeModelName, constructAzureURL } from '~/utils/azure';
|
||||
import { createFetch } from '~/utils/generators';
|
||||
import { isEnabled } from '~/utils/common';
|
||||
|
||||
type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
export const knownOpenAIParams = new Set([
|
||||
// Constructor/Instance Parameters
|
||||
'model',
|
||||
@@ -92,6 +95,7 @@ export function getOpenAIConfig(
|
||||
const {
|
||||
modelOptions: _modelOptions = {},
|
||||
reverseProxyUrl,
|
||||
directEndpoint,
|
||||
defaultQuery,
|
||||
headers,
|
||||
proxy,
|
||||
@@ -311,6 +315,13 @@ export function getOpenAIConfig(
|
||||
llmConfig.modelKwargs = modelKwargs;
|
||||
}
|
||||
|
||||
if (directEndpoint === true && configOptions?.baseURL != null) {
|
||||
configOptions.fetch = createFetch({
|
||||
directEndpoint: directEndpoint,
|
||||
reverseProxyUrl: configOptions?.baseURL,
|
||||
}) as unknown as Fetch;
|
||||
}
|
||||
|
||||
const result: t.LLMConfigResult = {
|
||||
llmConfig,
|
||||
configOptions,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* MCP */
|
||||
export * from './mcp/MCPManager';
|
||||
export * from './mcp/connection';
|
||||
export * from './mcp/oauth';
|
||||
export * from './mcp/auth';
|
||||
export * from './mcp/zod';
|
||||
|
||||
@@ -28,6 +28,7 @@ export class MCPConnectionFactory {
|
||||
protected readonly oauthStart?: (authURL: string) => Promise<void>;
|
||||
protected readonly oauthEnd?: () => Promise<void>;
|
||||
protected readonly returnOnOAuth?: boolean;
|
||||
protected readonly connectionTimeout?: number;
|
||||
|
||||
/** Creates a new MCP connection with optional OAuth support */
|
||||
static async create(
|
||||
@@ -47,6 +48,7 @@ export class MCPConnectionFactory {
|
||||
});
|
||||
this.serverName = basic.serverName;
|
||||
this.useOAuth = !!oauth?.useOAuth;
|
||||
this.connectionTimeout = oauth?.connectionTimeout;
|
||||
this.logPrefix = oauth?.user
|
||||
? `[MCP][${basic.serverName}][${oauth.user.id}]`
|
||||
: `[MCP][${basic.serverName}]`;
|
||||
@@ -82,8 +84,9 @@ export class MCPConnectionFactory {
|
||||
if (!this.tokenMethods?.findToken) return null;
|
||||
|
||||
try {
|
||||
const flowId = MCPOAuthHandler.generateFlowId(this.userId!, this.serverName);
|
||||
const tokens = await this.flowManager!.createFlowWithHandler(
|
||||
`tokens:${this.userId}:${this.serverName}`,
|
||||
flowId,
|
||||
'mcp_get_tokens',
|
||||
async () => {
|
||||
return await MCPTokenStorage.getTokens({
|
||||
@@ -203,7 +206,7 @@ export class MCPConnectionFactory {
|
||||
|
||||
/** Attempts to establish connection with timeout handling */
|
||||
protected async attemptToConnect(connection: MCPConnection): Promise<void> {
|
||||
const connectTimeout = this.serverConfig.initTimeout ?? 30000;
|
||||
const connectTimeout = this.connectionTimeout ?? this.serverConfig.initTimeout ?? 30000;
|
||||
const connectionTimeout = new Promise<void>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(`Connection timeout after ${connectTimeout}ms`)),
|
||||
@@ -347,6 +350,7 @@ export class MCPConnectionFactory {
|
||||
newFlowId,
|
||||
'mcp_oauth',
|
||||
flowMetadata as FlowMetadata,
|
||||
this.signal,
|
||||
);
|
||||
if (typeof this.oauthEnd === 'function') {
|
||||
await this.oauthEnd();
|
||||
|
||||
@@ -58,6 +58,21 @@ export class MCPManager extends UserConnectionManager {
|
||||
public getAppToolFunctions(): t.LCAvailableTools | null {
|
||||
return this.serversRegistry.toolFunctions!;
|
||||
}
|
||||
/** Returns all available tool functions from all connections available to user */
|
||||
public async getAllToolFunctions(userId: string): Promise<t.LCAvailableTools | null> {
|
||||
const allToolFunctions: t.LCAvailableTools = this.getAppToolFunctions() ?? {};
|
||||
const userConnections = this.getUserConnections(userId);
|
||||
if (!userConnections || userConnections.size === 0) {
|
||||
return allToolFunctions;
|
||||
}
|
||||
|
||||
for (const [serverName, connection] of userConnections.entries()) {
|
||||
const toolFunctions = await this.serversRegistry.getToolFunctions(serverName, connection);
|
||||
Object.assign(allToolFunctions, toolFunctions);
|
||||
}
|
||||
|
||||
return allToolFunctions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instructions for MCP servers
|
||||
@@ -101,34 +116,36 @@ ${formattedInstructions}
|
||||
Please follow these instructions when using tools from the respective MCP servers.`;
|
||||
}
|
||||
|
||||
/** Loads tools from all app-level connections into the manifest. */
|
||||
public async loadManifestTools({
|
||||
serverToolsCallback,
|
||||
getServerTools,
|
||||
}: {
|
||||
flowManager: FlowStateManager<MCPOAuthTokens | null>;
|
||||
serverToolsCallback?: (serverName: string, tools: t.LCManifestTool[]) => Promise<void>;
|
||||
getServerTools?: (serverName: string) => Promise<t.LCManifestTool[] | undefined>;
|
||||
}): Promise<t.LCToolManifest> {
|
||||
const mcpTools: t.LCManifestTool[] = [];
|
||||
private async loadAppManifestTools(): Promise<t.LCManifestTool[]> {
|
||||
const connections = await this.appConnections!.getAll();
|
||||
return await this.loadManifestTools(connections);
|
||||
}
|
||||
|
||||
private async loadUserManifestTools(userId: string): Promise<t.LCManifestTool[]> {
|
||||
const connections = this.getUserConnections(userId);
|
||||
return await this.loadManifestTools(connections);
|
||||
}
|
||||
|
||||
public async loadAllManifestTools(userId: string): Promise<t.LCManifestTool[]> {
|
||||
const appTools = await this.loadAppManifestTools();
|
||||
const userTools = await this.loadUserManifestTools(userId);
|
||||
return [...appTools, ...userTools];
|
||||
}
|
||||
|
||||
/** Loads tools from all app-level connections into the manifest. */
|
||||
private async loadManifestTools(
|
||||
connections?: Map<string, MCPConnection> | null,
|
||||
): Promise<t.LCToolManifest> {
|
||||
const mcpTools: t.LCManifestTool[] = [];
|
||||
if (!connections || connections.size === 0) {
|
||||
return mcpTools;
|
||||
}
|
||||
for (const [serverName, connection] of connections.entries()) {
|
||||
try {
|
||||
if (!(await connection.isConnected())) {
|
||||
logger.warn(
|
||||
`[MCP][${serverName}] Connection not available for ${serverName} manifest tools.`,
|
||||
);
|
||||
if (typeof getServerTools !== 'function') {
|
||||
logger.warn(
|
||||
`[MCP][${serverName}] No \`getServerTools\` function provided, skipping tool loading.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const serverTools = await getServerTools(serverName);
|
||||
if (serverTools && serverTools.length > 0) {
|
||||
logger.info(`[MCP][${serverName}] Loaded tools from cache for manifest`);
|
||||
mcpTools.push(...serverTools);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -157,9 +174,6 @@ Please follow these instructions when using tools from the respective MCP server
|
||||
mcpTools.push(manifestTool);
|
||||
serverTools.push(manifestTool);
|
||||
}
|
||||
if (typeof serverToolsCallback === 'function') {
|
||||
await serverToolsCallback(serverName, serverTools);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[MCP][${serverName}] Error fetching tools for manifest:`, error);
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export class MCPServersRegistry {
|
||||
}
|
||||
|
||||
/** Converts server tools to LibreChat-compatible tool functions format */
|
||||
private async getToolFunctions(
|
||||
public async getToolFunctions(
|
||||
serverName: string,
|
||||
conn: MCPConnection,
|
||||
): Promise<t.LCAvailableTools> {
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import type { TokenMethods } from '@librechat/data-schemas';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
import type { FlowStateManager } from '~/flow/manager';
|
||||
import type { MCPOAuthTokens } from '~/mcp/oauth';
|
||||
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
||||
import { MCPConnectionFactory } from '~/mcp/MCPConnectionFactory';
|
||||
import { MCPServersRegistry } from '~/mcp/MCPServersRegistry';
|
||||
import { MCPConnection } from './connection';
|
||||
import type { RequestBody } from '~/types';
|
||||
import type * as t from './types';
|
||||
|
||||
/**
|
||||
@@ -44,8 +39,9 @@ export abstract class UserConnectionManager {
|
||||
|
||||
/** Gets or creates a connection for a specific user */
|
||||
public async getUserConnection({
|
||||
user,
|
||||
serverName,
|
||||
forceNew,
|
||||
user,
|
||||
flowManager,
|
||||
customUserVars,
|
||||
requestBody,
|
||||
@@ -54,25 +50,18 @@ export abstract class UserConnectionManager {
|
||||
oauthEnd,
|
||||
signal,
|
||||
returnOnOAuth = false,
|
||||
connectionTimeout,
|
||||
}: {
|
||||
user: TUser;
|
||||
serverName: string;
|
||||
flowManager: FlowStateManager<MCPOAuthTokens | null>;
|
||||
customUserVars?: Record<string, string>;
|
||||
requestBody?: RequestBody;
|
||||
tokenMethods?: TokenMethods;
|
||||
oauthStart?: (authURL: string) => Promise<void>;
|
||||
oauthEnd?: () => Promise<void>;
|
||||
signal?: AbortSignal;
|
||||
returnOnOAuth?: boolean;
|
||||
}): Promise<MCPConnection> {
|
||||
forceNew?: boolean;
|
||||
} & Omit<t.OAuthConnectionOptions, 'useOAuth'>): Promise<MCPConnection> {
|
||||
const userId = user.id;
|
||||
if (!userId) {
|
||||
throw new McpError(ErrorCode.InvalidRequest, `[MCP] User object missing id property`);
|
||||
}
|
||||
|
||||
const userServerMap = this.userConnections.get(userId);
|
||||
let connection = userServerMap?.get(serverName);
|
||||
let connection = forceNew ? undefined : userServerMap?.get(serverName);
|
||||
const now = Date.now();
|
||||
|
||||
// Check if user is idle
|
||||
@@ -131,6 +120,7 @@ export abstract class UserConnectionManager {
|
||||
oauthEnd: oauthEnd,
|
||||
returnOnOAuth: returnOnOAuth,
|
||||
requestBody: requestBody,
|
||||
connectionTimeout: connectionTimeout,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('getUserMCPAuthMap', () => {
|
||||
},
|
||||
];
|
||||
|
||||
const tools = testCases.map((testCase) =>
|
||||
const toolInstances = testCases.map((testCase) =>
|
||||
createMockTool(testCase.normalizedToolName, testCase.originalName),
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('getUserMCPAuthMap', () => {
|
||||
|
||||
await getUserMCPAuthMap({
|
||||
userId: 'user123',
|
||||
tools,
|
||||
toolInstances,
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('getUserMCPAuthMap', () => {
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should return empty object when no tools have mcpRawServerName', async () => {
|
||||
const tools = [
|
||||
const toolInstances = [
|
||||
createMockTool('regular_tool', undefined, false),
|
||||
createMockTool('another_tool', undefined, false),
|
||||
createMockTool('test_mcp_Server_no_raw_name', undefined),
|
||||
@@ -77,7 +77,7 @@ describe('getUserMCPAuthMap', () => {
|
||||
|
||||
const result = await getUserMCPAuthMap({
|
||||
userId: 'user123',
|
||||
tools,
|
||||
toolInstances,
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
@@ -104,14 +104,14 @@ describe('getUserMCPAuthMap', () => {
|
||||
});
|
||||
|
||||
it('should handle database errors gracefully', async () => {
|
||||
const tools = [createMockTool('test_mcp_Server1', 'Server1')];
|
||||
const toolInstances = [createMockTool('test_mcp_Server1', 'Server1')];
|
||||
const dbError = new Error('Database connection failed');
|
||||
|
||||
mockGetPluginAuthMap.mockRejectedValue(dbError);
|
||||
|
||||
const result = await getUserMCPAuthMap({
|
||||
userId: 'user123',
|
||||
tools,
|
||||
toolInstances,
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
@@ -119,18 +119,119 @@ describe('getUserMCPAuthMap', () => {
|
||||
});
|
||||
|
||||
it('should handle non-Error exceptions gracefully', async () => {
|
||||
const tools = [createMockTool('test_mcp_Server1', 'Server1')];
|
||||
const toolInstances = [createMockTool('test_mcp_Server1', 'Server1')];
|
||||
|
||||
mockGetPluginAuthMap.mockRejectedValue('String error');
|
||||
|
||||
const result = await getUserMCPAuthMap({
|
||||
userId: 'user123',
|
||||
tools,
|
||||
toolInstances,
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle mixed null/undefined values in tools array', async () => {
|
||||
const tools = [
|
||||
'test_mcp_Server1',
|
||||
null,
|
||||
'test_mcp_Server2',
|
||||
undefined,
|
||||
'regular_tool',
|
||||
'test_mcp_Server3',
|
||||
];
|
||||
|
||||
mockGetPluginAuthMap.mockResolvedValue({
|
||||
mcp_Server1: { API_KEY: 'key1' },
|
||||
mcp_Server2: { API_KEY: 'key2' },
|
||||
mcp_Server3: { API_KEY: 'key3' },
|
||||
});
|
||||
|
||||
const result = await getUserMCPAuthMap({
|
||||
userId: 'user123',
|
||||
tools: tools as (string | undefined)[],
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
expect(mockGetPluginAuthMap).toHaveBeenCalledWith({
|
||||
userId: 'user123',
|
||||
pluginKeys: ['mcp_Server1', 'mcp_Server2', 'mcp_Server3'],
|
||||
throwError: false,
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
mcp_Server1: { API_KEY: 'key1' },
|
||||
mcp_Server2: { API_KEY: 'key2' },
|
||||
mcp_Server3: { API_KEY: 'key3' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed null/undefined values in servers array', async () => {
|
||||
const servers = ['Server1', null, 'Server2', undefined, 'Server3'];
|
||||
|
||||
mockGetPluginAuthMap.mockResolvedValue({
|
||||
mcp_Server1: { API_KEY: 'key1' },
|
||||
mcp_Server2: { API_KEY: 'key2' },
|
||||
mcp_Server3: { API_KEY: 'key3' },
|
||||
});
|
||||
|
||||
const result = await getUserMCPAuthMap({
|
||||
userId: 'user123',
|
||||
servers: servers as (string | undefined)[],
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
expect(mockGetPluginAuthMap).toHaveBeenCalledWith({
|
||||
userId: 'user123',
|
||||
pluginKeys: ['mcp_Server1', 'mcp_Server2', 'mcp_Server3'],
|
||||
throwError: false,
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
mcp_Server1: { API_KEY: 'key1' },
|
||||
mcp_Server2: { API_KEY: 'key2' },
|
||||
mcp_Server3: { API_KEY: 'key3' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed null/undefined values in toolInstances array', async () => {
|
||||
const toolInstances = [
|
||||
createMockTool('test_mcp_Server1', 'Server1'),
|
||||
null,
|
||||
createMockTool('test_mcp_Server2', 'Server2'),
|
||||
undefined,
|
||||
createMockTool('regular_tool', undefined, false),
|
||||
createMockTool('test_mcp_Server3', 'Server3'),
|
||||
];
|
||||
|
||||
mockGetPluginAuthMap.mockResolvedValue({
|
||||
mcp_Server1: { API_KEY: 'key1' },
|
||||
mcp_Server2: { API_KEY: 'key2' },
|
||||
mcp_Server3: { API_KEY: 'key3' },
|
||||
});
|
||||
|
||||
const result = await getUserMCPAuthMap({
|
||||
userId: 'user123',
|
||||
toolInstances: toolInstances as (GenericTool | null)[],
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
expect(mockGetPluginAuthMap).toHaveBeenCalledWith({
|
||||
userId: 'user123',
|
||||
pluginKeys: ['mcp_Server1', 'mcp_Server2', 'mcp_Server3'],
|
||||
throwError: false,
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
mcp_Server1: { API_KEY: 'key1' },
|
||||
mcp_Server2: { API_KEY: 'key2' },
|
||||
mcp_Server3: { API_KEY: 'key3' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration', () => {
|
||||
@@ -138,7 +239,7 @@ describe('getUserMCPAuthMap', () => {
|
||||
const originalServerName = 'Connector: Company';
|
||||
const toolName = 'test_auth_mcp_Connector__Company';
|
||||
|
||||
const tools = [createMockTool(toolName, originalServerName)];
|
||||
const toolInstances = [createMockTool(toolName, originalServerName)];
|
||||
|
||||
const mockCustomUserVars = {
|
||||
'mcp_Connector: Company': {
|
||||
@@ -151,7 +252,7 @@ describe('getUserMCPAuthMap', () => {
|
||||
|
||||
const result = await getUserMCPAuthMap({
|
||||
userId: 'user123',
|
||||
tools,
|
||||
toolInstances,
|
||||
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
|
||||
});
|
||||
|
||||
|
||||
@@ -7,33 +7,56 @@ import { getPluginAuthMap } from '~/agents/auth';
|
||||
export async function getUserMCPAuthMap({
|
||||
userId,
|
||||
tools,
|
||||
servers,
|
||||
toolInstances,
|
||||
findPluginAuthsByKeys,
|
||||
}: {
|
||||
userId: string;
|
||||
tools: GenericTool[] | undefined;
|
||||
tools?: (string | undefined)[];
|
||||
servers?: (string | undefined)[];
|
||||
toolInstances?: (GenericTool | null)[];
|
||||
findPluginAuthsByKeys: PluginAuthMethods['findPluginAuthsByKeys'];
|
||||
}) {
|
||||
if (!tools || tools.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const uniqueMcpServers = new Set<string>();
|
||||
|
||||
for (const tool of tools) {
|
||||
const mcpTool = tool as GenericTool & { mcpRawServerName?: string };
|
||||
if (mcpTool.mcpRawServerName) {
|
||||
uniqueMcpServers.add(`${Constants.mcp_prefix}${mcpTool.mcpRawServerName}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (uniqueMcpServers.size === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mcpPluginKeysToFetch = Array.from(uniqueMcpServers);
|
||||
|
||||
let allMcpCustomUserVars: Record<string, Record<string, string>> = {};
|
||||
let mcpPluginKeysToFetch: string[] = [];
|
||||
try {
|
||||
const uniqueMcpServers = new Set<string>();
|
||||
|
||||
if (servers != null && servers.length) {
|
||||
for (const serverName of servers) {
|
||||
if (!serverName) {
|
||||
continue;
|
||||
}
|
||||
uniqueMcpServers.add(`${Constants.mcp_prefix}${serverName}`);
|
||||
}
|
||||
} else if (tools != null && tools.length) {
|
||||
for (const toolName of tools) {
|
||||
if (!toolName) {
|
||||
continue;
|
||||
}
|
||||
const delimiterIndex = toolName.indexOf(Constants.mcp_delimiter);
|
||||
if (delimiterIndex === -1) continue;
|
||||
const mcpServer = toolName.slice(delimiterIndex + Constants.mcp_delimiter.length);
|
||||
if (!mcpServer) continue;
|
||||
uniqueMcpServers.add(`${Constants.mcp_prefix}${mcpServer}`);
|
||||
}
|
||||
} else if (toolInstances != null && toolInstances.length) {
|
||||
for (const tool of toolInstances) {
|
||||
if (!tool) {
|
||||
continue;
|
||||
}
|
||||
const mcpTool = tool as GenericTool & { mcpRawServerName?: string };
|
||||
if (mcpTool.mcpRawServerName) {
|
||||
uniqueMcpServers.add(`${Constants.mcp_prefix}${mcpTool.mcpRawServerName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uniqueMcpServers.size === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
mcpPluginKeysToFetch = Array.from(uniqueMcpServers);
|
||||
allMcpCustomUserVars = await getPluginAuthMap({
|
||||
userId,
|
||||
pluginKeys: mcpPluginKeysToFetch,
|
||||
|
||||
@@ -446,7 +446,7 @@ export class MCPConnection extends EventEmitter {
|
||||
const serverUrl = this.url;
|
||||
logger.debug(`${this.getLogPrefix()} Server URL for OAuth: ${serverUrl}`);
|
||||
|
||||
const oauthTimeout = this.options.initTimeout ?? 60000;
|
||||
const oauthTimeout = this.options.initTimeout ?? 60000 * 2;
|
||||
/** Promise that will resolve when OAuth is handled */
|
||||
const oauthHandledPromise = new Promise<void>((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -134,4 +134,5 @@ export interface OAuthConnectionOptions {
|
||||
oauthStart?: (authURL: string) => Promise<void>;
|
||||
oauthEnd?: () => Promise<void>;
|
||||
returnOnOAuth?: boolean;
|
||||
connectionTimeout?: number;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { AccessRoleIds, ResourceType, PrincipalType, Constants } from 'librechat-data-provider';
|
||||
import type { AccessRoleMethods, IPromptGroupDocument } from '@librechat/data-schemas';
|
||||
import type { Model } from 'mongoose';
|
||||
import type { Model, Mongoose, mongo } from 'mongoose';
|
||||
|
||||
const { GLOBAL_PROJECT_NAME } = Constants;
|
||||
|
||||
@@ -17,7 +17,8 @@ export interface PromptMigrationCheckDbMethods {
|
||||
}
|
||||
|
||||
export interface PromptMigrationCheckParams {
|
||||
db: PromptMigrationCheckDbMethods;
|
||||
mongoose: Mongoose;
|
||||
methods: PromptMigrationCheckDbMethods;
|
||||
PromptGroupModel: Model<IPromptGroupDocument>;
|
||||
}
|
||||
|
||||
@@ -44,16 +45,45 @@ export interface PromptMigrationCheckResult {
|
||||
* This performs a dry-run check similar to the migration script
|
||||
*/
|
||||
export async function checkPromptPermissionsMigration({
|
||||
db,
|
||||
methods,
|
||||
mongoose,
|
||||
PromptGroupModel,
|
||||
}: PromptMigrationCheckParams): Promise<PromptMigrationCheckResult> {
|
||||
logger.debug('Checking if prompt permissions migration is needed');
|
||||
|
||||
try {
|
||||
/** Ensurse `aclentries` collection exists for DocumentDB compatibility */
|
||||
async function ensureCollectionExists(db: mongo.Db, collectionName: string) {
|
||||
try {
|
||||
const collections = await db.listCollections({ name: collectionName }).toArray();
|
||||
if (collections.length === 0) {
|
||||
await db.createCollection(collectionName);
|
||||
logger.info(`Created collection: ${collectionName}`);
|
||||
} else {
|
||||
logger.debug(`Collection already exists: ${collectionName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`'Failed to check/create "${collectionName}" collection:`, error);
|
||||
// If listCollections fails, try alternative approach
|
||||
try {
|
||||
// Try to access the collection directly - this will create it in MongoDB if it doesn't exist
|
||||
await db.collection(collectionName).findOne({}, { projection: { _id: 1 } });
|
||||
} catch (createError) {
|
||||
logger.error(`Could not ensure collection ${collectionName} exists:`, createError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Native MongoDB database instance */
|
||||
const db = mongoose.connection.db;
|
||||
if (db) {
|
||||
await ensureCollectionExists(db, 'aclentries');
|
||||
}
|
||||
|
||||
// Verify required roles exist
|
||||
const ownerRole = await db.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER);
|
||||
const viewerRole = await db.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER);
|
||||
const editorRole = await db.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR);
|
||||
const ownerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_OWNER);
|
||||
const viewerRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_VIEWER);
|
||||
const editorRole = await methods.findRoleByIdentifier(AccessRoleIds.PROMPTGROUP_EDITOR);
|
||||
|
||||
if (!ownerRole || !viewerRole || !editorRole) {
|
||||
logger.warn(
|
||||
@@ -66,8 +96,8 @@ export async function checkPromptPermissionsMigration({
|
||||
};
|
||||
}
|
||||
|
||||
// Get global project prompt group IDs
|
||||
const globalProject = await db.getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']);
|
||||
/** Global project prompt group IDs */
|
||||
const globalProject = await methods.getProjectByName(GLOBAL_PROJECT_NAME, ['promptGroupIds']);
|
||||
const globalPromptGroupIds = new Set(
|
||||
(globalProject?.promptGroupIds || []).map((id) => id.toString()),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AuthType, Constants, EToolResources } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TPlugin, FunctionTool } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TPlugin } from 'librechat-data-provider';
|
||||
import { LCAvailableTools, LCFunctionTool } from '~/mcp/types';
|
||||
|
||||
/**
|
||||
* Filters out duplicate plugins from the list of plugins.
|
||||
@@ -46,6 +47,62 @@ export const checkPluginAuth = (plugin?: TPlugin): boolean => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts MCP function format tool to plugin format
|
||||
* @param params
|
||||
* @param params.toolKey
|
||||
* @param params.toolData
|
||||
* @param params.customConfig
|
||||
* @returns
|
||||
*/
|
||||
export function convertMCPToolToPlugin({
|
||||
toolKey,
|
||||
toolData,
|
||||
customConfig,
|
||||
}: {
|
||||
toolKey: string;
|
||||
toolData: LCFunctionTool;
|
||||
customConfig?: Partial<TCustomConfig> | null;
|
||||
}): TPlugin | undefined {
|
||||
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const functionData = toolData.function;
|
||||
const parts = toolKey.split(Constants.mcp_delimiter);
|
||||
const serverName = parts[parts.length - 1];
|
||||
|
||||
const serverConfig = customConfig?.mcpServers?.[serverName];
|
||||
|
||||
const plugin: TPlugin = {
|
||||
/** Tool name without server suffix */
|
||||
name: parts[0],
|
||||
pluginKey: toolKey,
|
||||
description: functionData.description || '',
|
||||
authenticated: true,
|
||||
icon: serverConfig?.iconPath,
|
||||
};
|
||||
|
||||
if (!serverConfig?.customUserVars) {
|
||||
/** `authConfig` for MCP tools */
|
||||
plugin.authConfig = [];
|
||||
return plugin;
|
||||
}
|
||||
|
||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
||||
if (customVarKeys.length === 0) {
|
||||
plugin.authConfig = [];
|
||||
} else {
|
||||
plugin.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
|
||||
authField: key,
|
||||
label: value.title || key,
|
||||
description: value.description || '',
|
||||
}));
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts MCP function format tools to plugin format
|
||||
* @param functionTools - Object with function format tools
|
||||
@@ -56,7 +113,7 @@ export function convertMCPToolsToPlugins({
|
||||
functionTools,
|
||||
customConfig,
|
||||
}: {
|
||||
functionTools?: Record<string, FunctionTool>;
|
||||
functionTools?: LCAvailableTools;
|
||||
customConfig?: Partial<TCustomConfig> | null;
|
||||
}): TPlugin[] | undefined {
|
||||
if (!functionTools || typeof functionTools !== 'object') {
|
||||
@@ -65,44 +122,10 @@ export function convertMCPToolsToPlugins({
|
||||
|
||||
const plugins: TPlugin[] = [];
|
||||
for (const [toolKey, toolData] of Object.entries(functionTools)) {
|
||||
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const functionData = toolData.function;
|
||||
const parts = toolKey.split(Constants.mcp_delimiter);
|
||||
const serverName = parts[parts.length - 1];
|
||||
|
||||
const serverConfig = customConfig?.mcpServers?.[serverName];
|
||||
|
||||
const plugin: TPlugin = {
|
||||
/** Tool name without server suffix */
|
||||
name: parts[0],
|
||||
pluginKey: toolKey,
|
||||
description: functionData.description || '',
|
||||
authenticated: true,
|
||||
icon: serverConfig?.iconPath,
|
||||
};
|
||||
|
||||
if (!serverConfig?.customUserVars) {
|
||||
/** `authConfig` for MCP tools */
|
||||
plugin.authConfig = [];
|
||||
const plugin = convertMCPToolToPlugin({ toolKey, toolData, customConfig });
|
||||
if (plugin) {
|
||||
plugins.push(plugin);
|
||||
continue;
|
||||
}
|
||||
|
||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
||||
if (customVarKeys.length === 0) {
|
||||
plugin.authConfig = [];
|
||||
} else {
|
||||
plugin.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
|
||||
authField: key,
|
||||
label: value.title || key,
|
||||
description: value.description || '',
|
||||
}));
|
||||
}
|
||||
|
||||
plugins.push(plugin);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
|
||||
@@ -12,6 +12,7 @@ export type OpenAIParameters = z.infer<typeof openAISchema>;
|
||||
*/
|
||||
export interface OpenAIConfigOptions {
|
||||
modelOptions?: Partial<OpenAIParameters>;
|
||||
directEndpoint?: boolean;
|
||||
reverseProxyUrl?: string;
|
||||
defaultQuery?: Record<string, string | undefined>;
|
||||
headers?: Record<string, string>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/client",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.7",
|
||||
"description": "React components for LibreChat",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
@@ -64,7 +64,9 @@
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"tailwind-merge": "^1.9.1"
|
||||
"tailwind-merge": "^1.9.1",
|
||||
"@dicebear/core": "^9.2.2",
|
||||
"@dicebear/collection": "^9.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
|
||||
102
packages/client/src/components/Avatar.tsx
Normal file
102
packages/client/src/components/Avatar.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import type { TUser } from 'librechat-data-provider';
|
||||
import { Skeleton } from './Skeleton';
|
||||
import { useAvatar } from '~/hooks';
|
||||
import { UserIcon } from '~/svgs';
|
||||
|
||||
export interface AvatarProps {
|
||||
user?: TUser;
|
||||
size?: number;
|
||||
className?: string;
|
||||
alt?: string;
|
||||
showDefaultWhenEmpty?: boolean;
|
||||
}
|
||||
|
||||
const Avatar: React.FC<AvatarProps> = ({
|
||||
user,
|
||||
size = 32,
|
||||
className = '',
|
||||
alt,
|
||||
showDefaultWhenEmpty = true,
|
||||
}) => {
|
||||
const avatarSrc = useAvatar(user);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [imageError, setImageError] = useState(false);
|
||||
|
||||
const avatarSeed = useMemo(
|
||||
() => user?.avatar || user?.username || user?.email || '',
|
||||
[user?.avatar, user?.username, user?.email],
|
||||
);
|
||||
|
||||
const altText = useMemo(
|
||||
() => alt || `${user?.name || user?.username || user?.email || ''}'s avatar`,
|
||||
[alt, user?.name, user?.username, user?.email],
|
||||
);
|
||||
|
||||
const imageSrc = useMemo(() => {
|
||||
if (!avatarSeed || imageError) return '';
|
||||
return (user?.avatar ?? '') || avatarSrc || '';
|
||||
}, [user?.avatar, avatarSrc, avatarSeed, imageError]);
|
||||
|
||||
const handleImageLoad = useCallback(() => {
|
||||
setImageLoaded(true);
|
||||
}, []);
|
||||
|
||||
const handleImageError = useCallback(() => {
|
||||
setImageError(true);
|
||||
setImageLoaded(false);
|
||||
}, []);
|
||||
|
||||
const DefaultAvatar = useCallback(
|
||||
() => (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'rgb(121, 137, 255)',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
|
||||
}}
|
||||
className={`relative flex items-center justify-center rounded-full p-1 text-text-primary ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<UserIcon />
|
||||
</div>
|
||||
),
|
||||
[size, className],
|
||||
);
|
||||
|
||||
if (avatarSeed.length === 0 && showDefaultWhenEmpty) {
|
||||
return <DefaultAvatar />;
|
||||
}
|
||||
|
||||
if (avatarSeed.length > 0 && !imageError) {
|
||||
return (
|
||||
<div className="relative" style={{ width: `${size}px`, height: `${size}px` }}>
|
||||
{!imageLoaded && (
|
||||
<Skeleton className="rounded-full" style={{ width: `${size}px`, height: `${size}px` }} />
|
||||
)}
|
||||
|
||||
<img
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
display: imageLoaded ? 'block' : 'none',
|
||||
}}
|
||||
className={`rounded-full ${className}`}
|
||||
src={imageSrc}
|
||||
alt={altText}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (imageError && showDefaultWhenEmpty) {
|
||||
return <DefaultAvatar />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
@@ -33,6 +33,7 @@ export * from './Resizable';
|
||||
export * from './Select';
|
||||
export { default as Radio } from './Radio';
|
||||
export { default as Badge } from './Badge';
|
||||
export { default as Avatar } from './Avatar';
|
||||
export { default as Combobox } from './Combobox';
|
||||
export { default as Dropdown } from './Dropdown';
|
||||
export { default as SplitText } from './SplitText';
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export type { TranslationKeys } from './useLocalize';
|
||||
|
||||
export { default as useToast } from './useToast';
|
||||
export { default as useAvatar } from './useAvatar';
|
||||
export { default as useCombobox } from './useCombobox';
|
||||
export { default as useLocalize } from './useLocalize';
|
||||
export { default as useMediaQuery } from './useMediaQuery';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user