mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
💬 refactor: MCP Chat Visibility Option, Google Rates, Remove OpenAPI Plugins (#7286)
* fix: Update Gemini 2.5 Pro Preview Model Name in Token Values * refactor: Update DeleteButton to close menu when deletion is successful * refactor: Add unmountOnHide prop to DropdownPopup in multiple components * chore: linting * chore: linting * feat: Add `chatMenu` option for MCP Servers to control visibility in MCPSelect dropdown * refactor: Update loadManifestTools to return combined tool manifest with MCP tools first * chore: remove deprecated openapi plugins * chore: linting * chore(AgentClient): linting, remove unnecessary `checkVisionRequest` logger * refactor(AuthService): change logoutUser logging from error to debug level * chore: new Gemini models token values and rates * chore(AskController): linting
This commit is contained in:
parent
d7390d24ec
commit
66093b1eb3
26 changed files with 80 additions and 402 deletions
|
|
@ -63,15 +63,15 @@ class BaseClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
setOptions() {
|
setOptions() {
|
||||||
throw new Error('Method \'setOptions\' must be implemented.');
|
throw new Error("Method 'setOptions' must be implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletion() {
|
async getCompletion() {
|
||||||
throw new Error('Method \'getCompletion\' must be implemented.');
|
throw new Error("Method 'getCompletion' must be implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendCompletion() {
|
async sendCompletion() {
|
||||||
throw new Error('Method \'sendCompletion\' must be implemented.');
|
throw new Error("Method 'sendCompletion' must be implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
getSaveOptions() {
|
getSaveOptions() {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
const { loadSpecs } = require('./loadSpecs');
|
|
||||||
|
|
||||||
function transformSpec(input) {
|
|
||||||
return {
|
|
||||||
name: input.name_for_human,
|
|
||||||
pluginKey: input.name_for_model,
|
|
||||||
description: input.description_for_human,
|
|
||||||
icon: input?.logo_url ?? 'https://placehold.co/70x70.png',
|
|
||||||
// TODO: add support for authentication
|
|
||||||
isAuthRequired: 'false',
|
|
||||||
authConfig: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addOpenAPISpecs(availableTools) {
|
|
||||||
try {
|
|
||||||
const specs = (await loadSpecs({})).map(transformSpec);
|
|
||||||
if (specs.length > 0) {
|
|
||||||
return [...specs, ...availableTools];
|
|
||||||
}
|
|
||||||
return availableTools;
|
|
||||||
} catch (error) {
|
|
||||||
return availableTools;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
transformSpec,
|
|
||||||
addOpenAPISpecs,
|
|
||||||
};
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
const { addOpenAPISpecs, transformSpec } = require('./addOpenAPISpecs');
|
|
||||||
const { loadSpecs } = require('./loadSpecs');
|
|
||||||
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
|
|
||||||
|
|
||||||
jest.mock('./loadSpecs');
|
|
||||||
jest.mock('../dynamic/OpenAPIPlugin');
|
|
||||||
|
|
||||||
describe('transformSpec', () => {
|
|
||||||
it('should transform input spec to a desired format', () => {
|
|
||||||
const input = {
|
|
||||||
name_for_human: 'Human Name',
|
|
||||||
name_for_model: 'Model Name',
|
|
||||||
description_for_human: 'Human Description',
|
|
||||||
logo_url: 'https://example.com/logo.png',
|
|
||||||
};
|
|
||||||
|
|
||||||
const expectedOutput = {
|
|
||||||
name: 'Human Name',
|
|
||||||
pluginKey: 'Model Name',
|
|
||||||
description: 'Human Description',
|
|
||||||
icon: 'https://example.com/logo.png',
|
|
||||||
isAuthRequired: 'false',
|
|
||||||
authConfig: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(transformSpec(input)).toEqual(expectedOutput);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use default icon if logo_url is not provided', () => {
|
|
||||||
const input = {
|
|
||||||
name_for_human: 'Human Name',
|
|
||||||
name_for_model: 'Model Name',
|
|
||||||
description_for_human: 'Human Description',
|
|
||||||
};
|
|
||||||
|
|
||||||
const expectedOutput = {
|
|
||||||
name: 'Human Name',
|
|
||||||
pluginKey: 'Model Name',
|
|
||||||
description: 'Human Description',
|
|
||||||
icon: 'https://placehold.co/70x70.png',
|
|
||||||
isAuthRequired: 'false',
|
|
||||||
authConfig: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(transformSpec(input)).toEqual(expectedOutput);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('addOpenAPISpecs', () => {
|
|
||||||
it('should add specs to available tools', async () => {
|
|
||||||
const availableTools = ['Tool1', 'Tool2'];
|
|
||||||
const specs = [
|
|
||||||
{
|
|
||||||
name_for_human: 'Human Name',
|
|
||||||
name_for_model: 'Model Name',
|
|
||||||
description_for_human: 'Human Description',
|
|
||||||
logo_url: 'https://example.com/logo.png',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
loadSpecs.mockResolvedValue(specs);
|
|
||||||
createOpenAPIPlugin.mockReturnValue('Plugin');
|
|
||||||
|
|
||||||
const result = await addOpenAPISpecs(availableTools);
|
|
||||||
expect(result).toEqual([...specs.map(transformSpec), ...availableTools]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return available tools if specs loading fails', async () => {
|
|
||||||
const availableTools = ['Tool1', 'Tool2'];
|
|
||||||
|
|
||||||
loadSpecs.mockRejectedValue(new Error('Failed to load specs'));
|
|
||||||
|
|
||||||
const result = await addOpenAPISpecs(availableTools);
|
|
||||||
expect(result).toEqual(availableTools);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -24,7 +24,6 @@ const { primeFiles: primeCodeFiles } = require('~/server/services/Files/Code/pro
|
||||||
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
|
const { createFileSearchTool, primeFiles: primeSearchFiles } = require('./fileSearch');
|
||||||
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
const { loadAuthValues } = require('~/server/services/Tools/credentials');
|
||||||
const { createMCPTool } = require('~/server/services/MCP');
|
const { createMCPTool } = require('~/server/services/MCP');
|
||||||
const { loadSpecs } = require('./loadSpecs');
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`);
|
const mcpToolPattern = new RegExp(`^.+${Constants.mcp_delimiter}.+$`);
|
||||||
|
|
@ -232,7 +231,6 @@ const loadTools = async ({
|
||||||
|
|
||||||
/** @type {Record<string, string>} */
|
/** @type {Record<string, string>} */
|
||||||
const toolContextMap = {};
|
const toolContextMap = {};
|
||||||
const remainingTools = [];
|
|
||||||
const appTools = options.req?.app?.locals?.availableTools ?? {};
|
const appTools = options.req?.app?.locals?.availableTools ?? {};
|
||||||
|
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
|
|
@ -292,30 +290,6 @@ const loadTools = async ({
|
||||||
requestedTools[tool] = toolInstance;
|
requestedTools[tool] = toolInstance;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (functions === true) {
|
|
||||||
remainingTools.push(tool);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let specs = null;
|
|
||||||
if (useSpecs === true && functions === true && remainingTools.length > 0) {
|
|
||||||
specs = await loadSpecs({
|
|
||||||
llm: model,
|
|
||||||
user,
|
|
||||||
message: options.message,
|
|
||||||
memory: options.memory,
|
|
||||||
signal: options.signal,
|
|
||||||
tools: remainingTools,
|
|
||||||
map: true,
|
|
||||||
verbose: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tool of remainingTools) {
|
|
||||||
if (specs && specs[tool]) {
|
|
||||||
requestedTools[tool] = specs[tool];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (returnMap) {
|
if (returnMap) {
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { z } = require('zod');
|
|
||||||
const { logger } = require('~/config');
|
|
||||||
const { createOpenAPIPlugin } = require('~/app/clients/tools/dynamic/OpenAPIPlugin');
|
|
||||||
|
|
||||||
// The minimum Manifest definition
|
|
||||||
const ManifestDefinition = z.object({
|
|
||||||
schema_version: z.string().optional(),
|
|
||||||
name_for_human: z.string(),
|
|
||||||
name_for_model: z.string(),
|
|
||||||
description_for_human: z.string(),
|
|
||||||
description_for_model: z.string(),
|
|
||||||
auth: z.object({}).optional(),
|
|
||||||
api: z.object({
|
|
||||||
// Spec URL or can be the filename of the OpenAPI spec yaml file,
|
|
||||||
// located in api\app\clients\tools\.well-known\openapi
|
|
||||||
url: z.string(),
|
|
||||||
type: z.string().optional(),
|
|
||||||
is_user_authenticated: z.boolean().nullable().optional(),
|
|
||||||
has_user_authentication: z.boolean().nullable().optional(),
|
|
||||||
}),
|
|
||||||
// use to override any params that the LLM will consistently get wrong
|
|
||||||
params: z.object({}).optional(),
|
|
||||||
logo_url: z.string().optional(),
|
|
||||||
contact_email: z.string().optional(),
|
|
||||||
legal_info_url: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
function validateJson(json) {
|
|
||||||
try {
|
|
||||||
return ManifestDefinition.parse(json);
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug('[validateJson] manifest parsing error', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// omit the LLM to return the well known jsons as objects
|
|
||||||
async function loadSpecs({ llm, user, message, tools = [], map = false, memory, signal }) {
|
|
||||||
const directoryPath = path.join(__dirname, '..', '.well-known');
|
|
||||||
let files = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < tools.length; i++) {
|
|
||||||
const filePath = path.join(directoryPath, tools[i] + '.json');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// If the access Promise is resolved, it means that the file exists
|
|
||||||
// Then we can add it to the files array
|
|
||||||
await fs.promises.access(filePath, fs.constants.F_OK);
|
|
||||||
files.push(tools[i] + '.json');
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`[loadSpecs] File ${tools[i] + '.json'} does not exist`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
files = (await fs.promises.readdir(directoryPath)).filter(
|
|
||||||
(file) => path.extname(file) === '.json',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validJsons = [];
|
|
||||||
const constructorMap = {};
|
|
||||||
|
|
||||||
logger.debug('[validateJson] files', files);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
if (path.extname(file) === '.json') {
|
|
||||||
const filePath = path.join(directoryPath, file);
|
|
||||||
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
||||||
const json = JSON.parse(fileContent);
|
|
||||||
|
|
||||||
if (!validateJson(json)) {
|
|
||||||
logger.debug('[validateJson] Invalid json', json);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (llm && map) {
|
|
||||||
constructorMap[json.name_for_model] = async () =>
|
|
||||||
await createOpenAPIPlugin({
|
|
||||||
data: json,
|
|
||||||
llm,
|
|
||||||
message,
|
|
||||||
memory,
|
|
||||||
signal,
|
|
||||||
user,
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (llm) {
|
|
||||||
validJsons.push(createOpenAPIPlugin({ data: json, llm }));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
validJsons.push(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (map) {
|
|
||||||
return constructorMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugins = (await Promise.all(validJsons)).filter((plugin) => plugin);
|
|
||||||
|
|
||||||
// logger.debug('[validateJson] plugins', plugins);
|
|
||||||
// logger.debug(plugins[0].name);
|
|
||||||
|
|
||||||
return plugins;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
loadSpecs,
|
|
||||||
validateJson,
|
|
||||||
ManifestDefinition,
|
|
||||||
};
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const { validateJson, loadSpecs, ManifestDefinition } = require('./loadSpecs');
|
|
||||||
const { createOpenAPIPlugin } = require('../dynamic/OpenAPIPlugin');
|
|
||||||
|
|
||||||
jest.mock('../dynamic/OpenAPIPlugin');
|
|
||||||
|
|
||||||
describe('ManifestDefinition', () => {
|
|
||||||
it('should validate correct json', () => {
|
|
||||||
const json = {
|
|
||||||
name_for_human: 'Test',
|
|
||||||
name_for_model: 'Test',
|
|
||||||
description_for_human: 'Test',
|
|
||||||
description_for_model: 'Test',
|
|
||||||
api: {
|
|
||||||
url: 'http://test.com',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() => ManifestDefinition.parse(json)).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not validate incorrect json', () => {
|
|
||||||
const json = {
|
|
||||||
name_for_human: 'Test',
|
|
||||||
name_for_model: 'Test',
|
|
||||||
description_for_human: 'Test',
|
|
||||||
description_for_model: 'Test',
|
|
||||||
api: {
|
|
||||||
url: 123, // incorrect type
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() => ManifestDefinition.parse(json)).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('validateJson', () => {
|
|
||||||
it('should return parsed json if valid', () => {
|
|
||||||
const json = {
|
|
||||||
name_for_human: 'Test',
|
|
||||||
name_for_model: 'Test',
|
|
||||||
description_for_human: 'Test',
|
|
||||||
description_for_model: 'Test',
|
|
||||||
api: {
|
|
||||||
url: 'http://test.com',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(validateJson(json)).toEqual(json);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if json is not valid', () => {
|
|
||||||
const json = {
|
|
||||||
name_for_human: 'Test',
|
|
||||||
name_for_model: 'Test',
|
|
||||||
description_for_human: 'Test',
|
|
||||||
description_for_model: 'Test',
|
|
||||||
api: {
|
|
||||||
url: 123, // incorrect type
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(validateJson(json)).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('loadSpecs', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.spyOn(fs.promises, 'readdir').mockResolvedValue(['test.json']);
|
|
||||||
jest.spyOn(fs.promises, 'readFile').mockResolvedValue(
|
|
||||||
JSON.stringify({
|
|
||||||
name_for_human: 'Test',
|
|
||||||
name_for_model: 'Test',
|
|
||||||
description_for_human: 'Test',
|
|
||||||
description_for_model: 'Test',
|
|
||||||
api: {
|
|
||||||
url: 'http://test.com',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
createOpenAPIPlugin.mockResolvedValue({});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return plugins', async () => {
|
|
||||||
const plugins = await loadSpecs({ llm: true, verbose: false });
|
|
||||||
|
|
||||||
expect(plugins).toHaveLength(1);
|
|
||||||
expect(createOpenAPIPlugin).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return constructorMap if map is true', async () => {
|
|
||||||
const plugins = await loadSpecs({ llm: {}, map: true, verbose: false });
|
|
||||||
|
|
||||||
expect(plugins).toHaveProperty('Test');
|
|
||||||
expect(createOpenAPIPlugin).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -118,7 +118,8 @@ const tokenValues = Object.assign(
|
||||||
'gemini-2.0-flash-lite': { prompt: 0.075, completion: 0.3 },
|
'gemini-2.0-flash-lite': { prompt: 0.075, completion: 0.3 },
|
||||||
'gemini-2.0-flash': { prompt: 0.1, completion: 0.4 },
|
'gemini-2.0-flash': { prompt: 0.1, completion: 0.4 },
|
||||||
'gemini-2.0': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
|
'gemini-2.0': { prompt: 0, completion: 0 }, // https://ai.google.dev/pricing
|
||||||
'gemini-2.5-pro-preview-03-25': { prompt: 1.25, completion: 10 },
|
'gemini-2.5-pro': { prompt: 1.25, completion: 10 },
|
||||||
|
'gemini-2.5-flash': { prompt: 0.15, completion: 3.5 },
|
||||||
'gemini-2.5': { prompt: 0, completion: 0 }, // Free for a period of time
|
'gemini-2.5': { prompt: 0, completion: 0 }, // Free for a period of time
|
||||||
'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 },
|
'gemini-1.5-flash-8b': { prompt: 0.075, completion: 0.3 },
|
||||||
'gemini-1.5-flash': { prompt: 0.15, completion: 0.6 },
|
'gemini-1.5-flash': { prompt: 0.15, completion: 0.6 },
|
||||||
|
|
|
||||||
|
|
@ -488,6 +488,9 @@ describe('getCacheMultiplier', () => {
|
||||||
|
|
||||||
describe('Google Model Tests', () => {
|
describe('Google Model Tests', () => {
|
||||||
const googleModels = [
|
const googleModels = [
|
||||||
|
'gemini-2.5-pro-preview-05-06',
|
||||||
|
'gemini-2.5-flash-preview-04-17',
|
||||||
|
'gemini-2.5-exp',
|
||||||
'gemini-2.0-flash-lite-preview-02-05',
|
'gemini-2.0-flash-lite-preview-02-05',
|
||||||
'gemini-2.0-flash-001',
|
'gemini-2.0-flash-001',
|
||||||
'gemini-2.0-flash-exp',
|
'gemini-2.0-flash-exp',
|
||||||
|
|
@ -525,6 +528,9 @@ describe('Google Model Tests', () => {
|
||||||
|
|
||||||
it('should map to the correct model keys', () => {
|
it('should map to the correct model keys', () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
|
'gemini-2.5-pro-preview-05-06': 'gemini-2.5-pro',
|
||||||
|
'gemini-2.5-flash-preview-04-17': 'gemini-2.5-flash',
|
||||||
|
'gemini-2.5-exp': 'gemini-2.5',
|
||||||
'gemini-2.0-flash-lite-preview-02-05': 'gemini-2.0-flash-lite',
|
'gemini-2.0-flash-lite-preview-02-05': 'gemini-2.0-flash-lite',
|
||||||
'gemini-2.0-flash-001': 'gemini-2.0-flash',
|
'gemini-2.0-flash-001': 'gemini-2.0-flash',
|
||||||
'gemini-2.0-flash-exp': 'gemini-2.0-flash',
|
'gemini-2.0-flash-exp': 'gemini-2.0-flash',
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
|
||||||
|
|
||||||
if (!client?.skipSaveUserMessage && latestUserMessage) {
|
if (!client?.skipSaveUserMessage && latestUserMessage) {
|
||||||
await saveMessage(req, latestUserMessage, {
|
await saveMessage(req, latestUserMessage, {
|
||||||
context: 'api/server/controllers/AskController.js - don\'t skip saving user message',
|
context: "api/server/controllers/AskController.js - don't skip saving user message",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
const { CacheKeys, AuthType } = require('librechat-data-provider');
|
const { CacheKeys, AuthType } = require('librechat-data-provider');
|
||||||
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
|
||||||
const { getToolkitKey } = require('~/server/services/ToolService');
|
const { getToolkitKey } = require('~/server/services/ToolService');
|
||||||
const { getCustomConfig } = require('~/server/services/Config');
|
const { getCustomConfig } = require('~/server/services/Config');
|
||||||
const { availableTools } = require('~/app/clients/tools');
|
const { availableTools } = require('~/app/clients/tools');
|
||||||
|
|
@ -70,7 +69,7 @@ const getAvailablePluginsController = async (req, res) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let plugins = await addOpenAPISpecs(authenticatedPlugins);
|
let plugins = authenticatedPlugins;
|
||||||
|
|
||||||
if (includedTools.length > 0) {
|
if (includedTools.length > 0) {
|
||||||
plugins = plugins.filter((plugin) => includedTools.includes(plugin.pluginKey));
|
plugins = plugins.filter((plugin) => includedTools.includes(plugin.pluginKey));
|
||||||
|
|
@ -106,11 +105,11 @@ const getAvailableTools = async (req, res) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginManifest = availableTools;
|
let pluginManifest = availableTools;
|
||||||
const customConfig = await getCustomConfig();
|
const customConfig = await getCustomConfig();
|
||||||
if (customConfig?.mcpServers != null) {
|
if (customConfig?.mcpServers != null) {
|
||||||
const mcpManager = getMCPManager();
|
const mcpManager = getMCPManager();
|
||||||
await mcpManager.loadManifestTools(pluginManifest);
|
pluginManifest = await mcpManager.loadManifestTools(pluginManifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {TPlugin[]} */
|
/** @type {TPlugin[]} */
|
||||||
|
|
|
||||||
|
|
@ -148,19 +148,13 @@ class AgentClient extends BaseClient {
|
||||||
* @param {MongoFile[]} attachments
|
* @param {MongoFile[]} attachments
|
||||||
*/
|
*/
|
||||||
checkVisionRequest(attachments) {
|
checkVisionRequest(attachments) {
|
||||||
logger.info(
|
|
||||||
'[api/server/controllers/agents/client.js #checkVisionRequest] not implemented',
|
|
||||||
attachments,
|
|
||||||
);
|
|
||||||
// if (!attachments) {
|
// if (!attachments) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// const availableModels = this.options.modelsConfig?.[this.options.endpoint];
|
// const availableModels = this.options.modelsConfig?.[this.options.endpoint];
|
||||||
// if (!availableModels) {
|
// if (!availableModels) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// let visionRequestDetected = false;
|
// let visionRequestDetected = false;
|
||||||
// for (const file of attachments) {
|
// for (const file of attachments) {
|
||||||
// if (file?.type?.includes('image')) {
|
// if (file?.type?.includes('image')) {
|
||||||
|
|
@ -171,13 +165,11 @@ class AgentClient extends BaseClient {
|
||||||
// if (!visionRequestDetected) {
|
// if (!visionRequestDetected) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
|
// this.isVisionModel = validateVisionModel({ model: this.modelOptions.model, availableModels });
|
||||||
// if (this.isVisionModel) {
|
// if (this.isVisionModel) {
|
||||||
// delete this.modelOptions.stop;
|
// delete this.modelOptions.stop;
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// for (const model of availableModels) {
|
// for (const model of availableModels) {
|
||||||
// if (!validateVisionModel({ model, availableModels })) {
|
// if (!validateVisionModel({ model, availableModels })) {
|
||||||
// continue;
|
// continue;
|
||||||
|
|
@ -187,14 +179,12 @@ class AgentClient extends BaseClient {
|
||||||
// delete this.modelOptions.stop;
|
// delete this.modelOptions.stop;
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// if (!availableModels.includes(this.defaultVisionModel)) {
|
// if (!availableModels.includes(this.defaultVisionModel)) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
// if (!validateVisionModel({ model: this.defaultVisionModel, availableModels })) {
|
// if (!validateVisionModel({ model: this.defaultVisionModel, availableModels })) {
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// this.modelOptions.model = this.defaultVisionModel;
|
// this.modelOptions.model = this.defaultVisionModel;
|
||||||
// this.isVisionModel = true;
|
// this.isVisionModel = true;
|
||||||
// delete this.modelOptions.stop;
|
// delete this.modelOptions.stop;
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ const logoutUser = async (req, refreshToken) => {
|
||||||
try {
|
try {
|
||||||
req.session.destroy();
|
req.session.destroy();
|
||||||
} catch (destroyErr) {
|
} catch (destroyErr) {
|
||||||
logger.error('[logoutUser] Failed to destroy session.', destroyErr);
|
logger.debug('[logoutUser] Failed to destroy session.', destroyErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { status: 200, message: 'Logout successful' };
|
return { status: 200, message: 'Logout successful' };
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,8 @@ const googleModels = {
|
||||||
'gemini-pro-vision': 12288,
|
'gemini-pro-vision': 12288,
|
||||||
'gemini-exp': 2000000,
|
'gemini-exp': 2000000,
|
||||||
'gemini-2.5': 1000000, // 1M input tokens, 64k output tokens
|
'gemini-2.5': 1000000, // 1M input tokens, 64k output tokens
|
||||||
|
'gemini-2.5-pro': 1000000,
|
||||||
|
'gemini-2.5-flash': 1000000,
|
||||||
'gemini-2.0': 2000000,
|
'gemini-2.0': 2000000,
|
||||||
'gemini-2.0-flash': 1000000,
|
'gemini-2.0-flash': 1000000,
|
||||||
'gemini-2.0-flash-lite': 1000000,
|
'gemini-2.0-flash-lite': 1000000,
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ export default function ExportAndShareMenu({
|
||||||
<DropdownPopup
|
<DropdownPopup
|
||||||
menuId={menuId}
|
menuId={menuId}
|
||||||
focusLoop={true}
|
focusLoop={true}
|
||||||
|
unmountOnHide={true}
|
||||||
isOpen={isPopoverActive}
|
isOpen={isPopoverActive}
|
||||||
setIsOpen={setIsPopoverActive}
|
setIsOpen={setIsPopoverActive}
|
||||||
trigger={
|
trigger={
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ const AttachFile = ({ disabled }: AttachFileProps) => {
|
||||||
isOpen={isPopoverActive}
|
isOpen={isPopoverActive}
|
||||||
setIsOpen={setIsPopoverActive}
|
setIsOpen={setIsPopoverActive}
|
||||||
modal={true}
|
modal={true}
|
||||||
|
unmountOnHide={true}
|
||||||
trigger={menuTrigger}
|
trigger={menuTrigger}
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
iconClassName="mr-0"
|
iconClassName="mr-0"
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ function MCPSelect({ conversationId }: { conversationId?: string | null }) {
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const serverNames = new Set<string>();
|
const serverNames = new Set<string>();
|
||||||
data.forEach((tool) => {
|
data.forEach((tool) => {
|
||||||
if (tool.pluginKey.includes(Constants.mcp_delimiter)) {
|
const isMCP = tool.pluginKey.includes(Constants.mcp_delimiter);
|
||||||
|
if (isMCP && tool.chatMenu !== false) {
|
||||||
const parts = tool.pluginKey.split(Constants.mcp_delimiter);
|
const parts = tool.pluginKey.split(Constants.mcp_delimiter);
|
||||||
serverNames.add(parts[parts.length - 1]);
|
serverNames.add(parts[parts.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ const BookmarkMenu: FC = () => {
|
||||||
focusLoop={true}
|
focusLoop={true}
|
||||||
menuId={menuId}
|
menuId={menuId}
|
||||||
isOpen={isMenuOpen}
|
isOpen={isMenuOpen}
|
||||||
|
unmountOnHide={true}
|
||||||
setIsOpen={setIsMenuOpen}
|
setIsOpen={setIsMenuOpen}
|
||||||
keyPrefix={`${conversationId}-bookmark-`}
|
keyPrefix={`${conversationId}-bookmark-`}
|
||||||
trigger={
|
trigger={
|
||||||
|
|
|
||||||
|
|
@ -235,10 +235,11 @@ function ConvoOptions({
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
title={title ?? ''}
|
title={title ?? ''}
|
||||||
retainView={retainView}
|
retainView={retainView}
|
||||||
conversationId={conversationId ?? ''}
|
|
||||||
showDeleteDialog={showDeleteDialog}
|
|
||||||
setShowDeleteDialog={setShowDeleteDialog}
|
|
||||||
triggerRef={deleteButtonRef}
|
triggerRef={deleteButtonRef}
|
||||||
|
setMenuOpen={setIsPopoverActive}
|
||||||
|
showDeleteDialog={showDeleteDialog}
|
||||||
|
conversationId={conversationId ?? ''}
|
||||||
|
setShowDeleteDialog={setShowDeleteDialog}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,12 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import {
|
import {
|
||||||
Label,
|
|
||||||
OGDialog,
|
|
||||||
OGDialogTitle,
|
|
||||||
OGDialogContent,
|
|
||||||
OGDialogHeader,
|
|
||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
OGDialog,
|
||||||
|
OGDialogTitle,
|
||||||
|
OGDialogHeader,
|
||||||
|
OGDialogContent,
|
||||||
} from '~/components';
|
} from '~/components';
|
||||||
import { useDeleteConversationMutation } from '~/data-provider';
|
import { useDeleteConversationMutation } from '~/data-provider';
|
||||||
import { useLocalize, useNewConvo } from '~/hooks';
|
import { useLocalize, useNewConvo } from '~/hooks';
|
||||||
|
|
@ -24,14 +23,17 @@ type DeleteButtonProps = {
|
||||||
showDeleteDialog?: boolean;
|
showDeleteDialog?: boolean;
|
||||||
setShowDeleteDialog?: (value: boolean) => void;
|
setShowDeleteDialog?: (value: boolean) => void;
|
||||||
triggerRef?: React.RefObject<HTMLButtonElement>;
|
triggerRef?: React.RefObject<HTMLButtonElement>;
|
||||||
|
setMenuOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DeleteConversationDialog({
|
export function DeleteConversationDialog({
|
||||||
setShowDeleteDialog,
|
setShowDeleteDialog,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
setMenuOpen,
|
||||||
retainView,
|
retainView,
|
||||||
title,
|
title,
|
||||||
}: {
|
}: {
|
||||||
|
setMenuOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setShowDeleteDialog: (value: boolean) => void;
|
setShowDeleteDialog: (value: boolean) => void;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
retainView: () => void;
|
retainView: () => void;
|
||||||
|
|
@ -51,6 +53,7 @@ export function DeleteConversationDialog({
|
||||||
newConversation();
|
newConversation();
|
||||||
navigate('/c/new', { replace: true });
|
navigate('/c/new', { replace: true });
|
||||||
}
|
}
|
||||||
|
setMenuOpen?.(false);
|
||||||
retainView();
|
retainView();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|
@ -98,6 +101,7 @@ export default function DeleteButton({
|
||||||
conversationId,
|
conversationId,
|
||||||
retainView,
|
retainView,
|
||||||
title,
|
title,
|
||||||
|
setMenuOpen,
|
||||||
showDeleteDialog,
|
showDeleteDialog,
|
||||||
setShowDeleteDialog,
|
setShowDeleteDialog,
|
||||||
triggerRef,
|
triggerRef,
|
||||||
|
|
@ -115,6 +119,7 @@ export default function DeleteButton({
|
||||||
<DeleteConversationDialog
|
<DeleteConversationDialog
|
||||||
setShowDeleteDialog={setShowDeleteDialog}
|
setShowDeleteDialog={setShowDeleteDialog}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
|
setMenuOpen={setMenuOpen}
|
||||||
retainView={retainView}
|
retainView={retainView}
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ const AdminSettings = () => {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">{localize('com_ui_role_select')}:</span>
|
<span className="font-medium">{localize('com_ui_role_select')}:</span>
|
||||||
<DropdownPopup
|
<DropdownPopup
|
||||||
|
unmountOnHide={true}
|
||||||
menuId="prompt-role-dropdown"
|
menuId="prompt-role-dropdown"
|
||||||
isOpen={isRoleMenuOpen}
|
isOpen={isRoleMenuOpen}
|
||||||
setIsOpen={setIsRoleMenuOpen}
|
setIsOpen={setIsRoleMenuOpen}
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ const AdminSettings = () => {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">{localize('com_ui_role_select')}:</span>
|
<span className="font-medium">{localize('com_ui_role_select')}:</span>
|
||||||
<DropdownPopup
|
<DropdownPopup
|
||||||
|
unmountOnHide={true}
|
||||||
menuId="role-dropdown"
|
menuId="role-dropdown"
|
||||||
isOpen={isRoleMenuOpen}
|
isOpen={isRoleMenuOpen}
|
||||||
setIsOpen={setIsRoleMenuOpen}
|
setIsOpen={setIsRoleMenuOpen}
|
||||||
|
|
|
||||||
|
|
@ -103,12 +103,15 @@ export function processPlugins(
|
||||||
export function mapToolCalls(toolCalls: t.ToolCallResults = []): {
|
export function mapToolCalls(toolCalls: t.ToolCallResults = []): {
|
||||||
[key: string]: t.ToolCallResult[] | undefined;
|
[key: string]: t.ToolCallResult[] | undefined;
|
||||||
} {
|
} {
|
||||||
return toolCalls.reduce((acc, call) => {
|
return toolCalls.reduce(
|
||||||
|
(acc, call) => {
|
||||||
const key = `${call.messageId}_${call.partIndex ?? 0}_${call.blockIndex ?? 0}_${call.toolId}`;
|
const key = `${call.messageId}_${call.partIndex ?? 0}_${call.blockIndex ?? 0}_${call.toolId}`;
|
||||||
const array = acc[key] ?? [];
|
const array = acc[key] ?? [];
|
||||||
array.push(call);
|
array.push(call);
|
||||||
acc[key] = array;
|
acc[key] = array;
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [key: string]: t.ToolCallResult[] | undefined });
|
},
|
||||||
|
{} as { [key: string]: t.ToolCallResult[] | undefined },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ const BaseOptionsSchema = z.object({
|
||||||
iconPath: z.string().optional(),
|
iconPath: z.string().optional(),
|
||||||
timeout: z.number().optional(),
|
timeout: z.number().optional(),
|
||||||
initTimeout: z.number().optional(),
|
initTimeout: z.number().optional(),
|
||||||
|
/** Controls visibility in chat dropdown menu (MCPSelect) */
|
||||||
|
chatMenu: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StdioOptionsSchema = BaseOptionsSchema.extend({
|
export const StdioOptionsSchema = BaseOptionsSchema.extend({
|
||||||
|
|
|
||||||
|
|
@ -417,6 +417,7 @@ export const tPluginSchema = z.object({
|
||||||
icon: z.string().optional(),
|
icon: z.string().optional(),
|
||||||
authConfig: z.array(tPluginAuthConfigSchema).optional(),
|
authConfig: z.array(tPluginAuthConfigSchema).optional(),
|
||||||
authenticated: z.boolean().optional(),
|
authenticated: z.boolean().optional(),
|
||||||
|
chatMenu: z.boolean().optional(),
|
||||||
isButton: z.boolean().optional(),
|
isButton: z.boolean().optional(),
|
||||||
toolkit: z.boolean().optional(),
|
toolkit: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,9 @@ export class MCPManager {
|
||||||
/**
|
/**
|
||||||
* Loads tools from all app-level connections into the manifest.
|
* Loads tools from all app-level connections into the manifest.
|
||||||
*/
|
*/
|
||||||
public async loadManifestTools(manifestTools: t.LCToolManifest): Promise<void> {
|
public async loadManifestTools(manifestTools: t.LCToolManifest): Promise<t.LCToolManifest> {
|
||||||
|
const mcpTools: t.LCManifestTool[] = [];
|
||||||
|
|
||||||
for (const [serverName, connection] of this.connections.entries()) {
|
for (const [serverName, connection] of this.connections.entries()) {
|
||||||
try {
|
try {
|
||||||
if (connection.isConnected() !== true) {
|
if (connection.isConnected() !== true) {
|
||||||
|
|
@ -383,17 +385,24 @@ export class MCPManager {
|
||||||
const tools = await connection.fetchTools();
|
const tools = await connection.fetchTools();
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
const pluginKey = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`;
|
const pluginKey = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`;
|
||||||
manifestTools.push({
|
const manifestTool: t.LCManifestTool = {
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
pluginKey,
|
pluginKey,
|
||||||
description: tool.description ?? '',
|
description: tool.description ?? '',
|
||||||
icon: connection.iconPath,
|
icon: connection.iconPath,
|
||||||
});
|
};
|
||||||
|
const config = this.mcpConfigs[serverName];
|
||||||
|
if (config?.chatMenu === false) {
|
||||||
|
manifestTool.chatMenu = false;
|
||||||
|
}
|
||||||
|
mcpTools.push(manifestTool);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`[MCP][${serverName}] Error fetching tools for manifest:`, error);
|
this.logger.error(`[MCP][${serverName}] Error fetching tools for manifest:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return [...mcpTools, ...manifestTools];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export interface LCFunctionTool {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LCAvailableTools = Record<string, LCFunctionTool>;
|
export type LCAvailableTools = Record<string, LCFunctionTool>;
|
||||||
|
export type LCManifestTool = TPlugin;
|
||||||
export type LCToolManifest = TPlugin[];
|
export type LCToolManifest = TPlugin[];
|
||||||
export interface MCPPrompt {
|
export interface MCPPrompt {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -84,7 +84,10 @@ export type FormattedContent =
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormattedContentResult = [string | FormattedContent[], undefined | { content: FormattedContent[] }];
|
export type FormattedContentResult = [
|
||||||
|
string | FormattedContent[],
|
||||||
|
undefined | { content: FormattedContent[] },
|
||||||
|
];
|
||||||
|
|
||||||
export type ImageFormatter = (item: ImageContent) => FormattedContent;
|
export type ImageFormatter = (item: ImageContent) => FormattedContent;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue