Compare commits

..

46 Commits

Author SHA1 Message Date
Danny Avila
62b4d29967 WIP: fix tests 2025-08-20 02:16:02 -04:00
Danny Avila
f7ea232760 refactor: decouple caching and DB operations from AppService, make part of consolidated getAppConfig 2025-08-20 02:03:38 -04:00
Danny Avila
28cd234a6c refactor: add FunctionTool interface and availableTools to AppConfig 2025-08-20 01:13:42 -04:00
Danny Avila
550bf56077 refactor: migrate checkEmailConfig to TypeScript and update imports 2025-08-20 01:07:31 -04:00
Danny Avila
ab4596206f refactor: move interface config logic to TS API 2025-08-20 01:00:25 -04:00
Danny Avila
84c78f5812 refactor: consolidate tool loading logic into a new tools module for startup logic 2025-08-20 00:57:42 -04:00
Danny Avila
1bdfb143eb refactor: separate manifest logic into its own module 2025-08-20 00:56:39 -04:00
Danny Avila
20a486a190 refactor: restructure toolkits to TS API 2025-08-20 00:56:12 -04:00
Danny Avila
6873b396ea chore: add missing FILE_CITATIONS permission to IRole interface 2025-08-20 00:54:35 -04:00
Danny Avila
1c7b3b53da refactor: move /services/Files/images/parse to TS API 2025-08-20 00:54:02 -04:00
Danny Avila
c160e7c7d5 refactor: add type annotation for loadedEndpoints in loadEndpoints function 2025-08-19 12:22:14 -04:00
Danny Avila
c0d6404385 refactor: replace getMCPAuthMap with getUserMCPAuthMap and remove unused getCustomConfig file 2025-08-19 12:22:14 -04:00
Danny Avila
71a14517cd refactor: streamline endpoint configuration and enhance appConfig usage across services 2025-08-19 12:22:11 -04:00
Danny Avila
647b1bbac6 refactor: update getAppConfig call to include user role parameter 2025-08-19 12:22:11 -04:00
Danny Avila
5eef6ea9e8 refactor: implement custom endpoints configuration and streamline endpoint loading logic 2025-08-19 12:22:11 -04:00
Danny Avila
240e3bd59e refactor: update appConfig access to use endpoints structure across various services 2025-08-19 12:22:10 -04:00
Danny Avila
89fb9c7e1c refactor: consolidate endpoint loading logic into loadEndpoints function 2025-08-19 12:22:10 -04:00
Danny Avila
8525c8df36 refactor: remove getCustomConfig usage and use app config in file citations 2025-08-19 12:22:10 -04:00
Danny Avila
46f9c90223 refactor: remove customConfig dependency for appConfig and streamline loadConfigModels logic 2025-08-19 12:22:09 -04:00
Danny Avila
1d2be247cf refactor: get balance config primarily from appConfig 2025-08-19 12:22:09 -04:00
Danny Avila
d853c10920 chore: remove deprecated unused RunManager 2025-08-19 12:22:09 -04:00
Danny Avila
5e70d518aa refactor: update imports to use normalizeEndpointName from @librechat/api and remove redundant definitions 2025-08-19 12:22:08 -04:00
Danny Avila
8df0ecd438 refactor: enhance AppConfig to include fileStrategies and update related file strategy logic 2025-08-19 12:22:08 -04:00
Danny Avila
f25e4253d0 fix: update appConfig usage to access allowedDomains from actions instead of registration 2025-08-19 12:22:07 -04:00
Danny Avila
493f60fa54 refactor: update getAppConfig call in getCustomConfigSpeech to include user role 2025-08-19 12:22:07 -04:00
Danny Avila
1168a7d82e ci: update related tests 2025-08-19 12:22:07 -04:00
Danny Avila
8f89fdc802 refactor: update getAppConfig calls in Conversation and Message models to include user role for temporary chat expiration 2025-08-19 12:22:06 -04:00
Danny Avila
57513f7ac9 refactor: replace getCustomConfig with getAppConfig in Conversation and Message models, update tempChatRetention functions to use AppConfig type 2025-08-19 12:22:06 -04:00
Danny Avila
c82c47ab6a refactor: replace getCustomConfig with getAppConfig in STTService, TTSService, and related files 2025-08-19 12:22:06 -04:00
Danny Avila
b0256510b5 refactor: remove getCustomConfig dependency and use getAppConfig in PluginController, multer, and MCP services 2025-08-19 12:22:05 -04:00
Danny Avila
e0980e796a refactor: update AppConfig interface to correct registration and turnstile configurations 2025-08-19 12:22:05 -04:00
Danny Avila
370e2d7ade chore: remove app parameter from AppService invocation 2025-08-19 12:22:05 -04:00
Danny Avila
0e0758edaf refactor: use appConfig registration property 2025-08-19 12:22:04 -04:00
Danny Avila
50bd6d3a02 refactor: update domain validation to use appConfig for allowed domains 2025-08-19 12:22:04 -04:00
Danny Avila
677481dde6 refactor: remove getCustomConfig dependency in config route 2025-08-19 12:22:01 -04:00
Danny Avila
2229672929 chore: correct parameter documentation for imageOutputType in ToolService.js 2025-08-19 12:21:31 -04:00
Danny Avila
68c6afd009 fix: streamline OpenAI image tools configuration by removing direct appConfig dependency and using function parameters 2025-08-19 12:21:30 -04:00
Danny Avila
0b5816d1be chore: rename Config/getAppConfig -> Config/app 2025-08-19 12:21:30 -04:00
Danny Avila
e7af3bdaed refactor: Update convertMCPToolsToPlugins to use mcpManager for server configuration and adjust related tests 2025-08-19 12:21:30 -04:00
Danny Avila
b2b2aee945 ci: Mock getAppConfig in various tests to provide default configurations 2025-08-19 12:21:29 -04:00
Danny Avila
2501d11fa0 refactor: Rename initializeAppConfig to setAppConfig and update related tests 2025-08-19 12:21:29 -04:00
Danny Avila
eeab69ff7f refactor: Update multer storage destination to use promise-based getAppConfig and improve error handling in tests 2025-08-19 12:21:29 -04:00
Danny Avila
5bb731764c ci: Integrate getAppConfig into remaining tests 2025-08-19 12:21:28 -04:00
Danny Avila
5b43bf6c95 ci: Update AppService tests to initialize app config instead of app.locals 2025-08-19 12:21:28 -04:00
Danny Avila
3ecb96149a 🏷️ refactor: Update tests to use getAppConfig for endpoint configurations 2025-08-19 12:21:28 -04:00
Danny Avila
b992fed16c WIP: app.locals refactoring
WIP: appConfig

fix: update memory configuration retrieval to use getAppConfig based on user role

fix: update comment for AppConfig interface to clarify purpose
2025-08-19 12:21:27 -04:00
277 changed files with 2231 additions and 5916 deletions

View File

@@ -1,10 +1,5 @@
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:
@@ -12,7 +7,6 @@ on:
- "api/**"
- "packages/data-provider/src/**"
- "packages/client/**"
- "packages/data-schemas/src/**"
jobs:
detect-unused-i18n-keys:
@@ -30,7 +24,7 @@ jobs:
# Define paths
I18N_FILE="client/src/locales/en/translation.json"
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client" "packages/data-schemas/src")
SOURCE_DIRS=("client/src" "api" "packages/data-provider/src" "packages/client")
# Check if translation file exists
if [[ ! -f "$I18N_FILE" ]]; then
@@ -58,31 +52,6 @@ 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

View File

@@ -1,4 +1,4 @@
# v0.8.0-rc3
# v0.8.0-rc2
# Base node image
FROM node:20-alpine AS node

View File

@@ -1,5 +1,5 @@
# Dockerfile.multi
# v0.8.0-rc3
# v0.8.0-rc2
# Base for all builds
FROM node:20-alpine AS base-min

View File

@@ -65,10 +65,8 @@
- 🔦 **Agents & Tools Integration**:
- **[LibreChat Agents](https://www.librechat.ai/docs/features/agents)**:
- 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
- No-Code Custom Assistants: Build specialized, AI-driven helpers without coding
- 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
@@ -89,18 +87,15 @@
- 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 (PT), Português (BR), Русский, 日本語, Svenska, 한국어, Tiếng Việt
- Türkçe, Nederlands, עברית, Català, Čeština, Dansk, Eesti, فارسی
- Suomi, Magyar, Հայերեն, Bahasa Indonesia, ქართული, Latviešu, ไทย, ئۇيغۇرچە
- 🌎 **Multilingual UI**:
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
- 🧠 **Reasoning UI**:
- Dynamic Reasoning UI for Chain-of-Thought/Reasoning AI models like DeepSeek-R1

View File

@@ -1,7 +1,5 @@
const crypto = require('crypto');
const fetch = require('node-fetch');
const { logger } = require('@librechat/data-schemas');
const { getBalanceConfig } = require('@librechat/api');
const {
supportsBalanceCheck,
isAgentsEndpoint,
@@ -13,10 +11,12 @@ const {
Constants,
} = require('librechat-data-provider');
const { getMessages, saveMessage, updateMessage, saveConvo, getConvo } = require('~/models');
const { getAppConfig } = require('~/server/services/Config');
const { checkBalance } = require('~/models/balanceMethods');
const { truncateToolCallOutputs } = require('./prompts');
const { getFiles } = require('~/models/File');
const TextStream = require('./TextStream');
const { logger } = require('~/config');
class BaseClient {
constructor(apiKey, options = {}) {
@@ -190,8 +190,7 @@ class BaseClient {
this.user = user;
const saveOptions = this.getSaveOptions();
this.abortController = opts.abortController ?? new AbortController();
const requestConvoId = overrideConvoId ?? opts.conversationId;
const conversationId = requestConvoId ?? crypto.randomUUID();
const conversationId = overrideConvoId ?? opts.conversationId ?? crypto.randomUUID();
const parentMessageId = opts.parentMessageId ?? Constants.NO_PARENT;
const userMessageId =
overrideUserMessageId ?? opts.overrideParentMessageId ?? crypto.randomUUID();
@@ -216,12 +215,11 @@ class BaseClient {
...opts,
user,
head,
saveOptions,
userMessageId,
requestConvoId,
conversationId,
parentMessageId,
userMessageId,
responseMessageId,
saveOptions,
};
}
@@ -240,12 +238,11 @@ class BaseClient {
const {
user,
head,
saveOptions,
userMessageId,
requestConvoId,
conversationId,
parentMessageId,
userMessageId,
responseMessageId,
saveOptions,
} = await this.setMessageOptions(opts);
const userMessage = opts.isEdited
@@ -267,8 +264,7 @@ class BaseClient {
}
if (typeof opts?.onStart === 'function') {
const isNewConvo = !requestConvoId && parentMessageId === Constants.NO_PARENT;
opts.onStart(userMessage, responseMessageId, isNewConvo);
opts.onStart(userMessage, responseMessageId);
}
return {
@@ -574,7 +570,7 @@ class BaseClient {
}
async sendMessage(message, opts = {}) {
const appConfig = this.options.req?.config;
const appConfig = await getAppConfig({ role: this.options.req?.user?.role });
/** @type {Promise<TMessage>} */
let userMessagePromise;
const { user, head, isEdited, conversationId, responseMessageId, saveOptions, userMessage } =
@@ -661,9 +657,9 @@ class BaseClient {
}
}
const balanceConfig = getBalanceConfig(appConfig);
const balance = appConfig?.balance;
if (
balanceConfig?.enabled &&
balance?.enabled &&
supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint]
) {
await checkBalance({
@@ -760,9 +756,9 @@ class BaseClient {
completionTokens = responseMessage.tokenCount;
await this.recordTokenUsage({
usage,
balance,
promptTokens,
completionTokens,
balance: balanceConfig,
model: responseMessage.model,
});
}

View File

@@ -34,6 +34,7 @@ const {
const { extractBaseURL, getModelMaxTokens, getModelMaxOutputTokens } = require('~/utils');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
const { addSpaceIfNeeded, sleep } = require('~/server/utils');
const { getAppConfig } = require('~/server/services/Config');
const { spendTokens } = require('~/models/spendTokens');
const { handleOpenAIErrors } = require('./tools/util');
const { summaryBuffer } = require('./memory');
@@ -688,7 +689,7 @@ class OpenAIClient extends BaseClient {
* In case of failure, it will return the default title, "New Chat".
*/
async titleConvo({ text, conversationId, responseText = '' }) {
const appConfig = this.options.req?.config;
const appConfig = await getAppConfig({ role: this.options.req?.user?.role });
this.conversationId = conversationId;
if (this.options.attachments) {
@@ -1106,7 +1107,7 @@ ${convo}
}
async chatCompletion({ payload, onProgress, abortController = null }) {
const appConfig = this.options.req?.config;
const appConfig = await getAppConfig({ role: this.options.req?.user?.role });
let error = null;
let intermediateReply = [];
const errorCallback = (err) => (error = err);

View File

@@ -1,6 +1,7 @@
const axios = require('axios');
const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { isEnabled, generateShortLivedToken } = require('@librechat/api');
const { generateShortLivedToken } = require('~/server/services/AuthService');
const footer = `Use the context as your learned knowledge to better answer the user.

View File

@@ -587,8 +587,6 @@ describe('BaseClient', () => {
expect(onStart).toHaveBeenCalledWith(
expect.objectContaining({ text: 'Hello, world!' }),
expect.any(String),
/** `isNewConvo` */
true,
);
});

View File

@@ -49,7 +49,7 @@
"pluginKey": "image_gen_oai",
"toolkit": true,
"description": "Image Generation and Editing using OpenAI's latest state-of-the-art models",
"icon": "assets/image_gen_oai.png",
"icon": "/assets/image_gen_oai.png",
"authConfig": [
{
"authField": "IMAGE_GEN_OAI_API_KEY",
@@ -75,7 +75,7 @@
"name": "Browser",
"pluginKey": "web-browser",
"description": "Scrape and summarize webpage data",
"icon": "assets/web-browser.svg",
"icon": "/assets/web-browser.svg",
"authConfig": [
{
"authField": "OPENAI_API_KEY",
@@ -170,7 +170,7 @@
"name": "OpenWeather",
"pluginKey": "open_weather",
"description": "Get weather forecasts and historical data from the OpenWeather API",
"icon": "assets/openweather.png",
"icon": "/assets/openweather.png",
"authConfig": [
{
"authField": "OPENWEATHER_API_KEY",

View File

@@ -11,14 +11,14 @@ const paths = require('~/config/paths');
const { logger } = require('~/config');
const displayMessage =
"Stable Diffusion displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.";
'Stable Diffusion displayed an image. All generated images are already plainly visible, so don\'t repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.';
class StableDiffusionAPI extends Tool {
constructor(fields) {
super();
/** @type {string} User ID */
this.userId = fields.userId;
/** @type {ServerRequest | undefined} Express Request object, only provided by ToolService */
/** @type {Express.Request | undefined} Express Request object, only provided by ToolService */
this.req = fields.req;
/** @type {boolean} Used to initialize the Tool without necessary variables. */
this.override = fields.override ?? false;
@@ -44,7 +44,7 @@ class StableDiffusionAPI extends Tool {
// "negative_prompt":"semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, anime, out of frame, low quality, ugly, mutation, deformed"
// - Generate images only once per human query unless explicitly requested by the user`;
this.description =
"You can generate images using text with 'stable-diffusion'. This tool is exclusively for visual content.";
'You can generate images using text with \'stable-diffusion\'. This tool is exclusively for visual content.';
this.schema = z.object({
prompt: z
.string()

View File

@@ -2,9 +2,9 @@ const { z } = require('zod');
const axios = require('axios');
const { tool } = require('@langchain/core/tools');
const { logger } = require('@librechat/data-schemas');
const { generateShortLivedToken } = require('@librechat/api');
const { Tools, EToolResources } = require('librechat-data-provider');
const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions');
const { generateShortLivedToken } = require('~/server/services/AuthService');
const { getFiles } = require('~/models/File');
/**

View File

@@ -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, Constants, EToolResources, replaceSpecialVars } = require('librechat-data-provider');
const { Tools, 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,8 +123,6 @@ const getAuthFields = (toolKey) => {
*
* @param {object} params
* @param {string} params.user
* @param {Record<string, Record<string, string>>} [object.userMCPAuthMap]
* @param {AbortSignal} [object.signal]
* @param {Pick<Agent, 'id' | 'provider' | 'model'>} [params.agent]
* @param {string} [params.model]
* @param {EModelEndpoint} [params.endpoint]
@@ -142,9 +140,7 @@ const loadTools = async ({
user,
agent,
model,
signal,
endpoint,
userMCPAuthMap,
tools = [],
options = {},
functions = true,
@@ -243,7 +239,6 @@ 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) {
@@ -311,35 +306,14 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
};
continue;
} else if (tool && cachedTools && mcpToolPattern.test(tool)) {
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) =>
requestedTools[tool] = async () =>
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;
}
@@ -379,34 +353,6 @@ 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 };
};

View File

@@ -31,6 +31,7 @@ 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),

View File

@@ -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_all, mcp_delimiter } =
const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } =
require('librechat-data-provider').Constants;
const {
removeAgentFromAllProjects,
@@ -78,7 +78,6 @@ 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)) {
@@ -86,17 +85,9 @@ 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;
@@ -681,7 +672,7 @@ const getListAgents = async (searchParameter) => {
* This function also updates the corresponding projects to include or exclude the agent ID.
*
* @param {Object} params - Parameters for updating the agent's projects.
* @param {IUser} params.user - Parameters for updating the agent's projects.
* @param {MongoUser} params.user - Parameters for updating the agent's projects.
* @param {string} params.agentId - The ID of the agent to update.
* @param {string[]} [params.projectIds] - Array of project IDs to add to the agent.
* @param {string[]} [params.removeProjectIds] - Array of project IDs to remove from the agent.

View File

@@ -1,5 +1,6 @@
const { logger } = require('@librechat/data-schemas');
const { createTempChatExpirationDate } = require('@librechat/api');
const { getAppConfig } = require('~/server/services/Config/app');
const { getMessages, deleteMessages } = require('./Message');
const { Conversation } = require('~/db/models');
@@ -101,7 +102,9 @@ module.exports = {
if (req?.body?.isTemporary) {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({
role: req.user.role,
});
update.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig);
} catch (err) {
logger.error('Error creating temporary chat expiration date:', err);

View File

@@ -15,6 +15,7 @@ const {
} = require('./Conversation');
jest.mock('~/server/services/Config/app');
jest.mock('./Message');
const { getAppConfig } = require('~/server/services/Config/app');
const { getMessages, deleteMessages } = require('./Message');
const { Conversation } = require('~/db/models');
@@ -49,11 +50,6 @@ describe('Conversation Operations', () => {
mockReq = {
user: { id: 'user123' },
body: {},
config: {
interfaceConfig: {
temporaryChatRetention: 24, // Default 24 hours
},
},
};
mockConversationData = {
@@ -123,7 +119,11 @@ describe('Conversation Operations', () => {
describe('isTemporary conversation handling', () => {
it('should save a conversation with expiredAt when isTemporary is true', async () => {
// Mock app config with 24 hour retention
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 24,
},
});
mockReq.body = { isTemporary: true };
@@ -168,7 +168,11 @@ describe('Conversation Operations', () => {
it('should use custom retention period from config', async () => {
// Mock app config with 48 hour retention
mockReq.config.interfaceConfig.temporaryChatRetention = 48;
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 48,
},
});
mockReq.body = { isTemporary: true };
@@ -191,7 +195,11 @@ describe('Conversation Operations', () => {
it('should handle minimum retention period (1 hour)', async () => {
// Mock app config with less than minimum retention
mockReq.config.interfaceConfig.temporaryChatRetention = 0.5; // Half hour - should be clamped to 1 hour
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour
},
});
mockReq.body = { isTemporary: true };
@@ -214,7 +222,11 @@ describe('Conversation Operations', () => {
it('should handle maximum retention period (8760 hours)', async () => {
// Mock app config with more than maximum retention
mockReq.config.interfaceConfig.temporaryChatRetention = 10000; // Should be clamped to 8760 hours
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 10000, // Should be clamped to 8760 hours
},
});
mockReq.body = { isTemporary: true };
@@ -235,36 +247,22 @@ describe('Conversation Operations', () => {
);
});
it('should handle missing config gracefully', async () => {
// Simulate missing config - should use default retention period
delete mockReq.config;
it('should handle getAppConfig errors gracefully', async () => {
// Mock getAppConfig to throw an error
getAppConfig.mockRejectedValue(new Error('Config service unavailable'));
mockReq.body = { isTemporary: true };
const beforeSave = new Date();
const result = await saveConvo(mockReq, mockConversationData);
const afterSave = new Date();
// Should still save the conversation with default retention period (30 days)
// Should still save the conversation but with expiredAt as null
expect(result.conversationId).toBe(mockConversationData.conversationId);
expect(result.expiredAt).toBeDefined();
expect(result.expiredAt).toBeInstanceOf(Date);
// Verify expiredAt is approximately 30 days in the future (720 hours)
const expectedExpirationTime = new Date(beforeSave.getTime() + 720 * 60 * 60 * 1000);
const actualExpirationTime = new Date(result.expiredAt);
expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual(
expectedExpirationTime.getTime() - 1000,
);
expect(actualExpirationTime.getTime()).toBeLessThanOrEqual(
new Date(afterSave.getTime() + 720 * 60 * 60 * 1000 + 1000).getTime(),
);
expect(result.expiredAt).toBeNull();
});
it('should use default retention when config is not provided', async () => {
// Mock getAppConfig to return empty config
mockReq.config = {}; // Empty config
getAppConfig.mockResolvedValue({});
mockReq.body = { isTemporary: true };
@@ -287,7 +285,11 @@ describe('Conversation Operations', () => {
it('should update expiredAt when saving existing temporary conversation', async () => {
// First save a temporary conversation
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 24,
},
});
mockReq.body = { isTemporary: true };
const firstSave = await saveConvo(mockReq, mockConversationData);

View File

@@ -1,6 +1,7 @@
const { z } = require('zod');
const { logger } = require('@librechat/data-schemas');
const { createTempChatExpirationDate } = require('@librechat/api');
const { getAppConfig } = require('~/server/services/Config/app');
const { Message } = require('~/db/models');
const idSchema = z.string().uuid();
@@ -10,7 +11,7 @@ const idSchema = z.string().uuid();
*
* @async
* @function saveMessage
* @param {ServerRequest} req - The request object containing user information.
* @param {Express.Request} req - The request object containing user information.
* @param {Object} params - The message data object.
* @param {string} params.endpoint - The endpoint where the message originated.
* @param {string} params.iconURL - The URL of the sender's icon.
@@ -56,7 +57,9 @@ async function saveMessage(req, params, metadata) {
if (req?.body?.isTemporary) {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({
role: req.user.role,
});
update.expiredAt = createTempChatExpirationDate(appConfig?.interfaceConfig);
} catch (err) {
logger.error('Error creating temporary chat expiration date:', err);

View File

@@ -14,6 +14,7 @@ const {
} = require('./Message');
jest.mock('~/server/services/Config/app');
const { getAppConfig } = require('~/server/services/Config/app');
/**
* @type {import('mongoose').Model<import('@librechat/data-schemas').IMessage>}
@@ -43,11 +44,6 @@ describe('Message Operations', () => {
mockReq = {
user: { id: 'user123' },
config: {
interfaceConfig: {
temporaryChatRetention: 24, // Default 24 hours
},
},
};
mockMessageData = {
@@ -331,7 +327,11 @@ describe('Message Operations', () => {
it('should save a message with expiredAt when isTemporary is true', async () => {
// Mock app config with 24 hour retention
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 24,
},
});
mockReq.body = { isTemporary: true };
@@ -376,7 +376,11 @@ describe('Message Operations', () => {
it('should use custom retention period from config', async () => {
// Mock app config with 48 hour retention
mockReq.config.interfaceConfig.temporaryChatRetention = 48;
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 48,
},
});
mockReq.body = { isTemporary: true };
@@ -399,7 +403,11 @@ describe('Message Operations', () => {
it('should handle minimum retention period (1 hour)', async () => {
// Mock app config with less than minimum retention
mockReq.config.interfaceConfig.temporaryChatRetention = 0.5; // Half hour - should be clamped to 1 hour
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 0.5, // Half hour - should be clamped to 1 hour
},
});
mockReq.body = { isTemporary: true };
@@ -422,7 +430,11 @@ describe('Message Operations', () => {
it('should handle maximum retention period (8760 hours)', async () => {
// Mock app config with more than maximum retention
mockReq.config.interfaceConfig.temporaryChatRetention = 10000; // Should be clamped to 8760 hours
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 10000, // Should be clamped to 8760 hours
},
});
mockReq.body = { isTemporary: true };
@@ -443,36 +455,22 @@ describe('Message Operations', () => {
);
});
it('should handle missing config gracefully', async () => {
// Simulate missing config - should use default retention period
delete mockReq.config;
it('should handle getAppConfig errors gracefully', async () => {
// Mock getAppConfig to throw an error
getAppConfig.mockRejectedValue(new Error('Config service unavailable'));
mockReq.body = { isTemporary: true };
const beforeSave = new Date();
const result = await saveMessage(mockReq, mockMessageData);
const afterSave = new Date();
// Should still save the message with default retention period (30 days)
// Should still save the message but with expiredAt as null
expect(result.messageId).toBe('msg123');
expect(result.expiredAt).toBeDefined();
expect(result.expiredAt).toBeInstanceOf(Date);
// Verify expiredAt is approximately 30 days in the future (720 hours)
const expectedExpirationTime = new Date(beforeSave.getTime() + 720 * 60 * 60 * 1000);
const actualExpirationTime = new Date(result.expiredAt);
expect(actualExpirationTime.getTime()).toBeGreaterThanOrEqual(
expectedExpirationTime.getTime() - 1000,
);
expect(actualExpirationTime.getTime()).toBeLessThanOrEqual(
new Date(afterSave.getTime() + 720 * 60 * 60 * 1000 + 1000).getTime(),
);
expect(result.expiredAt).toBeNull();
});
it('should use default retention when config is not provided', async () => {
// Mock getAppConfig to return empty config
mockReq.config = {}; // Empty config
getAppConfig.mockResolvedValue({});
mockReq.body = { isTemporary: true };
@@ -495,7 +493,11 @@ describe('Message Operations', () => {
it('should not update expiredAt on message update', async () => {
// First save a temporary message
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 24,
},
});
mockReq.body = { isTemporary: true };
const savedMessage = await saveMessage(mockReq, mockMessageData);
@@ -518,7 +520,11 @@ describe('Message Operations', () => {
it('should preserve expiredAt when saving existing temporary message', async () => {
// First save a temporary message
mockReq.config.interfaceConfig.temporaryChatRetention = 24;
getAppConfig.mockResolvedValue({
interfaceConfig: {
temporaryChatRetention: 24,
},
});
mockReq.body = { isTemporary: true };
const firstSave = await saveMessage(mockReq, mockMessageData);

View File

@@ -118,7 +118,7 @@ const addIntervalToDate = (date, value, unit) => {
* @async
* @function
* @param {Object} params - The function parameters.
* @param {ServerRequest} params.req - The Express request object.
* @param {Express.Request} params.req - The Express request object.
* @param {Express.Response} params.res - The Express response object.
* @param {Object} params.txData - The transaction data.
* @param {string} params.txData.user - The user ID or identifier.

View File

@@ -3,7 +3,7 @@ const bcrypt = require('bcryptjs');
/**
* Compares the provided password with the user's password.
*
* @param {IUser} user - The user to compare the password for.
* @param {MongoUser} user - The user to compare the password for.
* @param {string} candidatePassword - The password to test against the user's password.
* @returns {Promise<boolean>} A promise that resolves to a boolean indicating if the password matches.
*/

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "v0.8.0-rc3",
"version": "v0.8.0-rc2",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -97,6 +97,7 @@
"nodemailer": "^6.9.15",
"ollama": "^0.5.0",
"openai": "^5.10.1",
"openai-chat-tokens": "^0.2.8",
"openid-client": "^6.5.0",
"passport": "^0.6.0",
"passport-apple": "^2.0.2",

View File

@@ -0,0 +1,27 @@
const { CacheKeys } = require('librechat-data-provider');
const { loadOverrideConfig } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');
async function overrideController(req, res) {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
let overrideConfig = await cache.get(CacheKeys.OVERRIDE_CONFIG);
if (overrideConfig) {
res.send(overrideConfig);
return;
} else if (overrideConfig === false) {
res.send(false);
return;
}
overrideConfig = await loadOverrideConfig();
const { endpointsConfig, modelsConfig } = overrideConfig;
if (endpointsConfig) {
await cache.set(CacheKeys.ENDPOINT_CONFIG, endpointsConfig);
}
if (modelsConfig) {
await cache.set(CacheKeys.MODELS_CONFIG, modelsConfig);
}
await cache.set(CacheKeys.OVERRIDE_CONFIG, overrideConfig);
res.send(JSON.stringify(overrideConfig));
}
module.exports = overrideController;

View File

@@ -4,13 +4,11 @@ const {
getToolkitKey,
checkPluginAuth,
filterUniquePlugins,
convertMCPToolToPlugin,
convertMCPToolsToPlugins,
} = require('@librechat/api');
const { getCachedTools, setCachedTools, mergeUserTools } = require('~/server/services/Config');
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
const { availableTools, toolkits } = require('~/app/clients/tools');
const { getAppConfig } = require('~/server/services/Config');
const { getMCPManager } = require('~/config');
const { getMCPManager, getFlowStateManager } = require('~/config');
const { getLogStores } = require('~/cache');
const getAvailablePluginsController = async (req, res) => {
@@ -25,7 +23,6 @@ const getAvailablePluginsController = async (req, res) => {
const appConfig = await getAppConfig({ role: req.user?.role });
/** @type {{ filteredTools: string[], includedTools: string[] }} */
const { filteredTools = [], includedTools = [] } = appConfig;
/** @type {import('@librechat/api').LCManifestTool[]} */
const pluginManifest = availableTools;
const uniquePlugins = filterUniquePlugins(pluginManifest);
@@ -51,6 +48,45 @@ 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.
*
@@ -66,19 +102,13 @@ const getAvailablePluginsController = async (req, res) => {
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 appConfig = await getAppConfig();
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
const cachedUserTools = await getCachedTools({ userId });
const mcpManager = getMCPManager();
const userPlugins =
cachedUserTools != null
? convertMCPToolsToPlugins({ functionTools: cachedUserTools, mcpManager })
: undefined;
const userPlugins = convertMCPToolsToPlugins({ functionTools: cachedUserTools, mcpManager });
if (cachedToolsArray != null && userPlugins != null) {
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
@@ -86,42 +116,30 @@ 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;
/** @type {import('@librechat/api').LCManifestTool[]} */
let pluginManifest = availableTools;
const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
if (appConfig?.mcpConfig != null) {
try {
const mcpTools = await mcpManager.getAllToolFunctions(userId);
prelimCachedTools = prelimCachedTools ?? {};
for (const [toolKey, toolData] of Object.entries(mcpTools)) {
const plugin = convertMCPToolToPlugin({
toolKey,
toolData,
mcpManager,
});
if (plugin) {
pluginManifest.push(plugin);
}
prelimCachedTools[toolKey] = toolData;
}
await mergeUserTools({ userId, cachedUserTools, userTools: prelimCachedTools });
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];
} 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[]} Deduplicate and authenticate plugins */
/** @type {TPlugin[]} */
const uniquePlugins = filterUniquePlugins(pluginManifest);
const authenticatedPlugins = uniquePlugins.map((plugin) => {
if (checkPluginAuth(plugin)) {
return { ...plugin, authenticated: true };
@@ -130,7 +148,8 @@ const getAvailableTools = async (req, res) => {
}
});
/** Filter plugins based on availability and add MCP-specific auth config */
const toolDefinitions = (await getCachedTools({ includeGlobal: true })) || {};
const toolsOutput = [];
for (const plugin of authenticatedPlugins) {
const isToolDefined = toolDefinitions[plugin.pluginKey] !== undefined;
@@ -146,36 +165,41 @@ const getAvailableTools = async (req, res) => {
const toolToAdd = { ...plugin };
if (plugin.pluginKey.includes(Constants.mcp_delimiter)) {
const parts = plugin.pluginKey.split(Constants.mcp_delimiter);
const serverName = parts[parts.length - 1];
const serverConfig = appConfig?.mcpConfig?.[serverName];
if (!plugin.pluginKey.includes(Constants.mcp_delimiter)) {
toolsOutput.push(toolToAdd);
continue;
}
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;
}
}
const parts = plugin.pluginKey.split(Constants.mcp_delimiter);
const serverName = parts[parts.length - 1];
const serverConfig = appConfig?.mcpConfig?.[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;
}
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);

View File

@@ -6,26 +6,22 @@ jest.mock('@librechat/data-schemas', () => ({
logger: {
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
},
}));
jest.mock('~/server/services/Config', () => ({
getCachedTools: jest.fn(),
getAppConfig: jest.fn().mockResolvedValue({
filteredTools: [],
includedTools: [],
}),
setCachedTools: jest.fn(),
mergeUserTools: jest.fn(),
getAppConfig: jest.fn(),
}));
// loadAndFormatTools mock removed - no longer used in PluginController
jest.mock('~/server/services/ToolService', () => ({
getToolkitKey: jest.fn(),
}));
jest.mock('~/config', () => ({
getMCPManager: jest.fn(() => ({
getAllToolFunctions: jest.fn().mockResolvedValue({}),
getRawConfig: jest.fn().mockReturnValue({}),
loadManifestTools: jest.fn().mockResolvedValue([]),
getRawConfig: jest.fn(),
})),
getFlowStateManager: jest.fn(),
}));
@@ -39,87 +35,74 @@ 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');
describe('PluginController', () => {
let mockReq, mockRes, mockCache;
beforeEach(() => {
jest.clearAllMocks();
mockReq = {
user: { id: 'test-user-id' },
config: {
filteredTools: [],
includedTools: [],
},
};
mockReq = { user: { id: 'test-user-id' } };
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;
// Reset getCachedTools mock to ensure clean state
getCachedTools.mockReset();
// Reset getAppConfig mock to ensure clean state with default values
getAppConfig.mockReset();
getAppConfig.mockResolvedValue({
filteredTools: [],
includedTools: [],
});
});
describe('getAvailablePluginsController', () => {
beforeEach(() => {
getAppConfig.mockResolvedValue({
filteredTools: [],
includedTools: [],
});
});
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);
// Configure getAppConfig to return the expected config
getAppConfig.mockResolvedValueOnce({
filteredTools: [],
includedTools: [],
});
filterUniquePlugins.mockReturnValue(mockPlugins);
checkPluginAuth.mockReturnValue(true);
await getAvailablePluginsController(mockReq, mockRes);
expect(filterUniquePlugins).toHaveBeenCalled();
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
// The real filterUniquePlugins should have removed the duplicate
expect(responseData).toHaveLength(2);
expect(responseData[0].pluginKey).toBe('key1');
expect(responseData[1].pluginKey).toBe('key2');
// 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 },
]);
});
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);
// Configure getAppConfig to return the expected config
getAppConfig.mockResolvedValueOnce({
filteredTools: [],
includedTools: [],
});
filterUniquePlugins.mockReturnValue([mockPlugin]);
checkPluginAuth.mockReturnValueOnce(true);
await getAvailablePluginsController(mockReq, mockRes);
expect(checkPluginAuth).toHaveBeenCalledWith(mockPlugin);
const responseData = mockRes.json.mock.calls[0][0];
// The real checkPluginAuth returns false for plugins without authConfig, so authenticated property is not added
expect(responseData[0].authenticated).toBeUndefined();
expect(responseData[0].authenticated).toBe(true);
});
it('should return cached plugins when available', async () => {
@@ -131,7 +114,8 @@ describe('PluginController', () => {
await getAvailablePluginsController(mockReq, mockRes);
// When cache is hit, we return immediately without processing
expect(filterUniquePlugins).not.toHaveBeenCalled();
expect(checkPluginAuth).not.toHaveBeenCalled();
expect(mockRes.json).toHaveBeenCalledWith(cachedPlugins);
});
@@ -141,14 +125,13 @@ describe('PluginController', () => {
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
];
require('~/app/clients/tools').availableTools.push(...mockPlugins);
mockCache.get.mockResolvedValue(null);
// Configure getAppConfig to return config with includedTools
getAppConfig.mockResolvedValueOnce({
getAppConfig.mockResolvedValue({
filteredTools: [],
includedTools: ['key1'],
});
mockCache.get.mockResolvedValue(null);
filterUniquePlugins.mockReturnValue(mockPlugins);
checkPluginAuth.mockReturnValue(false);
await getAvailablePluginsController(mockReq, mockRes);
@@ -162,117 +145,67 @@ describe('PluginController', () => {
it('should use convertMCPToolsToPlugins for user-specific MCP tools', async () => {
const mockUserTools = {
[`tool1${Constants.mcp_delimiter}server1`]: {
type: 'function',
function: {
name: `tool1${Constants.mcp_delimiter}server1`,
description: 'Tool 1',
parameters: { type: 'object', properties: {} },
},
function: { name: 'tool1', description: 'Tool 1' },
},
};
const mockConvertedPlugins = [
{
name: 'tool1',
pluginKey: `tool1${Constants.mcp_delimiter}server1`,
description: 'Tool 1',
},
];
mockCache.get.mockResolvedValue(null);
getCachedTools.mockResolvedValueOnce(mockUserTools);
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
// Mock second call to return tool definitions (includeGlobal: true)
getCachedTools.mockResolvedValueOnce(mockUserTools);
convertMCPToolsToPlugins.mockReturnValue(mockConvertedPlugins);
filterUniquePlugins.mockImplementation((plugins) => plugins);
await getAvailableTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
expect(responseData).toBeDefined();
expect(Array.isArray(responseData)).toBe(true);
expect(responseData.length).toBeGreaterThan(0);
const convertedTool = responseData.find(
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}server1`,
);
expect(convertedTool).toBeDefined();
// The real convertMCPToolsToPlugins extracts the name from the delimiter
expect(convertedTool.name).toBe('tool1');
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
functionTools: mockUserTools,
mcpManager: expect.any(Object),
});
});
it('should use filterUniquePlugins to deduplicate combined tools', async () => {
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' },
const mockUserPlugins = [
{ name: 'UserTool', pluginKey: 'user-tool', description: 'User tool' },
];
const mockManifestPlugins = [
{ name: 'ManifestTool', pluginKey: 'manifest-tool', description: 'Manifest tool' },
];
mockCache.get.mockResolvedValue(mockCachedPlugins);
getCachedTools.mockResolvedValueOnce(mockUserTools);
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
// Mock second call to return tool definitions
getCachedTools.mockResolvedValueOnce(mockUserTools);
mockCache.get.mockResolvedValue(mockManifestPlugins);
getCachedTools.mockResolvedValueOnce({});
convertMCPToolsToPlugins.mockReturnValue(mockUserPlugins);
filterUniquePlugins.mockReturnValue([...mockUserPlugins, ...mockManifestPlugins]);
await getAvailableTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
expect(Array.isArray(responseData)).toBe(true);
// The real filterUniquePlugins should have deduplicated tools with same pluginKey
const userToolCount = responseData.filter((tool) => tool.pluginKey === 'user-tool').length;
expect(userToolCount).toBe(1);
// Should be called to deduplicate the combined array
expect(filterUniquePlugins).toHaveBeenLastCalledWith([
...mockUserPlugins,
...mockManifestPlugins,
]);
});
it('should use checkPluginAuth to verify authentication status', async () => {
// 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);
const mockPlugin = { name: 'Tool1', pluginKey: 'tool1', description: 'Tool 1' };
mockCache.get.mockResolvedValue(null);
// First call returns null for user tools
getCachedTools.mockResolvedValueOnce(null);
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
getCachedTools.mockResolvedValue({});
convertMCPToolsToPlugins.mockReturnValue([]);
filterUniquePlugins.mockReturnValue([mockPlugin]);
checkPluginAuth.mockReturnValue(true);
// Second call (with includeGlobal: true) returns the tool definitions
getCachedTools.mockResolvedValueOnce({
tool1: {
type: 'function',
function: {
name: 'tool1',
description: 'Tool 1',
parameters: {},
},
},
});
// Mock getCachedTools second call to return tool definitions
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({ tool1: true });
await getAvailableTools(mockReq, mockRes);
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();
// The real checkPluginAuth returns false for plugins without authConfig, so authenticated property is not added
expect(tool.authenticated).toBeUndefined();
expect(checkPluginAuth).toHaveBeenCalledWith(mockPlugin);
});
it('should use getToolkitKey for toolkit validation', async () => {
@@ -283,42 +216,21 @@ 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);
// First call returns null for user tools
getCachedTools.mockResolvedValueOnce(null);
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
getCachedTools.mockResolvedValue({});
convertMCPToolsToPlugins.mockReturnValue([]);
filterUniquePlugins.mockReturnValue([mockToolkit]);
checkPluginAuth.mockReturnValue(false);
getToolkitKey.mockReturnValue('toolkit1');
// Second call (with includeGlobal: true) returns the tool definitions
getCachedTools.mockResolvedValueOnce({
toolkit1_function: {
type: 'function',
function: {
name: 'toolkit1_function',
description: 'Toolkit function',
parameters: {},
},
},
// Mock getCachedTools second call to return tool definitions
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({
toolkit1_function: true,
});
await getAvailableTools(mockReq, mockRes);
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();
expect(getToolkitKey).toHaveBeenCalled();
});
});
@@ -328,40 +240,39 @@ describe('PluginController', () => {
const functionTools = {
[`test-tool${Constants.mcp_delimiter}test-server`]: {
type: 'function',
function: {
name: `test-tool${Constants.mcp_delimiter}test-server`,
description: 'A test tool',
parameters: { type: 'object', properties: {} },
},
function: { name: 'test-tool', description: 'A test tool' },
},
};
// Mock the MCP manager to return tools and server config
const mockConvertedPlugin = {
name: 'test-tool',
pluginKey: `test-tool${Constants.mcp_delimiter}test-server`,
description: 'A test tool',
icon: serverConfig?.iconPath,
authenticated: true,
authConfig: [],
};
// Mock the MCP manager to return the server config
const mockMCPManager = {
getAllToolFunctions: jest.fn().mockResolvedValue(functionTools),
loadManifestTools: jest.fn().mockResolvedValue([]),
getRawConfig: jest.fn().mockReturnValue(serverConfig),
};
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
// First call returns empty user tools
getCachedTools.mockResolvedValueOnce({});
// Mock getAppConfig to return the mcpConfig
mockReq.config = {
mcpConfig: {
'test-server': serverConfig,
},
};
// Second call (with includeGlobal: true) returns the tool definitions
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.pluginKey === `test-tool${Constants.mcp_delimiter}test-server`,
);
return responseData.find((tool) => tool.name === 'test-tool');
};
it('should set plugin.icon when iconPath is defined', async () => {
@@ -391,43 +302,46 @@ describe('PluginController', () => {
},
};
// 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: {},
},
// 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 the MCP manager to return tools
const mockMCPManager = {
getAllToolFunctions: jest.fn().mockResolvedValue(mcpToolFunctions),
getRawConfig: jest.fn().mockReturnValue({
customUserVars: {
API_KEY: { title: 'API Key', description: 'Your API key' },
},
}),
loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools),
getRawConfig: jest.fn(),
};
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
mockCache.get.mockResolvedValue(null);
mockReq.config = appConfig;
getAppConfig.mockResolvedValue(appConfig);
// First call returns user tools (empty in this case)
getCachedTools.mockResolvedValueOnce({});
// Second call (with includeGlobal: true) returns tool definitions including our MCP tool
getCachedTools.mockResolvedValueOnce(mcpToolFunctions);
// Mock convertMCPToolsToPlugins to return empty array for user tools
convertMCPToolsToPlugins.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,
});
await getAvailableTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
expect(Array.isArray(responseData)).toBe(true);
// Find the MCP tool in the response
const mcpTool = responseData.find(
@@ -463,36 +377,24 @@ describe('PluginController', () => {
it('should handle null cachedTools and cachedUserTools', async () => {
mockCache.get.mockResolvedValue(null);
// First call returns null for user tools
getCachedTools.mockResolvedValueOnce(null);
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
// Mock MCP manager to return no tools
const mockMCPManager = {
getAllToolFunctions: jest.fn().mockResolvedValue({}),
getRawConfig: jest.fn().mockReturnValue({}),
};
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
// Second call (with includeGlobal: true) returns empty object instead of null
getCachedTools.mockResolvedValueOnce({});
getCachedTools.mockResolvedValue(null);
convertMCPToolsToPlugins.mockReturnValue(undefined);
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
await getAvailableTools(mockReq, mockRes);
// Should handle null values gracefully
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
functionTools: null,
mcpManager: expect.any(Object),
});
});
it('should handle when getCachedTools returns undefined', async () => {
mockCache.get.mockResolvedValue(null);
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
getCachedTools.mockResolvedValue(undefined);
convertMCPToolsToPlugins.mockReturnValue(undefined);
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
checkPluginAuth.mockReturnValue(false);
// Mock getCachedTools to return undefined for both calls
getCachedTools.mockReset();
@@ -500,63 +402,36 @@ describe('PluginController', () => {
await getAvailableTools(mockReq, mockRes);
// Should handle undefined values gracefully
expect(mockRes.status).toHaveBeenCalledWith(200);
expect(mockRes.json).toHaveBeenCalledWith([]);
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
functionTools: undefined,
mcpManager: expect.any(Object),
});
});
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${Constants.mcp_delimiter}server1`]: {
type: 'function',
function: {
name: `user-tool${Constants.mcp_delimiter}server1`,
description: 'User tool',
parameters: {},
},
},
'user-tool': { function: { name: 'user-tool', description: 'User tool' } },
};
const userPlugins = [{ name: 'UserTool', pluginKey: 'user-tool', description: 'User tool' }];
mockCache.get.mockResolvedValue(cachedTools);
getCachedTools.mockResolvedValueOnce(userTools);
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
// The controller expects a second call to getCachedTools
getCachedTools.mockResolvedValueOnce({
'cached-tool': { type: 'function', function: { name: 'cached-tool' } },
[`user-tool${Constants.mcp_delimiter}server1`]:
userTools[`user-tool${Constants.mcp_delimiter}server1`],
});
getCachedTools.mockResolvedValue(userTools);
convertMCPToolsToPlugins.mockReturnValue(userPlugins);
filterUniquePlugins.mockReturnValue([...userPlugins, ...cachedTools]);
await getAvailableTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
// Should have both cached and user tools
expect(responseData.length).toBeGreaterThanOrEqual(2);
expect(mockRes.json).toHaveBeenCalledWith([...userPlugins, ...cachedTools]);
});
it('should handle empty toolDefinitions object', async () => {
mockCache.get.mockResolvedValue(null);
// Reset getCachedTools to ensure clean state
getCachedTools.mockReset();
getCachedTools.mockResolvedValue({});
mockReq.config = {}; // No mcpConfig at all
// Ensure no plugins are available
require('~/app/clients/tools').availableTools.length = 0;
// Reset MCP manager to default state
const mockMCPManager = {
getAllToolFunctions: jest.fn().mockResolvedValue({}),
getRawConfig: jest.fn().mockReturnValue({}),
};
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({});
convertMCPToolsToPlugins.mockReturnValue([]);
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
checkPluginAuth.mockReturnValue(true);
await getAvailableTools(mockReq, mockRes);
@@ -565,8 +440,8 @@ describe('PluginController', () => {
});
it('should handle MCP tools without customUserVars', async () => {
const appConfig = {
mcpConfig: {
const customConfig = {
mcpServers: {
'test-server': {
// No customUserVars defined
},
@@ -575,59 +450,54 @@ describe('PluginController', () => {
const mockUserTools = {
[`tool1${Constants.mcp_delimiter}test-server`]: {
type: 'function',
function: {
name: `tool1${Constants.mcp_delimiter}test-server`,
description: 'Tool 1',
parameters: { type: 'object', properties: {} },
},
function: { name: 'tool1', description: 'Tool 1' },
},
};
// Mock the MCP manager to return the tools
// Mock the MCP manager to return server config without customUserVars
const mockMCPManager = {
getAllToolFunctions: jest.fn().mockResolvedValue(mockUserTools),
getRawConfig: jest.fn().mockReturnValue({
// No customUserVars defined
}),
loadManifestTools: jest.fn().mockResolvedValue([]),
getRawConfig: jest.fn().mockReturnValue(customConfig.mcpServers['test-server']),
};
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
mockCache.get.mockResolvedValue(null);
mockReq.config = appConfig;
// First call returns empty user tools
getCachedTools.mockResolvedValueOnce({});
// Second call (with includeGlobal: true) returns the tool definitions
getAppConfig.mockResolvedValue({
mcpConfig: customConfig.mcpServers,
filteredTools: [],
includedTools: [],
});
getCachedTools.mockResolvedValueOnce(mockUserTools);
// Ensure no plugins in availableTools for clean test
require('~/app/clients/tools').availableTools.length = 0;
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,
});
await getAvailableTools(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(200);
const responseData = mockRes.json.mock.calls[0][0];
expect(Array.isArray(responseData)).toBe(true);
expect(responseData.length).toBeGreaterThan(0);
const mcpTool = responseData.find(
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}test-server`,
);
expect(mcpTool).toBeDefined();
expect(mcpTool.authenticated).toBe(true);
// The actual implementation sets authConfig to empty array when no customUserVars
expect(mcpTool.authConfig).toEqual([]);
expect(responseData[0].authenticated).toBe(true);
// The actual implementation doesn't set authConfig on tools without customUserVars
expect(responseData[0].authConfig).toEqual([]);
});
it('should handle undefined filteredTools and includedTools', async () => {
mockReq.config = {};
getAppConfig.mockResolvedValue({});
mockCache.get.mockResolvedValue(null);
// Configure getAppConfig to return config with undefined properties
// The controller will use default values [] for filteredTools and includedTools
getAppConfig.mockResolvedValueOnce({});
filterUniquePlugins.mockReturnValue([]);
checkPluginAuth.mockReturnValue(false);
await getAvailablePluginsController(mockReq, mockRes);
@@ -643,21 +513,15 @@ describe('PluginController', () => {
toolkit: true,
};
// No need to mock app.locals anymore as it's not used
// Add the toolkit to availableTools
require('~/app/clients/tools').availableTools.push(mockToolkit);
mockCache.get.mockResolvedValue(null);
// First call returns empty object
getCachedTools.mockResolvedValueOnce({});
mockReq.config = {
mcpConfig: null,
paths: { structuredTools: '/mock/path' },
};
getCachedTools.mockResolvedValue({});
convertMCPToolsToPlugins.mockReturnValue([]);
filterUniquePlugins.mockReturnValue([mockToolkit]);
checkPluginAuth.mockReturnValue(false);
getToolkitKey.mockReturnValue(undefined);
// Second call (with includeGlobal: true) returns empty object to avoid null reference error
getCachedTools.mockResolvedValueOnce({});
// Mock getCachedTools second call to return null
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce(null);
await getAvailableTools(mockReq, mockRes);

View File

@@ -24,7 +24,7 @@ const { getMCPManager } = require('~/config');
const getUserController = async (req, res) => {
const appConfig = await getAppConfig({ role: req.user?.role });
/** @type {IUser} */
/** @type {MongoUser} */
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
/**
* These fields should not exist due to secure field selection, but deletion

View File

@@ -246,7 +246,6 @@ function createToolEndCallback({ req, res, artifactPromises }) {
const attachment = await processFileCitations({
user,
metadata,
appConfig: req.config,
toolArtifact: output.artifact,
toolCallId: output.tool_call_id,
});

View File

@@ -7,13 +7,11 @@ const {
createRun,
Tokenizer,
checkAccess,
getBalanceConfig,
hasCustomUserVars,
getUserMCPAuthMap,
memoryInstructions,
formatContentStrings,
createMemoryProcessor,
encodeAndFormatAudios,
encodeAndFormatVideos,
encodeAndFormatDocuments,
} = require('@librechat/api');
const {
Callback,
@@ -36,21 +34,23 @@ const {
AgentCapabilities,
bedrockInputSchema,
removeNullishValues,
isDocumentSupportedEndpoint,
} = require('librechat-data-provider');
const {
findPluginAuthsByKeys,
getFormattedMemories,
deleteMemory,
setMemory,
} = require('~/models');
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 { checkCapability, getAppConfig } = require('~/server/services/Config');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
const { getProviderConfig } = require('~/server/services/Endpoints');
const { getStrategyFunctions } = require('~/server/services/Files');
const { checkCapability } = require('~/server/services/Config');
const BaseClient = require('~/app/clients/BaseClient');
const { getRoleByName } = require('~/models/Role');
const { loadAgent } = require('~/models/Agent');
const { getMCPManager } = require('~/config');
const { getFiles } = require('~/models');
const omitTitleOptions = new Set([
'stream',
@@ -228,168 +228,6 @@ class AgentClient extends BaseClient {
return files;
}
async addDocuments(message, attachments) {
const documentResult = await encodeAndFormatDocuments(
this.options.req,
attachments,
this.options.agent.provider,
getStrategyFunctions,
);
message.documents =
documentResult.documents && documentResult.documents.length
? documentResult.documents
: undefined;
return documentResult.files;
}
async addVideos(message, attachments) {
const videoResult = await encodeAndFormatVideos(
this.options.req,
attachments,
this.options.agent.provider,
getStrategyFunctions,
);
message.videos =
videoResult.videos && videoResult.videos.length ? videoResult.videos : undefined;
return videoResult.files;
}
async addAudios(message, attachments) {
const audioResult = await encodeAndFormatAudios(
this.options.req,
attachments,
this.options.agent.provider,
getStrategyFunctions,
);
message.audios =
audioResult.audios && audioResult.audios.length ? audioResult.audios : undefined;
return audioResult.files;
}
/**
* Override addPreviousAttachments to handle all file types, not just images
* @param {TMessage[]} _messages
* @returns {Promise<TMessage[]>}
*/
async addPreviousAttachments(_messages) {
if (!this.options.resendFiles) {
return _messages;
}
const seen = new Set();
const attachmentsProcessed =
this.options.attachments && !(this.options.attachments instanceof Promise);
if (attachmentsProcessed) {
for (const attachment of this.options.attachments) {
seen.add(attachment.file_id);
}
}
/**
*
* @param {TMessage} message
*/
const processMessage = async (message) => {
if (!this.message_file_map) {
/** @type {Record<string, MongoFile[]> */
this.message_file_map = {};
}
const fileIds = [];
for (const file of message.files) {
if (seen.has(file.file_id)) {
continue;
}
fileIds.push(file.file_id);
seen.add(file.file_id);
}
if (fileIds.length === 0) {
return message;
}
const files = await getFiles(
{
file_id: { $in: fileIds },
},
{},
{},
);
await this.processAttachments(message, files);
this.message_file_map[message.messageId] = files;
return message;
};
const promises = [];
for (const message of _messages) {
if (!message.files) {
promises.push(message);
continue;
}
promises.push(processMessage(message));
}
const messages = await Promise.all(promises);
this.checkVisionRequest(Object.values(this.message_file_map ?? {}).flat());
return messages;
}
async processAttachments(message, attachments) {
const categorizedAttachments = {
images: [],
documents: [],
videos: [],
audios: [],
};
for (const file of attachments) {
if (file.type.startsWith('image/')) {
categorizedAttachments.images.push(file);
} else if (file.type === 'application/pdf') {
categorizedAttachments.documents.push(file);
} else if (file.type.startsWith('video/')) {
categorizedAttachments.videos.push(file);
} else if (file.type.startsWith('audio/')) {
categorizedAttachments.audios.push(file);
}
}
const [imageFiles, documentFiles, videoFiles, audioFiles] = await Promise.all([
categorizedAttachments.images.length > 0
? this.addImageURLs(message, categorizedAttachments.images)
: Promise.resolve([]),
categorizedAttachments.documents.length > 0
? this.addDocuments(message, categorizedAttachments.documents)
: Promise.resolve([]),
categorizedAttachments.videos.length > 0
? this.addVideos(message, categorizedAttachments.videos)
: Promise.resolve([]),
categorizedAttachments.audios.length > 0
? this.addAudios(message, categorizedAttachments.audios)
: Promise.resolve([]),
]);
const allFiles = [...imageFiles, ...documentFiles, ...videoFiles, ...audioFiles];
const seenFileIds = new Set();
const uniqueFiles = [];
for (const file of allFiles) {
if (file.file_id && !seenFileIds.has(file.file_id)) {
seenFileIds.add(file.file_id);
uniqueFiles.push(file);
} else if (!file.file_id) {
uniqueFiles.push(file);
}
}
return uniqueFiles;
}
async buildMessages(
messages,
parentMessageId,
@@ -423,7 +261,7 @@ class AgentClient extends BaseClient {
};
}
const files = await this.processAttachments(
const files = await this.addImageURLs(
orderedMessages[orderedMessages.length - 1],
attachments,
);
@@ -446,47 +284,6 @@ class AgentClient extends BaseClient {
assistantName: this.options?.modelLabel,
});
const hasFiles =
(message.documents && message.documents.length > 0) ||
(message.videos && message.videos.length > 0) ||
(message.audios && message.audios.length > 0) ||
(message.image_urls && message.image_urls.length > 0);
if (
hasFiles &&
message.isCreatedByUser &&
isDocumentSupportedEndpoint(this.options.agent.provider)
) {
const contentParts = [];
if (message.documents && message.documents.length > 0) {
contentParts.push(...message.documents);
}
if (message.videos && message.videos.length > 0) {
contentParts.push(...message.videos);
}
if (message.audios && message.audios.length > 0) {
contentParts.push(...message.audios);
}
if (message.image_urls && message.image_urls.length > 0) {
contentParts.push(...message.image_urls);
}
if (typeof formattedMessage.content === 'string') {
contentParts.push({ type: 'text', text: formattedMessage.content });
} else {
const textPart = formattedMessage.content.find((part) => part.type === 'text');
if (textPart) {
contentParts.push(textPart);
}
}
formattedMessage.content = contentParts;
}
if (message.ocr && i !== orderedMessages.length - 1) {
if (typeof formattedMessage.content === 'string') {
formattedMessage.content = message.ocr + '\n' + formattedMessage.content;
@@ -656,7 +453,7 @@ class AgentClient extends BaseClient {
);
return;
}
const appConfig = this.options.req.config;
const appConfig = await getAppConfig({ role: user.role });
const memoryConfig = appConfig.memory;
if (!memoryConfig || memoryConfig.disabled === true) {
return;
@@ -787,7 +584,7 @@ class AgentClient extends BaseClient {
if (this.processMemory == null) {
return;
}
const appConfig = this.options.req.config;
const appConfig = await getAppConfig({ role: this.options.req.user?.role });
const memoryConfig = appConfig.memory;
const messageWindowSize = memoryConfig?.messageWindowSize ?? 5;
@@ -820,7 +617,6 @@ class AgentClient extends BaseClient {
await this.chatCompletion({
payload,
onProgress: opts.onProgress,
userMCPAuthMap: opts.userMCPAuthMap,
abortController: opts.abortController,
});
return this.contentParts;
@@ -960,13 +756,7 @@ class AgentClient extends BaseClient {
return currentMessageTokens > 0 ? currentMessageTokens : originalEstimate;
}
/**
* @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 }) {
async chatCompletion({ payload, abortController = null }) {
/** @type {Partial<GraphRunnableConfig>} */
let config;
/** @type {ReturnType<createRun>} */
@@ -978,7 +768,7 @@ class AgentClient extends BaseClient {
abortController = new AbortController();
}
const appConfig = this.options.req.config;
const appConfig = await getAppConfig({ role: this.options.req.user?.role });
/** @type {AppConfig['endpoints']['agents']} */
const agentsEConfig = appConfig.endpoints?.[EModelEndpoint.agents];
@@ -1002,7 +792,6 @@ class AgentClient extends BaseClient {
};
const toolSet = new Set((this.options.agent.tools ?? []).map((tool) => tool && tool.name));
let { messages: initialMessages, indexTokenCountMap } = formatAgentMessages(
payload,
this.indexTokenCountMap,
@@ -1124,9 +913,21 @@ class AgentClient extends BaseClient {
run.Graph.contentData = contentData;
}
if (userMCPAuthMap != null) {
config.configurable.userMCPAuthMap = userMCPAuthMap;
try {
if (agent.tools?.length && hasCustomUserVars(appConfig)) {
config.configurable.userMCPAuthMap = await getUserMCPAuthMap({
userId: this.options.req.user.id,
tools: agent.tools,
findPluginAuthsByKeys,
});
}
} catch (err) {
logger.error(
`[api/server/controllers/agents/client.js #chatCompletion] Error getting custom user vars for agent ${agent.id}`,
err,
);
}
await run.processStream({ messages }, config, {
keepContent: i !== 0,
tokenCounter: createTokenCounter(this.getEncoding()),
@@ -1249,8 +1050,7 @@ class AgentClient extends BaseClient {
this.artifactPromises.push(...attachments);
}
const balanceConfig = getBalanceConfig(appConfig);
await this.recordCollectedUsage({ context: 'message', balance: balanceConfig });
await this.recordCollectedUsage({ context: 'message', balance: appConfig?.balance });
} catch (err) {
logger.error(
'[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage',
@@ -1291,7 +1091,7 @@ class AgentClient extends BaseClient {
}
const { handleLLMEnd, collected: collectedMetadata } = createMetadataAggregator();
const { req, res, agent } = this.options;
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
let endpoint = agent.endpoint;
/** @type {import('@librechat/agents').ClientOptions} */
@@ -1429,12 +1229,11 @@ class AgentClient extends BaseClient {
};
});
const balanceConfig = getBalanceConfig(appConfig);
await this.recordCollectedUsage({
collectedUsage,
context: 'title',
model: clientOptions.model,
balance: balanceConfig,
balance: appConfig?.balance,
}).catch((err) => {
logger.error(
'[api/server/controllers/agents/client.js #titleConvo] Error recording collected usage',

View File

@@ -1,5 +1,6 @@
const { Providers } = require('@librechat/agents');
const { Constants, EModelEndpoint } = require('librechat-data-provider');
const { getAppConfig } = require('~/server/services/Config');
const AgentClient = require('./client');
jest.mock('@librechat/agents', () => ({
@@ -10,6 +11,10 @@ jest.mock('@librechat/agents', () => ({
}),
}));
jest.mock('~/server/services/Config', () => ({
getAppConfig: jest.fn(),
}));
describe('AgentClient - titleConvo', () => {
let client;
let mockRun;
@@ -39,6 +44,19 @@ describe('AgentClient - titleConvo', () => {
},
};
// Mock getAppConfig to return endpoint configurations
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.openAI]: {
// Match the agent endpoint
titleModel: 'gpt-3.5-turbo',
titlePrompt: 'Custom title prompt',
titleMethod: 'structured',
titlePromptTemplate: 'Template: {{content}}',
},
},
});
// Mock request and response
mockReq = {
user: {
@@ -49,17 +67,6 @@ describe('AgentClient - titleConvo', () => {
endpoint: EModelEndpoint.openAI,
key: null,
},
config: {
endpoints: {
[EModelEndpoint.openAI]: {
// Match the agent endpoint
titleModel: 'gpt-3.5-turbo',
titlePrompt: 'Custom title prompt',
titleMethod: 'structured',
titlePromptTemplate: 'Template: {{content}}',
},
},
},
};
mockRes = {};
@@ -143,7 +150,7 @@ describe('AgentClient - titleConvo', () => {
it('should handle missing endpoint config gracefully', async () => {
// Remove endpoint config
mockReq.config = { endpoints: {} };
getAppConfig.mockResolvedValue({ endpoints: {} });
const text = 'Test conversation text';
const abortController = new AbortController();
@@ -161,7 +168,7 @@ describe('AgentClient - titleConvo', () => {
it('should use agent model when titleModel is not provided', async () => {
// Remove titleModel from config
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.openAI]: {
titlePrompt: 'Custom title prompt',
@@ -170,7 +177,7 @@ describe('AgentClient - titleConvo', () => {
// titleModel is omitted
},
},
};
});
const text = 'Test conversation text';
const abortController = new AbortController();
@@ -182,7 +189,7 @@ describe('AgentClient - titleConvo', () => {
});
it('should not use titleModel when it equals CURRENT_MODEL constant', async () => {
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.openAI]: {
titleModel: Constants.CURRENT_MODEL,
@@ -191,7 +198,7 @@ describe('AgentClient - titleConvo', () => {
titlePromptTemplate: 'Template: {{content}}',
},
},
};
});
const text = 'Test conversation text';
const abortController = new AbortController();
@@ -234,9 +241,6 @@ describe('AgentClient - titleConvo', () => {
model: 'gpt-3.5-turbo',
context: 'title',
collectedUsage: expect.any(Array),
balance: {
enabled: false,
},
});
});
@@ -266,7 +270,7 @@ describe('AgentClient - titleConvo', () => {
process.env.ANTHROPIC_API_KEY = 'test-api-key';
// Add titleEndpoint to the config
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.openAI]: {
titleModel: 'gpt-3.5-turbo',
@@ -276,7 +280,7 @@ describe('AgentClient - titleConvo', () => {
titlePromptTemplate: 'Custom template',
},
},
};
});
const text = 'Test conversation text';
const abortController = new AbortController();
@@ -303,7 +307,7 @@ describe('AgentClient - titleConvo', () => {
it('should use all config when endpoint config is missing', async () => {
// Set 'all' config without endpoint-specific config
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
all: {
titleModel: 'gpt-4o-mini',
@@ -312,7 +316,7 @@ describe('AgentClient - titleConvo', () => {
titlePromptTemplate: 'All config template: {{content}}',
},
},
};
});
const text = 'Test conversation text';
const abortController = new AbortController();
@@ -335,7 +339,7 @@ describe('AgentClient - titleConvo', () => {
it('should prioritize all config over endpoint config for title settings', async () => {
// Set both endpoint and 'all' config
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.openAI]: {
titleModel: 'gpt-3.5-turbo',
@@ -350,7 +354,7 @@ describe('AgentClient - titleConvo', () => {
titlePromptTemplate: 'All config template',
},
},
};
});
const text = 'Test conversation text';
const abortController = new AbortController();
@@ -377,7 +381,7 @@ describe('AgentClient - titleConvo', () => {
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
// Set comprehensive 'all' config with all new title options
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
all: {
titleConvo: true,
@@ -388,7 +392,7 @@ describe('AgentClient - titleConvo', () => {
titleEndpoint: EModelEndpoint.anthropic, // Should switch provider to Anthropic
},
},
};
});
const text = 'Test conversation about AI and machine learning';
const abortController = new AbortController();
@@ -434,7 +438,7 @@ describe('AgentClient - titleConvo', () => {
mockRun.generateTitle.mockClear();
// Set 'all' config with specific titleMethod
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
all: {
titleModel: 'gpt-4o-mini',
@@ -443,7 +447,7 @@ describe('AgentClient - titleConvo', () => {
titlePromptTemplate: `Template for ${method}: {{content}}`,
},
},
};
});
const text = `Test conversation for ${method} method`;
const abortController = new AbortController();
@@ -487,7 +491,7 @@ describe('AgentClient - titleConvo', () => {
// Set up Azure endpoint with serverless config
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.azureOpenAI]: {
titleConvo: true,
@@ -516,7 +520,7 @@ describe('AgentClient - titleConvo', () => {
},
},
},
};
});
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'grok-3';
@@ -539,7 +543,7 @@ describe('AgentClient - titleConvo', () => {
// Set up Azure endpoint
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.azureOpenAI]: {
titleConvo: true,
@@ -567,7 +571,7 @@ describe('AgentClient - titleConvo', () => {
},
},
},
};
});
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'gpt-4o';
@@ -591,7 +595,7 @@ describe('AgentClient - titleConvo', () => {
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockAgent.model_parameters.model = 'gpt-4o-latest';
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.azureOpenAI]: {
titleConvo: true,
@@ -620,7 +624,7 @@ describe('AgentClient - titleConvo', () => {
},
},
},
};
});
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'gpt-4o-latest';
@@ -642,7 +646,7 @@ describe('AgentClient - titleConvo', () => {
// Set up Azure endpoint
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
[EModelEndpoint.azureOpenAI]: {
titleConvo: true,
@@ -698,7 +702,7 @@ describe('AgentClient - titleConvo', () => {
},
},
},
};
});
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'o1-mini';
@@ -728,7 +732,7 @@ describe('AgentClient - titleConvo', () => {
mockReq.body.model = 'gpt-4';
// Set 'all' config as fallback with a serverless Azure config
mockReq.config = {
getAppConfig.mockResolvedValue({
endpoints: {
all: {
titleConvo: true,
@@ -757,7 +761,7 @@ describe('AgentClient - titleConvo', () => {
},
},
},
};
});
const text = 'Test Azure with all config fallback';
const abortController = new AbortController();
@@ -1040,11 +1044,11 @@ describe('AgentClient - titleConvo', () => {
};
// Mock getAppConfig for memory tests
mockReq.config = {
getAppConfig.mockResolvedValue({
memory: {
messageWindowSize: 3,
},
};
});
mockRes = {};

View File

@@ -21,7 +21,7 @@ const getLogStores = require('~/cache/getLogStores');
/**
* @typedef {Object} ErrorHandlerDependencies
* @property {ServerRequest} req - The Express request object
* @property {Express.Request} req - The Express request object
* @property {Express.Response} res - The Express response object
* @property {() => ErrorHandlerContext} getContext - Function to get the current context
* @property {string} [originPath] - The origin path for the error handler

View File

@@ -9,24 +9,6 @@ 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,
@@ -49,6 +31,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
let userMessagePromise;
let getAbortData;
let client = null;
// Initialize as an array
let cleanupHandlers = [];
const newConvo = !conversationId;
@@ -79,7 +62,9 @@ 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') {
@@ -120,33 +105,8 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
};
try {
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();
}
/** @type {{ client: TAgentClient }} */
const result = await initializeClient({ req, res, endpointOption });
client = result.client;
// Register client with finalization registry if available
@@ -178,7 +138,22 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
};
const { abortController, onStart } = createAbortController(req, res, getAbortData, getReqData);
const closeHandler = createCloseHandler(abortController);
// 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');
};
res.on('close', closeHandler);
cleanupHandlers.push(() => {
try {
@@ -200,7 +175,6 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
abortController,
overrideParentMessageId,
isEdited: !!editedContent,
userMCPAuthMap: result.userMCPAuthMap,
responseMessageId: editedResponseMessageId,
progressOptions: {
res,

View File

@@ -31,12 +31,12 @@ const {
grantPermission,
} = require('~/server/services/PermissionService');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
const { refreshS3Url } = require('~/server/services/Files/S3/crud');
const { filterFile } = require('~/server/services/Files/process');
const { updateAction, getActions } = require('~/models/Action');
const { getCachedTools } = require('~/server/services/Config');
const { deleteFileByFilter } = require('~/models/File');
const { getCategoriesWithCounts } = require('~/models');
@@ -487,7 +487,7 @@ const getListAgentsHandler = async (req, res) => {
*/
const uploadAgentAvatarHandler = async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
filterFile({ req, file: req.file, image: true, isAvatar: true });
const { agent_id } = req.params;
if (!agent_id) {

View File

@@ -1,7 +1,7 @@
const { v4 } = require('uuid');
const { sleep } = require('@librechat/agents');
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { sendEvent, getBalanceConfig } = require('@librechat/api');
const {
Time,
Constants,
@@ -29,6 +29,7 @@ const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
const { createRunBody } = require('~/server/services/createRunBody');
const { sendResponse } = require('~/server/middleware/error');
const { getAppConfig } = require('~/server/services/Config');
const { getTransactions } = require('~/models/Transaction');
const { checkBalance } = require('~/models/balanceMethods');
const { getConvo } = require('~/models/Conversation');
@@ -47,7 +48,7 @@ const { getOpenAIClient } = require('./helpers');
* @returns {void}
*/
const chatV1 = async (req, res) => {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
logger.debug('[/assistants/chat/] req.body', req.body);
const {
@@ -252,8 +253,8 @@ const chatV1 = async (req, res) => {
}
const checkBalanceBeforeRun = async () => {
const balanceConfig = getBalanceConfig(appConfig);
if (!balanceConfig?.enabled) {
const balance = appConfig?.balance;
if (!balance?.enabled) {
return;
}
const transactions =

View File

@@ -1,7 +1,7 @@
const { v4 } = require('uuid');
const { sleep } = require('@librechat/agents');
const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { sendEvent, getBalanceConfig } = require('@librechat/api');
const {
Time,
Constants,
@@ -26,6 +26,7 @@ const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { createRun, StreamRunManager } = require('~/server/services/Runs');
const { addTitle } = require('~/server/services/Endpoints/assistants');
const { createRunBody } = require('~/server/services/createRunBody');
const { getAppConfig } = require('~/server/services/Config');
const { getTransactions } = require('~/models/Transaction');
const { checkBalance } = require('~/models/balanceMethods');
const { getConvo } = require('~/models/Conversation');
@@ -38,13 +39,13 @@ const { getOpenAIClient } = require('./helpers');
* @route POST /
* @desc Chat with an assistant
* @access Public
* @param {ServerRequest} req - The request object, containing the request data.
* @param {Express.Request} req - The request object, containing the request data.
* @param {Express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
const chatV2 = async (req, res) => {
logger.debug('[/assistants/chat/] req.body', req.body);
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
/** @type {{files: MongoFile[]}} */
const {
@@ -127,8 +128,8 @@ const chatV2 = async (req, res) => {
}
const checkBalanceBeforeRun = async () => {
const balanceConfig = getBalanceConfig(appConfig);
if (!balanceConfig?.enabled) {
const balance = appConfig?.balance;
if (!balance?.enabled) {
return;
}
const transactions =

View File

@@ -22,7 +22,7 @@ const getLogStores = require('~/cache/getLogStores');
/**
* @typedef {Object} ErrorHandlerDependencies
* @property {ServerRequest} req - The Express request object
* @property {Express.Request} req - The Express request object
* @property {Express.Response} res - The Express response object
* @property {() => ErrorHandlerContext} getContext - Function to get the current context
* @property {string} [originPath] - The origin path for the error handler

View File

@@ -7,11 +7,11 @@ const {
const {
initializeClient: initAzureClient,
} = require('~/server/services/Endpoints/azureAssistants');
const { getEndpointsConfig, getAppConfig } = require('~/server/services/Config');
const { initializeClient } = require('~/server/services/Endpoints/assistants');
const { getEndpointsConfig } = require('~/server/services/Config');
/**
* @param {ServerRequest} req
* @param {Express.Request} req
* @param {string} [endpoint]
* @returns {Promise<string>}
*/
@@ -210,7 +210,7 @@ async function getOpenAIClient({ req, res, endpointOption, initAppClient, overri
* @returns {Promise<AssistantListResponse>} 200 - success response - application/json
*/
const fetchAssistants = async ({ req, res, overrideEndpoint }) => {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const {
limit = 100,
order = 'desc',

View File

@@ -5,9 +5,9 @@ const { uploadImageBuffer, filterFile } = require('~/server/services/Files/proce
const validateAuthor = require('~/server/middleware/assistants/validateAuthor');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { deleteAssistantActions } = require('~/server/services/ActionService');
const { getCachedTools, getAppConfig } = require('~/server/services/Config');
const { updateAssistantDoc, getAssistants } = require('~/models/Assistant');
const { getOpenAIClient, fetchAssistants } = require('./helpers');
const { getCachedTools } = require('~/server/services/Config');
const { manifestToolMap } = require('~/app/clients/tools');
const { deleteFileByFilter } = require('~/models/File');
@@ -258,7 +258,7 @@ function filterAssistantDocs({ documents, userId, assistantsConfig = {} }) {
*/
const getAssistantDocuments = async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const endpoint = req.query;
const assistantsConfig = appConfig.endpoints?.[endpoint];
const documents = await getAssistants(
@@ -297,7 +297,7 @@ const getAssistantDocuments = async (req, res) => {
*/
const uploadAssistantAvatar = async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
filterFile({ req, file: req.file, image: true, isAvatar: true });
const { assistant_id } = req.params;
if (!assistant_id) {

View File

@@ -94,7 +94,7 @@ const createAssistant = async (req, res) => {
/**
* Modifies an assistant.
* @param {object} params
* @param {ServerRequest} params.req
* @param {Express.Request} params.req
* @param {OpenAIClient} params.openai
* @param {string} params.assistant_id
* @param {AssistantUpdateParams} params.updateData
@@ -199,7 +199,7 @@ const updateAssistant = async ({ req, openai, assistant_id, updateData }) => {
/**
* Modifies an assistant with the resource file id.
* @param {object} params
* @param {ServerRequest} params.req
* @param {Express.Request} params.req
* @param {OpenAIClient} params.openai
* @param {string} params.assistant_id
* @param {string} params.tool_resource
@@ -227,7 +227,7 @@ const addResourceFileId = async ({ req, openai, assistant_id, tool_resource, fil
/**
* Deletes a file ID from an assistant's resource.
* @param {object} params
* @param {ServerRequest} params.req
* @param {Express.Request} params.req
* @param {OpenAIClient} params.openai
* @param {string} params.assistant_id
* @param {string} [params.tool_resource]

View File

@@ -13,6 +13,7 @@ const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/p
const { processCodeOutput } = require('~/server/services/Files/Code/process');
const { createToolCall, getToolCallsByConvo } = require('~/models/ToolCall');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { getAppConfig } = require('~/server/services/Config');
const { loadTools } = require('~/app/clients/tools/util');
const { getRoleByName } = require('~/models/Role');
const { getMessage } = require('~/models/Message');
@@ -35,7 +36,7 @@ const toolAccessPermType = {
*/
const verifyWebSearchAuth = async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const userId = req.user.id;
/** @type {TCustomConfig['webSearch']} */
const webSearchConfig = appConfig?.webSearch || {};
@@ -111,7 +112,7 @@ const verifyToolAuth = async (req, res) => {
*/
const callTool = async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const { toolId = '' } = req.params;
if (!fieldsMap[toolId]) {
logger.warn(`[${toolId}/call] User ${req.user.id} attempted call to invalid tool`);

View File

@@ -52,20 +52,7 @@ const startServer = async () => {
const appConfig = await getAppConfig();
await updateInterfacePermissions(appConfig);
const indexPath = path.join(appConfig.paths.dist, 'index.html');
let indexHTML = fs.readFileSync(indexPath, 'utf8');
// In order to provide support to serving the application in a sub-directory
// We need to update the base href if the DOMAIN_CLIENT is specified and not the root path
if (process.env.DOMAIN_CLIENT) {
const clientUrl = new URL(process.env.DOMAIN_CLIENT);
const baseHref = clientUrl.pathname.endsWith('/')
? clientUrl.pathname
: `${clientUrl.pathname}/`;
if (baseHref !== '/') {
logger.info(`Setting base href to ${baseHref}`);
indexHTML = indexHTML.replace(/base href="\/"/, `base href="${baseHref}"`);
}
}
const indexHTML = fs.readFileSync(indexPath, 'utf8');
app.get('/health', (_req, res) => res.status(200).send('OK'));
@@ -148,8 +135,7 @@ const startServer = async () => {
const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US';
const saneLang = lang.replace(/"/g, '&quot;');
let updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${saneLang}"`);
const updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${saneLang}"`);
res.type('html');
res.send(updatedIndexHtml);
});

View File

@@ -5,6 +5,7 @@ const mongoose = require('mongoose');
jest.mock('~/server/services/Config', () => ({
loadCustomConfig: jest.fn(() => Promise.resolve({})),
setAppConfig: jest.fn(),
getAppConfig: jest.fn().mockResolvedValue({
paths: {
uploads: '/tmp',

View File

@@ -1,6 +1,6 @@
const { logger } = require('@librechat/data-schemas');
const { countTokens, isEnabled, sendEvent } = require('@librechat/api');
const { isAssistantsEndpoint, ErrorTypes, Constants } = require('librechat-data-provider');
const { isAssistantsEndpoint, ErrorTypes } = require('librechat-data-provider');
const { truncateText, smartTruncateText } = require('~/app/clients/prompts');
const clearPendingReq = require('~/cache/clearPendingReq');
const { sendError } = require('~/server/middleware/error');
@@ -11,10 +11,6 @@ const { abortRun } = require('./abortRun');
const abortDataMap = new WeakMap();
/**
* @param {string} abortKey
* @returns {boolean}
*/
function cleanupAbortController(abortKey) {
if (!abortControllers.has(abortKey)) {
return false;
@@ -75,20 +71,6 @@ 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;
@@ -190,15 +172,11 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
/**
* @param {TMessage} userMessage
* @param {string} responseMessageId
* @param {boolean} [isNewConvo]
*/
const onStart = (userMessage, responseMessageId, isNewConvo) => {
const onStart = (userMessage, responseMessageId) => {
sendEvent(res, { message: userMessage, created: true });
const prelimAbortKey = userMessage?.conversationId ?? req.user.id;
const abortKey = isNewConvo
? `${prelimAbortKey}${Constants.COMMON_DIVIDER}${Constants.NEW_CONVO}`
: prelimAbortKey;
const abortKey = userMessage?.conversationId ?? req.user.id;
getReqData({ abortKey });
const prevRequest = abortControllers.get(abortKey);
const { overrideUserMessageId } = req?.body ?? {};
@@ -216,7 +194,16 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
};
abortControllers.set(addedAbortKey, { abortController, ...minimalOptions });
const cleanupHandler = createCleanUpHandler(addedAbortKey);
// Use a simple function for cleanup to avoid capturing context
const cleanupHandler = () => {
try {
cleanupAbortController(addedAbortKey);
} catch (e) {
// Ignore cleanup errors
}
};
res.on('finish', cleanupHandler);
return;
}
@@ -229,7 +216,16 @@ const createAbortController = (req, res, getAbortData, getReqData) => {
};
abortControllers.set(abortKey, { abortController, ...minimalOptions });
const cleanupHandler = createCleanUpHandler(abortKey);
// Use a simple function for cleanup to avoid capturing context
const cleanupHandler = () => {
try {
cleanupAbortController(abortKey);
} catch (e) {
// Ignore cleanup errors
}
};
res.on('finish', cleanupHandler);
};
@@ -368,7 +364,15 @@ const handleAbortError = async (res, req, error, data) => {
};
}
const callback = createCleanUpHandler(conversationId);
// Create a simple callback without capturing parent scope
const callback = async () => {
try {
cleanupAbortController(conversationId);
} catch (e) {
// Ignore cleanup errors
}
};
await sendError(req, res, options, callback);
};

View File

@@ -1,5 +1,6 @@
const { v4 } = require('uuid');
const { handleAbortError } = require('~/server/middleware/abortMiddleware');
const { getAppConfig } = require('~/server/services/Config/app');
/**
* Checks if the assistant is supported or excluded
@@ -12,7 +13,7 @@ const { handleAbortError } = require('~/server/middleware/abortMiddleware');
const validateAssistant = async (req, res, next) => {
const { endpoint, conversationId, assistant_id, messageId } = req.body;
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
/** @type {Partial<TAssistantEndpoint>} */
const assistantsConfig = appConfig.endpoints?.[endpoint];
if (!assistantsConfig) {

View File

@@ -1,4 +1,5 @@
const { SystemRoles } = require('librechat-data-provider');
const { getAppConfig } = require('~/server/services/Config/app');
const { getAssistant } = require('~/models/Assistant');
/**
@@ -20,7 +21,7 @@ const validateAuthor = async ({ req, openai, overrideEndpoint, overrideAssistant
const assistant_id =
overrideAssistantId ?? req.params.id ?? req.body.assistant_id ?? req.query.assistant_id;
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
/** @type {Partial<TAssistantEndpoint>} */
const assistantsConfig = appConfig.endpoints?.[endpoint];
if (!assistantsConfig) {

View File

@@ -10,6 +10,7 @@ const azureAssistants = require('~/server/services/Endpoints/azureAssistants');
const assistants = require('~/server/services/Endpoints/assistants');
const { processFiles } = require('~/server/services/Files/process');
const anthropic = require('~/server/services/Endpoints/anthropic');
const { getAppConfig } = require('~/server/services/Config/app');
const bedrock = require('~/server/services/Endpoints/bedrock');
const openAI = require('~/server/services/Endpoints/openAI');
const agents = require('~/server/services/Endpoints/agents');
@@ -40,7 +41,7 @@ async function buildEndpointOption(req, res, next) {
return handleError(res, { text: 'Error parsing conversation' });
}
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
if (appConfig.modelSpecs?.list && appConfig.modelSpecs?.enforce) {
/** @type {{ list: TModelSpec[] }}*/
const { list } = appConfig.modelSpecs;

View File

@@ -15,10 +15,10 @@ const { getAppConfig } = require('~/server/services/Config');
*/
const checkDomainAllowed = async (req, res, next = () => {}) => {
const email = req?.user?.email;
const appConfig = await getAppConfig({
const appConfig = getAppConfig({
role: req?.user?.role,
});
if (email && !isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
if (email && !(await isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains))) {
logger.error(`[Social Login] [Social Login not allowed] [Email: ${email}]`);
return res.redirect('/login');
} else {

View File

@@ -1,27 +0,0 @@
const { logger } = require('@librechat/data-schemas');
const { getAppConfig } = require('~/server/services/Config');
const configMiddleware = async (req, res, next) => {
try {
const userRole = req.user?.role;
req.config = await getAppConfig({ role: userRole });
next();
} catch (error) {
logger.error('Config middleware error:', {
error: error.message,
userRole: req.user?.role,
path: req.path,
});
try {
req.config = await getAppConfig();
next();
} catch (fallbackError) {
logger.error('Fallback config middleware error:', fallbackError);
next(fallbackError);
}
}
};
module.exports = configMiddleware;

View File

@@ -82,7 +82,7 @@ const sendError = async (req, res, options, callback) => {
/**
* Sends the response based on whether headers have been sent or not.
* @param {ServerRequest} req - The server response.
* @param {Express.Request} req - The server response.
* @param {Express.Response} res - The server response.
* @param {Object} data - The data to be sent.
* @param {string} [errorMessage] - The error message, if any.

View File

@@ -13,7 +13,6 @@ const requireLdapAuth = require('./requireLdapAuth');
const abortMiddleware = require('./abortMiddleware');
const checkInviteUser = require('./checkInviteUser');
const requireJwtAuth = require('./requireJwtAuth');
const configMiddleware = require('./config/app');
const validateModel = require('./validateModel');
const moderateText = require('./moderateText');
const logHeaders = require('./logHeaders');
@@ -44,7 +43,6 @@ module.exports = {
requireLocalAuth,
canDeleteAccount,
validateEndpoint,
configMiddleware,
concurrentLimiter,
checkDomainAllowed,
validateMessageReq,

View File

@@ -15,7 +15,7 @@ const { USE_REDIS, CONVO_ACCESS_VIOLATION_SCORE: score = 0 } = process.env ?? {}
* If the `cache` store is not available, the middleware will skip its logic.
*
* @function
* @param {ServerRequest} req - Express request object containing user information.
* @param {Express.Request} req - Express request object containing user information.
* @param {Express.Response} res - Express response object.
* @param {function} next - Express next middleware function.
* @throws {Error} Throws an error if the user doesn't have access to the conversation.

View File

@@ -6,7 +6,7 @@ const { logViolation } = require('~/cache');
* Validates the model of the request.
*
* @async
* @param {ServerRequest} req - The Express request object.
* @param {Express.Request} req - The Express request object.
* @param {Express.Response} res - The Express response object.
* @param {Function} next - The Express next function.
*/

View File

@@ -1,17 +1,15 @@
const express = require('express');
const { MongoMemoryServer } = require('mongodb-memory-server');
const request = require('supertest');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const express = require('express');
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', () => ({
@@ -38,7 +36,6 @@ jest.mock('~/models', () => ({
updateToken: jest.fn(),
createToken: jest.fn(),
deleteTokens: jest.fn(),
findPluginAuthsByKeys: jest.fn(),
}));
jest.mock('~/server/services/Config', () => ({
@@ -47,10 +44,6 @@ 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(),
@@ -73,10 +66,6 @@ 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;
@@ -688,13 +677,6 @@ 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');
@@ -719,7 +701,6 @@ 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');
@@ -778,18 +759,8 @@ 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');
@@ -805,6 +776,7 @@ describe('MCP Routes', () => {
'test-user-id',
'test-server',
);
expect(setCachedTools).toHaveBeenCalled();
});
it('should handle server with custom user variables', async () => {
@@ -826,38 +798,21 @@ describe('MCP Routes', () => {
require('~/config').getMCPManager.mockReturnValue(mockMcpManager);
require('~/config').getFlowStateManager.mockReturnValue({});
require('~/cache').getLogStores.mockReturnValue({});
require('@librechat/api').getUserMCPAuthMap.mockResolvedValue({
'mcp:test-server': {
API_KEY: 'api-key-value',
},
});
require('~/models').findPluginAuthsByKeys.mockResolvedValue([
{ key: 'API_KEY', value: 'api-key-value' },
]);
require('~/server/services/PluginService').getUserPluginAuthValue.mockResolvedValue(
'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('@librechat/api').getUserMCPAuthMap).toHaveBeenCalledWith({
userId: 'test-user-id',
servers: ['test-server'],
findPluginAuthsByKeys: require('~/models').findPluginAuthsByKeys,
});
expect(
require('~/server/services/PluginService').getUserPluginAuthValue,
).toHaveBeenCalledWith('test-user-id', 'API_KEY', false);
});
});

View File

@@ -16,6 +16,7 @@ const { getAgent, updateAgent, getListAgentsByAccess } = require('~/models/Agent
const { updateAction, getActions, deleteAction } = require('~/models/Action');
const { isActionDomainAllowed } = require('~/server/services/domains');
const { canAccessAgentResource } = require('~/server/middleware');
const { getAppConfig } = require('~/server/services/Config/app');
const { getRoleByName } = require('~/models/Role');
const router = express.Router();
@@ -83,7 +84,7 @@ router.post(
}
let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user.role });
const isDomainAllowed = await isActionDomainAllowed(
metadata.domain,
appConfig?.actions?.allowedDomains,

View File

@@ -4,7 +4,6 @@ const {
checkBan,
requireJwtAuth,
messageIpLimiter,
configMiddleware,
concurrentLimiter,
messageUserLimiter,
} = require('~/server/middleware');
@@ -23,8 +22,6 @@ router.use(uaParser);
router.use('/', v1);
const chatRouter = express.Router();
chatRouter.use(configMiddleware);
if (isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
chatRouter.use(concurrentLimiter);
}
@@ -40,4 +37,6 @@ if (isEnabled(LIMIT_MESSAGE_USER)) {
chatRouter.use('/', chat);
router.use('/chat', chatRouter);
// Add marketplace routes
module.exports = router;

View File

@@ -1,7 +1,7 @@
const express = require('express');
const { callTool, verifyToolAuth, getToolCalls } = require('~/server/controllers/tools');
const { getAvailableTools } = require('~/server/controllers/PluginController');
const { toolCallLimiter } = require('~/server/middleware');
const { toolCallLimiter } = require('~/server/middleware/limiters');
const router = express.Router();

View File

@@ -1,7 +1,7 @@
const express = require('express');
const { generateCheckAccess } = require('@librechat/api');
const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider');
const { requireJwtAuth, configMiddleware, canAccessAgentResource } = require('~/server/middleware');
const { requireJwtAuth, canAccessAgentResource } = require('~/server/middleware');
const v1 = require('~/server/controllers/agents/v1');
const { getRoleByName } = require('~/models/Role');
const actions = require('./actions');
@@ -36,17 +36,17 @@ router.use(requireJwtAuth);
* Agent actions route.
* @route GET|POST /agents/actions
*/
router.use('/actions', configMiddleware, actions);
router.use('/actions', actions);
/**
* Get a list of available tools for agents.
* @route GET /agents/tools
*/
router.use('/tools', configMiddleware, tools);
router.use('/tools', tools);
/**
* Get all agent categories with counts
* @route GET /agents/categories
* @route GET /agents/marketplace/categories
*/
router.get('/categories', v1.getAgentCategories);
/**

View File

@@ -1,12 +1,13 @@
const express = require('express');
const { nanoid } = require('nanoid');
const { logger } = require('@librechat/data-schemas');
const { actionDelimiter, EModelEndpoint, removeNullishValues } = require('librechat-data-provider');
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { updateAction, getActions, deleteAction } = require('~/models/Action');
const { updateAssistantDoc, getAssistant } = require('~/models/Assistant');
const { isActionDomainAllowed } = require('~/server/services/domains');
const { getAppConfig } = require('~/server/services/Config');
const { logger } = require('~/config');
const router = express.Router();
@@ -21,7 +22,7 @@ const router = express.Router();
*/
router.post('/:assistant_id', async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const { assistant_id } = req.params;
/** @type {{ functions: FunctionTool[], action_id: string, metadata: ActionMetadata }} */

View File

@@ -1,6 +1,6 @@
const express = require('express');
const { uaParser, checkBan, requireJwtAuth, configMiddleware } = require('~/server/middleware');
const router = express.Router();
const { uaParser, checkBan, requireJwtAuth } = require('~/server/middleware');
const { v1 } = require('./v1');
const chatV1 = require('./chatV1');
@@ -10,7 +10,6 @@ const chatV2 = require('./chatV2');
router.use(requireJwtAuth);
router.use(checkBan);
router.use(uaParser);
router.use(configMiddleware);
router.use('/v1/', v1);
router.use('/v1/chat', chatV1);
router.use('/v2/', v2);

View File

@@ -1,5 +1,4 @@
const express = require('express');
const { configMiddleware } = require('~/server/middleware');
const v1 = require('~/server/controllers/assistants/v1');
const v2 = require('~/server/controllers/assistants/v2');
const documents = require('./documents');
@@ -7,7 +6,6 @@ const actions = require('./actions');
const tools = require('./tools');
const router = express.Router();
router.use(configMiddleware);
/**
* Assistant actions route.

View File

@@ -17,12 +17,12 @@ const {
const { verify2FAWithTempToken } = require('~/server/controllers/auth/TwoFactorAuthController');
const { logoutController } = require('~/server/controllers/auth/LogoutController');
const { loginController } = require('~/server/controllers/auth/LoginController');
const { getAppConfig } = require('~/server/services/Config');
const { getBalanceConfig } = require('~/server/services/Config');
const middleware = require('~/server/middleware');
const { Balance } = require('~/db/models');
const setBalanceConfig = createSetBalanceConfig({
getAppConfig,
getBalanceConfig,
Balance,
});

View File

@@ -1,6 +1,6 @@
const express = require('express');
const { isEnabled } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { isEnabled, getBalanceConfig } = require('@librechat/api');
const {
Constants,
CacheKeys,
@@ -62,8 +62,6 @@ router.get('/', async function (req, res) {
!!process.env.SAML_CERT &&
!!process.env.SAML_SESSION_SECRET;
const balanceConfig = getBalanceConfig(appConfig);
/** @type {TStartupConfig} */
const payload = {
appTitle: process.env.APP_TITLE || 'LibreChat',
@@ -103,7 +101,7 @@ router.get('/', async function (req, res) {
interface: appConfig?.interfaceConfig,
turnstile: appConfig?.turnstileConfig,
modelSpecs: appConfig?.modelSpecs,
balance: balanceConfig,
balance: appConfig?.balance,
sharedLinksEnabled,
publicSharedLinksEnabled,
analyticsGtmId: process.env.ANALYTICS_GTM_ID,

View File

@@ -1,7 +1,9 @@
const express = require('express');
const endpointController = require('~/server/controllers/EndpointController');
const router = express.Router();
const endpointController = require('~/server/controllers/EndpointController');
const overrideController = require('~/server/controllers/OverrideController');
router.get('/', endpointController);
router.get('/config/override', overrideController);
module.exports = router;

View File

@@ -1,16 +1,17 @@
const fs = require('fs').promises;
const express = require('express');
const { logger } = require('@librechat/data-schemas');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
const { filterFile } = require('~/server/services/Files/process');
const { getAppConfig } = require('~/server/services/Config');
const { logger } = require('~/config');
const router = express.Router();
router.post('/', async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
filterFile({ req, file: req.file, image: true, isAvatar: true });
const userId = req.user.id;
const { manual } = req.body;

View File

@@ -26,6 +26,7 @@ const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud');
const { hasAccessToFilesViaAgent } = require('~/server/services/Files');
const { getFiles, batchUpdateFiles } = require('~/models/File');
const { getAppConfig } = require('~/server/services/Config');
const { cleanFileName } = require('~/server/utils/files');
const { getAssistant } = require('~/models/Assistant');
const { getAgent } = require('~/models/Agent');
@@ -36,7 +37,7 @@ const router = express.Router();
router.get('/', async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const files = await getFiles({ user: req.user.id });
if (appConfig.fileStrategy === FileSources.s3) {
try {
@@ -115,7 +116,7 @@ router.get('/agent/:agent_id', async (req, res) => {
router.get('/config', async (req, res) => {
try {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
res.status(200).json(appConfig.fileConfig);
} catch (error) {
logger.error('[/files] Error getting fileConfig', error);
@@ -387,8 +388,7 @@ router.post('/', async (req, res) => {
if (
error.message?.includes('Invalid file format') ||
error.message?.includes('No OCR result') ||
error.message?.includes('exceeds token limit')
error.message?.includes('No OCR result')
) {
message = error.message;
}

View File

@@ -1,19 +1,20 @@
const path = require('path');
const fs = require('fs').promises;
const express = require('express');
const { logger } = require('@librechat/data-schemas');
const { isAgentsEndpoint } = require('librechat-data-provider');
const {
filterFile,
processImageFile,
processAgentFileUpload,
} = require('~/server/services/Files/process');
const { getAppConfig } = require('~/server/services/Config');
const { logger } = require('~/config');
const router = express.Router();
router.post('/', async (req, res) => {
const appConfig = await getAppConfig({ role: req.user?.role });
const metadata = req.body;
const appConfig = req.config;
try {
filterFile({ req, image: true });
@@ -29,17 +30,6 @@ router.post('/', async (req, res) => {
} catch (error) {
// TODO: delete remote file if it exists
logger.error('[/files/images] Error processing file:', error);
let message = 'Error processing file';
if (
error.message?.includes('Invalid file format') ||
error.message?.includes('No OCR result') ||
error.message?.includes('exceeds token limit')
) {
message = error.message;
}
try {
const filepath = path.join(
appConfig.paths.imageOutput,
@@ -50,7 +40,7 @@ router.post('/', async (req, res) => {
} catch (error) {
logger.error('[/files/images] Error deleting file:', error);
}
res.status(500).json({ message });
res.status(500).json({ message: 'Error processing file' });
} finally {
try {
await fs.unlink(req.file.path);

View File

@@ -1,11 +1,5 @@
const express = require('express');
const {
createFileLimiters,
configMiddleware,
requireJwtAuth,
uaParser,
checkBan,
} = require('~/server/middleware');
const { uaParser, checkBan, requireJwtAuth, createFileLimiters } = require('~/server/middleware');
const { avatar: asstAvatarRouter } = require('~/server/routes/assistants/v1');
const { avatar: agentAvatarRouter } = require('~/server/routes/agents/v1');
const { createMulterInstance } = require('./multer');
@@ -18,7 +12,6 @@ const speech = require('./speech');
const initialize = async () => {
const router = express.Router();
router.use(requireJwtAuth);
router.use(configMiddleware);
router.use(checkBan);
router.use(uaParser);

View File

@@ -8,12 +8,17 @@ const { getAppConfig } = require('~/server/services/Config');
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const appConfig = req.config;
const outputPath = path.join(appConfig.paths.uploads, 'temp', req.user.id);
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
cb(null, outputPath);
getAppConfig({ role: req.user?.role })
.then((appConfig) => {
const outputPath = path.join(appConfig.paths.uploads, 'temp', req.user.id);
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
cb(null, outputPath);
})
.catch((error) => {
cb(error);
});
},
filename: function (req, file, cb) {
req.file_id = crypto.randomUUID();

View File

@@ -24,11 +24,6 @@ describe('Multer Configuration', () => {
user: { id: 'test-user-123' },
body: {},
originalUrl: '/api/files/upload',
config: {
paths: {
uploads: tempDir,
},
},
};
mockFile = {
@@ -37,6 +32,14 @@ describe('Multer Configuration', () => {
size: 1024,
};
// Mock getAppConfig to return paths
const { getAppConfig } = require('~/server/services/Config');
getAppConfig.mockResolvedValue({
paths: {
uploads: tempDir,
},
});
// Clear mocks
jest.clearAllMocks();
});
@@ -63,7 +66,12 @@ describe('Multer Configuration', () => {
it("should create directory recursively if it doesn't exist", (done) => {
const deepPath = path.join(tempDir, 'deep', 'nested', 'path');
mockReq.config.paths.uploads = deepPath;
const { getAppConfig } = require('~/server/services/Config');
getAppConfig.mockResolvedValue({
paths: {
uploads: deepPath,
},
});
const cb = jest.fn((err, destination) => {
expect(err).toBeNull();
@@ -446,15 +454,27 @@ describe('Multer Configuration', () => {
}).not.toThrow();
});
it('should handle file system errors when directory creation fails', () => {
it('should handle file system errors when directory creation fails', (done) => {
// Test with a non-existent parent directory to simulate fs issues
const invalidPath = '/nonexistent/path/that/should/not/exist';
mockReq.config.paths.uploads = invalidPath;
const { getAppConfig } = require('~/server/services/Config');
getAppConfig.mockResolvedValue({
paths: {
uploads: invalidPath,
},
});
// The current implementation doesn't catch errors, so they're thrown synchronously
expect(() => {
storage.getDestination(mockReq, mockFile, jest.fn());
}).toThrow();
// Call getDestination which should fail due to permission/path issues
storage.getDestination(mockReq, mockFile, (err, destination) => {
// Now we expect the error to be passed to the callback
expect(err).toBeDefined();
// This is the expected behavior - mkdirSync throws synchronously for invalid paths
// On Linux, this typically returns EACCES (permission denied)
// On macOS/Darwin, this returns ENOENT (no such file or directory)
expect(['EACCES', 'ENOENT']).toContain(err.code);
expect(destination).toBeUndefined();
done();
});
});
it('should handle malformed filenames with real sanitization', (done) => {

View File

@@ -1,15 +1,13 @@
const { Router } = require('express');
const { logger } = require('@librechat/data-schemas');
const { MCPOAuthHandler, getUserMCPAuthMap } = require('@librechat/api');
const { MCPOAuthHandler } = require('@librechat/api');
const { Router } = require('express');
const { getMCPSetupData, getServerConnectionStatus } = require('~/server/services/MCP');
const { findToken, updateToken, createToken, deleteTokens } = require('~/models');
const { updateMCPUserTools } = require('~/server/services/Config/mcpToolsCache');
const { setCachedTools, getCachedTools } = require('~/server/services/Config');
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();
@@ -144,12 +142,33 @@ 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();
await updateMCPUserTools({
userId: flowState.userId,
serverName,
tools,
});
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}`,
);
} else {
logger.debug(`[MCP OAuth] System-level OAuth completed for ${serverName}`);
}
@@ -304,39 +323,124 @@ 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}`,
);
/** @type {Record<string, Record<string, string>> | undefined} */
let userMCPAuthMap;
let customUserVars = {};
if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') {
userMCPAuthMap = await getUserMCPAuthMap({
userId: user.id,
servers: [serverName],
findPluginAuthsByKeys,
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;
},
});
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' });
}
}
const result = await reinitMCPServer({
req,
serverName,
userMCPAuthMap,
});
if (userConnection && !oauthRequired) {
const userTools = (await getCachedTools({ userId: user.id })) || {};
if (!result) {
return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' });
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 });
}
const { success, message, oauthRequired, oauthUrl } = result;
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}'`;
};
res.json({
success,
message,
oauthUrl,
success: Boolean((userConnection && !oauthRequired) || (oauthRequired && oauthUrl)),
message: getResponseMessage(),
serverName,
oauthRequired,
oauthUrl,
});
} catch (error) {
logger.error('[MCP Reinitialize] Unexpected error', error);

View File

@@ -8,7 +8,8 @@ const {
deleteMemory,
setMemory,
} = require('~/models');
const { requireJwtAuth, configMiddleware } = require('~/server/middleware');
const { getAppConfig } = require('~/server/services/Config');
const { requireJwtAuth } = require('~/server/middleware');
const { getRoleByName } = require('~/models/Role');
const router = express.Router();
@@ -48,7 +49,7 @@ router.use(requireJwtAuth);
* Returns all memories for the authenticated user, sorted by updated_at (newest first).
* Also includes memory usage percentage based on token limit.
*/
router.get('/', checkMemoryRead, configMiddleware, async (req, res) => {
router.get('/', checkMemoryRead, async (req, res) => {
try {
const memories = await getAllUserMemories(req.user.id);
@@ -60,7 +61,7 @@ router.get('/', checkMemoryRead, configMiddleware, async (req, res) => {
return sum + (memory.tokenCount || 0);
}, 0);
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const memoryConfig = appConfig?.memory;
const tokenLimit = memoryConfig?.tokenLimit;
const charLimit = memoryConfig?.charLimit || 10000;
@@ -88,7 +89,7 @@ router.get('/', checkMemoryRead, configMiddleware, async (req, res) => {
* Body: { key: string, value: string }
* Returns 201 and { created: true, memory: <createdDoc> } when successful.
*/
router.post('/', memoryPayloadLimit, checkMemoryCreate, configMiddleware, async (req, res) => {
router.post('/', memoryPayloadLimit, checkMemoryCreate, async (req, res) => {
const { key, value } = req.body;
if (typeof key !== 'string' || key.trim() === '') {
@@ -99,7 +100,7 @@ router.post('/', memoryPayloadLimit, checkMemoryCreate, configMiddleware, async
return res.status(400).json({ error: 'Value is required and must be a non-empty string.' });
}
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const memoryConfig = appConfig?.memory;
const charLimit = memoryConfig?.charLimit || 10000;
@@ -120,7 +121,7 @@ router.post('/', memoryPayloadLimit, checkMemoryCreate, configMiddleware, async
const memories = await getAllUserMemories(req.user.id);
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const memoryConfig = appConfig?.memory;
const tokenLimit = memoryConfig?.tokenLimit;
@@ -196,7 +197,7 @@ router.patch('/preferences', checkMemoryOptOut, async (req, res) => {
* Body: { key?: string, value: string }
* Returns 200 and { updated: true, memory: <updatedDoc> } when successful.
*/
router.patch('/:key', memoryPayloadLimit, checkMemoryUpdate, configMiddleware, async (req, res) => {
router.patch('/:key', memoryPayloadLimit, checkMemoryUpdate, async (req, res) => {
const { key: urlKey } = req.params;
const { key: bodyKey, value } = req.body || {};
@@ -205,7 +206,7 @@ router.patch('/:key', memoryPayloadLimit, checkMemoryUpdate, configMiddleware, a
}
const newKey = bodyKey || urlKey;
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const memoryConfig = appConfig?.memory;
const charLimit = memoryConfig?.charLimit || 10000;

View File

@@ -8,11 +8,11 @@ const { isEnabled, createSetBalanceConfig } = require('@librechat/api');
const { checkDomainAllowed, loginLimiter, logHeaders, checkBan } = require('~/server/middleware');
const { syncUserEntraGroupMemberships } = require('~/server/services/PermissionService');
const { setAuthTokens, setOpenIDAuthTokens } = require('~/server/services/AuthService');
const { getAppConfig } = require('~/server/services/Config');
const { getBalanceConfig } = require('~/server/services/Config');
const { Balance } = require('~/db/models');
const setBalanceConfig = createSetBalanceConfig({
getAppConfig,
getBalanceConfig,
Balance,
});

View File

@@ -1,6 +1,6 @@
const express = require('express');
const { getAvailablePluginsController } = require('~/server/controllers/PluginController');
const { requireJwtAuth } = require('~/server/middleware');
const { getAvailablePluginsController } = require('../controllers/PluginController');
const requireJwtAuth = require('../middleware/requireJwtAuth');
const router = express.Router();

View File

@@ -424,7 +424,7 @@ router.get('/', async (req, res) => {
/**
* Deletes a prompt
*
* @param {ServerRequest} req - The request object.
* @param {Express.Request} req - The request object.
* @param {TDeletePromptVariables} req.params - The request parameters
* @param {import('mongoose').ObjectId} req.params.promptId - The prompt ID
* @param {Express.Response} res - The response object.

View File

@@ -1,14 +1,14 @@
const express = require('express');
const { requireJwtAuth, canDeleteAccount, verifyEmailLimiter } = require('~/server/middleware');
const {
getUserController,
deleteUserController,
verifyEmailController,
updateUserPluginsController,
resendVerificationController,
getTermsStatusController,
acceptTermsController,
verifyEmailController,
deleteUserController,
getUserController,
} = require('~/server/controllers/UserController');
const { requireJwtAuth, canDeleteAccount, verifyEmailLimiter } = require('~/server/middleware');
const router = express.Router();

View File

@@ -246,7 +246,7 @@ describe('AppService', () => {
});
it('should load and format tools accurately with defined structure', async () => {
const { loadAndFormatTools } = require('./start/tools');
const { loadAndFormatTools } = require('./ToolService');
const result = await AppService();
@@ -254,6 +254,8 @@ describe('AppService', () => {
adminFilter: undefined,
adminIncluded: undefined,
directory: expect.anything(),
imageOutputType: expect.any(String),
fileStrategy: expect.any(String),
});
// Verify tools are included in the returned config

View File

@@ -16,6 +16,7 @@ const { retrieveAndProcessFile } = require('~/server/services/Files/process');
const { processRequiredActions } = require('~/server/services/ToolService');
const { RunManager, waitForRun } = require('~/server/services/Runs');
const { processMessages } = require('~/server/services/Threads');
const { getAppConfig } = require('~/server/services/Config');
const { createOnProgress } = require('~/server/utils');
const { TextStream } = require('~/app/clients');
@@ -350,7 +351,7 @@ async function runAssistant({
accumulatedMessages = [],
in_progress: inProgress,
}) {
const appConfig = openai.req.config;
const appConfig = await getAppConfig({ role: openai.req.user?.role });
let steps = accumulatedSteps;
let messages = accumulatedMessages;
const in_progress = inProgress ?? createInProgressHandler(openai, thread_id, messages);

View File

@@ -78,7 +78,7 @@ const createTokenHash = () => {
/**
* Send Verification Email
* @param {Partial<IUser>} user
* @param {Partial<MongoUser> & { _id: ObjectId, email: string, name: string}} user
* @returns {Promise<void>}
*/
const sendVerificationEmail = async (user) => {
@@ -112,7 +112,7 @@ const sendVerificationEmail = async (user) => {
/**
* Verify Email
* @param {ServerRequest} req
* @param {Express.Request} req
*/
const verifyEmail = async (req) => {
const { email, token } = req.body;
@@ -160,9 +160,9 @@ const verifyEmail = async (req) => {
/**
* Register a new user.
* @param {IUser} user <email, password, name, username>
* @param {Partial<IUser>} [additionalData={}]
* @returns {Promise<{status: number, message: string, user?: IUser}>}
* @param {MongoUser} user <email, password, name, username>
* @param {Partial<MongoUser>} [additionalData={}]
* @returns {Promise<{status: number, message: string, user?: MongoUser}>}
*/
const registerUser = async (user, additionalData = {}) => {
const { error } = registerSchema.safeParse(user);
@@ -196,7 +196,7 @@ const registerUser = async (user, additionalData = {}) => {
}
const appConfig = await getAppConfig({ role: user.role });
if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
if (!(await isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains))) {
const errorMessage =
'The email address provided cannot be used. Please use a different email address.';
logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`);
@@ -248,7 +248,7 @@ const registerUser = async (user, additionalData = {}) => {
/**
* Request password reset
* @param {ServerRequest} req
* @param {Express.Request} req
*/
const requestPasswordReset = async (req) => {
const { email } = req.body;
@@ -500,6 +500,18 @@ const resendVerificationEmail = async (req) => {
};
}
};
/**
* Generate a short-lived JWT token
* @param {String} userId - The ID of the user
* @param {String} [expireIn='5m'] - The expiration time for the token (default is 5 minutes)
* @returns {String} - The generated JWT token
*/
const generateShortLivedToken = (userId, expireIn = '5m') => {
return jwt.sign({ id: userId }, process.env.JWT_SECRET, {
expiresIn: expireIn,
algorithm: 'HS256',
});
};
module.exports = {
logoutUser,
@@ -510,4 +522,5 @@ module.exports = {
setOpenIDAuthTokens,
requestPasswordReset,
resendVerificationEmail,
generateShortLivedToken,
};

View File

@@ -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<LCAvailableTools|null>} The available tools object or null if not cached
* @returns {Promise<Object|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) {
/** @type {LCAvailableTools | null} Check if we have pre-computed effective tools for this user */
// Check if we have pre-computed effective tools for this user
const effectiveTools = await cache.get(ToolCacheKeys.EFFECTIVE(userId));
if (effectiveTools) {
return effectiveTools;
}
/** @type {LCAvailableTools | null} Otherwise, compute from individual sources */
// Otherwise, compute from individual sources
const toolSources = [];
if (includeGlobal) {

View File

@@ -22,7 +22,7 @@ async function getEndpointsConfig(req) {
return cachedEndpointsConfig;
}
const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
const appConfig = await getAppConfig({ role: req.user?.role });
const defaultEndpointsConfig = await loadDefaultEndpointsConfig(appConfig);
const customEndpointsConfig = loadCustomEndpointsConfig(appConfig?.endpoints?.custom);

View File

@@ -1,11 +1,11 @@
const appConfig = require('./app');
const { config } = require('./EndpointService');
const getCachedTools = require('./getCachedTools');
const mcpToolsCache = require('./mcpToolsCache');
const loadCustomConfig = require('./loadCustomConfig');
const loadConfigModels = require('./loadConfigModels');
const loadDefaultModels = require('./loadDefaultModels');
const getEndpointsConfig = require('./getEndpointsConfig');
const loadOverrideConfig = require('./loadOverrideConfig');
const loadAsyncEndpoints = require('./loadAsyncEndpoints');
module.exports = {
@@ -13,9 +13,9 @@ module.exports = {
loadCustomConfig,
loadConfigModels,
loadDefaultModels,
loadOverrideConfig,
loadAsyncEndpoints,
...appConfig,
...getCachedTools,
...mcpToolsCache,
...getEndpointsConfig,
};

View File

@@ -6,7 +6,7 @@ const { getAppConfig } = require('./app');
/**
* Load config endpoints from the cached configuration object
* @function loadConfigModels
* @param {ServerRequest} req - The Express request object.
* @param {Express.Request} req - The Express request object.
*/
async function loadConfigModels(req) {
const appConfig = await getAppConfig({ role: req.user?.role });
@@ -72,11 +72,10 @@ async function loadConfigModels(req) {
fetchPromisesMap[uniqueKey] =
fetchPromisesMap[uniqueKey] ||
fetchModels({
name,
apiKey: API_KEY,
baseURL: BASE_URL,
user: req.user.id,
direct: endpoint.directEndpoint,
baseURL: BASE_URL,
apiKey: API_KEY,
name,
userIdQuery: models.userIdQuery,
});
uniqueKeyToEndpointsMap[uniqueKey] = uniqueKeyToEndpointsMap[uniqueKey] || [];

View File

@@ -11,7 +11,7 @@ const {
* Loads the default models for the application.
* @async
* @function
* @param {ServerRequest} req - The Express request object.
* @param {Express.Request} req - The Express request object.
*/
async function loadDefaultModels(req) {
try {

View File

@@ -0,0 +1,6 @@
// fetch some remote config
async function loadOverrideConfig() {
return false;
}
module.exports = loadOverrideConfig;

View File

@@ -1,143 +0,0 @@
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,
};

View File

@@ -16,6 +16,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getProviderConfig } = require('~/server/services/Endpoints');
const { processFiles } = require('~/server/services/Files/process');
const { getFiles, getToolFilesByIds } = require('~/models/File');
const { getAppConfig } = require('~/server/services/Config');
const { getConvoFiles } = require('~/models/Conversation');
const { getModelMaxTokens } = require('~/utils');
@@ -30,13 +31,7 @@ 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,
* userMCPAuthMap?: Record<string, Record<string, string>>
* }>}
* @returns {Promise<Agent & { tools: StructuredTool[], attachments: Array<MongoFile>, toolContextMap: Record<string, unknown>, maxContextTokens: number }>}
*/
const initializeAgent = async ({
req,
@@ -49,7 +44,7 @@ const initializeAgent = async ({
allowedProviders,
isInitialAgent = false,
}) => {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
if (
isAgentsEndpoint(endpointOption?.endpoint) &&
allowedProviders.size > 0 &&
@@ -99,19 +94,16 @@ const initializeAgent = async ({
});
const provider = agent.provider;
const {
tools: structuredTools,
toolContextMap,
userMCPAuthMap,
} = (await loadTools?.({
req,
res,
provider,
agentId: agent.id,
tools: agent.tools,
model: agent.model,
tool_resources,
})) ?? {};
const { tools: structuredTools, toolContextMap } =
(await loadTools?.({
req,
res,
provider,
agentId: agent.id,
tools: agent.tools,
model: agent.model,
tool_resources,
})) ?? {};
agent.endpoint = provider;
const { getOptions, overrideProvider } = getProviderConfig({ provider, appConfig });
@@ -200,7 +192,6 @@ const initializeAgent = async ({
tools,
attachments,
resendFiles,
userMCPAuthMap,
toolContextMap,
useLegacyContent: !!options.useLegacyContent,
maxContextTokens: Math.round((agentMaxContextTokens - maxTokens) * 0.9),

View File

@@ -15,13 +15,11 @@ const { initializeAgent } = require('~/server/services/Endpoints/agents/agent');
const { getModelsConfig } = require('~/server/controllers/ModelController');
const { loadAgentTools } = require('~/server/services/ToolService');
const AgentClient = require('~/server/controllers/agents/client');
const { getAppConfig } = require('~/server/services/Config');
const { getAgent } = require('~/models/Agent');
const { logViolation } = require('~/cache');
/**
* @param {AbortSignal} signal
*/
function createToolLoader(signal) {
function createToolLoader() {
/**
* @param {object} params
* @param {ServerRequest} params.req
@@ -31,11 +29,7 @@ function createToolLoader(signal) {
* @param {string} params.provider
* @param {string} params.model
* @param {AgentToolResources} params.tool_resources
* @returns {Promise<{
* tools: StructuredTool[],
* toolContextMap: Record<string, unknown>,
* userMCPAuthMap?: Record<string, Record<string, string>>
* } | undefined>}
* @returns {Promise<{ tools: StructuredTool[], toolContextMap: Record<string, unknown> } | undefined>}
*/
return async function loadTools({ req, res, agentId, tools, provider, model, tool_resources }) {
const agent = { id: agentId, tools, provider, model };
@@ -44,7 +38,6 @@ function createToolLoader(signal) {
req,
res,
agent,
signal,
tool_resources,
});
} catch (error) {
@@ -53,11 +46,11 @@ function createToolLoader(signal) {
};
}
const initializeClient = async ({ req, res, signal, endpointOption }) => {
const initializeClient = async ({ req, res, endpointOption }) => {
if (!endpointOption) {
throw new Error('Endpoint option not provided');
}
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
// TODO: use endpointOption to determine options/modelOptions
/** @type {Array<UsageMetadata>} */
@@ -99,7 +92,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
const agentConfigs = new Map();
const allowedProviders = new Set(appConfig?.endpoints?.[EModelEndpoint.agents]?.allowedProviders);
const loadTools = createToolLoader(signal);
const loadTools = createToolLoader();
/** @type {Array<MongoFile>} */
const requestFiles = req.body.files ?? [];
/** @type {string} */
@@ -118,7 +111,6 @@ const initializeClient = async ({ req, res, signal, 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 });
@@ -148,11 +140,6 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
endpointOption,
allowedProviders,
});
if (userMCPAuthMap != null) {
Object.assign(userMCPAuthMap, config.userMCPAuthMap ?? {});
} else {
userMCPAuthMap = config.userMCPAuthMap;
}
agentConfigs.set(agentId, config);
}
}
@@ -204,7 +191,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
: EModelEndpoint.agents,
});
return { client, userMCPAuthMap };
return { client };
};
module.exports = { initializeClient };

View File

@@ -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

View File

@@ -6,7 +6,6 @@ const buildOptions = (endpoint, parsedBody) => {
modelLabel,
promptPrefix,
maxContextTokens,
fileTokenLimit,
resendFiles = anthropicSettings.resendFiles.default,
promptCache = anthropicSettings.promptCache.default,
thinking = anthropicSettings.thinking.default,
@@ -30,7 +29,6 @@ const buildOptions = (endpoint, parsedBody) => {
greeting,
spec,
maxContextTokens,
fileTokenLimit,
modelOptions,
});

View File

@@ -2,9 +2,10 @@ const { EModelEndpoint } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getLLMConfig } = require('~/server/services/Endpoints/anthropic/llm');
const AnthropicClient = require('~/app/clients/AnthropicClient');
const { getAppConfig } = require('~/server/services/Config');
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
const expiresAt = req.body.key;
const isUserProvided = ANTHROPIC_API_KEY === 'user_provided';
@@ -39,9 +40,8 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
if (optionsOnly) {
clientOptions = Object.assign(
{
proxy: PROXY ?? null,
userId: req.user.id,
reverseProxyUrl: ANTHROPIC_REVERSE_PROXY ?? null,
proxy: PROXY ?? null,
modelOptions: endpointOption?.model_parameters ?? {},
},
clientOptions,

View File

@@ -15,7 +15,6 @@ 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.
*
@@ -48,11 +47,6 @@ function getLLMConfig(apiKey, options = {}) {
maxTokens:
mergedOptions.maxOutputTokens || anthropicSettings.maxOutputTokens.reset(mergedOptions.model),
clientOptions: {},
invocationKwargs: {
metadata: {
user_id: options.userId,
},
},
};
requestOptions = configureReasoning(requestOptions, systemOptions);

View File

@@ -7,6 +7,7 @@ const {
getUserKeyValues,
getUserKeyExpiry,
} = require('~/server/services/UserService');
const { getAppConfig } = require('~/server/services/Config');
const OAIClient = require('~/app/clients/OpenAIClient');
class Files {
@@ -48,7 +49,7 @@ class Files {
}
const initializeClient = async ({ req, res, version, endpointOption, initAppClient = false }) => {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const { PROXY, OPENAI_ORGANIZATION, AZURE_ASSISTANTS_API_KEY, AZURE_ASSISTANTS_BASE_URL } =
process.env;

View File

@@ -1,6 +1,6 @@
// const OpenAI = require('openai');
const { ProxyAgent } = require('undici');
const { ErrorTypes, EModelEndpoint } = require('librechat-data-provider');
const { ErrorTypes } = require('librechat-data-provider');
const { getUserKey, getUserKeyExpiry, getUserKeyValues } = require('~/server/services/UserService');
const initializeClient = require('./initialize');
// const { OpenAIClient } = require('~/app');
@@ -12,7 +12,14 @@ jest.mock('~/server/services/UserService', () => ({
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
}));
// Config is now passed via req.config, not getAppConfig
jest.mock('~/server/services/Config', () => ({
getAppConfig: jest.fn().mockResolvedValue({
azureAssistants: {
apiKey: 'test-key',
baseURL: 'https://test.url',
},
}),
}));
const today = new Date();
const tenDaysFromToday = new Date(today.setDate(today.getDate() + 10));
@@ -43,11 +50,7 @@ describe('initializeClient', () => {
isUserProvided: jest.fn().mockReturnValueOnce(false),
}));
const req = {
user: { id: 'user123' },
app,
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
};
const req = { user: { id: 'user123' }, app };
const res = {};
const { openai, openAIApiKey } = await initializeClient({ req, res });
@@ -63,11 +66,7 @@ describe('initializeClient', () => {
getUserKeyValues.mockResolvedValue({ apiKey: 'user-api-key', baseURL: 'https://user.api.url' });
getUserKeyExpiry.mockResolvedValue(isoString);
const req = {
user: { id: 'user123' },
app,
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
};
const req = { user: { id: 'user123' }, app };
const res = {};
const { openai, openAIApiKey } = await initializeClient({ req, res });
@@ -84,7 +83,7 @@ describe('initializeClient', () => {
let userValues = getUserKey();
try {
userValues = JSON.parse(userValues);
} catch {
} catch (e) {
throw new Error(
JSON.stringify({
type: ErrorTypes.INVALID_USER_KEY,
@@ -94,10 +93,7 @@ describe('initializeClient', () => {
return userValues;
});
const req = {
user: { id: 'user123' },
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
};
const req = { user: { id: 'user123' } };
const res = {};
await expect(initializeClient({ req, res })).rejects.toThrow(/invalid_user_key/);
@@ -106,11 +102,7 @@ describe('initializeClient', () => {
test('throws error if API key is not provided', async () => {
delete process.env.AZURE_ASSISTANTS_API_KEY; // Simulate missing API key
const req = {
user: { id: 'user123' },
app,
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
};
const req = { user: { id: 'user123' }, app };
const res = {};
await expect(initializeClient({ req, res })).rejects.toThrow(/Assistants API key not/);
@@ -120,11 +112,7 @@ describe('initializeClient', () => {
process.env.AZURE_ASSISTANTS_API_KEY = 'test-key';
process.env.PROXY = 'http://proxy.server';
const req = {
user: { id: 'user123' },
app,
config: { endpoints: { [EModelEndpoint.azureOpenAI]: {} } },
};
const req = { user: { id: 'user123' }, app };
const res = {};
const { openai } = await initializeClient({ req, res });

View File

@@ -6,7 +6,6 @@ const buildOptions = (endpoint, parsedBody) => {
modelLabel: name,
promptPrefix,
maxContextTokens,
fileTokenLimit,
resendFiles = true,
imageDetail,
iconURL,
@@ -25,7 +24,6 @@ const buildOptions = (endpoint, parsedBody) => {
spec,
promptPrefix,
maxContextTokens,
fileTokenLimit,
model_parameters,
});

View File

@@ -9,9 +9,10 @@ const {
removeNullishValues,
} = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getAppConfig } = require('~/server/services/Config');
const getOptions = async ({ req, overrideModel, endpointOption }) => {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const {
BEDROCK_AWS_SECRET_ACCESS_KEY,
BEDROCK_AWS_ACCESS_KEY_ID,

View File

@@ -7,7 +7,6 @@ const buildOptions = (endpoint, parsedBody, endpointType) => {
chatGptLabel,
promptPrefix,
maxContextTokens,
fileTokenLimit,
resendFiles = true,
imageDetail,
iconURL,
@@ -28,7 +27,6 @@ const buildOptions = (endpoint, parsedBody, endpointType) => {
greeting,
spec,
maxContextTokens,
fileTokenLimit,
modelOptions,
});

View File

@@ -1,11 +1,6 @@
const { Providers } = require('@librechat/agents');
const {
resolveHeaders,
isUserProvided,
getOpenAIConfig,
getCustomEndpointConfig,
createHandleLLMNewToken,
} = require('@librechat/api');
const { isUserProvided, getCustomEndpointConfig } = require('@librechat/api');
const { getOpenAIConfig, createHandleLLMNewToken, resolveHeaders } = require('@librechat/api');
const {
CacheKeys,
ErrorTypes,
@@ -15,13 +10,14 @@ const {
} = require('librechat-data-provider');
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
const { fetchModels } = require('~/server/services/ModelService');
const { getAppConfig } = require('~/server/services/Config');
const OpenAIClient = require('~/app/clients/OpenAIClient');
const getLogStores = require('~/cache/getLogStores');
const { PROXY } = process.env;
const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrideEndpoint }) => {
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const { key: expiresAt } = req.body;
const endpoint = overrideEndpoint ?? req.body.endpoint;

View File

@@ -18,7 +18,14 @@ jest.mock('~/server/services/UserService', () => ({
checkUserKeyExpiry: jest.fn(),
}));
// Config is now passed via req.config, not getAppConfig
jest.mock('~/server/services/Config', () => ({
getAppConfig: jest.fn().mockResolvedValue({
'test-endpoint': {
apiKey: 'test-key',
baseURL: 'https://test.com',
},
}),
}));
jest.mock('~/server/services/ModelService', () => ({
fetchModels: jest.fn(),
@@ -41,13 +48,6 @@ describe('custom/initializeClient', () => {
body: { endpoint: 'test-endpoint' },
user: { id: 'user-123', email: 'test@example.com', role: 'user' },
app: { locals: {} },
config: {
endpoints: {
all: {
streamRate: 25,
},
},
},
};
const mockResponse = {};
@@ -64,9 +64,6 @@ describe('custom/initializeClient', () => {
getOpenAIConfig.mockReturnValue({
useLegacyContent: true,
endpointTokenConfig: null,
llmConfig: {
callbacks: [],
},
});
});

View File

@@ -12,7 +12,6 @@ const buildOptions = (endpoint, parsedBody) => {
spec,
artifacts,
maxContextTokens,
fileTokenLimit,
...modelOptions
} = parsedBody;
const endpointOption = removeNullishValues({
@@ -25,7 +24,6 @@ const buildOptions = (endpoint, parsedBody) => {
greeting,
spec,
maxContextTokens,
fileTokenLimit,
modelOptions,
});

View File

@@ -2,6 +2,7 @@ const path = require('path');
const { EModelEndpoint, AuthKeys } = require('librechat-data-provider');
const { getGoogleConfig, isEnabled, loadServiceKey } = require('@librechat/api');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getAppConfig } = require('~/server/services/Config');
const { GoogleClient } = require('~/app');
const initializeClient = async ({ req, res, endpointOption, overrideModel, optionsOnly }) => {
@@ -46,7 +47,7 @@ const initializeClient = async ({ req, res, endpointOption, overrideModel, optio
let clientOptions = {};
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
/** @type {undefined | TBaseEndpoint} */
const allConfig = appConfig.endpoints?.all;
/** @type {undefined | TBaseEndpoint} */

View File

@@ -8,7 +8,13 @@ jest.mock('~/server/services/UserService', () => ({
getUserKey: jest.fn().mockImplementation(() => ({})),
}));
// Config is now passed via req.config, not getAppConfig
jest.mock('~/server/services/Config', () => ({
getAppConfig: jest.fn().mockResolvedValue({
google: {
apiKey: 'test-key',
},
}),
}));
const app = { locals: {} };
@@ -28,12 +34,6 @@ describe('google/initializeClient', () => {
body: { key: expiresAt },
user: { id: '123' },
app,
config: {
endpoints: {
all: {},
google: {},
},
},
};
const res = {};
const endpointOption = { modelOptions: { model: 'default-model' } };
@@ -56,12 +56,6 @@ describe('google/initializeClient', () => {
body: { key: null },
user: { id: '123' },
app,
config: {
endpoints: {
all: {},
google: {},
},
},
};
const res = {};
const endpointOption = { modelOptions: { model: 'default-model' } };
@@ -85,12 +79,6 @@ describe('google/initializeClient', () => {
body: { key: expiresAt },
user: { id: '123' },
app,
config: {
endpoints: {
all: {},
google: {},
},
},
};
const res = {};
const endpointOption = { modelOptions: { model: 'default-model' } };

View File

@@ -1,5 +1,6 @@
const { isEnabled } = require('@librechat/api');
const { EModelEndpoint, CacheKeys, Constants, googleSettings } = require('librechat-data-provider');
const { getAppConfig } = require('~/server/services/Config');
const getLogStores = require('~/cache/getLogStores');
const initializeClient = require('./initialize');
const { saveConvo } = require('~/models');
@@ -14,7 +15,7 @@ const addTitle = async (req, { text, response, client }) => {
return;
}
const { GOOGLE_TITLE_MODEL } = process.env ?? {};
const appConfig = req.config;
const appConfig = await getAppConfig({ role: req.user?.role });
const providerConfig = appConfig.endpoints?.[EModelEndpoint.google];
let model =
providerConfig?.titleModel ??

Some files were not shown because too many files have changed in this diff Show More