Compare commits

..

3 Commits

Author SHA1 Message Date
Dustin Healy
dd8a9d5d45 wip: needs to be pared down so much, but is functional and relatively robust 2025-07-20 23:53:18 -07:00
Dustin Healy
94c329680f 🔃 refactor: Simplify Plugin Deduplication and Clear Cache Post-MCP Initialization
- Replaced manual deduplication of tools with the dedicated `filterUniquePlugins` function for improved readability.
- Added back cache clearing for tools after MCP initialization to ensure fresh data is used.
- Removed unused exports from `PluginController.js` to clean up the codebase.
2025-07-15 15:49:48 -07:00
Dustin Healy
abafbfeefa feat: Add MCP Reinitialization to MCPPanel
- Refactored tool caching to include user-specific tools in various service files.
- Refactored MCPManager class for clarity
- Added a new endpoint for reinitializing MCP servers, allowing for dynamic updates of server configurations.
- Enhanced the MCPPanel component to support server reinitialization with user feedback.
2025-07-15 15:49:48 -07:00
139 changed files with 3352 additions and 8563 deletions

View File

@@ -7,7 +7,7 @@ on:
- release/*
paths:
- 'api/**'
- 'packages/**'
- 'packages/api/**'
jobs:
tests_Backend:
name: Run Backend unit tests

View File

@@ -1,32 +0,0 @@
name: Publish `@librechat/client` to NPM
on:
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: false
default: 'Manual publish requested'
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18.x'
- name: Check if client package exists
run: |
if [ -d "packages/client" ]; then
echo "Client package directory found"
else
echo "Client package directory not found - workflow ready for future use"
exit 0
fi
- name: Placeholder for future publishing
run: echo "Client package publishing workflow is ready"

View File

@@ -8,7 +8,7 @@ on:
- release/*
paths:
- 'client/**'
- 'packages/data-provider/**'
- 'packages/**'
jobs:
tests_frontend_ubuntu:

View File

@@ -1,4 +1,4 @@
# v0.7.9
# v0.7.9-rc1
# Base node image
FROM node:20-alpine AS node

View File

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

View File

@@ -108,15 +108,12 @@ class BaseClient {
/**
* Abstract method to record token usage. Subclasses must implement this method.
* If a correction to the token usage is needed, the method should return an object with the corrected token counts.
* Should only be used if `recordCollectedUsage` was not used instead.
* @param {string} [model]
* @param {number} promptTokens
* @param {number} completionTokens
* @returns {Promise<void>}
*/
async recordTokenUsage({ model, promptTokens, completionTokens }) {
async recordTokenUsage({ promptTokens, completionTokens }) {
logger.debug('[BaseClient] `recordTokenUsage` not implemented.', {
model,
promptTokens,
completionTokens,
});
@@ -744,13 +741,9 @@ class BaseClient {
} else {
responseMessage.tokenCount = this.getTokenCountForResponse(responseMessage);
completionTokens = responseMessage.tokenCount;
await this.recordTokenUsage({
usage,
promptTokens,
completionTokens,
model: responseMessage.model,
});
}
await this.recordTokenUsage({ promptTokens, completionTokens, usage });
}
if (userMessagePromise) {

View File

@@ -237,9 +237,41 @@ const formatAgentMessages = (payload) => {
return messages;
};
/**
* Formats an array of messages for LangChain, making sure all content fields are strings
* @param {Array<(HumanMessage|AIMessage|SystemMessage|ToolMessage)>} payload - The array of messages to format.
* @returns {Array<(HumanMessage|AIMessage|SystemMessage|ToolMessage)>} - The array of formatted LangChain messages, including ToolMessages for tool calls.
*/
const formatContentStrings = (payload) => {
const messages = [];
for (const message of payload) {
if (typeof message.content === 'string') {
continue;
}
if (!Array.isArray(message.content)) {
continue;
}
// Reduce text types to a single string, ignore all other types
const content = message.content.reduce((acc, curr) => {
if (curr.type === ContentTypes.TEXT) {
return `${acc}${curr[ContentTypes.TEXT]}\n`;
}
return acc;
}, '');
message.content = content.trim();
}
return messages;
};
module.exports = {
formatMessage,
formatFromLangChain,
formatAgentMessages,
formatContentStrings,
formatLangChainMessages,
};

View File

@@ -1,9 +1,14 @@
const { mcpToolPattern } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { SerpAPI } = require('@langchain/community/tools/serpapi');
const { Calculator } = require('@langchain/community/tools/calculator');
const { mcpToolPattern, loadWebSearchAuth } = require('@librechat/api');
const { EnvVar, createCodeExecutionTool, createSearchTool } = require('@librechat/agents');
const { Tools, EToolResources, replaceSpecialVars } = require('librechat-data-provider');
const {
Tools,
EToolResources,
loadWebSearchAuth,
replaceSpecialVars,
} = require('librechat-data-provider');
const {
availableTools,
manifestToolMap,

View File

@@ -3,7 +3,7 @@ const { Keyv } = require('keyv');
const { cacheConfig } = require('./cacheConfig');
const { keyvRedisClient, ioredisClient, GLOBAL_PREFIX_SEPARATOR } = require('./redisClients');
const { Time } = require('librechat-data-provider');
const { RedisStore: ConnectRedis } = require('connect-redis');
const ConnectRedis = require('connect-redis').default;
const MemoryStore = require('memorystore')(require('express-session'));
const { violationFile } = require('./keyvFiles');
const { RedisStore } = require('rate-limit-redis');

View File

@@ -44,7 +44,9 @@ jest.mock('./keyvFiles', () => ({
violationFile: mockViolationFile,
}));
jest.mock('connect-redis', () => ({ RedisStore: mockConnectRedis }));
jest.mock('connect-redis', () => ({
default: mockConnectRedis,
}));
jest.mock('memorystore', () => jest.fn(() => mockMemoryStore));

View File

@@ -21,7 +21,7 @@ const findFileById = async (file_id, options = {}) => {
* @param {string} agentId - The agent ID that might grant access
* @returns {Promise<Map<string, boolean>>} Map of fileId to access status
*/
const hasAccessToFilesViaAgent = async (userId, fileIds, agentId, checkCollaborative = true) => {
const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => {
const accessMap = new Map();
// Initialize all files as no access
@@ -55,11 +55,11 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId, checkCollabora
}
// Agent is globally shared - check if it's collaborative
if (checkCollaborative && !agent.isCollaborative) {
if (!agent.isCollaborative) {
return accessMap;
}
// Check which files are actually attached
// Agent is globally shared and collaborative - check which files are actually attached
const attachedFileIds = new Set();
if (agent.tool_resources) {
for (const [_resourceType, resource] of Object.entries(agent.tool_resources)) {
@@ -118,12 +118,7 @@ const getFiles = async (filter, _sortOptions, selectFields = { text: 0 }, option
// Batch check access for all non-owned files
const fileIds = filesToCheck.map((f) => f.file_id);
const accessMap = await hasAccessToFilesViaAgent(
options.userId,
fileIds,
options.agentId,
false,
);
const accessMap = await hasAccessToFilesViaAgent(options.userId, fileIds, options.agentId);
// Filter files based on access
const accessibleFiles = filesToCheck.filter((file) => accessMap.get(file.file_id));

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
"version": "v0.7.9",
"version": "v0.7.9-rc1",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -44,20 +44,19 @@
"@googleapis/youtube": "^20.0.0",
"@keyv/redis": "^4.3.3",
"@langchain/community": "^0.3.47",
"@langchain/core": "^0.3.62",
"@langchain/core": "^0.3.60",
"@langchain/google-genai": "^0.2.13",
"@langchain/google-vertexai": "^0.2.13",
"@langchain/openai": "^0.5.18",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.4.67",
"@librechat/agents": "^2.4.60",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@node-saml/passport-saml": "^5.0.0",
"@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2",
"bcryptjs": "^2.4.3",
"compression": "^1.8.1",
"connect-redis": "^8.1.0",
"compression": "^1.7.4",
"connect-redis": "^7.1.0",
"cookie": "^0.7.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
@@ -67,11 +66,10 @@
"express": "^4.21.2",
"express-mongo-sanitize": "^2.2.0",
"express-rate-limit": "^7.4.1",
"express-session": "^1.18.2",
"express-session": "^1.18.1",
"express-static-gzip": "^2.2.0",
"file-type": "^18.7.0",
"firebase": "^11.0.2",
"form-data": "^4.0.4",
"googleapis": "^126.0.1",
"handlebars": "^4.7.7",
"https-proxy-agent": "^7.0.6",
@@ -89,12 +87,12 @@
"mime": "^3.0.0",
"module-alias": "^2.2.3",
"mongoose": "^8.12.1",
"multer": "^2.0.2",
"multer": "^2.0.1",
"nanoid": "^3.3.7",
"node-fetch": "^2.7.0",
"nodemailer": "^6.9.15",
"ollama": "^0.5.0",
"openai": "^5.10.1",
"openai": "^4.96.2",
"openai-chat-tokens": "^0.2.8",
"openid-client": "^6.5.0",
"passport": "^0.6.0",

View File

@@ -97,7 +97,7 @@ function createServerToolsCallback() {
return;
}
await mcpToolsCache.set(serverName, serverTools);
logger.debug(`MCP tools for ${serverName} added to cache.`);
logger.warn(`MCP tools for ${serverName} added to cache.`);
} catch (error) {
logger.error('Error retrieving MCP tools from cache:', error);
}
@@ -143,7 +143,7 @@ const getAvailableTools = async (req, res) => {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
const cachedUserTools = await getCachedTools({ userId });
const userPlugins = convertMCPToolsToPlugins(cachedUserTools, customConfig);
const userPlugins = await convertMCPToolsToPlugins(cachedUserTools, customConfig, userId);
if (cachedToolsArray && userPlugins) {
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
@@ -202,23 +202,102 @@ const getAvailableTools = async (req, res) => {
const serverName = parts[parts.length - 1];
const serverConfig = customConfig?.mcpServers?.[serverName];
if (!serverConfig?.customUserVars) {
logger.warn(
`[getAvailableTools] Processing MCP tool:`,
JSON.stringify({
pluginKey: plugin.pluginKey,
serverName,
hasServerConfig: !!serverConfig,
hasCustomUserVars: !!serverConfig?.customUserVars,
}),
);
if (!serverConfig) {
logger.warn(
`[getAvailableTools] No server config found for ${serverName}, skipping auth check`,
);
toolsOutput.push(toolToAdd);
continue;
}
const customVarKeys = Object.keys(serverConfig.customUserVars);
// Handle MCP servers with customUserVars (user-level auth required)
if (serverConfig.customUserVars) {
logger.warn(`[getAvailableTools] Processing user-level MCP server: ${serverName}`);
const customVarKeys = Object.keys(serverConfig.customUserVars);
if (customVarKeys.length === 0) {
toolToAdd.authConfig = [];
toolToAdd.authenticated = true;
} else {
// Build authConfig for MCP tools
toolToAdd.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
authField: key,
label: value.title || key,
description: value.description || '',
}));
toolToAdd.authenticated = false;
// Check actual connection status for MCP tools with auth requirements
if (userId) {
try {
const mcpManager = getMCPManager(userId);
const connectionStatus = await mcpManager.getUserConnectionStatus(userId, serverName);
toolToAdd.authenticated = connectionStatus.connected;
logger.warn(`[getAvailableTools] User-level connection status for ${serverName}:`, {
connected: connectionStatus.connected,
hasConnection: connectionStatus.hasConnection,
});
} catch (error) {
logger.error(
`[getAvailableTools] Error checking connection status for ${serverName}:`,
error,
);
toolToAdd.authenticated = false;
}
} else {
// For non-authenticated requests, default to false
toolToAdd.authenticated = false;
}
} else {
// Handle app-level MCP servers (no auth required)
logger.warn(`[getAvailableTools] Processing app-level MCP server: ${serverName}`);
toolToAdd.authConfig = [];
// Check if the app-level connection is active
try {
const mcpManager = getMCPManager();
const allConnections = mcpManager.getAllConnections();
logger.warn(`[getAvailableTools] All app-level connections:`, {
connectionNames: Array.from(allConnections.keys()),
serverName,
});
const appConnection = mcpManager.getConnection(serverName);
logger.warn(`[getAvailableTools] Checking app-level connection for ${serverName}:`, {
hasConnection: !!appConnection,
connectionState: appConnection?.getConnectionState?.(),
});
if (appConnection) {
const connectionState = appConnection.getConnectionState();
logger.warn(`[getAvailableTools] App-level connection status for ${serverName}:`, {
connectionState,
hasConnection: !!appConnection,
});
// For app-level connections, consider them authenticated if they're in 'connected' state
// This is more reliable than isConnected() which does network calls
toolToAdd.authenticated = connectionState === 'connected';
logger.warn(`[getAvailableTools] Final authenticated status for ${serverName}:`, {
authenticated: toolToAdd.authenticated,
connectionState,
});
} else {
logger.warn(`[getAvailableTools] No app-level connection found for ${serverName}`);
toolToAdd.authenticated = false;
}
} catch (error) {
logger.error(
`[getAvailableTools] Error checking app-level connection status for ${serverName}:`,
error,
);
toolToAdd.authenticated = false;
}
}
toolsOutput.push(toolToAdd);
@@ -241,7 +320,7 @@ const getAvailableTools = async (req, res) => {
* @param {Object} customConfig - Custom configuration for MCP servers
* @returns {Array} Array of plugin objects
*/
function convertMCPToolsToPlugins(functionTools, customConfig) {
async function convertMCPToolsToPlugins(functionTools, customConfig, userId = null) {
const plugins = [];
for (const [toolKey, toolData] of Object.entries(functionTools)) {
@@ -253,19 +332,19 @@ function convertMCPToolsToPlugins(functionTools, customConfig) {
const parts = toolKey.split(Constants.mcp_delimiter);
const serverName = parts[parts.length - 1];
const serverConfig = customConfig?.mcpServers?.[serverName];
const plugin = {
name: parts[0], // Use the tool name without server suffix
pluginKey: toolKey,
description: functionData.description || '',
authenticated: true,
icon: serverConfig?.iconPath,
authenticated: false, // Default to false, will be updated based on connection status
icon: undefined,
};
// Build authConfig for MCP tools
const serverConfig = customConfig?.mcpServers?.[serverName];
if (!serverConfig?.customUserVars) {
plugin.authConfig = [];
plugin.authenticated = true; // No auth required
plugins.push(plugin);
continue;
}
@@ -273,12 +352,30 @@ function convertMCPToolsToPlugins(functionTools, customConfig) {
const customVarKeys = Object.keys(serverConfig.customUserVars);
if (customVarKeys.length === 0) {
plugin.authConfig = [];
plugin.authenticated = true; // No auth required
} else {
plugin.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
authField: key,
label: value.title || key,
description: value.description || '',
}));
// Check actual connection status for MCP tools with auth requirements
if (userId) {
try {
const mcpManager = getMCPManager(userId);
const connectionStatus = await mcpManager.getUserConnectionStatus(userId, serverName);
plugin.authenticated = connectionStatus.connected;
} catch (error) {
logger.error(
`[convertMCPToolsToPlugins] Error checking connection status for ${serverName}:`,
error,
);
plugin.authenticated = false;
}
} else {
plugin.authenticated = false;
}
}
plugins.push(plugin);

View File

@@ -1,89 +0,0 @@
const { Constants } = require('librechat-data-provider');
const { getCustomConfig, getCachedTools } = require('~/server/services/Config');
const { getLogStores } = require('~/cache');
// Mock the dependencies
jest.mock('@librechat/data-schemas', () => ({
logger: {
debug: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('~/server/services/Config', () => ({
getCustomConfig: jest.fn(),
getCachedTools: jest.fn(),
}));
jest.mock('~/server/services/ToolService', () => ({
getToolkitKey: jest.fn(),
}));
jest.mock('~/config', () => ({
getMCPManager: jest.fn(() => ({
loadManifestTools: jest.fn().mockResolvedValue([]),
})),
getFlowStateManager: jest.fn(),
}));
jest.mock('~/app/clients/tools', () => ({
availableTools: [],
}));
jest.mock('~/cache', () => ({
getLogStores: jest.fn(),
}));
// Import the actual module with the function we want to test
const { getAvailableTools } = require('./PluginController');
describe('PluginController', () => {
describe('plugin.icon behavior', () => {
let mockReq, mockRes, mockCache;
const callGetAvailableToolsWithMCPServer = async (mcpServers) => {
mockCache.get.mockResolvedValue(null);
getCustomConfig.mockResolvedValue({ mcpServers });
const functionTools = {
[`test-tool${Constants.mcp_delimiter}test-server`]: {
function: { name: 'test-tool', description: 'A test tool' },
},
};
getCachedTools.mockResolvedValueOnce(functionTools);
getCachedTools.mockResolvedValueOnce({
[`test-tool${Constants.mcp_delimiter}test-server`]: true,
});
await getAvailableTools(mockReq, mockRes);
const responseData = mockRes.json.mock.calls[0][0];
return responseData.find((tool) => tool.name === 'test-tool');
};
beforeEach(() => {
jest.clearAllMocks();
mockReq = { user: { id: 'test-user-id' } };
mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn() };
mockCache = { get: jest.fn(), set: jest.fn() };
getLogStores.mockReturnValue(mockCache);
});
it('should set plugin.icon when iconPath is defined', async () => {
const mcpServers = {
'test-server': {
iconPath: '/path/to/icon.png',
},
};
const testTool = await callGetAvailableToolsWithMCPServer(mcpServers);
expect(testTool.icon).toBe('/path/to/icon.png');
});
it('should set plugin.icon to undefined when iconPath is not defined', async () => {
const mcpServers = {
'test-server': {},
};
const testTool = await callGetAvailableToolsWithMCPServer(mcpServers);
expect(testTool.icon).toBeUndefined();
});
});
});

View File

@@ -1,5 +1,11 @@
const {
Tools,
Constants,
FileSources,
webSearchKeys,
extractWebSearchEnvVars,
} = require('librechat-data-provider');
const { logger } = require('@librechat/data-schemas');
const { webSearchKeys, extractWebSearchEnvVars } = require('@librechat/api');
const {
getFiles,
updateUser,
@@ -14,7 +20,6 @@ const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/service
const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService');
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud');
const { Tools, Constants, FileSources } = require('librechat-data-provider');
const { processDeleteRequest } = require('~/server/services/Files/process');
const { Transaction, Balance, User } = require('~/db/models');
const { deleteToolCalls } = require('~/models/ToolCall');
@@ -175,12 +180,14 @@ const updateUserPluginsController = async (req, res) => {
try {
const mcpManager = getMCPManager(user.id);
if (mcpManager) {
// Extract server name from pluginKey (format: "mcp_<serverName>")
// Extract server name from pluginKey (e.g., "mcp_myserver" -> "myserver")
const serverName = pluginKey.replace(Constants.mcp_prefix, '');
logger.info(
`[updateUserPluginsController] Disconnecting MCP server ${serverName} for user ${user.id} after plugin auth update for ${pluginKey}.`,
`[updateUserPluginsController] Disconnecting MCP connection for user ${user.id} and server ${serverName} after plugin auth update for ${pluginKey}.`,
);
await mcpManager.disconnectUserConnection(user.id, serverName);
// COMMENTED OUT: Don't kill the server connection on revoke
// await mcpManager.disconnectUserConnection(user.id, serverName);
}
} catch (disconnectError) {
logger.error(

View File

@@ -8,16 +8,15 @@ const {
Tokenizer,
checkAccess,
memoryInstructions,
formatContentStrings,
createMemoryProcessor,
} = require('@librechat/api');
const {
Callback,
Providers,
GraphEvents,
TitleMethod,
formatMessage,
formatAgentMessages,
formatContentStrings,
getTokenCountForMessage,
createMetadataAggregator,
} = require('@librechat/agents');
@@ -27,6 +26,7 @@ const {
VisionModes,
ContentTypes,
EModelEndpoint,
KnownEndpoints,
PermissionTypes,
isAgentsEndpoint,
AgentCapabilities,
@@ -71,15 +71,13 @@ const payloadParser = ({ req, agent, endpoint }) => {
if (isAgentsEndpoint(endpoint)) {
return { model: undefined };
} else if (endpoint === EModelEndpoint.bedrock) {
const parsedValues = bedrockInputSchema.parse(agent.model_parameters);
if (parsedValues.thinking == null) {
parsedValues.thinking = false;
}
return parsedValues;
return bedrockInputSchema.parse(agent.model_parameters);
}
return req.body.endpointOption.model_parameters;
};
const legacyContentEndpoints = new Set([KnownEndpoints.groq, KnownEndpoints.deepseek]);
const noSystemModelRegex = [/\b(o1-preview|o1-mini|amazon\.titan-text)\b/gi];
function createTokenCounter(encoding) {
@@ -720,6 +718,9 @@ class AgentClient extends BaseClient {
this.indexTokenCountMap,
toolSet,
);
if (legacyContentEndpoints.has(this.options.agent.endpoint?.toLowerCase())) {
initialMessages = formatContentStrings(initialMessages);
}
/**
*
@@ -735,9 +736,6 @@ class AgentClient extends BaseClient {
if (i > 0) {
this.model = agent.model_parameters.model;
}
if (i > 0 && config.signal == null) {
config.signal = abortController.signal;
}
if (agent.recursion_limit && typeof agent.recursion_limit === 'number') {
config.recursionLimit = agent.recursion_limit;
}
@@ -786,9 +784,6 @@ class AgentClient extends BaseClient {
}
let messages = _messages;
if (agent.useLegacyContent === true) {
messages = formatContentStrings(messages);
}
if (
agent.model_parameters?.clientOptions?.defaultHeaders?.['anthropic-beta']?.includes(
'prompt-caching',
@@ -1017,7 +1012,7 @@ class AgentClient extends BaseClient {
}
const { handleLLMEnd, collected: collectedMetadata } = createMetadataAggregator();
const { req, res, agent } = this.options;
let endpoint = agent.endpoint;
const endpoint = agent.endpoint;
/** @type {import('@librechat/agents').ClientOptions} */
let clientOptions = {
@@ -1025,32 +1020,17 @@ class AgentClient extends BaseClient {
model: agent.model_parameters.model,
};
let titleProviderConfig = await getProviderConfig(endpoint);
const { getOptions, overrideProvider, customEndpointConfig } =
await getProviderConfig(endpoint);
/** @type {TEndpoint | undefined} */
const endpointConfig =
req.app.locals.all ?? req.app.locals[endpoint] ?? titleProviderConfig.customEndpointConfig;
const endpointConfig = req.app.locals[endpoint] ?? customEndpointConfig;
if (!endpointConfig) {
logger.warn(
'[api/server/controllers/agents/client.js #titleConvo] Error getting endpoint config',
);
}
if (endpointConfig?.titleEndpoint && endpointConfig.titleEndpoint !== endpoint) {
try {
titleProviderConfig = await getProviderConfig(endpointConfig.titleEndpoint);
endpoint = endpointConfig.titleEndpoint;
} catch (error) {
logger.warn(
`[api/server/controllers/agents/client.js #titleConvo] Error getting title endpoint config for ${endpointConfig.titleEndpoint}, falling back to default`,
error,
);
// Fall back to original provider config
endpoint = agent.endpoint;
titleProviderConfig = await getProviderConfig(endpoint);
}
}
if (
endpointConfig &&
endpointConfig.titleModel &&
@@ -1059,7 +1039,7 @@ class AgentClient extends BaseClient {
clientOptions.model = endpointConfig.titleModel;
}
const options = await titleProviderConfig.getOptions({
const options = await getOptions({
req,
res,
optionsOnly: true,
@@ -1068,7 +1048,7 @@ class AgentClient extends BaseClient {
endpointOption: { model_parameters: clientOptions },
});
let provider = options.provider ?? titleProviderConfig.overrideProvider ?? agent.provider;
let provider = options.provider ?? overrideProvider ?? agent.provider;
if (
endpoint === EModelEndpoint.azureOpenAI &&
options.llmConfig?.azureOpenAIApiInstanceName == null
@@ -1101,23 +1081,16 @@ class AgentClient extends BaseClient {
),
);
if (
provider === Providers.GOOGLE &&
(endpointConfig?.titleMethod === TitleMethod.FUNCTIONS ||
endpointConfig?.titleMethod === TitleMethod.STRUCTURED)
) {
if (provider === Providers.GOOGLE) {
clientOptions.json = true;
}
try {
const titleResult = await this.run.generateTitle({
provider,
clientOptions,
inputText: text,
contentParts: this.contentParts,
titleMethod: endpointConfig?.titleMethod,
titlePrompt: endpointConfig?.titlePrompt,
titlePromptTemplate: endpointConfig?.titlePromptTemplate,
clientOptions,
chainOptions: {
signal: abortController.signal,
callbacks: [
@@ -1165,52 +1138,8 @@ class AgentClient extends BaseClient {
}
}
/**
* @param {object} params
* @param {number} params.promptTokens
* @param {number} params.completionTokens
* @param {OpenAIUsageMetadata} [params.usage]
* @param {string} [params.model]
* @param {string} [params.context='message']
* @returns {Promise<void>}
*/
async recordTokenUsage({ model, promptTokens, completionTokens, usage, context = 'message' }) {
try {
await spendTokens(
{
model,
context,
conversationId: this.conversationId,
user: this.user ?? this.options.req.user?.id,
endpointTokenConfig: this.options.endpointTokenConfig,
},
{ promptTokens, completionTokens },
);
if (
usage &&
typeof usage === 'object' &&
'reasoning_tokens' in usage &&
typeof usage.reasoning_tokens === 'number'
) {
await spendTokens(
{
model,
context: 'reasoning',
conversationId: this.conversationId,
user: this.user ?? this.options.req.user?.id,
endpointTokenConfig: this.options.endpointTokenConfig,
},
{ completionTokens: usage.reasoning_tokens },
);
}
} catch (error) {
logger.error(
'[api/server/controllers/agents/client.js #recordTokenUsage] Error recording token usage',
error,
);
}
}
/** Silent method, as `recordCollectedUsage` is used instead */
async recordTokenUsage() {}
getEncoding() {
return 'o200k_base';

View File

@@ -1,730 +0,0 @@
const { Providers } = require('@librechat/agents');
const { Constants, EModelEndpoint } = require('librechat-data-provider');
const AgentClient = require('./client');
jest.mock('@librechat/agents', () => ({
...jest.requireActual('@librechat/agents'),
createMetadataAggregator: () => ({
handleLLMEnd: jest.fn(),
collected: [],
}),
}));
describe('AgentClient - titleConvo', () => {
let client;
let mockRun;
let mockReq;
let mockRes;
let mockAgent;
let mockOptions;
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Mock run object
mockRun = {
generateTitle: jest.fn().mockResolvedValue({
title: 'Generated Title',
}),
};
// Mock agent - with both endpoint and provider
mockAgent = {
id: 'agent-123',
endpoint: EModelEndpoint.openAI, // Use a valid provider as endpoint for getProviderConfig
provider: EModelEndpoint.openAI, // Add provider property
model_parameters: {
model: 'gpt-4',
},
};
// Mock request and response
mockReq = {
app: {
locals: {
[EModelEndpoint.openAI]: {
// Match the agent endpoint
titleModel: 'gpt-3.5-turbo',
titlePrompt: 'Custom title prompt',
titleMethod: 'structured',
titlePromptTemplate: 'Template: {{content}}',
},
},
},
user: {
id: 'user-123',
},
body: {
model: 'gpt-4',
endpoint: EModelEndpoint.openAI,
key: null,
},
};
mockRes = {};
// Mock options
mockOptions = {
req: mockReq,
res: mockRes,
agent: mockAgent,
endpointTokenConfig: {},
};
// Create client instance
client = new AgentClient(mockOptions);
client.run = mockRun;
client.responseMessageId = 'response-123';
client.conversationId = 'convo-123';
client.contentParts = [{ type: 'text', text: 'Test content' }];
client.recordCollectedUsage = jest.fn().mockResolvedValue(); // Mock as async function that resolves
});
describe('titleConvo method', () => {
it('should throw error if run is not initialized', async () => {
client.run = null;
await expect(
client.titleConvo({ text: 'Test', abortController: new AbortController() }),
).rejects.toThrow('Run not initialized');
});
it('should use titlePrompt from endpoint config', async () => {
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
titlePrompt: 'Custom title prompt',
}),
);
});
it('should use titlePromptTemplate from endpoint config', async () => {
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
titlePromptTemplate: 'Template: {{content}}',
}),
);
});
it('should use titleMethod from endpoint config', async () => {
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
provider: Providers.OPENAI,
titleMethod: 'structured',
}),
);
});
it('should use titleModel from endpoint config when provided', async () => {
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Check that generateTitle was called with correct clientOptions
const generateTitleCall = mockRun.generateTitle.mock.calls[0][0];
expect(generateTitleCall.clientOptions.model).toBe('gpt-3.5-turbo');
});
it('should handle missing endpoint config gracefully', async () => {
// Remove endpoint config
mockReq.app.locals[EModelEndpoint.openAI] = undefined;
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
titlePrompt: undefined,
titlePromptTemplate: undefined,
titleMethod: undefined,
}),
);
});
it('should use agent model when titleModel is not provided', async () => {
// Remove titleModel from config
delete mockReq.app.locals[EModelEndpoint.openAI].titleModel;
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
const generateTitleCall = mockRun.generateTitle.mock.calls[0][0];
expect(generateTitleCall.clientOptions.model).toBe('gpt-4'); // Should use agent's model
});
it('should not use titleModel when it equals CURRENT_MODEL constant', async () => {
mockReq.app.locals[EModelEndpoint.openAI].titleModel = Constants.CURRENT_MODEL;
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
const generateTitleCall = mockRun.generateTitle.mock.calls[0][0];
expect(generateTitleCall.clientOptions.model).toBe('gpt-4'); // Should use agent's model
});
it('should pass all required parameters to generateTitle', async () => {
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
expect(mockRun.generateTitle).toHaveBeenCalledWith({
provider: expect.any(String),
inputText: text,
contentParts: client.contentParts,
clientOptions: expect.objectContaining({
model: 'gpt-3.5-turbo',
}),
titlePrompt: 'Custom title prompt',
titlePromptTemplate: 'Template: {{content}}',
titleMethod: 'structured',
chainOptions: expect.objectContaining({
signal: abortController.signal,
}),
});
});
it('should record collected usage after title generation', async () => {
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
expect(client.recordCollectedUsage).toHaveBeenCalledWith({
model: 'gpt-3.5-turbo',
context: 'title',
collectedUsage: expect.any(Array),
});
});
it('should return the generated title', async () => {
const text = 'Test conversation text';
const abortController = new AbortController();
const result = await client.titleConvo({ text, abortController });
expect(result).toBe('Generated Title');
});
it('should handle errors gracefully and return undefined', async () => {
mockRun.generateTitle.mockRejectedValue(new Error('Title generation failed'));
const text = 'Test conversation text';
const abortController = new AbortController();
const result = await client.titleConvo({ text, abortController });
expect(result).toBeUndefined();
});
it('should pass titleEndpoint configuration to generateTitle', async () => {
// Mock the API key just for this test
const originalApiKey = process.env.ANTHROPIC_API_KEY;
process.env.ANTHROPIC_API_KEY = 'test-api-key';
// Add titleEndpoint to the config
mockReq.app.locals[EModelEndpoint.openAI].titleEndpoint = EModelEndpoint.anthropic;
mockReq.app.locals[EModelEndpoint.openAI].titleMethod = 'structured';
mockReq.app.locals[EModelEndpoint.openAI].titlePrompt = 'Custom title prompt';
mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate = 'Custom template';
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify generateTitle was called with the custom configuration
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
titleMethod: 'structured',
provider: Providers.ANTHROPIC,
titlePrompt: 'Custom title prompt',
titlePromptTemplate: 'Custom template',
}),
);
// Restore the original API key
if (originalApiKey) {
process.env.ANTHROPIC_API_KEY = originalApiKey;
} else {
delete process.env.ANTHROPIC_API_KEY;
}
});
it('should use all config when endpoint config is missing', async () => {
// Remove endpoint-specific config
delete mockReq.app.locals[EModelEndpoint.openAI].titleModel;
delete mockReq.app.locals[EModelEndpoint.openAI].titlePrompt;
delete mockReq.app.locals[EModelEndpoint.openAI].titleMethod;
delete mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate;
// Set 'all' config
mockReq.app.locals.all = {
titleModel: 'gpt-4o-mini',
titlePrompt: 'All config title prompt',
titleMethod: 'completion',
titlePromptTemplate: 'All config template: {{content}}',
};
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify generateTitle was called with 'all' config values
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
titleMethod: 'completion',
titlePrompt: 'All config title prompt',
titlePromptTemplate: 'All config template: {{content}}',
}),
);
// Check that the model was set from 'all' config
const generateTitleCall = mockRun.generateTitle.mock.calls[0][0];
expect(generateTitleCall.clientOptions.model).toBe('gpt-4o-mini');
});
it('should prioritize all config over endpoint config for title settings', async () => {
// Set both endpoint and 'all' config
mockReq.app.locals[EModelEndpoint.openAI].titleModel = 'gpt-3.5-turbo';
mockReq.app.locals[EModelEndpoint.openAI].titlePrompt = 'Endpoint title prompt';
mockReq.app.locals[EModelEndpoint.openAI].titleMethod = 'structured';
// Remove titlePromptTemplate from endpoint config to test fallback
delete mockReq.app.locals[EModelEndpoint.openAI].titlePromptTemplate;
mockReq.app.locals.all = {
titleModel: 'gpt-4o-mini',
titlePrompt: 'All config title prompt',
titleMethod: 'completion',
titlePromptTemplate: 'All config template',
};
const text = 'Test conversation text';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify 'all' config takes precedence over endpoint config
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
titleMethod: 'completion',
titlePrompt: 'All config title prompt',
titlePromptTemplate: 'All config template',
}),
);
// Check that the model was set from 'all' config
const generateTitleCall = mockRun.generateTitle.mock.calls[0][0];
expect(generateTitleCall.clientOptions.model).toBe('gpt-4o-mini');
});
it('should use all config with titleEndpoint and verify provider switch', async () => {
// Mock the API key for the titleEndpoint provider
const originalApiKey = process.env.ANTHROPIC_API_KEY;
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
// Remove endpoint-specific config to test 'all' config
delete mockReq.app.locals[EModelEndpoint.openAI];
// Set comprehensive 'all' config with all new title options
mockReq.app.locals.all = {
titleConvo: true,
titleModel: 'claude-3-haiku-20240307',
titleMethod: 'completion', // Testing the new default method
titlePrompt: 'Generate a concise, descriptive title for this conversation',
titlePromptTemplate: 'Conversation summary: {{content}}',
titleEndpoint: EModelEndpoint.anthropic, // Should switch provider to Anthropic
};
const text = 'Test conversation about AI and machine learning';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify all config values were used
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
provider: Providers.ANTHROPIC, // Critical: Verify provider switched to Anthropic
titleMethod: 'completion',
titlePrompt: 'Generate a concise, descriptive title for this conversation',
titlePromptTemplate: 'Conversation summary: {{content}}',
inputText: text,
contentParts: client.contentParts,
}),
);
// Verify the model was set from 'all' config
const generateTitleCall = mockRun.generateTitle.mock.calls[0][0];
expect(generateTitleCall.clientOptions.model).toBe('claude-3-haiku-20240307');
// Verify other client options are set correctly
expect(generateTitleCall.clientOptions).toMatchObject({
model: 'claude-3-haiku-20240307',
// Note: Anthropic's getOptions may set its own maxTokens value
});
// Restore the original API key
if (originalApiKey) {
process.env.ANTHROPIC_API_KEY = originalApiKey;
} else {
delete process.env.ANTHROPIC_API_KEY;
}
});
it('should test all titleMethod options from all config', async () => {
// Test each titleMethod: 'completion', 'functions', 'structured'
const titleMethods = ['completion', 'functions', 'structured'];
for (const method of titleMethods) {
// Clear previous calls
mockRun.generateTitle.mockClear();
// Remove endpoint config
delete mockReq.app.locals[EModelEndpoint.openAI];
// Set 'all' config with specific titleMethod
mockReq.app.locals.all = {
titleModel: 'gpt-4o-mini',
titleMethod: method,
titlePrompt: `Testing ${method} method`,
titlePromptTemplate: `Template for ${method}: {{content}}`,
};
const text = `Test conversation for ${method} method`;
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify the correct titleMethod was used
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
titleMethod: method,
titlePrompt: `Testing ${method} method`,
titlePromptTemplate: `Template for ${method}: {{content}}`,
}),
);
}
});
describe('Azure-specific title generation', () => {
let originalEnv;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Save original environment variables
originalEnv = { ...process.env };
// Mock Azure API keys
process.env.AZURE_OPENAI_API_KEY = 'test-azure-key';
process.env.AZURE_API_KEY = 'test-azure-key';
process.env.EASTUS_API_KEY = 'test-eastus-key';
process.env.EASTUS2_API_KEY = 'test-eastus2-key';
});
afterEach(() => {
// Restore environment variables
process.env = originalEnv;
});
it('should use OPENAI provider for Azure serverless endpoints', async () => {
// Set up Azure endpoint with serverless config
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockReq.app.locals[EModelEndpoint.azureOpenAI] = {
titleConvo: true,
titleModel: 'grok-3',
titleMethod: 'completion',
titlePrompt: 'Azure serverless title prompt',
streamRate: 35,
modelGroupMap: {
'grok-3': {
group: 'Azure AI Foundry',
deploymentName: 'grok-3',
},
},
groupMap: {
'Azure AI Foundry': {
apiKey: '${AZURE_API_KEY}',
baseURL: 'https://test.services.ai.azure.com/models',
version: '2024-05-01-preview',
serverless: true,
models: {
'grok-3': {
deploymentName: 'grok-3',
},
},
},
},
};
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'grok-3';
const text = 'Test Azure serverless conversation';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify provider was switched to OPENAI for serverless
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
provider: Providers.OPENAI, // Should be OPENAI for serverless
titleMethod: 'completion',
titlePrompt: 'Azure serverless title prompt',
}),
);
});
it('should use AZURE provider for Azure endpoints with instanceName', async () => {
// Set up Azure endpoint
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockReq.app.locals[EModelEndpoint.azureOpenAI] = {
titleConvo: true,
titleModel: 'gpt-4o',
titleMethod: 'structured',
titlePrompt: 'Azure instance title prompt',
streamRate: 35,
modelGroupMap: {
'gpt-4o': {
group: 'eastus',
deploymentName: 'gpt-4o',
},
},
groupMap: {
eastus: {
apiKey: '${EASTUS_API_KEY}',
instanceName: 'region-instance',
version: '2024-02-15-preview',
models: {
'gpt-4o': {
deploymentName: 'gpt-4o',
},
},
},
},
};
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'gpt-4o';
const text = 'Test Azure instance conversation';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify provider remains AZURE with instanceName
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
provider: Providers.AZURE,
titleMethod: 'structured',
titlePrompt: 'Azure instance title prompt',
}),
);
});
it('should handle Azure titleModel with CURRENT_MODEL constant', async () => {
// Set up Azure endpoint
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockAgent.model_parameters.model = 'gpt-4o-latest';
mockReq.app.locals[EModelEndpoint.azureOpenAI] = {
titleConvo: true,
titleModel: Constants.CURRENT_MODEL,
titleMethod: 'functions',
streamRate: 35,
modelGroupMap: {
'gpt-4o-latest': {
group: 'region-eastus',
deploymentName: 'gpt-4o-mini',
version: '2024-02-15-preview',
},
},
groupMap: {
'region-eastus': {
apiKey: '${EASTUS2_API_KEY}',
instanceName: 'test-instance',
version: '2024-12-01-preview',
models: {
'gpt-4o-latest': {
deploymentName: 'gpt-4o-mini',
version: '2024-02-15-preview',
},
},
},
},
};
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'gpt-4o-latest';
const text = 'Test Azure current model';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify it uses the correct model when titleModel is CURRENT_MODEL
const generateTitleCall = mockRun.generateTitle.mock.calls[0][0];
// When CURRENT_MODEL is used with Azure, the model gets mapped to the deployment name
// In this case, 'gpt-4o-latest' is mapped to 'gpt-4o-mini' deployment
expect(generateTitleCall.clientOptions.model).toBe('gpt-4o-mini');
// Also verify that CURRENT_MODEL constant was not passed as the model
expect(generateTitleCall.clientOptions.model).not.toBe(Constants.CURRENT_MODEL);
});
it('should handle Azure with multiple model groups', async () => {
// Set up Azure endpoint
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockReq.app.locals[EModelEndpoint.azureOpenAI] = {
titleConvo: true,
titleModel: 'o1-mini',
titleMethod: 'completion',
streamRate: 35,
modelGroupMap: {
'gpt-4o': {
group: 'eastus',
deploymentName: 'gpt-4o',
},
'o1-mini': {
group: 'region-eastus',
deploymentName: 'o1-mini',
},
'codex-mini': {
group: 'codex-mini',
deploymentName: 'codex-mini',
},
},
groupMap: {
eastus: {
apiKey: '${EASTUS_API_KEY}',
instanceName: 'region-eastus',
version: '2024-02-15-preview',
models: {
'gpt-4o': {
deploymentName: 'gpt-4o',
},
},
},
'region-eastus': {
apiKey: '${EASTUS2_API_KEY}',
instanceName: 'region-eastus2',
version: '2024-12-01-preview',
models: {
'o1-mini': {
deploymentName: 'o1-mini',
},
},
},
'codex-mini': {
apiKey: '${AZURE_API_KEY}',
baseURL: 'https://example.cognitiveservices.azure.com/openai/',
version: '2025-04-01-preview',
serverless: true,
models: {
'codex-mini': {
deploymentName: 'codex-mini',
},
},
},
},
};
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'o1-mini';
const text = 'Test Azure multi-group conversation';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify correct model and provider are used
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
provider: Providers.AZURE,
titleMethod: 'completion',
}),
);
const generateTitleCall = mockRun.generateTitle.mock.calls[0][0];
expect(generateTitleCall.clientOptions.model).toBe('o1-mini');
expect(generateTitleCall.clientOptions.maxTokens).toBeUndefined(); // o1 models shouldn't have maxTokens
});
it('should use all config as fallback for Azure endpoints', async () => {
// Set up Azure endpoint with minimal config
mockAgent.endpoint = EModelEndpoint.azureOpenAI;
mockAgent.provider = EModelEndpoint.azureOpenAI;
mockReq.body.endpoint = EModelEndpoint.azureOpenAI;
mockReq.body.model = 'gpt-4';
// Remove Azure-specific config
delete mockReq.app.locals[EModelEndpoint.azureOpenAI];
// Set 'all' config as fallback with a serverless Azure config
mockReq.app.locals.all = {
titleConvo: true,
titleModel: 'gpt-4',
titleMethod: 'structured',
titlePrompt: 'Fallback title prompt from all config',
titlePromptTemplate: 'Template: {{content}}',
modelGroupMap: {
'gpt-4': {
group: 'default-group',
deploymentName: 'gpt-4',
},
},
groupMap: {
'default-group': {
apiKey: '${AZURE_API_KEY}',
baseURL: 'https://default.openai.azure.com/',
version: '2024-02-15-preview',
serverless: true,
models: {
'gpt-4': {
deploymentName: 'gpt-4',
},
},
},
},
};
const text = 'Test Azure with all config fallback';
const abortController = new AbortController();
await client.titleConvo({ text, abortController });
// Verify all config is used
expect(mockRun.generateTitle).toHaveBeenCalledWith(
expect.objectContaining({
provider: Providers.OPENAI, // Should be OPENAI when no instanceName
titleMethod: 'structured',
titlePrompt: 'Fallback title prompt from all config',
titlePromptTemplate: 'Template: {{content}}',
}),
);
});
});
});
});

View File

@@ -1,13 +1,14 @@
const { nanoid } = require('nanoid');
const { EnvVar } = require('@librechat/agents');
const { checkAccess } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { checkAccess, loadWebSearchAuth } = require('@librechat/api');
const {
Tools,
AuthType,
Permissions,
ToolCallTypes,
PermissionTypes,
loadWebSearchAuth,
} = require('librechat-data-provider');
const { processFileURL, uploadImageBuffer } = require('~/server/services/Files/process');
const { processCodeOutput } = require('~/server/services/Files/Code/process');

View File

@@ -106,6 +106,7 @@ router.get('/', async function (req, res) {
const serverConfig = config.mcpServers[serverName];
payload.mcpServers[serverName] = {
customUserVars: serverConfig?.customUserVars || {},
requiresOAuth: req.app.locals.mcpOAuthRequirements?.[serverName] || false,
};
}
}

View File

@@ -4,7 +4,7 @@ const { MCPOAuthHandler } = require('@librechat/api');
const { CacheKeys, Constants } = require('librechat-data-provider');
const { findToken, updateToken, createToken, deleteTokens } = require('~/models');
const { setCachedTools, getCachedTools, loadCustomConfig } = require('~/server/services/Config');
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
const { getUserPluginAuthValueByPlugin } = require('~/server/services/PluginService');
const { getMCPManager, getFlowStateManager } = require('~/config');
const { requireJwtAuth } = require('~/server/middleware');
const { getLogStores } = require('~/cache');
@@ -120,73 +120,9 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
const tokens = await MCPOAuthHandler.completeOAuthFlow(flowId, code, flowManager);
logger.info('[MCP OAuth] OAuth flow completed, tokens received in callback route');
// Try to establish the MCP connection with the new tokens
try {
const mcpManager = getMCPManager(flowState.userId);
logger.debug(`[MCP OAuth] Attempting to reconnect ${serverName} with new OAuth tokens`);
// For user-level OAuth, try to establish the connection
if (flowState.userId !== 'system') {
// We need to get the user object - in this case we'll need to reconstruct it
const user = { id: flowState.userId };
// Try to establish connection with the new tokens
const userConnection = await mcpManager.getUserConnection({
user,
serverName,
flowManager,
tokenMethods: {
findToken,
updateToken,
createToken,
deleteTokens,
},
});
logger.info(
`[MCP OAuth] Successfully reconnected ${serverName} for user ${flowState.userId}`,
);
// Fetch and cache tools now that we have a successful connection
const userTools = (await getCachedTools({ userId: flowState.userId })) || {};
// Remove any old tools from this server in the user's cache
const mcpDelimiter = Constants.mcp_delimiter;
for (const key of Object.keys(userTools)) {
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
delete userTools[key];
}
}
// Add the new tools from this server
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,
},
};
}
// Save the updated user tool cache
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}`);
}
} catch (error) {
// Don't fail the OAuth callback if reconnection fails - the tokens are still saved
logger.warn(
`[MCP OAuth] Failed to reconnect ${serverName} after OAuth, but tokens are saved:`,
error,
);
// For system-level OAuth, we need to store the tokens and retry the connection
if (flowState.userId === 'system') {
logger.debug(`[MCP OAuth] System-level OAuth completed for ${serverName}`);
}
/** ID of the flow that the tool/connection is waiting for */
@@ -269,205 +205,9 @@ router.get('/oauth/status/:flowId', async (req, res) => {
}
});
/**
* Cancel OAuth flow
* This endpoint cancels a pending OAuth flow
*/
router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => {
try {
const { serverName } = req.params;
const user = req.user;
if (!user?.id) {
return res.status(401).json({ error: 'User not authenticated' });
}
logger.info(`[MCP OAuth Cancel] Cancelling OAuth flow for ${serverName} by user ${user.id}`);
const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = getFlowStateManager(flowsCache);
// Generate the flow ID for this user/server combination
const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName);
// Check if flow exists
const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth');
if (!flowState) {
logger.debug(`[MCP OAuth Cancel] No active flow found for ${serverName}`);
return res.json({
success: true,
message: 'No active OAuth flow to cancel',
});
}
// Cancel the flow by marking it as failed
await flowManager.completeFlow(flowId, 'mcp_oauth', null, 'User cancelled OAuth flow');
logger.info(`[MCP OAuth Cancel] Successfully cancelled OAuth flow for ${serverName}`);
res.json({
success: true,
message: `OAuth flow for ${serverName} cancelled successfully`,
});
} catch (error) {
logger.error('[MCP OAuth Cancel] Failed to cancel OAuth flow', error);
res.status(500).json({ error: 'Failed to cancel OAuth flow' });
}
});
/**
* Reinitialize MCP server
* This endpoint allows reinitializing a specific MCP server
*/
router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
try {
const { serverName } = req.params;
const user = req.user;
if (!user?.id) {
return res.status(401).json({ error: 'User not authenticated' });
}
logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`);
const config = await loadCustomConfig();
if (!config || !config.mcpServers || !config.mcpServers[serverName]) {
return res.status(404).json({
error: `MCP server '${serverName}' not found in configuration`,
});
}
const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = getFlowStateManager(flowsCache);
const mcpManager = getMCPManager();
await mcpManager.disconnectServer(serverName);
logger.info(`[MCP Reinitialize] Disconnected existing server: ${serverName}`);
const serverConfig = config.mcpServers[serverName];
mcpManager.mcpConfigs[serverName] = serverConfig;
let customUserVars = {};
if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') {
for (const varName of Object.keys(serverConfig.customUserVars)) {
try {
const value = await getUserPluginAuthValue(user.id, varName, false);
if (value) {
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, // Return immediately when OAuth is initiated
// Add OAuth handlers to capture the OAuth URL when needed
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'}`,
);
// Check if this is an OAuth error - if so, the flow state should be set up now
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;
// Don't return error - continue so frontend can handle OAuth
} 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' });
}
}
// Only fetch and cache tools if we successfully connected (no OAuth required)
if (userConnection && !oauthRequired) {
const userTools = (await getCachedTools({ userId: user.id })) || {};
// Remove any old tools from this server in the user's cache
const mcpDelimiter = Constants.mcp_delimiter;
for (const key of Object.keys(userTools)) {
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
delete userTools[key];
}
}
// Add the new tools from this server
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,
},
};
}
// Save the updated user tool cache
await setCachedTools(userTools, { userId: user.id });
}
logger.debug(
`[MCP Reinitialize] Sending response for ${serverName} - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
);
res.json({
success: true,
message: oauthRequired
? `MCP server '${serverName}' ready for OAuth authentication`
: `MCP server '${serverName}' reinitialized successfully`,
serverName,
oauthRequired,
oauthUrl,
});
} catch (error) {
logger.error('[MCP Reinitialize] Unexpected error', error);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* Get connection status for all MCP servers
* This endpoint returns the actual connection status from MCPManager without disconnecting idle connections
* This endpoint returns the actual connection status from MCPManager
*/
router.get('/connection/status', requireJwtAuth, async (req, res) => {
try {
@@ -477,86 +217,65 @@ router.get('/connection/status', requireJwtAuth, async (req, res) => {
return res.status(401).json({ error: 'User not authenticated' });
}
const mcpManager = getMCPManager(user.id);
const mcpManager = getMCPManager();
const connectionStatus = {};
const printConfig = false;
const config = await loadCustomConfig(printConfig);
// Get all MCP server names from custom config
const config = await loadCustomConfig();
const mcpConfig = config?.mcpServers;
const appConnections = mcpManager.getAllConnections() || new Map();
const userConnections = mcpManager.getUserConnections(user.id) || new Map();
const oauthServers = mcpManager.getOAuthServers() || new Set();
if (!mcpConfig) {
return res.status(404).json({ error: 'MCP config not found' });
}
// Get flow manager to check for active/timed-out OAuth flows
const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = getFlowStateManager(flowsCache);
for (const [serverName] of Object.entries(mcpConfig)) {
const getConnectionState = (serverName) =>
appConnections.get(serverName)?.connectionState ??
userConnections.get(serverName)?.connectionState ??
'disconnected';
const baseConnectionState = getConnectionState(serverName);
let hasActiveOAuthFlow = false;
let hasFailedOAuthFlow = false;
if (baseConnectionState === 'disconnected' && oauthServers.has(serverName)) {
if (mcpConfig) {
for (const [serverName, config] of Object.entries(mcpConfig)) {
try {
// Check for user-specific OAuth flows
const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName);
const flowState = await flowManager.getFlowState(flowId, 'mcp_oauth');
if (flowState) {
// Check if flow failed or timed out
const flowAge = Date.now() - flowState.createdAt;
const flowTTL = flowState.ttl || 180000; // Default 3 minutes
// Check if this is an app-level connection (exists in mcpManager.connections)
const appConnection = mcpManager.getConnection(serverName);
const hasAppConnection = !!appConnection;
if (flowState.status === 'FAILED' || flowAge > flowTTL) {
hasFailedOAuthFlow = true;
logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, {
flowId,
status: flowState.status,
flowAge,
flowTTL,
timedOut: flowAge > flowTTL,
});
} else if (flowState.status === 'PENDING') {
hasActiveOAuthFlow = true;
logger.debug(`[MCP Connection Status] Found active OAuth flow for ${serverName}`, {
flowId,
flowAge,
flowTTL,
});
}
// Check if this is a user-level connection (exists in mcpManager.userConnections)
const userConnection = mcpManager.getUserConnectionIfExists(user.id, serverName);
const hasUserConnection = !!userConnection;
// Determine if connected based on actual connection state
let connected = false;
if (hasAppConnection) {
connected = await appConnection.isConnected();
} else if (hasUserConnection) {
connected = await userConnection.isConnected();
}
// Determine if this server requires user authentication
const hasAuthConfig =
config.customUserVars && Object.keys(config.customUserVars).length > 0;
const requiresOAuth = req.app.locals.mcpOAuthRequirements?.[serverName] || false;
connectionStatus[serverName] = {
connected,
hasAuthConfig,
hasConnection: hasAppConnection || hasUserConnection,
isAppLevel: hasAppConnection,
isUserLevel: hasUserConnection,
requiresOAuth,
};
} catch (error) {
logger.error(
`[MCP Connection Status] Error checking OAuth flows for ${serverName}:`,
`[MCP Connection Status] Error checking connection for ${serverName}:`,
error,
);
connectionStatus[serverName] = {
connected: false,
hasAuthConfig: config.customUserVars && Object.keys(config.customUserVars).length > 0,
hasConnection: false,
isAppLevel: false,
isUserLevel: false,
requiresOAuth: req.app.locals.mcpOAuthRequirements?.[serverName] || false,
error: error.message,
};
}
}
// Determine the final connection state
let finalConnectionState = baseConnectionState;
if (hasFailedOAuthFlow) {
finalConnectionState = 'error'; // Report as error if OAuth failed
} else if (hasActiveOAuthFlow && baseConnectionState === 'disconnected') {
finalConnectionState = 'connecting'; // Still waiting for OAuth
}
connectionStatus[serverName] = {
requiresOAuth: oauthServers.has(serverName),
connectionState: finalConnectionState,
};
}
logger.info(`[MCP Connection Status] Returning status for user ${user.id}:`, connectionStatus);
res.json({
success: true,
connectionStatus,
@@ -580,8 +299,7 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => {
return res.status(401).json({ error: 'User not authenticated' });
}
const printConfig = false;
const config = await loadCustomConfig(printConfig);
const config = await loadCustomConfig();
if (!config || !config.mcpServers || !config.mcpServers[serverName]) {
return res.status(404).json({
error: `MCP server '${serverName}' not found in configuration`,
@@ -596,7 +314,7 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => {
if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') {
for (const varName of Object.keys(serverConfig.customUserVars)) {
try {
const value = await getUserPluginAuthValue(user.id, varName, false, pluginKey);
const value = await getUserPluginAuthValueByPlugin(user.id, varName, pluginKey, false);
// Only store boolean flag indicating if value exists
authValueFlags[varName] = !!(value && value.length > 0);
} catch (err) {
@@ -624,4 +342,337 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => {
}
});
/**
* Check if a specific MCP server requires OAuth
* This endpoint checks if a specific MCP server requires OAuth authentication
*/
router.get('/:serverName/oauth/required', requireJwtAuth, async (req, res) => {
try {
const { serverName } = req.params;
const user = req.user;
if (!user?.id) {
return res.status(401).json({ error: 'User not authenticated' });
}
const mcpManager = getMCPManager();
const requiresOAuth = await mcpManager.isOAuthRequired(serverName);
res.json({
success: true,
serverName,
requiresOAuth,
});
} catch (error) {
logger.error(
`[MCP OAuth Required] Failed to check OAuth requirement for ${req.params.serverName}`,
error,
);
res.status(500).json({ error: 'Failed to check OAuth requirement' });
}
});
/**
* Complete MCP server reinitialization after OAuth
* This endpoint completes the reinitialization process after OAuth authentication
*/
router.post('/:serverName/reinitialize/complete', requireJwtAuth, async (req, res) => {
let responseSent = false;
try {
const { serverName } = req.params;
const user = req.user;
if (!user?.id) {
responseSent = true;
return res.status(401).json({ error: 'User not authenticated' });
}
logger.info(`[MCP Complete Reinitialize] Starting completion for ${serverName}`);
const mcpManager = getMCPManager();
// Wait for connection to be established via event-driven approach
const userConnection = await new Promise((resolve, reject) => {
// Set a reasonable timeout (10 seconds)
const timeout = setTimeout(() => {
mcpManager.removeListener('connectionEstablished', connectionHandler);
reject(new Error('Timeout waiting for connection establishment'));
}, 10000);
const connectionHandler = ({
userId: eventUserId,
serverName: eventServerName,
connection,
}) => {
if (eventUserId === user.id && eventServerName === serverName) {
clearTimeout(timeout);
mcpManager.removeListener('connectionEstablished', connectionHandler);
resolve(connection);
}
};
// Check if connection already exists
const existingConnection = mcpManager.getUserConnectionIfExists(user.id, serverName);
if (existingConnection) {
clearTimeout(timeout);
resolve(existingConnection);
return;
}
// Listen for the connection establishment event
mcpManager.on('connectionEstablished', connectionHandler);
});
if (!userConnection) {
responseSent = true;
return res.status(404).json({ error: 'User connection not found' });
}
const userTools = (await getCachedTools({ userId: user.id })) || {};
// Remove any old tools from this server in the user's cache
const mcpDelimiter = Constants.mcp_delimiter;
for (const key of Object.keys(userTools)) {
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
delete userTools[key];
}
}
// Add the new tools from this server
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,
},
};
}
// Save the updated user tool cache
await setCachedTools(userTools, { userId: user.id });
responseSent = true;
res.json({
success: true,
message: `MCP server '${serverName}' reinitialized successfully`,
serverName,
});
} catch (error) {
logger.error(
`[MCP Complete Reinitialize] Error completing reinitialization for ${req.params.serverName}:`,
error,
);
if (!responseSent) {
res.status(500).json({
success: false,
message: 'Failed to complete MCP server reinitialization',
serverName: req.params.serverName,
});
}
}
});
/**
* Reinitialize MCP server
* This endpoint allows reinitializing a specific MCP server
*/
router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
let responseSent = false;
try {
const { serverName } = req.params;
const user = req.user;
if (!user?.id) {
responseSent = true;
return res.status(401).json({ error: 'User not authenticated' });
}
logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`);
const config = await loadCustomConfig();
if (!config || !config.mcpServers || !config.mcpServers[serverName]) {
responseSent = true;
return res.status(404).json({
error: `MCP server '${serverName}' not found in configuration`,
});
}
const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = getFlowStateManager(flowsCache);
const mcpManager = getMCPManager();
// Clean up any stale OAuth flows for this server
try {
const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName);
const existingFlow = await flowManager.getFlowState(flowId, 'mcp_oauth');
if (existingFlow && existingFlow.status === 'PENDING') {
logger.info(`[MCP Reinitialize] Cleaning up stale OAuth flow for ${serverName}`);
await flowManager.failFlow(flowId, 'mcp_oauth', new Error('OAuth flow interrupted'));
}
} catch (error) {
logger.warn(
`[MCP Reinitialize] Error cleaning up stale OAuth flow for ${serverName}:`,
error,
);
}
await mcpManager.disconnectServer(serverName);
logger.info(`[MCP Reinitialize] Disconnected existing server: ${serverName}`);
const serverConfig = config.mcpServers[serverName];
mcpManager.mcpConfigs[serverName] = serverConfig;
let customUserVars = {};
if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') {
for (const varName of Object.keys(serverConfig.customUserVars)) {
try {
const pluginKey = `${Constants.mcp_prefix}${serverName}`;
const value = await getUserPluginAuthValueByPlugin(user.id, varName, pluginKey, false);
if (value) {
customUserVars[varName] = value;
}
} catch (err) {
logger.error(`[MCP Reinitialize] Error fetching ${varName} for user ${user.id}:`, err);
}
}
}
let userConnection = null;
let oauthRequired = false;
try {
userConnection = await mcpManager.getUserConnection({
user,
serverName,
flowManager,
customUserVars,
tokenMethods: {
findToken,
updateToken,
createToken,
deleteTokens,
},
oauthStart: (authURL) => {
// This will be called if OAuth is required
oauthRequired = true;
responseSent = true;
logger.info(`[MCP Reinitialize] OAuth required for ${serverName}, auth URL: ${authURL}`);
// Get the flow ID for polling
const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName);
// Return the OAuth response immediately - client will poll for completion
res.json({
success: false,
oauthRequired: true,
authURL,
flowId,
message: `OAuth authentication required for MCP server '${serverName}'`,
serverName,
});
},
oauthEnd: () => {
// This will be called when OAuth flow completes
logger.info(`[MCP Reinitialize] OAuth flow completed for ${serverName}`);
},
});
// If response was already sent for OAuth, don't continue
if (responseSent) {
return;
}
} catch (err) {
logger.error(`[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`, err);
// Check if this is an OAuth error
if (err.message && err.message.includes('OAuth required')) {
// Try to get the OAuth URL from the flow manager
try {
const flowId = MCPOAuthHandler.generateFlowId(user.id, serverName);
const existingFlow = await flowManager.getFlowState(flowId, 'mcp_oauth');
if (existingFlow && existingFlow.metadata) {
const { serverUrl, oauth: oauthConfig } = existingFlow.metadata;
if (serverUrl && oauthConfig) {
const { authorizationUrl: authUrl } = await MCPOAuthHandler.initiateOAuthFlow(
serverName,
serverUrl,
user.id,
oauthConfig,
);
return res.json({
success: false,
oauthRequired: true,
authURL: authUrl,
flowId,
message: `OAuth authentication required for MCP server '${serverName}'`,
serverName,
});
}
}
} catch (oauthErr) {
logger.error(`[MCP Reinitialize] Error getting OAuth URL for ${serverName}:`, oauthErr);
}
responseSent = true;
return res.status(401).json({
success: false,
oauthRequired: true,
message: `OAuth authentication required for MCP server '${serverName}'`,
serverName,
});
}
responseSent = true;
return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' });
}
const userTools = (await getCachedTools({ userId: user.id })) || {};
// Remove any old tools from this server in the user's cache
const mcpDelimiter = Constants.mcp_delimiter;
for (const key of Object.keys(userTools)) {
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
delete userTools[key];
}
}
// Add the new tools from this server
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,
},
};
}
// Save the updated user tool cache
await setCachedTools(userTools, { userId: user.id });
responseSent = true;
res.json({
success: true,
message: `MCP server '${serverName}' reinitialized successfully`,
serverName,
});
} catch (error) {
logger.error('[MCP Reinitialize] Unexpected error', error);
if (!responseSent) {
res.status(500).json({ error: 'Internal server error' });
}
}
});
module.exports = router;

View File

@@ -1,11 +1,12 @@
const { agentsConfigSetup, loadWebSearchConfig } = require('@librechat/api');
const {
FileSources,
loadOCRConfig,
EModelEndpoint,
loadMemoryConfig,
getConfigDefaults,
loadWebSearchConfig,
} = require('librechat-data-provider');
const { agentsConfigSetup } = require('@librechat/api');
const {
checkHealth,
checkConfig,
@@ -157,10 +158,6 @@ const AppService = async (app) => {
}
});
if (endpoints?.all) {
endpointLocals.all = endpoints.all;
}
app.locals = {
...defaultLocals,
fileConfig: config?.fileConfig,

View File

@@ -543,206 +543,6 @@ describe('AppService', () => {
expect(process.env.IMPORT_USER_MAX).toEqual('initialUserMax');
expect(process.env.IMPORT_USER_WINDOW).toEqual('initialUserWindow');
});
it('should correctly configure endpoint with titlePrompt, titleMethod, and titlePromptTemplate', async () => {
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
Promise.resolve({
endpoints: {
[EModelEndpoint.openAI]: {
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
titleMethod: 'structured',
titlePrompt: 'Custom title prompt for conversation',
titlePromptTemplate: 'Summarize this conversation: {{conversation}}',
},
[EModelEndpoint.assistants]: {
titleMethod: 'functions',
titlePrompt: 'Generate a title for this assistant conversation',
titlePromptTemplate: 'Assistant conversation template: {{messages}}',
},
[EModelEndpoint.azureOpenAI]: {
groups: azureGroups,
titleConvo: true,
titleMethod: 'completion',
titleModel: 'gpt-4',
titlePrompt: 'Azure title prompt',
titlePromptTemplate: 'Azure conversation: {{context}}',
},
},
}),
);
await AppService(app);
// Check OpenAI endpoint configuration
expect(app.locals).toHaveProperty(EModelEndpoint.openAI);
expect(app.locals[EModelEndpoint.openAI]).toEqual(
expect.objectContaining({
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
titleMethod: 'structured',
titlePrompt: 'Custom title prompt for conversation',
titlePromptTemplate: 'Summarize this conversation: {{conversation}}',
}),
);
// Check Assistants endpoint configuration
expect(app.locals).toHaveProperty(EModelEndpoint.assistants);
expect(app.locals[EModelEndpoint.assistants]).toMatchObject({
titleMethod: 'functions',
titlePrompt: 'Generate a title for this assistant conversation',
titlePromptTemplate: 'Assistant conversation template: {{messages}}',
});
// Check Azure OpenAI endpoint configuration
expect(app.locals).toHaveProperty(EModelEndpoint.azureOpenAI);
expect(app.locals[EModelEndpoint.azureOpenAI]).toEqual(
expect.objectContaining({
titleConvo: true,
titleMethod: 'completion',
titleModel: 'gpt-4',
titlePrompt: 'Azure title prompt',
titlePromptTemplate: 'Azure conversation: {{context}}',
}),
);
});
it('should configure Agent endpoint with title generation settings', async () => {
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
Promise.resolve({
endpoints: {
[EModelEndpoint.agents]: {
disableBuilder: false,
titleConvo: true,
titleModel: 'gpt-4',
titleMethod: 'structured',
titlePrompt: 'Generate a descriptive title for this agent conversation',
titlePromptTemplate: 'Agent conversation summary: {{content}}',
recursionLimit: 15,
capabilities: [AgentCapabilities.tools, AgentCapabilities.actions],
},
},
}),
);
await AppService(app);
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
expect(app.locals[EModelEndpoint.agents]).toMatchObject({
disableBuilder: false,
titleConvo: true,
titleModel: 'gpt-4',
titleMethod: 'structured',
titlePrompt: 'Generate a descriptive title for this agent conversation',
titlePromptTemplate: 'Agent conversation summary: {{content}}',
recursionLimit: 15,
capabilities: expect.arrayContaining([AgentCapabilities.tools, AgentCapabilities.actions]),
});
});
it('should handle missing title configuration options with defaults', async () => {
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
Promise.resolve({
endpoints: {
[EModelEndpoint.openAI]: {
titleConvo: true,
// titlePrompt and titlePromptTemplate are not provided
},
},
}),
);
await AppService(app);
expect(app.locals).toHaveProperty(EModelEndpoint.openAI);
expect(app.locals[EModelEndpoint.openAI]).toMatchObject({
titleConvo: true,
});
// Check that the optional fields are undefined when not provided
expect(app.locals[EModelEndpoint.openAI].titlePrompt).toBeUndefined();
expect(app.locals[EModelEndpoint.openAI].titlePromptTemplate).toBeUndefined();
expect(app.locals[EModelEndpoint.openAI].titleMethod).toBeUndefined();
});
it('should correctly configure titleEndpoint when specified', async () => {
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
Promise.resolve({
endpoints: {
[EModelEndpoint.openAI]: {
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
titleEndpoint: EModelEndpoint.anthropic,
titlePrompt: 'Generate a concise title',
},
[EModelEndpoint.agents]: {
titleEndpoint: 'custom-provider',
titleMethod: 'structured',
},
},
}),
);
await AppService(app);
// Check OpenAI endpoint has titleEndpoint
expect(app.locals).toHaveProperty(EModelEndpoint.openAI);
expect(app.locals[EModelEndpoint.openAI]).toMatchObject({
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
titleEndpoint: EModelEndpoint.anthropic,
titlePrompt: 'Generate a concise title',
});
// Check Agents endpoint has titleEndpoint
expect(app.locals).toHaveProperty(EModelEndpoint.agents);
expect(app.locals[EModelEndpoint.agents]).toMatchObject({
titleEndpoint: 'custom-provider',
titleMethod: 'structured',
});
});
it('should correctly configure all endpoint when specified', async () => {
require('./Config/loadCustomConfig').mockImplementationOnce(() =>
Promise.resolve({
endpoints: {
all: {
titleConvo: true,
titleModel: 'gpt-4o-mini',
titleMethod: 'structured',
titlePrompt: 'Default title prompt for all endpoints',
titlePromptTemplate: 'Default template: {{conversation}}',
titleEndpoint: EModelEndpoint.anthropic,
streamRate: 50,
},
[EModelEndpoint.openAI]: {
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
},
},
}),
);
await AppService(app);
// Check that 'all' endpoint config is loaded
expect(app.locals).toHaveProperty('all');
expect(app.locals.all).toMatchObject({
titleConvo: true,
titleModel: 'gpt-4o-mini',
titleMethod: 'structured',
titlePrompt: 'Default title prompt for all endpoints',
titlePromptTemplate: 'Default template: {{conversation}}',
titleEndpoint: EModelEndpoint.anthropic,
streamRate: 50,
});
// Check that OpenAI endpoint has its own config
expect(app.locals).toHaveProperty(EModelEndpoint.openAI);
expect(app.locals[EModelEndpoint.openAI]).toMatchObject({
titleConvo: true,
titleModel: 'gpt-3.5-turbo',
});
});
});
describe('AppService updating app.locals and issuing warnings', () => {

View File

@@ -25,7 +25,7 @@ let i = 0;
* @function loadCustomConfig
* @returns {Promise<TCustomConfig | null>} A promise that resolves to null or the custom config object.
* */
async function loadCustomConfig(printConfig = true) {
async function loadCustomConfig() {
// Use CONFIG_PATH if set, otherwise fallback to defaultConfigPath
const configPath = process.env.CONFIG_PATH || defaultConfigPath;
@@ -108,11 +108,7 @@ https://www.librechat.ai/docs/configuration/stt_tts`);
return null;
} else {
if (printConfig) {
logger.info('Custom config file loaded:');
logger.info(JSON.stringify(customConfig, null, 2));
logger.debug('Custom config:', customConfig);
}
logger.debug('Custom config:', customConfig);
}
(customConfig.endpoints?.custom ?? [])

View File

@@ -104,7 +104,7 @@ const initializeAgent = async ({
agent.endpoint = provider;
const { getOptions, overrideProvider } = await getProviderConfig(provider);
if (overrideProvider !== agent.provider) {
if (overrideProvider) {
agent.provider = overrideProvider;
}
@@ -131,7 +131,7 @@ const initializeAgent = async ({
);
const agentMaxContextTokens = optionalChainWithEmptyCheck(
maxContextTokens,
getModelMaxTokens(tokensModel, providerEndpointMap[provider], options.endpointTokenConfig),
getModelMaxTokens(tokensModel, providerEndpointMap[provider]),
4096,
);
@@ -186,12 +186,11 @@ const initializeAgent = async ({
return {
...agent,
tools,
attachments,
resendFiles,
toolContextMap,
useLegacyContent: !!options.useLegacyContent,
maxContextTokens: Math.round((agentMaxContextTokens - maxTokens) * 0.9),
tools,
maxContextTokens: (agentMaxContextTokens - maxTokens) * 0.9,
};
};

View File

@@ -1,5 +1,5 @@
const OpenAI = require('openai');
const { ProxyAgent } = require('undici');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { ErrorTypes, EModelEndpoint } = require('librechat-data-provider');
const {
getUserKeyValues,
@@ -59,10 +59,7 @@ const initializeClient = async ({ req, res, endpointOption, version, initAppClie
}
if (PROXY) {
const proxyAgent = new ProxyAgent(PROXY);
opts.fetchOptions = {
dispatcher: proxyAgent,
};
opts.httpAgent = new HttpsProxyAgent(PROXY);
}
if (OPENAI_ORGANIZATION) {

View File

@@ -1,5 +1,5 @@
// const OpenAI = require('openai');
const { ProxyAgent } = require('undici');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { ErrorTypes } = require('librechat-data-provider');
const { getUserKey, getUserKeyExpiry, getUserKeyValues } = require('~/server/services/UserService');
const initializeClient = require('./initalize');
@@ -107,7 +107,6 @@ describe('initializeClient', () => {
const res = {};
const { openai } = await initializeClient({ req, res });
expect(openai.fetchOptions).toBeDefined();
expect(openai.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent);
expect(openai.httpAgent).toBeInstanceOf(HttpsProxyAgent);
});
});

View File

@@ -1,5 +1,5 @@
const OpenAI = require('openai');
const { ProxyAgent } = require('undici');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { constructAzureURL, isUserProvided, resolveHeaders } = require('@librechat/api');
const { ErrorTypes, EModelEndpoint, mapModelToAzureConfig } = require('librechat-data-provider');
const {
@@ -158,10 +158,7 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
}
if (PROXY) {
const proxyAgent = new ProxyAgent(PROXY);
opts.fetchOptions = {
dispatcher: proxyAgent,
};
opts.httpAgent = new HttpsProxyAgent(PROXY);
}
if (OPENAI_ORGANIZATION) {

View File

@@ -1,5 +1,5 @@
// const OpenAI = require('openai');
const { ProxyAgent } = require('undici');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { ErrorTypes } = require('librechat-data-provider');
const { getUserKey, getUserKeyExpiry, getUserKeyValues } = require('~/server/services/UserService');
const initializeClient = require('./initialize');
@@ -107,7 +107,6 @@ describe('initializeClient', () => {
const res = {};
const { openai } = await initializeClient({ req, res });
expect(openai.fetchOptions).toBeDefined();
expect(openai.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent);
expect(openai.httpAgent).toBeInstanceOf(HttpsProxyAgent);
});
});

View File

@@ -139,11 +139,7 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
);
clientOptions.modelOptions.user = req.user.id;
const options = getOpenAIConfig(apiKey, clientOptions, endpoint);
if (options != null) {
options.useLegacyContent = true;
options.endpointTokenConfig = endpointTokenConfig;
}
if (!clientOptions.streamRate) {
if (!customOptions.streamRate) {
return options;
}
options.llmConfig.callbacks = [
@@ -160,7 +156,6 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
}
return {
useLegacyContent: true,
llmConfig: modelOptions,
};
}

View File

@@ -34,13 +34,13 @@ const providerConfigMap = {
* @param {string} provider - The provider string
* @returns {Promise<{
* getOptions: Function,
* overrideProvider: string,
* overrideProvider?: string,
* customEndpointConfig?: TEndpoint
* }>}
*/
async function getProviderConfig(provider) {
let getOptions = providerConfigMap[provider];
let overrideProvider = provider;
let overrideProvider;
/** @type {TEndpoint | undefined} */
let customEndpointConfig;
@@ -56,7 +56,7 @@ async function getProviderConfig(provider) {
overrideProvider = Providers.OPENAI;
}
if (isKnownCustomProvider(overrideProvider) && !customEndpointConfig) {
if (isKnownCustomProvider(overrideProvider || provider) && !customEndpointConfig) {
customEndpointConfig = await getCustomEndpointConfig(provider);
if (!customEndpointConfig) {
throw new Error(`Provider ${provider} not supported`);

View File

@@ -65,20 +65,19 @@ const initializeClient = async ({
const isAzureOpenAI = endpoint === EModelEndpoint.azureOpenAI;
/** @type {false | TAzureConfig} */
const azureConfig = isAzureOpenAI && req.app.locals[EModelEndpoint.azureOpenAI];
let serverless = false;
if (isAzureOpenAI && azureConfig) {
const { modelGroupMap, groupMap } = azureConfig;
const {
azureOptions,
baseURL,
headers = {},
serverless: _serverless,
serverless,
} = mapModelToAzureConfig({
modelName,
modelGroupMap,
groupMap,
});
serverless = _serverless;
clientOptions.reverseProxyUrl = baseURL ?? clientOptions.reverseProxyUrl;
clientOptions.headers = resolveHeaders(
@@ -144,9 +143,6 @@ const initializeClient = async ({
clientOptions = Object.assign({ modelOptions }, clientOptions);
clientOptions.modelOptions.user = req.user.id;
const options = getOpenAIConfig(apiKey, clientOptions);
if (options != null && serverless === true) {
options.useLegacyContent = true;
}
const streamRate = clientOptions.streamRate;
if (!streamRate) {
return options;

View File

@@ -8,7 +8,6 @@ const { findOnePluginAuth, updatePluginAuth, deletePluginAuth } = require('~/mod
* @param {string} userId - The unique identifier of the user for whom the plugin authentication value is to be retrieved.
* @param {string} authField - The specific authentication field (e.g., 'API_KEY', 'URL') whose value is to be retrieved and decrypted.
* @param {boolean} throwError - Whether to throw an error if the authentication value does not exist. Defaults to `true`.
* @param {string} [pluginKey] - Optional plugin key to make the lookup more specific to a particular plugin.
* @returns {Promise<string|null>} A promise that resolves to the decrypted authentication value if found, or `null` if no such authentication value exists for the given user and field.
*
* The function throws an error if it encounters any issue during the retrieval or decryption process, or if the authentication value does not exist.
@@ -21,28 +20,14 @@ const { findOnePluginAuth, updatePluginAuth, deletePluginAuth } = require('~/mod
* console.error(err);
* });
*
* @example
* // To get the decrypted value of the 'API_KEY' field for a specific plugin:
* getUserPluginAuthValue('12345', 'API_KEY', true, 'mcp-server-name').then(value => {
* console.log(value);
* }).catch(err => {
* console.error(err);
* });
*
* @throws {Error} Throws an error if there's an issue during the retrieval or decryption process, or if the authentication value does not exist.
* @async
*/
const getUserPluginAuthValue = async (userId, authField, throwError = true, pluginKey) => {
const getUserPluginAuthValue = async (userId, authField, throwError = true) => {
try {
const searchParams = { userId, authField };
if (pluginKey) {
searchParams.pluginKey = pluginKey;
}
const pluginAuth = await findOnePluginAuth(searchParams);
const pluginAuth = await findOnePluginAuth({ userId, authField });
if (!pluginAuth) {
const pluginInfo = pluginKey ? ` for plugin ${pluginKey}` : '';
throw new Error(`No plugin auth ${authField} found for user ${userId}${pluginInfo}`);
throw new Error(`No plugin auth ${authField} found for user ${userId}`);
}
const decryptedValue = await decrypt(pluginAuth.value);
@@ -56,6 +41,38 @@ const getUserPluginAuthValue = async (userId, authField, throwError = true, plug
}
};
/**
* Asynchronously retrieves and decrypts the authentication value for a user's specific plugin, based on a specified authentication field and plugin key.
*
* @param {string} userId - The unique identifier of the user for whom the plugin authentication value is to be retrieved.
* @param {string} authField - The specific authentication field (e.g., 'API_KEY', 'URL') whose value is to be retrieved and decrypted.
* @param {string} pluginKey - The plugin key to filter by (e.g., 'mcp_github-mcp').
* @param {boolean} throwError - Whether to throw an error if the authentication value does not exist. Defaults to `true`.
* @returns {Promise<string|null>} A promise that resolves to the decrypted authentication value if found, or `null` if no such authentication value exists for the given user, field, and plugin.
*
* @throws {Error} Throws an error if there's an issue during the retrieval or decryption process, or if the authentication value does not exist.
* @async
*/
const getUserPluginAuthValueByPlugin = async (userId, authField, pluginKey, throwError = true) => {
try {
const pluginAuth = await findOnePluginAuth({ userId, authField, pluginKey });
if (!pluginAuth) {
throw new Error(
`No plugin auth ${authField} found for user ${userId} and plugin ${pluginKey}`,
);
}
const decryptedValue = await decrypt(pluginAuth.value);
return decryptedValue;
} catch (err) {
if (!throwError) {
return null;
}
logger.error('[getUserPluginAuthValueByPlugin]', err);
throw err;
}
};
// const updateUserPluginAuth = async (userId, authField, pluginKey, value) => {
// try {
// const encryptedValue = encrypt(value);
@@ -134,6 +151,7 @@ const deleteUserPluginAuth = async (userId, authField, all = false, pluginKey) =
module.exports = {
getUserPluginAuthValue,
getUserPluginAuthValueByPlugin,
updateUserPluginAuth,
deleteUserPluginAuth,
};

View File

@@ -10,6 +10,15 @@ const { getLogStores } = require('~/cache');
* @param {import('express').Application} app - Express app instance
*/
async function initializeMCPs(app) {
// TEMPORARY: Reset all OAuth tokens for fresh testing
try {
logger.info('[MCP] Resetting all OAuth tokens for fresh testing...');
await deleteTokens({});
logger.info('[MCP] All OAuth tokens reset successfully');
} catch (error) {
logger.error('[MCP] Error resetting OAuth tokens:', error);
}
const mcpServers = app.locals.mcpConfig;
if (!mcpServers) {
return;
@@ -36,7 +45,7 @@ async function initializeMCPs(app) {
const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null;
try {
await mcpManager.initializeMCPs({
const oauthRequirements = await mcpManager.initializeMCPs({
mcpServers: filteredServers,
flowManager,
tokenMethods: {
@@ -64,6 +73,9 @@ async function initializeMCPs(app) {
logger.debug('Cleared tools array cache after MCP initialization');
logger.info('MCP servers initialized successfully');
// Store OAuth requirement information in app locals for client access
app.locals.mcpOAuthRequirements = oauthRequirements;
} catch (error) {
logger.error('Failed to initialize MCP servers:', error);
}

View File

@@ -52,11 +52,6 @@ function assistantsConfigSetup(config, assistantsEndpoint, prevConfig = {}) {
privateAssistants: parsedConfig.privateAssistants,
timeoutMs: parsedConfig.timeoutMs,
streamRate: parsedConfig.streamRate,
titlePrompt: parsedConfig.titlePrompt,
titleMethod: parsedConfig.titleMethod,
titleModel: parsedConfig.titleModel,
titleEndpoint: parsedConfig.titleEndpoint,
titlePromptTemplate: parsedConfig.titlePromptTemplate,
};
}

View File

@@ -1,6 +1,6 @@
const { webSearchKeys } = require('@librechat/api');
const {
Constants,
webSearchKeys,
deprecatedAzureVariables,
conflictingAzureVariables,
extractVariableName,

View File

@@ -50,7 +50,6 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
temporaryChat: interfaceConfig?.temporaryChat ?? defaults.temporaryChat,
runCode: interfaceConfig?.runCode ?? defaults.runCode,
webSearch: interfaceConfig?.webSearch ?? defaults.webSearch,
fileSearch: interfaceConfig?.fileSearch ?? defaults.fileSearch,
customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome,
});
@@ -66,7 +65,6 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch },
});
await updateAccessPermissions(SystemRoles.ADMIN, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
@@ -80,7 +78,6 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch },
});
let i = 0;

View File

@@ -18,7 +18,6 @@ describe('loadDefaultInterface', () => {
temporaryChat: true,
runCode: true,
webSearch: true,
fileSearch: true,
},
};
const configDefaults = { interface: {} };
@@ -28,13 +27,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
});
});
@@ -49,7 +47,6 @@ describe('loadDefaultInterface', () => {
temporaryChat: false,
runCode: false,
webSearch: false,
fileSearch: false,
},
};
const configDefaults = { interface: {} };
@@ -59,13 +56,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: false },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: false, [Permissions.OPT_OUT]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: false },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
[PermissionTypes.AGENTS]: { [Permissions.USE]: false },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: false },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: false },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false },
});
});
@@ -78,16 +74,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MEMORIES]: {
[Permissions.USE]: undefined,
[Permissions.OPT_OUT]: undefined,
},
[PermissionTypes.MEMORIES]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined },
});
});
@@ -102,7 +94,6 @@ describe('loadDefaultInterface', () => {
temporaryChat: undefined,
runCode: undefined,
webSearch: undefined,
fileSearch: undefined,
},
};
const configDefaults = { interface: {} };
@@ -112,16 +103,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MEMORIES]: {
[Permissions.USE]: undefined,
[Permissions.OPT_OUT]: undefined,
},
[PermissionTypes.MEMORIES]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined },
});
});
@@ -136,7 +123,6 @@ describe('loadDefaultInterface', () => {
temporaryChat: undefined,
runCode: false,
webSearch: true,
fileSearch: false,
},
};
const configDefaults = { interface: {} };
@@ -146,13 +132,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false },
});
});
@@ -168,7 +153,6 @@ describe('loadDefaultInterface', () => {
temporaryChat: true,
runCode: true,
webSearch: true,
fileSearch: true,
},
};
@@ -177,13 +161,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: true },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
});
});
@@ -196,16 +179,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MEMORIES]: {
[Permissions.USE]: undefined,
[Permissions.OPT_OUT]: undefined,
},
[PermissionTypes.MEMORIES]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined },
});
});
@@ -218,16 +197,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MEMORIES]: {
[Permissions.USE]: undefined,
[Permissions.OPT_OUT]: undefined,
},
[PermissionTypes.MEMORIES]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined },
});
});
@@ -240,16 +215,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MEMORIES]: {
[Permissions.USE]: undefined,
[Permissions.OPT_OUT]: undefined,
},
[PermissionTypes.MEMORIES]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined },
});
});
@@ -263,7 +234,6 @@ describe('loadDefaultInterface', () => {
agents: false,
temporaryChat: true,
runCode: false,
fileSearch: true,
},
};
const configDefaults = { interface: {} };
@@ -273,13 +243,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: false },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
});
});
@@ -295,7 +264,6 @@ describe('loadDefaultInterface', () => {
temporaryChat: undefined,
runCode: undefined,
webSearch: undefined,
fileSearch: true,
},
};
@@ -304,13 +272,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: false, [Permissions.OPT_OUT]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: false },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: false },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
});
});
@@ -333,90 +300,12 @@ describe('loadDefaultInterface', () => {
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: false },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: undefined },
});
});
it('should call updateAccessPermissions with the correct parameters when FILE_SEARCH is true', async () => {
const config = {
interface: {
fileSearch: true,
},
};
const configDefaults = { interface: {} };
await loadDefaultInterface(config, configDefaults);
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
});
});
it('should call updateAccessPermissions with false when FILE_SEARCH is false', async () => {
const config = {
interface: {
fileSearch: false,
},
};
const configDefaults = { interface: {} };
await loadDefaultInterface(config, configDefaults);
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: undefined },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: undefined },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: undefined },
[PermissionTypes.AGENTS]: { [Permissions.USE]: undefined },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: undefined },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: undefined },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: undefined },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: false },
});
});
it('should call updateAccessPermissions with all interface options including fileSearch', async () => {
const config = {
interface: {
prompts: true,
bookmarks: false,
memories: true,
multiConvo: true,
agents: false,
temporaryChat: true,
runCode: false,
webSearch: true,
fileSearch: true,
},
};
const configDefaults = { interface: {} };
await loadDefaultInterface(config, configDefaults);
expect(updateAccessPermissions).toHaveBeenCalledWith(SystemRoles.USER, {
[PermissionTypes.PROMPTS]: { [Permissions.USE]: true },
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false },
[PermissionTypes.MEMORIES]: { [Permissions.USE]: true, [Permissions.OPT_OUT]: undefined },
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
[PermissionTypes.AGENTS]: { [Permissions.USE]: false },
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: false },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true },
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
});
});
});

View File

@@ -10,4 +10,3 @@ process.env.JWT_SECRET = 'test';
process.env.JWT_REFRESH_SECRET = 'test';
process.env.CREDS_KEY = 'test';
process.env.CREDS_IV = 'test';
process.env.OPENAI_API_KEY = 'test';

View File

@@ -226,14 +226,7 @@ const xAIModels = {
'grok-4': 256000, // 256K context
};
const aggregateModels = {
...openAIModels,
...googleModels,
...bedrockModels,
...xAIModels,
// misc.
kimi: 131000,
};
const aggregateModels = { ...openAIModels, ...googleModels, ...bedrockModels, ...xAIModels };
const maxTokensMap = {
[EModelEndpoint.azureOpenAI]: openAIModels,

View File

@@ -714,45 +714,3 @@ describe('Claude Model Tests', () => {
});
});
});
describe('Kimi Model Tests', () => {
describe('getModelMaxTokens', () => {
test('should return correct tokens for Kimi models', () => {
expect(getModelMaxTokens('kimi')).toBe(131000);
expect(getModelMaxTokens('kimi-k2')).toBe(131000);
expect(getModelMaxTokens('kimi-vl')).toBe(131000);
});
test('should return correct tokens for Kimi models with provider prefix', () => {
expect(getModelMaxTokens('moonshotai/kimi-k2')).toBe(131000);
expect(getModelMaxTokens('moonshotai/kimi')).toBe(131000);
expect(getModelMaxTokens('moonshotai/kimi-vl')).toBe(131000);
});
test('should handle partial matches for Kimi models', () => {
expect(getModelMaxTokens('kimi-k2-latest')).toBe(131000);
expect(getModelMaxTokens('kimi-vl-preview')).toBe(131000);
expect(getModelMaxTokens('kimi-2024')).toBe(131000);
});
});
describe('matchModelName', () => {
test('should match exact Kimi model names', () => {
expect(matchModelName('kimi')).toBe('kimi');
expect(matchModelName('kimi-k2')).toBe('kimi');
expect(matchModelName('kimi-vl')).toBe('kimi');
});
test('should match Kimi model variations with provider prefix', () => {
expect(matchModelName('moonshotai/kimi')).toBe('kimi');
expect(matchModelName('moonshotai/kimi-k2')).toBe('kimi');
expect(matchModelName('moonshotai/kimi-vl')).toBe('kimi');
});
test('should match Kimi model variations with suffixes', () => {
expect(matchModelName('kimi-k2-latest')).toBe('kimi');
expect(matchModelName('kimi-vl-preview')).toBe('kimi');
expect(matchModelName('kimi-2024')).toBe('kimi');
});
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/frontend",
"version": "v0.7.9",
"version": "v0.7.9-rc1",
"description": "",
"type": "module",
"scripts": {

View File

@@ -1,31 +0,0 @@
import React, { createContext, useContext, useMemo } from 'react';
import type { EModelEndpoint } from 'librechat-data-provider';
import { useChatContext } from './ChatContext';
interface SidePanelContextValue {
endpoint?: EModelEndpoint | null;
}
const SidePanelContext = createContext<SidePanelContextValue | undefined>(undefined);
export function SidePanelProvider({ children }: { children: React.ReactNode }) {
const { conversation } = useChatContext();
/** Context value only created when endpoint changes */
const contextValue = useMemo<SidePanelContextValue>(
() => ({
endpoint: conversation?.endpoint,
}),
[conversation?.endpoint],
);
return <SidePanelContext.Provider value={contextValue}>{children}</SidePanelContext.Provider>;
}
export function useSidePanelContext() {
const context = useContext(SidePanelContext);
if (!context) {
throw new Error('useSidePanelContext must be used within SidePanelProvider');
}
return context;
}

View File

@@ -24,5 +24,4 @@ export * from './ToolCallsMapContext';
export * from './SetConvoContext';
export * from './SearchContext';
export * from './BadgeRowContext';
export * from './SidePanelContext';
export { default as BadgeRowProvider } from './BadgeRowContext';

View File

@@ -1,24 +1,14 @@
import React, { memo } from 'react';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import CheckboxButton from '~/components/ui/CheckboxButton';
import { useLocalize, useHasAccess } from '~/hooks';
import { useBadgeRowContext } from '~/Providers';
import { VectorIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
function FileSearch() {
const localize = useLocalize();
const { fileSearch } = useBadgeRowContext();
const { toggleState: fileSearchEnabled, debouncedChange, isPinned } = fileSearch;
const canUseFileSearch = useHasAccess({
permissionType: PermissionTypes.FILE_SEARCH,
permission: Permissions.USE,
});
if (!canUseFileSearch) {
return null;
}
return (
<>
{(fileSearchEnabled || isPinned) && (

View File

@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { ImageUpIcon, FileSearch, TerminalSquareIcon, FileType2Icon } from 'lucide-react';
import { EToolResources, defaultAgentCapabilities } from 'librechat-data-provider';
import { FileSearch, ImageUpIcon, FileType2Icon, TerminalSquareIcon } from 'lucide-react';
import { useLocalize, useGetAgentsConfig, useAgentCapabilities } from '~/hooks';
import { OGDialog, OGDialogTemplate } from '~/components/ui';

View File

@@ -1,43 +1,65 @@
import { useQueryClient } from '@tanstack/react-query';
import { Constants, QueryKeys } from 'librechat-data-provider';
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
import React, { memo, useCallback, useState, useMemo, useRef } from 'react';
import React, { memo, useCallback, useState, useMemo } from 'react';
import { SettingsIcon, PlugZap } from 'lucide-react';
import { Constants } from 'librechat-data-provider';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import MCPConfigDialog, { ConfigFieldDetail } from '~/components/ui/MCP/MCPConfigDialog';
import { useMCPServerInitialization } from '~/hooks/MCP/useMCPServerInitialization';
import MCPServerStatusIcon from '~/components/ui/MCP/MCPServerStatusIcon';
import { useMCPConnectionStatusQuery, useMCPAuthValuesQuery } from '~/data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys } from 'librechat-data-provider';
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
import { MCPConfigDialog, type ConfigFieldDetail } from '~/components/ui/MCP';
import { useToastContext, useBadgeRowContext } from '~/Providers';
import MultiSelect from '~/components/ui/MultiSelect';
import { MCPIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
const getBaseMCPPluginKey = (fullPluginKey: string): string => {
const parts = fullPluginKey.split(Constants.mcp_delimiter);
return Constants.mcp_prefix + parts[parts.length - 1];
};
function MCPSelect() {
const localize = useLocalize();
const { showToast } = useToastContext();
const { mcpSelect, startupConfig } = useBadgeRowContext();
const { mcpValues, setMCPValues, mcpToolDetails, isPinned } = mcpSelect;
// Get all configured MCP servers from config
const configuredServers = useMemo(() => {
return Object.keys(startupConfig?.mcpServers || {});
}, [startupConfig?.mcpServers]);
// Get real connection status from MCPManager
const { data: statusQuery } = useMCPConnectionStatusQuery();
const mcpServerStatuses = statusQuery?.connectionStatus || {};
console.log('mcpServerStatuses', mcpServerStatuses);
console.log('statusQuery', statusQuery);
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
// Fetch auth values for the selected server
const { data: authValuesData } = useMCPAuthValuesQuery(selectedToolForConfig?.name || '', {
enabled: isConfigModalOpen && !!selectedToolForConfig?.name,
});
const queryClient = useQueryClient();
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
onSuccess: async () => {
onSuccess: async (data, variables) => {
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
// tools so we dont leave tools available for use in chat if we revoke and thus kill mcp server
// auth values so customUserVars flags are updated in customUserVarsSection
// connection status so connection indicators are updated in the dropdown
// // For 'uninstall' actions (revoke), remove the server from selected values
// if (variables.action === 'uninstall') {
// const serverName = variables.pluginKey.replace(Constants.mcp_prefix, '');
// const currentValues = mcpValues ?? [];
// const filteredValues = currentValues.filter((name) => name !== serverName);
// setMCPValues(filteredValues);
// }
// Wait for all refetches to complete before ending loading state
await Promise.all([
queryClient.invalidateQueries([QueryKeys.tools]),
queryClient.refetchQueries([QueryKeys.tools]),
queryClient.invalidateQueries([QueryKeys.mcpAuthValues]),
queryClient.refetchQueries([QueryKeys.mcpAuthValues]),
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]),
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]),
]);
},
@@ -50,61 +72,6 @@ function MCPSelect() {
},
});
// Use the shared initialization hook
const { initializeServer, isInitializing, connectionStatus, cancelOAuthFlow, isCancellable } =
useMCPServerInitialization({
onSuccess: (serverName) => {
// Add to selected values after successful initialization
const currentValues = mcpValues ?? [];
if (!currentValues.includes(serverName)) {
setMCPValues([...currentValues, serverName]);
}
},
onError: (serverName) => {
// Find the tool/server configuration
const tool = mcpToolDetails?.find((t) => t.name === serverName);
const serverConfig = startupConfig?.mcpServers?.[serverName];
const serverStatus = connectionStatus[serverName];
// Check if this server would show a config button
const hasAuthConfig =
(tool?.authConfig && tool.authConfig.length > 0) ||
(serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0);
// Only open dialog if the server would have shown a config button
// (disconnected/error states always show button, connected only shows if hasAuthConfig)
const wouldShowButton =
!serverStatus ||
serverStatus.connectionState === 'disconnected' ||
serverStatus.connectionState === 'error' ||
(serverStatus.connectionState === 'connected' && hasAuthConfig);
if (!wouldShowButton) {
return; // Don't open dialog if no button would be shown
}
// Create tool object if it doesn't exist
const configTool = tool || {
name: serverName,
pluginKey: `${Constants.mcp_prefix}${serverName}`,
authConfig: serverConfig?.customUserVars
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
authField: key,
label: config.title,
description: config.description,
}))
: [],
authenticated: false,
};
previousFocusRef.current = document.activeElement as HTMLElement;
// Open the config dialog on error
setSelectedToolForConfig(configTool);
setIsConfigModalOpen(true);
},
});
const renderSelectedValues = useCallback(
(values: string[], placeholder?: string) => {
if (values.length === 0) {
@@ -140,22 +107,21 @@ function MCPSelect() {
(targetName: string) => {
if (selectedToolForConfig && selectedToolForConfig.name === targetName) {
// Use the pluginKey directly since it's already in the correct format
console.log(
`[MCP Select] Revoking config for ${targetName}, pluginKey: ${`${Constants.mcp_prefix}${targetName}`}`,
);
const payload: TUpdateUserPlugins = {
pluginKey: `${Constants.mcp_prefix}${targetName}`,
action: 'uninstall',
auth: {},
};
updateUserPluginsMutation.mutate(payload);
// Remove the server from selected values after revoke
const currentValues = mcpValues ?? [];
const filteredValues = currentValues.filter((name) => name !== targetName);
setMCPValues(filteredValues);
}
},
[selectedToolForConfig, updateUserPluginsMutation, mcpValues, setMCPValues],
[selectedToolForConfig, updateUserPluginsMutation],
);
// Create stable callback references to prevent stale closures
const handleSave = useCallback(
(authData: Record<string, string>) => {
if (selectedToolForConfig) {
@@ -171,142 +137,122 @@ function MCPSelect() {
}
}, [selectedToolForConfig, handleConfigRevoke]);
const handleDialogOpenChange = useCallback((open: boolean) => {
setIsConfigModalOpen(open);
// Restore focus when dialog closes
if (!open && previousFocusRef.current) {
// Use setTimeout to ensure the dialog has fully closed before restoring focus
setTimeout(() => {
if (previousFocusRef.current && typeof previousFocusRef.current.focus === 'function') {
previousFocusRef.current.focus();
}
previousFocusRef.current = null;
}, 0);
}
}, []);
// Get connection status for all MCP servers (now from hook)
// Remove the duplicate useMCPConnectionStatusQuery since it's in the hook
// Modified setValue function that attempts to initialize disconnected servers
const filteredSetMCPValues = useCallback(
// Only allow connected servers to be selected
const handleSetSelectedValues = useCallback(
(values: string[]) => {
// Separate connected and disconnected servers
const connectedServers: string[] = [];
const disconnectedServers: string[] = [];
values.forEach((serverName) => {
const serverStatus = connectionStatus[serverName];
if (serverStatus?.connectionState === 'connected') {
connectedServers.push(serverName);
} else {
disconnectedServers.push(serverName);
}
});
// Only set connected servers as selected values
setMCPValues(connectedServers);
// Attempt to initialize each disconnected server (once)
disconnectedServers.forEach((serverName) => {
initializeServer(serverName);
// Filter to only include connected servers
const connectedValues = values.filter((serverName) => {
const serverStatus = mcpServerStatuses?.[serverName];
return serverStatus?.connected || false;
});
setMCPValues(connectedValues);
},
[connectionStatus, setMCPValues, initializeServer],
[setMCPValues, mcpServerStatuses],
);
const renderItemContent = useCallback(
(serverName: string, defaultContent: React.ReactNode) => {
const tool = mcpToolDetails?.find((t) => t.name === serverName);
const serverStatus = connectionStatus[serverName];
const serverConfig = startupConfig?.mcpServers?.[serverName];
const serverStatus = mcpServerStatuses?.[serverName];
const connected = serverStatus?.connected || false;
const hasAuthConfig = serverStatus?.hasAuthConfig || false;
const handleConfigClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
// Icon logic:
// - connected with auth config = gear (green)
// - connected without auth config = no icon (just text)
// - not connected = zap (orange)
let icon: React.ReactNode = null;
let tooltip = 'Configure server';
previousFocusRef.current = document.activeElement as HTMLElement;
const configTool = tool || {
name: serverName,
pluginKey: `${Constants.mcp_prefix}${serverName}`,
authConfig: serverConfig?.customUserVars
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
authField: key,
label: config.title,
description: config.description,
}))
: [],
authenticated: false,
};
setSelectedToolForConfig(configTool);
setIsConfigModalOpen(true);
};
const handleCancelClick = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
cancelOAuthFlow(serverName);
};
// Common wrapper for the main content (check mark + text)
// Ensures Check & Text are adjacent and the group takes available space.
const mainContentWrapper = (
<button
type="button"
className="flex flex-grow items-center rounded bg-transparent p-0 text-left transition-colors focus:outline-none"
tabIndex={0}
>
{defaultContent}
</button>
);
// Check if this server has customUserVars to configure
const hasCustomUserVars =
serverConfig?.customUserVars && Object.keys(serverConfig.customUserVars).length > 0;
const statusIcon = (
<MCPServerStatusIcon
serverName={serverName}
serverStatus={serverStatus}
tool={tool}
onConfigClick={handleConfigClick}
isInitializing={isInitializing(serverName)}
canCancel={isCancellable(serverName)}
onCancel={handleCancelClick}
hasCustomUserVars={hasCustomUserVars}
/>
);
if (statusIcon) {
return (
<div className="flex w-full items-center justify-between">
{mainContentWrapper}
<div className="ml-2 flex items-center">{statusIcon}</div>
</div>
);
if (connected) {
if (hasAuthConfig) {
icon = <SettingsIcon className="h-4 w-4 text-green-500" />;
tooltip = 'Configure connected server';
} else {
// No icon for connected servers without auth config
tooltip = 'Connected server (no configuration needed)';
}
} else {
icon = <PlugZap className="h-4 w-4 text-orange-400" />;
tooltip = 'Configure server';
}
return mainContentWrapper;
const onClick = () => {
const serverConfig = startupConfig?.mcpServers?.[serverName];
if (serverConfig) {
const serverTool = {
name: serverName,
pluginKey: `${Constants.mcp_prefix}${serverName}`,
authConfig: Object.entries(serverConfig.customUserVars || {}).map(([key, config]) => ({
authField: key,
label: config.title,
description: config.description,
requiresOAuth: serverConfig.requiresOAuth || false,
})),
authenticated: connected,
};
setSelectedToolForConfig(serverTool);
setIsConfigModalOpen(true);
}
};
return (
<div className="flex w-full items-center justify-between">
<div className={`flex flex-grow items-center ${!connected ? 'opacity-50' : ''}`}>
{defaultContent}
</div>
{icon && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onClick();
}}
className="ml-2 flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={tooltip}
title={tooltip}
>
{icon}
</button>
)}
</div>
);
},
[
isInitializing,
isCancellable,
mcpToolDetails,
cancelOAuthFlow,
connectionStatus,
startupConfig?.mcpServers,
],
[mcpServerStatuses, setSelectedToolForConfig, setIsConfigModalOpen, startupConfig],
);
// Don't render if no servers are selected and not pinned
if ((!mcpValues || mcpValues.length === 0) && !isPinned) {
// Memoize schema and initial values to prevent unnecessary re-renders
const fieldsSchema = useMemo(() => {
const schema: Record<string, ConfigFieldDetail> = {};
if (selectedToolForConfig?.authConfig) {
selectedToolForConfig.authConfig.forEach((field) => {
schema[field.authField] = {
title: field.label,
description: field.description,
};
});
}
return schema;
}, [selectedToolForConfig?.authConfig]);
const initialValues = useMemo(() => {
const initial: Record<string, string> = {};
// Always start with empty values for security - never prefill sensitive data
if (selectedToolForConfig?.authConfig) {
selectedToolForConfig.authConfig.forEach((field) => {
initial[field.authField] = '';
});
}
return initial;
}, [selectedToolForConfig?.authConfig]);
// Don't render if no MCP servers are available at all
if (!mcpServerStatuses || Object.keys(mcpServerStatuses).length === 0) {
return null;
}
// Don't render if no MCP servers are configured
if (!configuredServers || configuredServers.length === 0) {
// Don't render if no servers are selected and not pinned
if ((!mcpValues || mcpValues.length === 0) && !isPinned) {
return null;
}
@@ -315,9 +261,9 @@ function MCPSelect() {
return (
<>
<MultiSelect
items={configuredServers}
items={Object.keys(mcpServerStatuses) || []}
selectedValues={mcpValues ?? []}
setSelectedValues={filteredSetMCPValues}
setSelectedValues={handleSetSelectedValues}
defaultSelectedValues={mcpValues ?? []}
renderSelectedValues={renderSelectedValues}
renderItemContent={renderItemContent}
@@ -330,35 +276,16 @@ function MCPSelect() {
/>
{selectedToolForConfig && (
<MCPConfigDialog
serverName={selectedToolForConfig.name}
serverStatus={connectionStatus[selectedToolForConfig.name]}
isOpen={isConfigModalOpen}
onOpenChange={handleDialogOpenChange}
fieldsSchema={(() => {
const schema: Record<string, ConfigFieldDetail> = {};
if (selectedToolForConfig?.authConfig) {
selectedToolForConfig.authConfig.forEach((field) => {
schema[field.authField] = {
title: field.label,
description: field.description,
};
});
}
return schema;
})()}
initialValues={(() => {
const initial: Record<string, string> = {};
// Note: Actual initial values might need to be fetched if they are stored user-specifically
if (selectedToolForConfig?.authConfig) {
selectedToolForConfig.authConfig.forEach((field) => {
initial[field.authField] = ''; // Or fetched value
});
}
return initial;
})()}
onOpenChange={setIsConfigModalOpen}
serverName={selectedToolForConfig.name}
fieldsSchema={fieldsSchema}
initialValues={initialValues}
onSave={handleSave}
onRevoke={handleRevoke}
isSubmitting={updateUserPluginsMutation.isLoading}
isConnected={mcpServerStatuses?.[selectedToolForConfig.name]?.connected || false}
authConfig={selectedToolForConfig.authConfig}
/>
)}
</>

View File

@@ -72,11 +72,6 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
permission: Permissions.USE,
});
const canUseFileSearch = useHasAccess({
permissionType: PermissionTypes.FILE_SEARCH,
permission: Permissions.USE,
});
const showWebSearchSettings = useMemo(() => {
const authTypes = webSearchAuthData?.authTypes ?? [];
if (authTypes.length === 0) return true;
@@ -145,7 +140,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
const dropdownItems: MenuItemProps[] = [];
if (fileSearchEnabled && canUseFileSearch) {
if (fileSearchEnabled) {
dropdownItems.push({
onClick: handleFileSearchToggle,
hideOnClick: false,

View File

@@ -4,10 +4,10 @@ import { FileSources, LocalStorageKeys } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation } from '~/data-provider';
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
import { EditorProvider, SidePanelProvider } from '~/Providers';
import Artifacts from '~/components/Artifacts/Artifacts';
import { SidePanelGroup } from '~/components/SidePanel';
import { useSetFilesToDelete } from '~/hooks';
import { EditorProvider } from '~/Providers';
import store from '~/store';
export default function Presentation({ children }: { children: React.ReactNode }) {
@@ -59,24 +59,22 @@ export default function Presentation({ children }: { children: React.ReactNode }
return (
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
<SidePanelProvider>
<SidePanelGroup
defaultLayout={defaultLayout}
fullPanelCollapse={fullCollapse}
defaultCollapsed={defaultCollapsed}
artifacts={
artifactsVisibility === true && Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>
) : null
}
>
<main className="flex h-full flex-col overflow-y-auto" role="main">
{children}
</main>
</SidePanelGroup>
</SidePanelProvider>
<SidePanelGroup
defaultLayout={defaultLayout}
fullPanelCollapse={fullCollapse}
defaultCollapsed={defaultCollapsed}
artifacts={
artifactsVisibility === true && Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>
) : null
}
>
<main className="flex h-full flex-col overflow-y-auto" role="main">
{children}
</main>
</SidePanelGroup>
</DragDropWrapper>
);
}

View File

@@ -6,18 +6,25 @@ import { useGetStartupConfig } from '~/data-provider';
import type { TDialogProps } from '~/common';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import {
Personalization,
Commands,
GearIcon,
DataIcon,
SpeechIcon,
UserIcon,
ExperimentIcon,
PersonalizationIcon,
} from '~/components/svg';
import {
General,
Chat,
Speech,
Commands,
Data,
Account,
Balance,
Speech,
Data,
Chat,
Personalization,
} from './SettingsTabs';
import { GearIcon, DataIcon, SpeechIcon, UserIcon, PersonalizationIcon } from '~/components/svg';
import usePersonalizationAccess from '~/hooks/usePersonalizationAccess';
import { useMediaQuery, useLocalize, TranslationKeys } from '~/hooks';
import usePersonalizationAccess from '~/hooks/usePersonalizationAccess';
import { cn } from '~/utils';
export default function Settings({ open, onOpenChange }: TDialogProps) {
@@ -32,6 +39,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
const tabs: SettingsTabValues[] = [
SettingsTabValues.GENERAL,
SettingsTabValues.CHAT,
SettingsTabValues.BETA,
SettingsTabValues.COMMANDS,
SettingsTabValues.SPEECH,
...(hasAnyPersonalizationFeature ? [SettingsTabValues.PERSONALIZATION] : []),
@@ -76,6 +84,11 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
icon: <MessageSquare className="icon-sm" />,
label: 'com_nav_setting_chat',
},
{
value: SettingsTabValues.BETA,
icon: <ExperimentIcon />,
label: 'com_nav_setting_beta',
},
{
value: SettingsTabValues.COMMANDS,
icon: <Command className="icon-sm" />,

View File

@@ -2,20 +2,14 @@ import React, { useState, useMemo, useCallback } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { Controller, useWatch, useFormContext } from 'react-hook-form';
import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
import {
removeFocusOutlines,
processAgentOption,
getEndpointField,
defaultTextProps,
getIconKey,
cn,
} from '~/utils';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useToastContext, useFileMapContext, useAgentPanelContext } from '~/Providers';
import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities';
import Action from '~/components/SidePanel/Builder/Action';
import { ToolSelectDialog } from '~/components/Tools';
import { useGetAgentFiles } from '~/data-provider';
import { icons } from '~/hooks/Endpoint/Icons';
import { processAgentOption } from '~/utils';
import Instructions from './Instructions';
import AgentAvatar from './AgentAvatar';
import FileContext from './FileContext';

View File

@@ -1,37 +1,92 @@
import { ChevronLeft } from 'lucide-react';
import { Constants } from 'librechat-data-provider';
import { ChevronLeft, RefreshCw } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import {
useUpdateUserPluginsMutation,
useReinitializeMCPServerMutation,
} from 'librechat-data-provider/react-query';
import { useQueryClient } from '@tanstack/react-query';
import React, { useState, useMemo, useCallback } from 'react';
import { Constants, QueryKeys } from 'librechat-data-provider';
import { QueryKeys } from 'librechat-data-provider';
import type { TUpdateUserPlugins } from 'librechat-data-provider';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import ServerInitializationSection from '~/components/ui/MCP/ServerInitializationSection';
import CustomUserVarsSection from '~/components/ui/MCP/CustomUserVarsSection';
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
import { Button, Input, Label } from '~/components/ui';
import { useGetStartupConfig } from '~/data-provider';
import MCPPanelSkeleton from './MCPPanelSkeleton';
import { useToastContext } from '~/Providers';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
interface ServerConfigWithVars {
serverName: string;
config: {
customUserVars: Record<string, { title: string; description: string }>;
};
}
export default function MCPPanel() {
const localize = useLocalize();
const { showToast } = useToastContext();
const queryClient = useQueryClient();
const { data: startupConfig, isLoading: startupConfigLoading } = useGetStartupConfig();
const { data: connectionStatusData } = useMCPConnectionStatusQuery();
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
null,
);
const [rotatingServers, setRotatingServers] = useState<Set<string>>(new Set());
const reinitializeMCPMutation = useReinitializeMCPServerMutation();
const queryClient = useQueryClient();
const mcpServerDefinitions = useMemo(() => {
if (!startupConfig?.mcpServers) {
return [];
}
return Object.entries(startupConfig.mcpServers)
.filter(
([, serverConfig]) =>
serverConfig.customUserVars && Object.keys(serverConfig.customUserVars).length > 0,
)
.map(([serverName, config]) => ({
serverName,
iconPath: null,
config: {
...config,
customUserVars: config.customUserVars ?? {},
},
}));
}, [startupConfig?.mcpServers]);
const updateUserPluginsMutation = useUpdateUserPluginsMutation({
onSuccess: async () => {
onSuccess: async (data, variables) => {
showToast({ message: localize('com_nav_mcp_vars_updated'), status: 'success' });
await Promise.all([
queryClient.refetchQueries([QueryKeys.tools]),
queryClient.refetchQueries([QueryKeys.mcpAuthValues]),
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]),
]);
// Refetch tools query to refresh authentication state in the dropdown
queryClient.refetchQueries([QueryKeys.tools]);
// For 'uninstall' actions (revoke), remove the server from selected values
if (variables.action === 'uninstall') {
const serverName = variables.pluginKey.replace(Constants.mcp_prefix, '');
// Note: MCPPanel doesn't directly manage selected values, but this ensures
// the tools query is refreshed so MCPSelect will pick up the changes
}
// Only reinitialize for 'install' actions (save), not 'uninstall' actions (revoke)
if (variables.action === 'install') {
// Extract server name from pluginKey (e.g., "mcp_myServer" -> "myServer")
const serverName = variables.pluginKey.replace(Constants.mcp_prefix, '');
// Reinitialize the MCP server to pick up the new authentication values
try {
await reinitializeMCPMutation.mutateAsync(serverName);
console.log(
`[MCP Panel] Successfully reinitialized server ${serverName} after auth update`,
);
} catch (error) {
console.error(
`[MCP Panel] Error reinitializing server ${serverName} after auth update:`,
error,
);
// Don't show error toast to user as the auth update was successful
}
}
// For 'uninstall' actions (revoke), the backend already disconnects the connections
// so no additional action is needed here
},
onError: (error: unknown) => {
console.error('Error updating MCP auth:', error);
@@ -42,23 +97,28 @@ export default function MCPPanel() {
},
});
const mcpServerDefinitions = useMemo(() => {
if (!startupConfig?.mcpServers) {
return [];
}
return Object.entries(startupConfig.mcpServers).map(([serverName, config]) => ({
serverName,
iconPath: null,
config: {
...config,
customUserVars: config.customUserVars ?? {},
},
}));
}, [startupConfig?.mcpServers]);
const handleSaveServerVars = useCallback(
(serverName: string, updatedValues: Record<string, string>) => {
const payload: TUpdateUserPlugins = {
pluginKey: `${Constants.mcp_prefix}${serverName}`,
action: 'install', // 'install' action is used to set/update credentials/variables
auth: updatedValues,
};
updateUserPluginsMutation.mutate(payload);
},
[updateUserPluginsMutation],
);
const connectionStatus = useMemo(
() => connectionStatusData?.connectionStatus || {},
[connectionStatusData?.connectionStatus],
const handleRevokeServerVars = useCallback(
(serverName: string) => {
const payload: TUpdateUserPlugins = {
pluginKey: `${Constants.mcp_prefix}${serverName}`,
action: 'uninstall', // 'uninstall' action clears the variables
auth: {}, // Empty auth for uninstall
};
updateUserPluginsMutation.mutate(payload);
},
[updateUserPluginsMutation],
);
const handleServerClickToEdit = (serverName: string) => {
@@ -69,31 +129,92 @@ export default function MCPPanel() {
setSelectedServerNameForEditing(null);
};
const handleConfigSave = useCallback(
(targetName: string, authData: Record<string, string>) => {
console.log(
`[MCP Panel] Saving config for ${targetName}, pluginKey: ${`${Constants.mcp_prefix}${targetName}`}`,
);
const payload: TUpdateUserPlugins = {
pluginKey: `${Constants.mcp_prefix}${targetName}`,
action: 'install',
auth: authData,
};
updateUserPluginsMutation.mutate(payload);
},
[updateUserPluginsMutation],
);
const handleReinitializeServer = useCallback(
async (serverName: string) => {
setRotatingServers((prev) => new Set(prev).add(serverName));
try {
const response = await reinitializeMCPMutation.mutateAsync(serverName);
const handleConfigRevoke = useCallback(
(targetName: string) => {
const payload: TUpdateUserPlugins = {
pluginKey: `${Constants.mcp_prefix}${targetName}`,
action: 'uninstall',
auth: {},
};
updateUserPluginsMutation.mutate(payload);
// Check if OAuth is required
if (response.oauthRequired) {
if (response.authorizationUrl) {
// Show OAuth URL to user
showToast({
message: `OAuth required for ${serverName}. Please visit the authorization URL.`,
status: 'info',
});
// Open OAuth URL in new window/tab
window.open(response.authorizationUrl, '_blank', 'noopener,noreferrer');
// Show a more detailed message with the URL
setTimeout(() => {
showToast({
message: `OAuth URL opened for ${serverName}. Complete authentication and try reinitializing again.`,
status: 'info',
});
}, 1000);
} else {
showToast({
message: `OAuth authentication required for ${serverName}. Please configure OAuth credentials.`,
status: 'warning',
});
}
} else if (response.oauthCompleted) {
showToast({
message:
response.message ||
`MCP server '${serverName}' reinitialized successfully after OAuth`,
status: 'success',
});
} else {
showToast({
message: response.message || `MCP server '${serverName}' reinitialized successfully`,
status: 'success',
});
}
} catch (error) {
console.error('Error reinitializing MCP server:', error);
// Check if the error response contains OAuth information
if (error?.response?.data?.oauthRequired) {
const errorData = error.response.data;
if (errorData.authorizationUrl) {
showToast({
message: `OAuth required for ${serverName}. Please visit the authorization URL.`,
status: 'info',
});
// Open OAuth URL in new window/tab
window.open(errorData.authorizationUrl, '_blank', 'noopener,noreferrer');
setTimeout(() => {
showToast({
message: `OAuth URL opened for ${serverName}. Complete authentication and try reinitializing again.`,
status: 'info',
});
}, 1000);
} else {
showToast({
message: errorData.message || `OAuth authentication required for ${serverName}`,
status: 'warning',
});
}
} else {
showToast({
message: 'Failed to reinitialize MCP server',
status: 'error',
});
}
} finally {
setRotatingServers((prev) => {
const next = new Set(prev);
next.delete(serverName);
return next;
});
}
},
[updateUserPluginsMutation],
[showToast, reinitializeMCPMutation],
);
if (startupConfigLoading) {
@@ -124,8 +245,6 @@ export default function MCPPanel() {
);
}
const serverStatus = connectionStatus[selectedServerNameForEditing];
return (
<div className="h-auto max-w-full overflow-x-hidden p-3">
<Button
@@ -136,33 +255,13 @@ export default function MCPPanel() {
<ChevronLeft className="mr-1 h-4 w-4" />
{localize('com_ui_back')}
</Button>
<h3 className="mb-3 text-lg font-medium">
{localize('com_sidepanel_mcp_variables_for', { '0': serverBeingEdited.serverName })}
</h3>
{/* Server Initialization Section */}
<div className="mb-4">
<ServerInitializationSection
serverName={selectedServerNameForEditing}
requiresOAuth={serverStatus?.requiresOAuth || false}
/>
</div>
{/* Custom User Variables Section */}
<CustomUserVarsSection
serverName={selectedServerNameForEditing}
fields={serverBeingEdited.config.customUserVars}
onSave={(authData) => {
if (selectedServerNameForEditing) {
handleConfigSave(selectedServerNameForEditing, authData);
}
}}
onRevoke={() => {
if (selectedServerNameForEditing) {
handleConfigRevoke(selectedServerNameForEditing);
}
}}
<MCPVariableEditor
server={serverBeingEdited}
onSave={handleSaveServerVars}
onRevoke={handleRevokeServerVars}
isSubmitting={updateUserPluginsMutation.isLoading}
/>
</div>
@@ -172,37 +271,124 @@ export default function MCPPanel() {
return (
<div className="h-auto max-w-full overflow-x-hidden p-3">
<div className="space-y-2">
{mcpServerDefinitions.map((server) => {
const serverStatus = connectionStatus[server.serverName];
const isConnected = serverStatus?.connectionState === 'connected';
return (
<div key={server.serverName} className="flex items-center gap-2">
<Button
variant="outline"
className="flex-1 justify-start dark:hover:bg-gray-700"
onClick={() => handleServerClickToEdit(server.serverName)}
>
<div className="flex items-center gap-2">
<span>{server.serverName}</span>
{serverStatus && (
<span
className={`rounded px-2 py-0.5 text-xs ${
isConnected
? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
}`}
>
{serverStatus.connectionState}
</span>
)}
</div>
</Button>
</div>
);
})}
{mcpServerDefinitions.map((server) => (
<div key={server.serverName} className="flex items-center gap-2">
<Button
variant="outline"
className="flex-1 justify-start dark:hover:bg-gray-700"
onClick={() => handleServerClickToEdit(server.serverName)}
>
{server.serverName}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleReinitializeServer(server.serverName)}
className="px-2 py-1"
title="Reinitialize MCP server"
disabled={reinitializeMCPMutation.isLoading}
>
<RefreshCw
className={`h-4 w-4 ${rotatingServers.has(server.serverName) ? 'animate-spin' : ''}`}
/>
</Button>
</div>
))}
</div>
</div>
);
}
}
// Inner component for the form - remains the same
interface MCPVariableEditorProps {
server: ServerConfigWithVars;
onSave: (serverName: string, updatedValues: Record<string, string>) => void;
onRevoke: (serverName: string) => void;
isSubmitting: boolean;
}
function MCPVariableEditor({ server, onSave, onRevoke, isSubmitting }: MCPVariableEditorProps) {
const localize = useLocalize();
const {
control,
handleSubmit,
reset,
formState: { errors, isDirty },
} = useForm<Record<string, string>>({
defaultValues: {}, // Initialize empty, will be reset by useEffect
});
useEffect(() => {
// Always initialize with empty strings based on the schema
const initialFormValues = Object.keys(server.config.customUserVars).reduce(
(acc, key) => {
acc[key] = '';
return acc;
},
{} as Record<string, string>,
);
reset(initialFormValues);
}, [reset, server.config.customUserVars]);
const onFormSubmit = (data: Record<string, string>) => {
onSave(server.serverName, data);
};
const handleRevokeClick = () => {
onRevoke(server.serverName);
};
return (
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-4 mt-2 space-y-4">
{Object.entries(server.config.customUserVars).map(([key, details]) => (
<div key={key} className="space-y-2">
<Label htmlFor={`${server.serverName}-${key}`} className="text-sm font-medium">
{details.title}
</Label>
<Controller
name={key}
control={control}
defaultValue={''}
render={({ field }) => (
<Input
id={`${server.serverName}-${key}`}
type="text"
{...field}
placeholder={localize('com_sidepanel_mcp_enter_value', { '0': details.title })}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"
/>
)}
/>
{details.description && (
<p
className="text-xs text-text-secondary [&_a]:text-blue-500 [&_a]:hover:text-blue-600 dark:[&_a]:text-blue-400 dark:[&_a]:hover:text-blue-300"
dangerouslySetInnerHTML={{ __html: details.description }}
/>
)}
{errors[key] && <p className="text-xs text-red-500">{errors[key]?.message}</p>}
</div>
))}
<div className="flex justify-end gap-2 pt-2">
{Object.keys(server.config.customUserVars).length > 0 && (
<Button
type="button"
onClick={handleRevokeClick}
className="bg-red-600 text-white hover:bg-red-700 dark:hover:bg-red-800"
disabled={isSubmitting}
>
{localize('com_ui_revoke')}
</Button>
)}
<Button
type="submit"
className="bg-green-500 text-white hover:bg-green-600"
disabled={isSubmitting || !isDirty}
>
{isSubmitting ? localize('com_ui_saving') : localize('com_ui_save')}
</Button>
</div>
</form>
);
}

View File

@@ -7,8 +7,8 @@ import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import { useGetEndpointsQuery } from '~/data-provider';
import NavToggle from '~/components/Nav/NavToggle';
import { useSidePanelContext } from '~/Providers';
import { cn, getEndpointField } from '~/utils';
import { useChatContext } from '~/Providers';
import Nav from './Nav';
const defaultMinSize = 20;
@@ -43,13 +43,13 @@ const SidePanel = ({
interfaceConfig: TInterfaceConfig;
}) => {
const localize = useLocalize();
const { endpoint } = useSidePanelContext();
const [isHovering, setIsHovering] = useState(false);
const [newUser, setNewUser] = useLocalStorage('newUser', true);
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const { conversation } = useChatContext();
const { endpoint } = conversation ?? {};
const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? '');
const defaultActive = useMemo(() => {

View File

@@ -22,139 +22,132 @@ interface SidePanelProps {
const defaultMinSize = 20;
const defaultInterface = getConfigDefaults().interface;
const SidePanelGroup = memo(
({
defaultLayout = [97, 3],
defaultCollapsed = false,
fullPanelCollapse = false,
navCollapsedSize = 3,
artifacts,
children,
}: SidePanelProps) => {
const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
const SidePanelGroup = ({
defaultLayout = [97, 3],
defaultCollapsed = false,
fullPanelCollapse = false,
navCollapsedSize = 3,
artifacts,
children,
}: SidePanelProps) => {
const { data: startupConfig } = useGetStartupConfig();
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
const panelRef = useRef<ImperativePanelHandle>(null);
const [minSize, setMinSize] = useState(defaultMinSize);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
const panelRef = useRef<ImperativePanelHandle>(null);
const [minSize, setMinSize] = useState(defaultMinSize);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse);
const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize);
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const hideSidePanel = useRecoilValue(store.hideSidePanel);
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const hideSidePanel = useRecoilValue(store.hideSidePanel);
const calculateLayout = useCallback(() => {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
return [100 - navSize, navSize];
} else {
const navSize = 0;
const remainingSpace = 100 - navSize;
const newMainSize = Math.floor(remainingSpace / 2);
const artifactsSize = remainingSpace - newMainSize;
return [newMainSize, artifactsSize, navSize];
}
}, [artifacts, defaultLayout]);
const calculateLayout = useCallback(() => {
if (artifacts == null) {
const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2];
return [100 - navSize, navSize];
} else {
const navSize = 0;
const remainingSpace = 100 - navSize;
const newMainSize = Math.floor(remainingSpace / 2);
const artifactsSize = remainingSpace - newMainSize;
return [newMainSize, artifactsSize, navSize];
}
}, [artifacts, defaultLayout]);
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]);
const throttledSaveLayout = useMemo(
() =>
throttle((sizes: number[]) => {
const normalizedSizes = normalizeLayout(sizes);
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
}, 350),
[],
);
const throttledSaveLayout = useMemo(
() =>
throttle((sizes: number[]) => {
const normalizedSizes = normalizeLayout(sizes);
localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes));
}, 350),
[],
);
useEffect(() => {
if (isSmallScreen) {
setIsCollapsed(true);
setCollapsedSize(0);
setMinSize(defaultMinSize);
setFullCollapse(true);
localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse();
return;
} else {
setIsCollapsed(defaultCollapsed);
setCollapsedSize(navCollapsedSize);
setMinSize(defaultMinSize);
}
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
/** Memoized close button handler to prevent re-creating it */
const handleClosePanel = useCallback(() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');
setFullCollapse(true);
setCollapsedSize(0);
setMinSize(0);
return false;
});
useEffect(() => {
if (isSmallScreen) {
setIsCollapsed(true);
setCollapsedSize(0);
setMinSize(defaultMinSize);
setFullCollapse(true);
localStorage.setItem('fullPanelCollapse', 'true');
panelRef.current?.collapse();
}, []);
return;
} else {
setIsCollapsed(defaultCollapsed);
setCollapsedSize(navCollapsedSize);
setMinSize(defaultMinSize);
}
}, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]);
return (
<>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]);
return (
<>
<ResizablePanelGroup
direction="horizontal"
onLayout={(sizes) => throttledSaveLayout(sizes)}
className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
>
<ResizablePanel
defaultSize={currentLayout[0]}
minSize={minSizeMain}
order={1}
id="messages-view"
>
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
<ResizablePanel
defaultSize={currentLayout[1]}
minSize={minSizeMain}
order={2}
id="artifacts-panel"
>
{artifacts}
</ResizablePanel>
</>
)}
{!hideSidePanel && interfaceConfig.sidePanel === true && (
<SidePanel
panelRef={panelRef}
minSize={minSize}
setMinSize={setMinSize}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
collapsedSize={collapsedSize}
setCollapsedSize={setCollapsedSize}
fullCollapse={fullCollapse}
setFullCollapse={setFullCollapse}
defaultSize={currentLayout[currentLayout.length - 1]}
hasArtifacts={artifacts != null}
interfaceConfig={interfaceConfig}
/>
)}
</ResizablePanelGroup>
<button
aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
onClick={handleClosePanel}
/>
</>
);
},
);
{children}
</ResizablePanel>
{artifacts != null && (
<>
<ResizableHandleAlt withHandle className="ml-3 bg-border-medium text-text-primary" />
<ResizablePanel
defaultSize={currentLayout[1]}
minSize={minSizeMain}
order={2}
id="artifacts-panel"
>
{artifacts}
</ResizablePanel>
</>
)}
{!hideSidePanel && interfaceConfig.sidePanel === true && (
<SidePanel
panelRef={panelRef}
minSize={minSize}
setMinSize={setMinSize}
isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed}
collapsedSize={collapsedSize}
setCollapsedSize={setCollapsedSize}
fullCollapse={fullCollapse}
setFullCollapse={setFullCollapse}
defaultSize={currentLayout[currentLayout.length - 1]}
hasArtifacts={artifacts != null}
interfaceConfig={interfaceConfig}
/>
)}
</ResizablePanelGroup>
<button
aria-label="Close right side panel"
className={`nav-mask ${!isCollapsed ? 'active' : ''}`}
onClick={() => {
setIsCollapsed(() => {
localStorage.setItem('fullPanelCollapse', 'true');
setFullCollapse(true);
setCollapsedSize(0);
setMinSize(0);
return false;
});
panelRef.current?.collapse();
}}
/>
</>
);
};
SidePanelGroup.displayName = 'SidePanelGroup';
export default SidePanelGroup;
export default memo(SidePanelGroup);

View File

@@ -57,7 +57,7 @@ function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps)
{...field}
placeholder={
hasValue
? localize('com_ui_mcp_update_var', { 0: config.title })
? `Update ${config.title} (currently saved)`
: localize('com_ui_mcp_enter_var', { 0: config.title })
}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white sm:text-sm"

View File

@@ -1,6 +1,10 @@
import React from 'react';
import { Loader2, KeyRound, PlugZap, AlertTriangle } from 'lucide-react';
import { MCPServerStatus } from 'librechat-data-provider/dist/types/types/queries';
import React, { useMemo, useCallback } from 'react';
import { useLocalize } from '~/hooks';
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
import { CustomUserVarsSection, ServerInitializationSection } from './';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys } from 'librechat-data-provider';
import {
OGDialog,
OGDialogContent,
@@ -8,9 +12,6 @@ import {
OGDialogTitle,
OGDialogDescription,
} from '~/components/ui/OriginalDialog';
import CustomUserVarsSection from './CustomUserVarsSection';
import ServerInitializationSection from './ServerInitializationSection';
import { useLocalize } from '~/hooks';
export interface ConfigFieldDetail {
title: string;
@@ -26,7 +27,13 @@ interface MCPConfigDialogProps {
isSubmitting?: boolean;
onRevoke?: () => void;
serverName: string;
serverStatus?: MCPServerStatus;
isConnected?: boolean;
authConfig?: Array<{
authField: string;
label: string;
description: string;
requiresOAuth?: boolean;
}>;
}
export default function MCPConfigDialog({
@@ -37,9 +44,25 @@ export default function MCPConfigDialog({
isSubmitting = false,
onRevoke,
serverName,
serverStatus,
}: MCPConfigDialogProps) {
const localize = useLocalize();
const queryClient = useQueryClient();
// Get connection status to determine OAuth requirements with aggressive refresh
const { data: statusQuery, refetch: refetchConnectionStatus } = useMCPConnectionStatusQuery({
refetchOnMount: true,
refetchOnWindowFocus: true,
staleTime: 0,
cacheTime: 0,
});
const mcpServerStatuses = statusQuery?.connectionStatus || {};
// Derive real-time connection status and OAuth requirements
const serverStatus = mcpServerStatuses[serverName];
const isRealTimeConnected = serverStatus?.connected || false;
const requiresOAuth = useMemo(() => {
return serverStatus?.requiresOAuth || false;
}, [serverStatus?.requiresOAuth]);
const hasFields = Object.keys(fieldsSchema).length > 0;
const dialogTitle = hasFields
@@ -49,69 +72,18 @@ export default function MCPConfigDialog({
? localize('com_ui_mcp_dialog_desc')
: `Manage connection and settings for the ${serverName} MCP server.`;
// Helper function to render status badge based on connection state
const renderStatusBadge = () => {
if (!serverStatus) {
return null;
}
const { connectionState, requiresOAuth } = serverStatus;
if (connectionState === 'connecting') {
return (
<div className="flex items-center gap-2 rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-600 dark:bg-blue-950 dark:text-blue-400">
<Loader2 className="h-3 w-3 animate-spin" />
<span>{localize('com_ui_connecting')}</span>
</div>
);
}
if (connectionState === 'disconnected') {
if (requiresOAuth) {
return (
<div className="flex items-center gap-2 rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-600 dark:bg-amber-950 dark:text-amber-400">
<KeyRound className="h-3 w-3" />
<span>{localize('com_ui_oauth')}</span>
</div>
);
} else {
return (
<div className="flex items-center gap-2 rounded-full bg-orange-50 px-2 py-0.5 text-xs font-medium text-orange-600 dark:bg-orange-950 dark:text-orange-400">
<PlugZap className="h-3 w-3" />
<span>{localize('com_ui_offline')}</span>
</div>
);
}
}
if (connectionState === 'error') {
return (
<div className="flex items-center gap-2 rounded-full bg-red-50 px-2 py-0.5 text-xs font-medium text-red-600 dark:bg-red-950 dark:text-red-400">
<AlertTriangle className="h-3 w-3" />
<span>{localize('com_ui_error')}</span>
</div>
);
}
if (connectionState === 'connected') {
return (
<div className="flex items-center gap-2 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900 dark:text-green-300">
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
<span>{localize('com_ui_active')}</span>
</div>
);
}
return null;
};
return (
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
<OGDialogContent className="flex max-h-[90vh] w-full max-w-md flex-col">
<OGDialogHeader>
<div className="flex items-center gap-3">
<OGDialogTitle>{dialogTitle}</OGDialogTitle>
{renderStatusBadge()}
{isRealTimeConnected && (
<div className="flex items-center gap-2 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700 dark:bg-green-900 dark:text-green-300">
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
<span>{localize('com_ui_active')}</span>
</div>
)}
</div>
<OGDialogDescription>{dialogDescription}</OGDialogDescription>
</OGDialogHeader>
@@ -129,10 +101,7 @@ export default function MCPConfigDialog({
</div>
{/* Server Initialization Section */}
<ServerInitializationSection
serverName={serverName}
requiresOAuth={serverStatus?.requiresOAuth || false}
/>
<ServerInitializationSection serverName={serverName} requiresOAuth={requiresOAuth} />
</OGDialogContent>
</OGDialog>
);

View File

@@ -1,190 +0,0 @@
import React from 'react';
import { SettingsIcon, AlertTriangle, Loader2, KeyRound, PlugZap, X } from 'lucide-react';
import type { MCPServerStatus, TPlugin } from 'librechat-data-provider';
import { useLocalize } from '~/hooks';
let localize: ReturnType<typeof useLocalize>;
interface StatusIconProps {
serverName: string;
onConfigClick: (e: React.MouseEvent) => void;
}
interface InitializingStatusProps extends StatusIconProps {
onCancel: (e: React.MouseEvent) => void;
canCancel: boolean;
}
interface MCPServerStatusIconProps {
serverName: string;
serverStatus?: MCPServerStatus;
tool?: TPlugin;
onConfigClick: (e: React.MouseEvent) => void;
isInitializing: boolean;
canCancel: boolean;
onCancel: (e: React.MouseEvent) => void;
hasCustomUserVars?: boolean;
}
/**
* Renders the appropriate status icon for an MCP server based on its state
*/
export default function MCPServerStatusIcon({
serverName,
serverStatus,
tool,
onConfigClick,
isInitializing,
canCancel,
onCancel,
hasCustomUserVars = false,
}: MCPServerStatusIconProps) {
localize = useLocalize();
if (isInitializing) {
return (
<InitializingStatusIcon
serverName={serverName}
onConfigClick={onConfigClick}
onCancel={onCancel}
canCancel={canCancel}
/>
);
}
if (!serverStatus) {
return null;
}
const { connectionState, requiresOAuth } = serverStatus;
if (connectionState === 'connecting') {
return <ConnectingStatusIcon serverName={serverName} onConfigClick={onConfigClick} />;
}
if (connectionState === 'disconnected') {
if (requiresOAuth) {
return <DisconnectedOAuthStatusIcon serverName={serverName} onConfigClick={onConfigClick} />;
}
return <DisconnectedStatusIcon serverName={serverName} onConfigClick={onConfigClick} />;
}
if (connectionState === 'error') {
return <ErrorStatusIcon serverName={serverName} onConfigClick={onConfigClick} />;
}
if (connectionState === 'connected') {
// Only show config button if there are customUserVars to configure
if (hasCustomUserVars) {
const isAuthenticated = tool?.authenticated || requiresOAuth;
return (
<AuthenticatedStatusIcon
serverName={serverName}
onConfigClick={onConfigClick}
isAuthenticated={isAuthenticated}
/>
);
}
return null; // No config button for connected servers without customUserVars
}
return null;
}
function InitializingStatusIcon({ serverName, onCancel, canCancel }: InitializingStatusProps) {
if (canCancel) {
return (
<button
type="button"
onClick={onCancel}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-red-100 dark:hover:bg-red-900/20"
aria-label={localize('com_ui_cancel')}
title={localize('com_ui_cancel')}
>
<div className="group relative h-4 w-4">
<Loader2 className="h-4 w-4 animate-spin text-blue-500 group-hover:opacity-0" />
<X className="absolute inset-0 h-4 w-4 text-red-500 opacity-0 group-hover:opacity-100" />
</div>
</button>
);
}
return (
<div className="flex h-6 w-6 items-center justify-center rounded p-1">
<Loader2
className="h-4 w-4 animate-spin text-blue-500"
aria-label={localize('com_nav_mcp_status_connecting', { 0: serverName })}
/>
</div>
);
}
function ConnectingStatusIcon({ serverName }: StatusIconProps) {
return (
<div className="flex h-6 w-6 items-center justify-center rounded p-1">
<Loader2
className="h-4 w-4 animate-spin text-blue-500"
aria-label={localize('com_nav_mcp_status_connecting', { 0: serverName })}
/>
</div>
);
}
function DisconnectedOAuthStatusIcon({ serverName, onConfigClick }: StatusIconProps) {
return (
<button
type="button"
onClick={onConfigClick}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<KeyRound className="h-4 w-4 text-amber-500" />
</button>
);
}
function DisconnectedStatusIcon({ serverName, onConfigClick }: StatusIconProps) {
return (
<button
type="button"
onClick={onConfigClick}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<PlugZap className="h-4 w-4 text-orange-500" />
</button>
);
}
function ErrorStatusIcon({ serverName, onConfigClick }: StatusIconProps) {
return (
<button
type="button"
onClick={onConfigClick}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<AlertTriangle className="h-4 w-4 text-red-500" />
</button>
);
}
interface AuthenticatedStatusProps extends StatusIconProps {
isAuthenticated: boolean;
}
function AuthenticatedStatusIcon({
serverName,
onConfigClick,
isAuthenticated,
}: AuthenticatedStatusProps) {
return (
<button
type="button"
onClick={onConfigClick}
className="flex h-6 w-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<SettingsIcon className={`h-4 w-4 ${isAuthenticated ? 'text-green-500' : 'text-gray-400'}`} />
</button>
);
}

View File

@@ -1,8 +1,17 @@
import { RefreshCw, Link } from 'lucide-react';
import React, { useState, useCallback } from 'react';
import { useMCPServerInitialization } from '~/hooks/MCP/useMCPServerInitialization';
import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { useToastContext } from '~/Providers';
import {
useReinitializeMCPServerMutation,
useMCPOAuthStatusQuery,
useCompleteMCPServerReinitializeMutation,
} from 'librechat-data-provider/react-query';
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys } from 'librechat-data-provider';
import { RefreshCw, Link } from 'lucide-react';
interface ServerInitializationSectionProps {
serverName: string;
@@ -14,47 +23,143 @@ export default function ServerInitializationSection({
requiresOAuth,
}: ServerInitializationSectionProps) {
const localize = useLocalize();
const { showToast } = useToastContext();
const queryClient = useQueryClient();
const [oauthUrl, setOauthUrl] = useState<string | null>(null);
const [oauthFlowId, setOauthFlowId] = useState<string | null>(null);
// Use the shared initialization hook
const { initializeServer, isLoading, connectionStatus, cancelOAuthFlow, isCancellable } =
useMCPServerInitialization({
onOAuthStarted: (name, url) => {
// Store the OAuth URL locally for display
setOauthUrl(url);
const { data: statusQuery } = useMCPConnectionStatusQuery();
const mcpServerStatuses = statusQuery?.connectionStatus || {};
const serverStatus = mcpServerStatuses[serverName];
const isConnected = serverStatus?.connected || false;
// Helper function to invalidate caches after successful connection
const handleSuccessfulConnection = useCallback(
async (message: string) => {
showToast({ message, status: 'success' });
// Force immediate refetch to update UI
await Promise.all([
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]),
queryClient.refetchQueries([QueryKeys.tools]),
]);
},
[showToast, queryClient],
);
// Main initialization mutation
const reinitializeMutation = useReinitializeMCPServerMutation();
// OAuth completion mutation (stores our tools)
const completeReinitializeMutation = useCompleteMCPServerReinitializeMutation();
// Override the mutation success handlers
const handleInitializeServer = useCallback(() => {
// Reset OAuth state before starting
setOauthUrl(null);
setOauthFlowId(null);
// Trigger initialization
reinitializeMutation.mutate(serverName, {
onSuccess: (response) => {
if (response.oauthRequired) {
if (response.authURL && response.flowId) {
setOauthUrl(response.authURL);
setOauthFlowId(response.flowId);
// Keep loading state - OAuth completion will handle success
} else {
showToast({
message: `OAuth authentication required for ${serverName}. Please configure OAuth credentials.`,
status: 'warning',
});
}
} else if (response.success) {
handleSuccessfulConnection(
response.message || `MCP server '${serverName}' initialized successfully`,
);
}
},
onSuccess: () => {
// Clear OAuth URL on success
setOauthUrl(null);
onError: (error: any) => {
console.error('Error initializing MCP server:', error);
showToast({
message: 'Failed to initialize MCP server',
status: 'error',
});
},
});
}, [reinitializeMutation, serverName, showToast, handleSuccessfulConnection]);
const serverStatus = connectionStatus[serverName];
const isConnected = serverStatus?.connectionState === 'connected';
const canCancel = isCancellable(serverName);
// OAuth status polling (only when we have a flow ID)
const oauthStatusQuery = useMCPOAuthStatusQuery(oauthFlowId || '', {
enabled: !!oauthFlowId,
refetchInterval: oauthFlowId ? 2000 : false,
retry: false,
onSuccess: (data) => {
if (data?.completed) {
// Immediately reset OAuth state to stop polling
setOauthUrl(null);
setOauthFlowId(null);
const handleInitializeClick = useCallback(() => {
setOauthUrl(null);
initializeServer(serverName);
}, [initializeServer, serverName]);
// OAuth completed, trigger completion mutation
completeReinitializeMutation.mutate(serverName, {
onSuccess: (response) => {
handleSuccessfulConnection(
response.message || `MCP server '${serverName}' initialized successfully after OAuth`,
);
},
onError: (error: any) => {
// Check if it initialized anyway
if (isConnected) {
handleSuccessfulConnection('MCP server initialized successfully after OAuth');
return;
}
const handleCancelClick = useCallback(() => {
setOauthUrl(null);
cancelOAuthFlow(serverName);
}, [cancelOAuthFlow, serverName]);
console.error('Error completing MCP initialization:', error);
showToast({
message: 'Failed to complete MCP server initialization after OAuth',
status: 'error',
});
// OAuth state already reset above
},
});
} else if (data?.failed) {
showToast({
message: `OAuth authentication failed: ${data.error || 'Unknown error'}`,
status: 'error',
});
// Reset OAuth state on failure
setOauthUrl(null);
setOauthFlowId(null);
}
},
});
// Reset OAuth state when component unmounts or server changes
useEffect(() => {
return () => {
setOauthUrl(null);
setOauthFlowId(null);
};
}, [serverName]);
const isLoading =
reinitializeMutation.isLoading ||
completeReinitializeMutation.isLoading ||
(!!oauthFlowId && oauthStatusQuery.isFetching);
// Show subtle reinitialize option if connected
if (isConnected) {
return (
<div className="flex justify-start">
<button
onClick={handleInitializeClick}
onClick={handleInitializeServer}
disabled={isLoading}
className="flex items-center gap-1 text-xs text-gray-400 hover:text-gray-600 disabled:opacity-50 dark:text-gray-500 dark:hover:text-gray-400"
>
<RefreshCw className={`h-3 w-3 ${isLoading ? 'animate-spin' : ''}`} />
{isLoading ? localize('com_ui_loading') : localize('com_ui_reinitialize')}
{isLoading ? localize('com_ui_loading') : 'Reinitialize'}
</button>
</div>
);
@@ -66,14 +171,14 @@ export default function ServerInitializationSection({
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-red-700 dark:text-red-300">
{requiresOAuth
? localize('com_ui_mcp_not_authenticated', { 0: serverName })
: localize('com_ui_mcp_not_initialized', { 0: serverName })}
? `${serverName} not authenticated (OAuth Required)`
: `${serverName} not initialized`}
</span>
</div>
{/* Only show authenticate button when OAuth URL is not present */}
{!oauthUrl && (
<Button
onClick={handleInitializeClick}
onClick={handleInitializeServer}
disabled={isLoading}
className="flex items-center gap-2 bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:hover:bg-blue-800"
>
@@ -102,24 +207,16 @@ export default function ServerInitializationSection({
<Link className="h-2.5 w-2.5 text-white" />
</div>
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">
{localize('com_ui_auth_url')}
{localize('com_ui_authorization_url')}
</span>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => window.open(oauthUrl, '_blank', 'noopener,noreferrer')}
className="flex-1 bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-800"
className="w-full bg-blue-600 text-white hover:bg-blue-700 dark:hover:bg-blue-800"
>
{localize('com_ui_continue_oauth')}
</Button>
<Button
onClick={handleCancelClick}
disabled={!canCancel}
className="bg-gray-200 text-gray-700 hover:bg-gray-300 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
title={!canCancel ? 'disabled' : undefined}
>
{localize('com_ui_cancel')}
</Button>
</div>
<p className="mt-2 text-xs text-blue-600 dark:text-blue-400">
{localize('com_ui_oauth_flow_desc')}

View File

@@ -0,0 +1,5 @@
export { default as MCPConfigDialog } from './MCPConfigDialog';
export { default as CustomUserVarsSection } from './CustomUserVarsSection';
export { default as ServerInitializationSection } from './ServerInitializationSection';
export type { ConfigFieldDetail } from './MCPConfigDialog';
export type { CustomUserVarConfig } from './CustomUserVarsSection';

View File

@@ -113,7 +113,7 @@ export default function MultiSelect<T extends string>({
{items.map((value) => {
const defaultContent = (
<>
<SelectItemCheck className="mr-0.5 text-primary" />
<SelectItemCheck className="text-primary" />
<span className="truncate">{value}</span>
</>
);

View File

@@ -41,35 +41,75 @@ export const useGetToolCalls = <TData = t.ToolCallResults>(
);
};
export const useMCPConnectionStatusQuery = (
config?: UseQueryOptions<t.MCPConnectionStatusResponse>,
): QueryObserverResult<t.MCPConnectionStatusResponse> => {
return useQuery<t.MCPConnectionStatusResponse>(
/**
* Hook for getting MCP connection status
*/
export const useMCPConnectionStatusQuery = <TData = t.TMCPConnectionStatusResponse>(
config?: UseQueryOptions<t.TMCPConnectionStatusResponse, unknown, TData>,
): QueryObserverResult<TData, unknown> => {
return useQuery<t.TMCPConnectionStatusResponse, unknown, TData>(
[QueryKeys.mcpConnectionStatus],
() => dataService.getMCPConnectionStatus(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
staleTime: 10000, // 10 seconds
// refetchOnWindowFocus: false,
// refetchOnReconnect: false,
// refetchOnMount: true,
...config,
},
);
};
/**
* Hook for getting MCP auth value flags for a specific server
*/
export const useMCPAuthValuesQuery = (
serverName: string,
config?: UseQueryOptions<t.MCPAuthValuesResponse>,
): QueryObserverResult<t.MCPAuthValuesResponse> => {
return useQuery<t.MCPAuthValuesResponse>(
[QueryKeys.mcpAuthValues, serverName],
() => dataService.getMCPAuthValues(serverName),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
enabled: !!serverName,
...config,
},
);
config?: UseQueryOptions<
{ success: boolean; serverName: string; authValueFlags: Record<string, boolean> },
unknown,
{ success: boolean; serverName: string; authValueFlags: Record<string, boolean> }
>,
): QueryObserverResult<
{ success: boolean; serverName: string; authValueFlags: Record<string, boolean> },
unknown
> => {
return useQuery<
{ success: boolean; serverName: string; authValueFlags: Record<string, boolean> },
unknown,
{ success: boolean; serverName: string; authValueFlags: Record<string, boolean> }
>([QueryKeys.mcpAuthValues, serverName], () => dataService.getMCPAuthValues(serverName), {
// refetchOnWindowFocus: false,
// refetchOnReconnect: false,
// refetchOnMount: true,
enabled: !!serverName,
...config,
});
};
/**
* Hook for getting MCP OAuth status for a specific flow
*/
export const useMCPOAuthStatusQuery = (
flowId: string,
config?: UseQueryOptions<
{ status: string; completed: boolean; failed: boolean; error?: string },
unknown,
{ status: string; completed: boolean; failed: boolean; error?: string }
>,
): QueryObserverResult<
{ status: string; completed: boolean; failed: boolean; error?: string },
unknown
> => {
return useQuery<
{ status: string; completed: boolean; failed: boolean; error?: string },
unknown,
{ status: string; completed: boolean; failed: boolean; error?: string }
>([QueryKeys.mcpOAuthStatus, flowId], () => dataService.getMCPOAuthStatus(flowId), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: true,
staleTime: 1000, // Consider data stale after 1 second for polling
enabled: !!flowId,
...config,
});
};

View File

@@ -1,11 +1,6 @@
import { useCallback, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
Constants,
QueryKeys,
EModelEndpoint,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import { EModelEndpoint, isAgentsEndpoint, Constants, QueryKeys } from 'librechat-data-provider';
import type { TConversation, TPreset, Agent } from 'librechat-data-provider';
import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo';
import { useAgentsMapContext } from '~/Providers/AgentsMapContext';
@@ -29,22 +24,22 @@ export default function useSelectAgent() {
const updateConversation = useCallback(
(agent: Partial<Agent>, template: Partial<TPreset | TConversation>) => {
logger.log('conversation', 'Updating conversation with agent', agent);
if (isAssistantsEndpoint(conversation?.endpoint)) {
if (isAgentsEndpoint(conversation?.endpoint)) {
const currentConvo = getDefaultConversation({
conversation: { ...(conversation ?? {}), agent_id: agent.id },
preset: template,
});
newConversation({
template: currentConvo,
preset: template as Partial<TPreset>,
keepLatestMessage: true,
});
} else {
newConversation({
template: { ...(template as Partial<TConversation>) },
preset: template as Partial<TPreset>,
});
return;
}
const currentConvo = getDefaultConversation({
conversation: { ...(conversation ?? {}), agent_id: agent.id },
preset: template,
});
newConversation({
template: currentConvo,
preset: template as Partial<TPreset>,
keepLatestMessage: true,
});
},
[conversation, getDefaultConversation, newConversation],
);

View File

@@ -1,11 +1,5 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
TPreset,
TPlugin,
TConversation,
tConvoUpdateSchema,
EModelEndpoint,
} from 'librechat-data-provider';
import { TPreset, TPlugin, TConversation, tConvoUpdateSchema } from 'librechat-data-provider';
import type { TSetExample, TSetOption, TSetOptionsPayload } from '~/common';
import usePresetIndexOptions from './usePresetIndexOptions';
import { useChatContext } from '~/Providers/ChatContext';
@@ -36,19 +30,11 @@ const useSetIndexOptions: TUseSetOptions = (preset = false) => {
};
}
// Auto-enable Responses API when web search is enabled (only for OpenAI/Azure/Custom endpoints)
// Auto-enable Responses API when web search is enabled
if (param === 'web_search' && newValue === true) {
const currentEndpoint = conversation?.endpoint;
const isOpenAICompatible =
currentEndpoint === EModelEndpoint.openAI ||
currentEndpoint === EModelEndpoint.azureOpenAI ||
currentEndpoint === EModelEndpoint.custom;
if (isOpenAICompatible) {
const currentUseResponsesApi = conversation?.useResponsesApi ?? false;
if (!currentUseResponsesApi) {
update['useResponsesApi'] = true;
}
const currentUseResponsesApi = conversation?.useResponsesApi ?? false;
if (!currentUseResponsesApi) {
update['useResponsesApi'] = true;
}
}

View File

@@ -4,11 +4,36 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { LocalStorageKeys, Constants } from 'librechat-data-provider';
import type { TFile } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { clearDraft, getDraft, setDraft } from '~/utils';
import { useChatFormContext } from '~/Providers';
import { useGetFiles } from '~/data-provider';
import store from '~/store';
const clearDraft = debounce((id?: string | null) => {
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id ?? ''}`);
}, 2500);
const encodeBase64 = (plainText: string): string => {
try {
const textBytes = new TextEncoder().encode(plainText);
return btoa(String.fromCharCode(...textBytes));
} catch (e) {
return '';
}
};
const decodeBase64 = (base64String: string): string => {
try {
const bytes = atob(base64String);
const uint8Array = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
uint8Array[i] = bytes.charCodeAt(i);
}
return new TextDecoder().decode(uint8Array);
} catch (e) {
return '';
}
};
export const useAutoSave = ({
isSubmitting,
conversationId: _conversationId,
@@ -73,11 +98,8 @@ export const useAutoSave = ({
const restoreText = useCallback(
(id: string) => {
const savedDraft = getDraft(id);
if (!savedDraft) {
return;
}
setValue('text', savedDraft);
const savedDraft = (localStorage.getItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`) ?? '') || '';
setValue('text', decodeBase64(savedDraft));
},
[setValue],
);
@@ -91,7 +113,10 @@ export const useAutoSave = ({
if (textAreaRef.current.value === '' || textAreaRef.current.value.length === 1) {
clearDraft(id);
} else {
setDraft({ id, value: textAreaRef.current.value });
localStorage.setItem(
`${LocalStorageKeys.TEXT_DRAFT}${id}`,
encodeBase64(textAreaRef.current.value),
);
}
},
[textAreaRef],
@@ -105,7 +130,16 @@ export const useAutoSave = ({
return;
}
const handleInput = debounce((value: string) => setDraft({ id: conversationId, value }), 750);
const handleInput = debounce((value: string) => {
if (value && value.length > 1) {
localStorage.setItem(
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
encodeBase64(value),
);
} else {
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`);
}
}, 750);
const eventListener = (e: Event) => {
const target = e.target as HTMLTextAreaElement;
@@ -160,7 +194,10 @@ export const useAutoSave = ({
if (pendingDraft) {
localStorage.setItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`, pendingDraft);
} else if (textAreaRef?.current?.value) {
setDraft({ id: conversationId, value: textAreaRef.current.value });
localStorage.setItem(
`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`,
encodeBase64(textAreaRef.current.value),
);
}
const pendingFileDraft = localStorage.getItem(
`${LocalStorageKeys.FILES_DRAFT}${Constants.PENDING_CONVO}`,

View File

@@ -1 +0,0 @@
export { useMCPServerInitialization } from './useMCPServerInitialization';

View File

@@ -1,289 +0,0 @@
import { useCallback, useState, useEffect, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys } from 'librechat-data-provider';
import {
useReinitializeMCPServerMutation,
useCancelMCPOAuthMutation,
} from 'librechat-data-provider/react-query';
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';
import { logger } from '~/utils';
interface UseMCPServerInitializationOptions {
onSuccess?: (serverName: string) => void;
onOAuthStarted?: (serverName: string, oauthUrl: string) => void;
onError?: (serverName: string, error: any) => void;
}
export function useMCPServerInitialization(options?: UseMCPServerInitializationOptions) {
const localize = useLocalize();
const { showToast } = useToastContext();
const queryClient = useQueryClient();
// OAuth state management
const [oauthPollingServers, setOauthPollingServers] = useState<Map<string, string>>(new Map());
const [oauthStartTimes, setOauthStartTimes] = useState<Map<string, number>>(new Map());
const [initializingServers, setInitializingServers] = useState<Set<string>>(new Set());
const [cancellableServers, setCancellableServers] = useState<Set<string>>(new Set());
// Get connection status
const { data: connectionStatusData } = useMCPConnectionStatusQuery();
const connectionStatus = useMemo(
() => connectionStatusData?.connectionStatus || {},
[connectionStatusData],
);
// Main initialization mutation
const reinitializeMutation = useReinitializeMCPServerMutation();
// Cancel OAuth mutation
const cancelOAuthMutation = useCancelMCPOAuthMutation();
// Helper function to clean up OAuth state
const cleanupOAuthState = useCallback((serverName: string) => {
setOauthPollingServers((prev) => {
const newMap = new Map(prev);
newMap.delete(serverName);
return newMap;
});
setOauthStartTimes((prev) => {
const newMap = new Map(prev);
newMap.delete(serverName);
return newMap;
});
setInitializingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
setCancellableServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
}, []);
// Cancel OAuth flow
const cancelOAuthFlow = useCallback(
(serverName: string) => {
logger.info(`[MCP OAuth] User cancelling OAuth flow for ${serverName}`);
cancelOAuthMutation.mutate(serverName, {
onSuccess: () => {
cleanupOAuthState(serverName);
showToast({
message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }),
status: 'info',
});
},
onError: (error) => {
logger.error(`[MCP OAuth] Failed to cancel OAuth flow for ${serverName}:`, error);
// Clean up state anyway
cleanupOAuthState(serverName);
},
});
},
[cancelOAuthMutation, cleanupOAuthState, showToast, localize],
);
// Helper function to handle successful connection
const handleSuccessfulConnection = useCallback(
async (serverName: string, message: string) => {
showToast({ message, status: 'success' });
// Force immediate refetch to update UI
await Promise.all([
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]),
queryClient.refetchQueries([QueryKeys.tools]),
]);
// Clean up OAuth state
cleanupOAuthState(serverName);
// Call optional success callback
options?.onSuccess?.(serverName);
},
[showToast, queryClient, options, cleanupOAuthState],
);
// Helper function to handle OAuth timeout/failure
const handleOAuthFailure = useCallback(
(serverName: string, isTimeout: boolean) => {
logger.warn(
`[MCP OAuth] OAuth ${isTimeout ? 'timed out' : 'failed'} for ${serverName}, stopping poll`,
);
// Clean up OAuth state
cleanupOAuthState(serverName);
// Show error toast
showToast({
message: isTimeout
? localize('com_ui_mcp_oauth_timeout', { 0: serverName })
: localize('com_ui_mcp_init_failed'),
status: 'error',
});
},
[showToast, localize, cleanupOAuthState],
);
// Poll for OAuth completion
useEffect(() => {
if (oauthPollingServers.size === 0) {
return;
}
const pollInterval = setInterval(() => {
// Check each polling server
oauthPollingServers.forEach((oauthUrl, serverName) => {
const serverStatus = connectionStatus[serverName];
// Check for client-side timeout (3 minutes)
const startTime = oauthStartTimes.get(serverName);
const hasTimedOut = startTime && Date.now() - startTime > 180000; // 3 minutes
if (serverStatus?.connectionState === 'connected') {
// OAuth completed successfully
handleSuccessfulConnection(
serverName,
localize('com_ui_mcp_authenticated_success', { 0: serverName }),
);
} else if (serverStatus?.connectionState === 'error' || hasTimedOut) {
// OAuth failed or timed out
handleOAuthFailure(serverName, !!hasTimedOut);
}
setCancellableServers((prev) => new Set(prev).add(serverName));
});
queryClient.refetchQueries([QueryKeys.mcpConnectionStatus]);
}, 3500);
return () => {
clearInterval(pollInterval);
};
}, [
oauthPollingServers,
oauthStartTimes,
connectionStatus,
queryClient,
handleSuccessfulConnection,
handleOAuthFailure,
localize,
]);
// Initialize server function
const initializeServer = useCallback(
(serverName: string) => {
// Prevent spam - check if already initializing
if (initializingServers.has(serverName)) {
return;
}
// Add to initializing set
setInitializingServers((prev) => new Set(prev).add(serverName));
// Trigger initialization
reinitializeMutation.mutate(serverName, {
onSuccess: (response: any) => {
if (response.success) {
if (response.oauthRequired && response.oauthUrl) {
// OAuth required - store URL and start polling
setOauthPollingServers((prev) => new Map(prev).set(serverName, response.oauthUrl));
// Track when OAuth started for timeout detection
setOauthStartTimes((prev) => new Map(prev).set(serverName, Date.now()));
// Call optional OAuth callback or open URL directly
if (options?.onOAuthStarted) {
options.onOAuthStarted(serverName, response.oauthUrl);
} else {
window.open(response.oauthUrl, '_blank', 'noopener,noreferrer');
}
showToast({
message: localize('com_ui_connecting'),
status: 'info',
});
} else if (response.oauthRequired) {
// OAuth required but no URL - shouldn't happen
showToast({
message: localize('com_ui_mcp_oauth_no_url'),
status: 'warning',
});
// Remove from initializing since it failed
setInitializingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
} else {
// Successful connection without OAuth
handleSuccessfulConnection(
serverName,
response.message || localize('com_ui_mcp_initialized_success', { 0: serverName }),
);
}
} else {
// Remove from initializing if not successful
setInitializingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
}
},
onError: (error: any) => {
console.error('Error initializing MCP server:', error);
showToast({
message: localize('com_ui_mcp_init_failed'),
status: 'error',
});
// Remove from initializing on error
setInitializingServers((prev) => {
const newSet = new Set(prev);
newSet.delete(serverName);
return newSet;
});
// Remove from OAuth tracking
setOauthPollingServers((prev) => {
const newMap = new Map(prev);
newMap.delete(serverName);
return newMap;
});
setOauthStartTimes((prev) => {
const newMap = new Map(prev);
newMap.delete(serverName);
return newMap;
});
// Call optional error callback
options?.onError?.(serverName, error);
},
});
},
[
reinitializeMutation,
showToast,
localize,
handleSuccessfulConnection,
initializingServers,
options,
],
);
return {
initializeServer,
isInitializing: (serverName: string) => initializingServers.has(serverName),
isCancellable: (serverName: string) => cancellableServers.has(serverName),
initializingServers,
oauthPollingServers,
oauthStartTimes,
connectionStatus,
isLoading: reinitializeMutation.isLoading,
cancelOAuthFlow,
};
}

View File

@@ -7,10 +7,10 @@ import {
QueryKeys,
Constants,
EndpointURLs,
ContentTypes,
tPresetSchema,
tMessageSchema,
tConvoUpdateSchema,
ContentTypes,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import type { TMessage, TConversation, EventSubmission } from 'librechat-data-provider';
@@ -21,7 +21,6 @@ import type { SetterOrUpdater, Resetter } from 'recoil';
import type { ConversationCursorData } from '~/utils';
import {
logger,
setDraft,
scrollToEnd,
getAllContentText,
addConvoToAllQueries,
@@ -458,38 +457,6 @@ export default function useEventHandlers({
announcePolite({ message: 'end', isStatus: true });
announcePolite({ message: getAllContentText(responseMessage) });
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
const setFinalMessages = (id: string | null, _messages: TMessage[]) => {
setMessages(_messages);
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, id], _messages);
};
/** Handle edge case where stream is cancelled before any response, which creates a blank page */
if (
!conversation.conversationId &&
responseMessage?.content?.[0]?.['text']?.value ===
submission.initialResponse?.content?.[0]?.['text']?.value
) {
const currentConvoId =
(submissionConvo.conversationId ?? conversation.conversationId) || Constants.NEW_CONVO;
if (isNewConvo && submissionConvo.conversationId) {
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId);
}
const isNewChat =
location.pathname === `/c/${Constants.NEW_CONVO}` &&
currentConvoId === Constants.NEW_CONVO;
setFinalMessages(currentConvoId, isNewChat ? [] : [...messages]);
setDraft({ id: currentConvoId, value: requestMessage?.text });
setIsSubmitting(false);
if (isNewChat) {
navigate(`/c/${Constants.NEW_CONVO}`, { replace: true, state: { focusChat: true } });
}
return;
}
/* Update messages; if assistants endpoint, client doesn't receive responseMessage */
let finalMessages: TMessage[] = [];
if (runMessages) {
@@ -500,7 +467,11 @@ export default function useEventHandlers({
finalMessages = [...messages, requestMessage, responseMessage];
}
if (finalMessages.length > 0) {
setFinalMessages(conversation.conversationId, finalMessages);
setMessages(finalMessages);
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation.conversationId],
finalMessages,
);
} else if (
isAssistantsEndpoint(submissionConvo.endpoint) &&
(!submissionConvo.conversationId || submissionConvo.conversationId === Constants.NEW_CONVO)
@@ -511,8 +482,9 @@ export default function useEventHandlers({
);
}
if (isNewConvo && submissionConvo.conversationId) {
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId);
const isNewConvo = conversation.conversationId !== submissionConvo.conversationId;
if (isNewConvo) {
removeConvoFromAllQueries(queryClient, submissionConvo.conversationId as string);
}
/* Refresh title */
@@ -555,7 +527,7 @@ export default function useEventHandlers({
);
}
if (location.pathname === `/c/${Constants.NEW_CONVO}`) {
if (location.pathname === '/c/new') {
navigate(`/c/${conversation.conversationId}`, { replace: true });
}
}

View File

@@ -377,6 +377,7 @@
"com_nav_search_placeholder": "بحث في الرسائل",
"com_nav_send_message": "إرسال رسالة",
"com_nav_setting_account": "الحساب",
"com_nav_setting_beta": "ميزات تجريبية",
"com_nav_setting_chat": "دردشة",
"com_nav_setting_data": "تحكم في البيانات",
"com_nav_setting_general": "عام",

View File

@@ -411,6 +411,7 @@
"com_nav_search_placeholder": "Cerca missatges",
"com_nav_send_message": "Envia missatge",
"com_nav_setting_account": "Compte",
"com_nav_setting_beta": "Funcionalitats beta",
"com_nav_setting_chat": "Xat",
"com_nav_setting_data": "Controls de dades",
"com_nav_setting_general": "General",

View File

@@ -1,5 +1,4 @@
{
"chat_direction_left_to_right": "něco sem musí přijít. bylo prázdné",
"chat_direction_right_to_left": "něco sem musí přijít. bylo prázdné",
"com_a11y_ai_composing": "AI stále tvoří odpověď.",
"com_a11y_end": "AI dokončila svou odpověď.",
@@ -58,14 +57,6 @@
"com_auth_discord_login": "Pokračovat přes Discord",
"com_auth_email": "E-mail",
"com_auth_email_address": "E-mailová adresa",
"com_auth_email_max_length": "E-mail by neměl být delší než 120 znaků",
"com_auth_email_min_length": "E-mail musí mít alespoň 6 znaků",
"com_auth_email_pattern": "Musíte zadat platnou e-mailovou adresu",
"com_auth_email_required": "E-mail je povinný",
"com_auth_email_resend_link": "Znovu odeslat e-mail",
"com_auth_email_resent_failed": "Nepodařilo se znovu odeslat ověřovací e-mail",
"com_auth_email_resent_success": "Ověřovací e-mail byl úspěšně odeslán",
"com_auth_email_verification_in_progress": "Ověřujeme váš e-mail, počkejte prosím",
"com_auth_email_verification_invalid": "Neplatné ověření e-mailu",
"com_auth_email_verification_redirecting": "Přesměrování za {{0}} sekund...",
"com_auth_email_verification_resend_prompt": "Nedostali jste e-mail?",
@@ -130,10 +121,6 @@
"com_endpoint_assistant_model": "Model asistenta",
"com_endpoint_completion": "Dokončení",
"com_endpoint_completion_model": "Model dokončení (Doporučeno: GPT-4)",
"com_endpoint_config_click_here": "Klikněte zde",
"com_endpoint_config_google_api_info": "Chcete-li získat klíč Generative Language API (pro Gemini),",
"com_endpoint_config_key_google_need_to": "Musíte",
"com_endpoint_config_key_import_json_key_success": "Úspěšně importovaný klíč JSON servisního účtu",
"com_endpoint_config_key_name": "Klíč",
"com_endpoint_config_key_never_expires": "Váš klíč nikdy nevyprší",
"com_endpoint_config_placeholder": "Nastavte svůj klíč v nabídce záhlaví pro chat.",
@@ -192,7 +179,7 @@
"com_error_files_upload": "Při nahrávání souboru došlo k chybě.",
"com_error_files_upload_canceled": "Požadavek na nahrání souboru byl zrušen. Poznámka: nahrávání souboru může stále probíhat a bude nutné jej ručně smazat.",
"com_error_files_validation": "Při ověřování souboru došlo k chybě.",
"com_error_input_length": "Počet tokenů v poslední zprávě je příliš dlouhý, překračuje limit tokenů nebo jsou parametry limitu tokenů nesprávně nakonfigurovány, což má negativní vliv na kontextové okno. Více informací: {{0}}. Zkraťte prosím svou zprávu, upravte maximální velikost kontextu v parametrech konverzace nebo rozdělte konverzaci, abyste mohli pokračovat.",
"com_error_input_length": "Počet tokenů v poslední zprávě je příliš dlouhý a přesahuje limit tokenů ({{0}}). Zkraťte svou zprávu, upravte maximální velikost kontextu v parametrech konverzace nebo rozdělte konverzaci.",
"com_error_invalid_user_key": "Zadaný klíč je neplatný. Zadejte platný klíč a zkuste to znovu.",
"com_error_moderation": "Zdá se, že obsah vaší zprávy byl označen naším moderovacím systémem, protože neodpovídá našim komunitním zásadám. Nemůžeme v této záležitosti pokračovat. Pokud máte jiné otázky nebo témata, která chcete prozkoumat, upravte svou zprávu nebo vytvořte novou konverzaci.",
"com_error_no_base_url": "Nebyla nalezena základní URL. Zadejte ji a zkuste to znovu.",
@@ -317,6 +304,7 @@
"com_nav_search_placeholder": "Hledat zprávy",
"com_nav_send_message": "Odeslat zprávu",
"com_nav_setting_account": "Účet",
"com_nav_setting_beta": "Beta funkce",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Ovládání dat",
"com_nav_setting_general": "Obecné",

View File

@@ -438,6 +438,7 @@
"com_nav_send_message": "Send besked",
"com_nav_setting_account": "Konto",
"com_nav_setting_balance": "Balance",
"com_nav_setting_beta": "Beta-funktioner",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Datakontrol",
"com_nav_setting_general": "Generel",

View File

@@ -33,14 +33,14 @@
"com_agents_update_error": "Beim Aktualisieren deines Agenten ist ein Fehler aufgetreten.",
"com_assistants_action_attempt": "Assistent möchte kommunizieren mit {{0}}",
"com_assistants_actions": "Aktionen",
"com_assistants_actions_disabled": "Du musst einen Agenten erstellen, bevor du Aktionen hinzufügen kannst.",
"com_assistants_actions_info": "Lasse deinen Agenten Informationen abrufen oder Aktionen über APIs ausführen",
"com_assistants_actions_disabled": "Du musst einen Assistenten erstellen, bevor du Aktionen hinzufügen kannst.",
"com_assistants_actions_info": "Lasse deinen Assistenten Informationen abrufen oder Aktionen über APIs ausführen",
"com_assistants_add_actions": "Aktionen hinzufügen",
"com_assistants_add_tools": "Werkzeuge hinzufügen",
"com_assistants_allow_sites_you_trust": "Erlaube nur Webseiten, denen du vertraust.",
"com_assistants_append_date": "Aktuelles Datum & Uhrzeit anhängen",
"com_assistants_append_date_tooltip": "Wenn aktiviert, werden das aktuelle Client-Datum und die Uhrzeit an die Systemanweisungen des Assistenten angehängt.",
"com_assistants_attempt_info": "Agent möchte Folgendes senden:",
"com_assistants_attempt_info": "Assistent möchte Folgendes senden:",
"com_assistants_available_actions": "Verfügbare Aktionen",
"com_assistants_capabilities": "Fähigkeiten",
"com_assistants_code_interpreter": "Code-Interpreter",
@@ -160,7 +160,6 @@
"com_endpoint_anthropic_thinking_budget": "Bestimmt die maximale Anzahl an Token, die Claude für seinen internen Denkprozess verwenden darf. Ein höheres Budget kann die Antwortqualität verbessern, indem es eine gründlichere Analyse bei komplexen Problemen ermöglicht. Claude nutzt jedoch möglicherweise nicht das gesamte zugewiesene Budget, insbesondere bei Werten über 32.000. Diese Einstellung muss niedriger sein als \"Max. Ausgabe-Token\".",
"com_endpoint_anthropic_topk": "Top-k ändert, wie das Modell Token für die Ausgabe auswählt. Ein Top-k von 1 bedeutet, dass das ausgewählte Token das wahrscheinlichste unter allen Token im Vokabular des Modells ist (auch \"Greedy Decoding\" genannt), während ein Top-k von 3 bedeutet, dass das nächste Token aus den 3 wahrscheinlichsten Token ausgewählt wird (unter Verwendung der Temperatur).",
"com_endpoint_anthropic_topp": "Top-p ändert, wie das Modell Token für die Ausgabe auswählt. Token werden von den wahrscheinlichsten K (siehe topK-Parameter) bis zu den am wenigsten wahrscheinlichen ausgewählt, bis die Summe ihrer Wahrscheinlichkeiten dem Top-p-Wert entspricht.",
"com_endpoint_anthropic_use_web_search": "Ermöglichen Sie die Websuche über die integrierten Suchfähigkeiten von Anthropic. Das Modell kann so das Web nach aktuellen Informationen durchsuchen und präzisere, aktuellere Antworten geben.",
"com_endpoint_assistant": "Assistent",
"com_endpoint_assistant_model": "Assistentenmodell",
"com_endpoint_assistant_placeholder": "Bitte wähle einen Assistenten aus dem rechten Seitenpanel aus",
@@ -198,8 +197,6 @@
"com_endpoint_deprecated": "Veraltet",
"com_endpoint_deprecated_info": "Dieser Endpunkt ist veraltet und wird möglicherweise in zukünftigen Versionen entfernt. Bitte verwende stattdessen den Agent-Endpunkt.",
"com_endpoint_deprecated_info_a11y": "Der Plugin-Endpunkt ist veraltet und wird möglicherweise in zukünftigen Versionen entfernt. Bitte verwende stattdessen den Agent-Endpunkt.",
"com_endpoint_disable_streaming": "Deaktiviere das Streaming von Antworten und erhalte die vollständige Antwort auf einmal. Nützlich für Modelle wie o3, die eine Organisationsverifizierung für Streaming erfordern",
"com_endpoint_disable_streaming_label": "Streaming deaktivieren",
"com_endpoint_examples": " Voreinstellungen",
"com_endpoint_export": "Exportieren",
"com_endpoint_export_share": "Exportieren/Teilen",
@@ -406,13 +403,12 @@
"com_nav_info_show_thinking": "Wenn aktiviert, sind die Denkprozess-Dropdowns standardmäßig geöffnet, sodass du die Gedankengänge der KI in Echtzeit sehen kannst. Wenn deaktiviert, bleiben sie standardmäßig geschlossen, für eine übersichtlichere Oberfläche.",
"com_nav_info_user_name_display": "Wenn aktiviert, wird der Benutzername des Absenders über jeder Nachricht angezeigt, die du sendest. Wenn deaktiviert, siehst du nur \"Du\" über deinen Nachrichten.",
"com_nav_lang_arabic": "العربية",
"com_nav_lang_armenian": "Armenisch",
"com_nav_lang_auto": "Automatisch erkennen",
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
"com_nav_lang_catalan": "Katalonisch",
"com_nav_lang_catalan": "Català",
"com_nav_lang_chinese": "中文",
"com_nav_lang_czech": "Tschechisch",
"com_nav_lang_danish": "Dänisch",
"com_nav_lang_czech": "Čeština",
"com_nav_lang_danish": "Dansk",
"com_nav_lang_dutch": "Nederlands",
"com_nav_lang_english": "English",
"com_nav_lang_estonian": "Eesti keel",
@@ -426,7 +422,6 @@
"com_nav_lang_italian": "Italiano",
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_latvian": "Lettisch",
"com_nav_lang_persian": "Persisch",
"com_nav_lang_polish": "Polski",
"com_nav_lang_portuguese": "Português",
@@ -436,7 +431,6 @@
"com_nav_lang_thai": "ไทย",
"com_nav_lang_traditional_chinese": "繁體中文",
"com_nav_lang_turkish": "Türkçe",
"com_nav_lang_uyghur": "Uyghurisch",
"com_nav_lang_vietnamese": "Tiếng Việt",
"com_nav_language": "Sprache",
"com_nav_latex_parsing": "LaTeX in Nachrichten parsen (kann die Leistung beeinflussen)",
@@ -465,6 +459,7 @@
"com_nav_send_message": "Nachricht senden",
"com_nav_setting_account": "Konto",
"com_nav_setting_balance": "Saldo",
"com_nav_setting_beta": "Beta-Funktionen",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Datensteuerung",
"com_nav_setting_general": "Allgemein",
@@ -486,7 +481,7 @@
"com_nav_theme_system": "System",
"com_nav_tool_dialog": "Assistenten-Werkzeuge",
"com_nav_tool_dialog_agents": "Agent-Tools",
"com_nav_tool_dialog_description": "Agent muss gespeichert werden, um Werkzeugauswahlen zu speichern.",
"com_nav_tool_dialog_description": "Assistent muss gespeichert werden, um Werkzeugauswahlen zu speichern.",
"com_nav_tool_remove": "Entfernen",
"com_nav_tool_search": "Werkzeuge suchen",
"com_nav_user": "BENUTZER",
@@ -502,6 +497,7 @@
"com_sidepanel_conversation_tags": "Lesezeichen",
"com_sidepanel_hide_panel": "Seitenleiste ausblenden",
"com_sidepanel_manage_files": "Dateien verwalten",
"com_sidepanel_mcp_enter_value": "Gib den Wert für {{0}} ein",
"com_sidepanel_mcp_no_servers_with_vars": "Keine MCP-Server mit konfigurierbaren Variablen.",
"com_sidepanel_mcp_variables_for": "MCP Variablen für {{0}}",
"com_sidepanel_parameters": "KI-Einstellungen",
@@ -575,7 +571,6 @@
"com_ui_archive_error": "Konversation konnte nicht archiviert werden",
"com_ui_artifact_click": "Zum Öffnen klicken",
"com_ui_artifacts": "Artefakte",
"com_ui_artifacts_options": "Artefakt Optionen",
"com_ui_artifacts_toggle": "Artefakte-Funktion einschalten",
"com_ui_artifacts_toggle_agent": "Artefakte aktivieren",
"com_ui_ascending": "Aufsteigend",
@@ -645,7 +640,6 @@
"com_ui_command_placeholder": "Optional: Gib einen Promptbefehl ein oder den Namen.",
"com_ui_command_usage_placeholder": "Wähle einen Prompt nach Befehl oder Name aus",
"com_ui_complete_setup": "Einrichtung abschließen",
"com_ui_concise": "Prägnant",
"com_ui_configure_mcp_variables_for": "Konfiguriere Variablen für {{0}}",
"com_ui_confirm_action": "Aktion bestätigen",
"com_ui_confirm_admin_use_change": "Wenn du diese Einstellung änderst, wird der Zugriff für Administratoren, einschließlich dir selbst, gesperrt. Bist du sicher, dass du fortfahren möchtest?",
@@ -702,10 +696,8 @@
"com_ui_delete_mcp_error": "Fehler beim Löschen des MCP-Servers",
"com_ui_delete_mcp_success": "MCP-Server erfolgreich gelöscht",
"com_ui_delete_memory": "Gedächtnis löschen",
"com_ui_delete_not_allowed": "Löschvorgang ist nicht erlaubt",
"com_ui_delete_prompt": "Prompt löschen?",
"com_ui_delete_shared_link": "Geteilten Link löschen?",
"com_ui_delete_success": "Erfolgreich gelöscht",
"com_ui_delete_tool": "Werkzeug löschen",
"com_ui_delete_tool_confirm": "Bist du sicher, dass du dieses Werkzeug löschen möchtest?",
"com_ui_deleted": "Gelöscht",
@@ -713,7 +705,6 @@
"com_ui_description": "Beschreibung",
"com_ui_description_placeholder": "Optional: Gib eine Beschreibung für den Prompt ein",
"com_ui_deselect_all": "Alle abwählen",
"com_ui_detailed": "Detailliert",
"com_ui_disabling": "Deaktiviere …",
"com_ui_download": "Herunterladen",
"com_ui_download_artifact": "Artefakt herunterladen",
@@ -751,7 +742,6 @@
"com_ui_feedback_negative": "Muss verbessert werden",
"com_ui_feedback_placeholder": "Geben Sie hier bitte weiteres Feedback an",
"com_ui_feedback_positive": "Prima, sehr gut",
"com_ui_feedback_tag_accurate_reliable": "Akkurat und Zuverlässig",
"com_ui_feedback_tag_attention_to_detail": "Liebe zum Detail",
"com_ui_feedback_tag_bad_style": "Clear and Well-Written",
"com_ui_feedback_tag_clear_well_written": "Klar und gut geschrieben",
@@ -775,7 +765,6 @@
"com_ui_fork_change_default": "Standard-Abzweigungsoption",
"com_ui_fork_default": "Standard-Abzweigungsoption verwenden",
"com_ui_fork_error": "Beim Abzweigen der Konversation ist ein Fehler aufgetreten",
"com_ui_fork_error_rate_limit": "Zu viele Forks Anfragen. Versuche es später noch einmal.",
"com_ui_fork_from_message": "Wähle eine Abzweigungsoption",
"com_ui_fork_info_1": "Verwende diese Einstellung, um Nachrichten mit dem gewünschten Verhalten abzuzweigen.",
"com_ui_fork_info_2": "\"Abzweigen\" bezieht sich auf das Erstellen einer neuen Konversation, die von bestimmten Nachrichten in der aktuellen Konversation ausgeht/endet und eine Kopie gemäß den ausgewählten Optionen erstellt.",
@@ -808,9 +797,7 @@
"com_ui_good_morning": "Guten Morgen",
"com_ui_happy_birthday": "Es ist mein 1. Geburtstag!",
"com_ui_hide_image_details": "Details zum Bild ausblenden",
"com_ui_hide_password": "Passwort verbergen",
"com_ui_hide_qr": "QR-Code ausblenden",
"com_ui_high": "Hoch",
"com_ui_host": "Host",
"com_ui_icon": "Icon",
"com_ui_idea": "Ideen",
@@ -837,7 +824,6 @@
"com_ui_loading": "Lade …",
"com_ui_locked": "Gesperrt",
"com_ui_logo": "{{0}} Logo",
"com_ui_low": "Niedrig",
"com_ui_manage": "Verwalten",
"com_ui_max_tags": "Die maximale Anzahl ist {{0}}, es werden die neuesten Werte verwendet.",
"com_ui_mcp_dialog_desc": "Bitte geben Sie unten die erforderlichen Informationen ein.",
@@ -845,8 +831,7 @@
"com_ui_mcp_server_not_found": "Server nicht gefunden",
"com_ui_mcp_servers": "MCP Server",
"com_ui_mcp_url": "MCP-Server-URL",
"com_ui_medium": "Mittel",
"com_ui_memories": "Erinnerungen",
"com_ui_memories": "Gedächtnisse",
"com_ui_memories_allow_create": "Erinnerungen erstellen erlauben",
"com_ui_memories_allow_opt_out": "Benutzern erlauben, die Erinnerungsnutzung zu deaktivieren",
"com_ui_memories_allow_read": "Erinnerungen lesen erlauben",
@@ -854,17 +839,12 @@
"com_ui_memories_allow_use": "Erinnerungen nutzen erlauben",
"com_ui_memories_filter": "Erinnerungen filtern...",
"com_ui_memory": "Erinnerung",
"com_ui_memory_already_exceeded": "Erinnerungen Speicherplatz bereits voll - durch {{tokens}} Tokens überschritten. Lösche Erinnerungen aus dem vorhandene Speicher, bevor du neue hinzufügst.",
"com_ui_memory_created": "Erinnerung erfolgreich erstellt",
"com_ui_memory_deleted": "Erinnerung gelöscht",
"com_ui_memory_deleted_items": "Gelöschte Erinnerungen",
"com_ui_memory_error": "Fehler bei den Erinnerungen",
"com_ui_memory_key_exists": "Eine Erinnerung mit diesem Schlüssel existiert bereits. Bitte verwende einen anderen Schlüssel.",
"com_ui_memory_key_validation": "Der Erinnerungsschlüssel darf nur Kleinbuchstaben und Unterstriche enthalten.",
"com_ui_memory_storage_full": "Speicherplatz für Erinnerungen voll",
"com_ui_memory_updated": "Erinnerung aktualisiert",
"com_ui_memory_updated_items": "Aktualisierte Erinnerungen",
"com_ui_memory_would_exceed": "Speichern nicht möglich - würde Limit um {{tokens}} Tokens überschreiten. Löschen Sie vorhandene Erinnerungen, um Platz zu schaffen.",
"com_ui_mention": "Erwähne einen Endpunkt, Assistenten oder eine Voreinstellung, um schnell dorthin zu wechseln",
"com_ui_min_tags": "Es können nicht mehr Werte entfernt werden, mindestens {{0}} sind erforderlich.",
"com_ui_misc": "Verschiedenes",
@@ -990,7 +970,6 @@
"com_ui_show": "Anzeigen",
"com_ui_show_all": "Alle anzeigen",
"com_ui_show_image_details": "Details zum Bild anzeigen",
"com_ui_show_password": "Passwort anzeigen",
"com_ui_show_qr": "QR-Code anzeigen",
"com_ui_sign_in_to_domain": "Anmelden bei {{0}}",
"com_ui_simple": "Einfach",
@@ -1064,7 +1043,6 @@
"com_ui_web_search_jina_key": "Den Jina API Schlüssel eingeben",
"com_ui_web_search_processing": "Ergebnisse verarbeiten",
"com_ui_web_search_provider": "Anbieter der Suche",
"com_ui_web_search_provider_searxng": "SearXNG",
"com_ui_web_search_provider_serper": "Serper API\n",
"com_ui_web_search_provider_serper_key": "Einen Serper API Schlüssel holen",
"com_ui_web_search_reading": "Lesen der Suchergebnisse",
@@ -1076,8 +1054,6 @@
"com_ui_web_search_scraper": "Scraper",
"com_ui_web_search_scraper_firecrawl": "Firecrawl API\n",
"com_ui_web_search_scraper_firecrawl_key": "Einen Firecrawl API Schlüssel holen",
"com_ui_web_search_searxng_api_key": "SearXNG API Key (optional) einfügen",
"com_ui_web_search_searxng_instance_url": "SearXNG Instanz URL",
"com_ui_web_searching": "Internetsuche läuft",
"com_ui_web_searching_again": "Sucht erneut im Internet",
"com_ui_weekend_morning": "Schönes Wochenende",

View File

@@ -198,8 +198,6 @@
"com_endpoint_deprecated": "Deprecated",
"com_endpoint_deprecated_info": "This endpoint is deprecated and may be removed in future versions, please use the agent endpoint instead",
"com_endpoint_deprecated_info_a11y": "The plugin endpoint is deprecated and may be removed in future versions, please use the agent endpoint instead",
"com_endpoint_disable_streaming": "Disable streaming responses and receive the complete response at once. Useful for models like o3 that require organization verification for streaming",
"com_endpoint_disable_streaming_label": "Disable Streaming",
"com_endpoint_examples": " Presets",
"com_endpoint_export": "Export",
"com_endpoint_export_share": "Export/Share",
@@ -444,8 +442,6 @@
"com_nav_log_out": "Log out",
"com_nav_long_audio_warning": "Longer texts will take longer to process.",
"com_nav_maximize_chat_space": "Maximize chat space",
"com_nav_mcp_configure_server": "Configure {{0}}",
"com_nav_mcp_status_connecting": "{{0}} - Connecting",
"com_nav_mcp_vars_update_error": "Error updating MCP custom user variables: {{0}}",
"com_nav_mcp_vars_updated": "MCP custom user variables updated successfully.",
"com_nav_modular_chat": "Enable switching Endpoints mid-conversation",
@@ -468,6 +464,7 @@
"com_nav_send_message": "Send message",
"com_nav_setting_account": "Account",
"com_nav_setting_balance": "Balance",
"com_nav_setting_beta": "Beta features",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Data controls",
"com_nav_setting_general": "General",
@@ -505,6 +502,7 @@
"com_sidepanel_conversation_tags": "Bookmarks",
"com_sidepanel_hide_panel": "Hide Panel",
"com_sidepanel_manage_files": "Manage Files",
"com_sidepanel_mcp_enter_value": "Enter value for {{0}}",
"com_sidepanel_mcp_no_servers_with_vars": "No MCP servers with configurable variables.",
"com_sidepanel_mcp_variables_for": "MCP Variables for {{0}}",
"com_sidepanel_parameters": "Parameters",
@@ -528,6 +526,7 @@
"com_ui_action_button": "Action Button",
"com_ui_active": "Active",
"com_ui_add": "Add",
"com_ui_authenticate": "Authenticate",
"com_ui_add_mcp": "Add MCP",
"com_ui_add_mcp_server": "Add MCP Server",
"com_ui_add_model_preset": "Add a model or preset for an additional response",
@@ -597,7 +596,6 @@
"com_ui_attachment": "Attachment",
"com_ui_auth_type": "Auth Type",
"com_ui_auth_url": "Authorization URL",
"com_ui_authenticate": "Authenticate",
"com_ui_authentication": "Authentication",
"com_ui_authentication_type": "Authentication Type",
"com_ui_auto": "Auto",
@@ -655,10 +653,8 @@
"com_ui_confirm_action": "Confirm Action",
"com_ui_confirm_admin_use_change": "Changing this setting will block access for admins, including yourself. Are you sure you want to proceed?",
"com_ui_confirm_change": "Confirm Change",
"com_ui_connecting": "Connecting",
"com_ui_context": "Context",
"com_ui_continue": "Continue",
"com_ui_continue_oauth": "Continue with OAuth",
"com_ui_controls": "Controls",
"com_ui_convo_delete_error": "Failed to delete conversation",
"com_ui_copied": "Copied!",
@@ -848,20 +844,11 @@
"com_ui_low": "Low",
"com_ui_manage": "Manage",
"com_ui_max_tags": "Maximum number allowed is {{0}}, using latest values.",
"com_ui_mcp_authenticated_success": "MCP server '{{0}}' authenticated successfully",
"com_ui_mcp_dialog_desc": "Please enter the necessary information below.",
"com_ui_mcp_enter_var": "Enter value for {{0}}",
"com_ui_mcp_init_failed": "Failed to initialize MCP server",
"com_ui_mcp_initialize": "Initialize",
"com_ui_mcp_initialized_success": "MCP server '{{0}}' initialized successfully",
"com_ui_mcp_not_authenticated": "{{0}} not authenticated (OAuth Required)",
"com_ui_mcp_not_initialized": "{{0}} not initialized",
"com_ui_mcp_oauth_cancelled": "OAuth login cancelled for {{0}}",
"com_ui_mcp_oauth_no_url": "OAuth authentication required but no URL provided",
"com_ui_mcp_oauth_timeout": "OAuth login timed out for {{0}}",
"com_ui_mcp_server_not_found": "Server not found.",
"com_ui_mcp_servers": "MCP Servers",
"com_ui_mcp_update_var": "Update {{0}}",
"com_ui_mcp_url": "MCP Server URL",
"com_ui_medium": "Medium",
"com_ui_memories": "Memories",
@@ -916,12 +903,10 @@
"com_ui_oauth_error_missing_code": "Authorization code is missing. Please try again.",
"com_ui_oauth_error_missing_state": "State parameter is missing. Please try again.",
"com_ui_oauth_error_title": "Authentication Failed",
"com_ui_oauth_flow_desc": "Complete the OAuth flow in the new window, then return here.",
"com_ui_oauth_success_description": "Your authentication was successful. This window will close in",
"com_ui_oauth_success_title": "Authentication Successful",
"com_ui_of": "of",
"com_ui_off": "Off",
"com_ui_offline": "Offline",
"com_ui_on": "On",
"com_ui_openai": "OpenAI",
"com_ui_optional": "(optional)",
@@ -954,7 +939,6 @@
"com_ui_regenerate_backup": "Regenerate Backup Codes",
"com_ui_regenerating": "Regenerating...",
"com_ui_region": "Region",
"com_ui_reinitialize": "Reinitialize",
"com_ui_rename": "Rename",
"com_ui_rename_conversation": "Rename Conversation",
"com_ui_rename_failed": "Failed to rename conversation",
@@ -978,6 +962,13 @@
"com_ui_save_submit": "Save & Submit",
"com_ui_saved": "Saved!",
"com_ui_saving": "Saving...",
"com_ui_set": "Set",
"com_ui_unset": "Unset",
"com_ui_configuration": "Configuration",
"com_ui_mcp_auth_desc": "Configure authentication credentials for this MCP server.",
"com_ui_authorization_url": "Authorization URL",
"com_ui_continue_oauth": "Continue OAuth Flow",
"com_ui_oauth_flow_desc": "Click the button above to continue the OAuth flow in a new tab.",
"com_ui_schema": "Schema",
"com_ui_scope": "Scope",
"com_ui_search": "Search",
@@ -994,7 +985,6 @@
"com_ui_select_search_plugin": "Search plugin by name",
"com_ui_select_search_provider": "Search provider by name",
"com_ui_select_search_region": "Search region by name",
"com_ui_set": "Set",
"com_ui_share": "Share",
"com_ui_share_create_message": "Your name and any messages you add after sharing stay private.",
"com_ui_share_delete_error": "There was an error deleting the shared link",
@@ -1047,7 +1037,6 @@
"com_ui_unarchive": "Unarchive",
"com_ui_unarchive_error": "Failed to unarchive conversation",
"com_ui_unknown": "Unknown",
"com_ui_unset": "Unset",
"com_ui_untitled": "Untitled",
"com_ui_update": "Update",
"com_ui_update_mcp_error": "There was an error creating or updating the MCP.",

View File

@@ -11,14 +11,10 @@
"com_agents_create_error": "Hubo un error al crear su agente.",
"com_agents_description_placeholder": "Opcional: Describa su Agente aquí",
"com_agents_enable_file_search": "Habilitar búsqueda de archivos",
"com_agents_file_context": "Archivo de contexto (OCR)",
"com_agents_file_context_disabled": "Es necesario crear el Agente antes de subir archivos.",
"com_agents_file_search_disabled": "Es necesario crear el Agente antes de subir archivos para la Búsqueda de Archivos.",
"com_agents_file_search_info": "Cuando está habilitado, se informará al agente sobre los nombres exactos de los archivos listados a continuación, permitiéndole recuperar el contexto relevante de estos archivos.",
"com_agents_instructions_placeholder": "Las instrucciones del sistema que utiliza el agente",
"com_agents_mcp_description_placeholder": "Explica que hace en pocas palabras",
"com_agents_mcp_icon_size": "Tamaño minimo 128 x128 px",
"com_agents_mcp_trust_subtext": "LibreChat no verifica los conectores personalizados",
"com_agents_missing_provider_model": "Por favor, seleccione un proveedor y un modelo antes de crear un agente.",
"com_agents_name_placeholder": "Opcional: El nombre del agente",
"com_agents_no_access": "No tiene acceso para editar este agente",
@@ -395,6 +391,7 @@
"com_nav_search_placeholder": "Buscar mensajes",
"com_nav_send_message": "Enviar mensaje",
"com_nav_setting_account": "Cuenta",
"com_nav_setting_beta": "Funciones beta",
"com_nav_setting_chat": "Configuración del chat",
"com_nav_setting_data": "Controles de datos",
"com_nav_setting_general": "General",

View File

@@ -439,6 +439,7 @@
"com_nav_send_message": "Saada sõnum",
"com_nav_setting_account": "Konto",
"com_nav_setting_balance": "Saldo",
"com_nav_setting_beta": "Beeta funktsioonid",
"com_nav_setting_chat": "Vestlus",
"com_nav_setting_data": "Andmekontroll",
"com_nav_setting_general": "Üldine",

View File

@@ -408,6 +408,7 @@
"com_nav_search_placeholder": "جستجوی پیام ها",
"com_nav_send_message": "ارسال پیام",
"com_nav_setting_account": "حساب",
"com_nav_setting_beta": "ویژگی های بتا",
"com_nav_setting_chat": "چت کنید",
"com_nav_setting_data": "کنترل های داده",
"com_nav_setting_general": "ژنرال",

View File

@@ -1,49 +1,17 @@
{
"com_a11y_end": "Tekoälyn vastaus on valmis.",
"com_a11y_start": "Tekoäly on aloittanut vastaamisen.",
"com_agents_allow_editing": "Anna muiden käyttäjien muokata agenttiasi",
"com_agents_create_error": "Agentin luonnissa tapahtui virhe.",
"com_agents_description_placeholder": "Valinnainen: Lisää tähän agentin kuvaus",
"com_agents_enable_file_search": "Käytä Tiedostohakua",
"com_agents_file_context": "Tiedostokonteksti (OCR)",
"com_agents_file_context_disabled": "Agentti täytyy luoda ennen tiedostojen lataamista Tiedostokontekstiin",
"com_agents_file_context_info": "\"Kontekstiksi\" ladatuista tiedostoista luetaan sisältö tekstintunnistuksen (OCR) avulla agentin ohjeisiin lisättäväksi. Soveltuu erityisesti asiakirjojen, tekstiä sisältävien kuvien tai PDF-tiedostojen käsittelyyn, kun haluat hyödyntää tiedoston koko tekstisisällön.",
"com_agents_file_search_disabled": "Agentti täytyy luoda ennen tiedostojen lataamista Tiedostohakuun",
"com_agents_file_search_info": "Asetuksen ollessa päällä agentti saa tiedoksi alla luetellut tiedostonimet, jolloin se voi hakea vastausten pohjaksi asiayhteyksiä tiedostojen sisällöistä.",
"com_agents_instructions_placeholder": "Agentin käyttämät järjestelmäohjeet",
"com_agents_mcp_description_placeholder": "Anna muutaman sanan kuvaus sen toiminnasta",
"com_agents_mcp_icon_size": "Minimikoko 128 x 128 px",
"com_agents_mcp_info": "Lisää MCP-palvelimia agentillesi, jotta se voi ottaa yhteyttä ulkoisiin palveluihin ja suorittaa tehtäviä niissä.",
"com_agents_mcp_name_placeholder": "Mukautettu työkalu",
"com_agents_mcp_trust_subtext": "LibreChat ei varmenna mukautettuja liittymiä.",
"com_agents_mcps_disabled": "Agentti on luotava ennen MCP:n lisäämistä.",
"com_agents_missing_provider_model": "Valitse palvelun tuottaja ja malli ennen agentin luomista.",
"com_agents_name_placeholder": "Valinnainen: Agentin nimi",
"com_agents_no_access": "Sinulla ei ole lupaa muokata tätä agenttia.",
"com_agents_no_agent_id_error": "Agentin tunnistetta ei löytynyt. Varmista ensin, että agentti on luotu.",
"com_agents_not_available": "Agentti ei ole saatavilla.",
"com_agents_search_info": "Asetuksen ollessa päällä agentti voi tehdä verkkohakuja ajantasaisen tiedon hakemista varten. Vaatii voimassaolevan API-avaimen.",
"com_agents_search_name": "Etsi agentteja nimen perusteella",
"com_agents_update_error": "Agentin päivittämisessä tapahtui virhe.",
"com_assistants_action_attempt": "Assistentti haluaa keskustella {{0}}:n kanssa",
"com_assistants_actions": "Toiminnot",
"com_assistants_actions_disabled": "Avustaja täytyy luoda ennen toimintojen lisäämistä",
"com_assistants_actions_info": "Salli Avustajalle Tiedonhaku tai Toimintojen suorittaminen API-kutsujen kautta",
"com_assistants_add_actions": "Lisää Toimintoja",
"com_assistants_add_tools": "Lisää Työkaluja",
"com_assistants_allow_sites_you_trust": "Salli vain sellaiset sivustot, joihin luotat,",
"com_assistants_append_date": "Lisää nykyinen päivämäärä ja aika",
"com_assistants_append_date_tooltip": "Kun käytössä, nykyinen asiakkaan päivämäärä ja aika lisätään avustajan järjestelmäohjeisiin.",
"com_assistants_attempt_info": "Assistentti haluaa lähettää seuraavan:",
"com_assistants_available_actions": "Käytettävissä olevat Toiminnot",
"com_assistants_capabilities": "Kyvykkyydet",
"com_assistants_code_interpreter": "Kooditulkki",
"com_assistants_code_interpreter_files": "Seuraavat tiedostot ovat vain Kooditulkin käytettävissä:",
"com_assistants_code_interpreter_info": "Kooditulkki mahdollistaa assistentille koodin kirjoittamisen ja ajamisen. Tämä työkalu voi käsitellä tiedostoja, joissa on eri tyyppisiä tietoja ja muotoiluja, ja luoda tiedostoja kuten kaavioita.",
"com_assistants_completed_action": "Puhuttiin {{0}}:lle",
"com_assistants_completed_function": "Suoritettiin {{0}}",
"com_assistants_conversation_starters": "Keskustelun aloitukset",
"com_assistants_conversation_starters_placeholder": "Lisää keskustelun aloitus",
"com_assistants_create_error": "Avustajan luonnissa tapahtui virhe.",
"com_assistants_create_success": "Luonti onnistui",
"com_assistants_delete_actions_error": "Toiminnon poistamisessa tapahtui virhe.",
@@ -58,19 +26,16 @@
"com_assistants_knowledge": "Tiedot",
"com_assistants_knowledge_disabled": "Avustaja täytyy ensin luoda, ja Kooditulkki tai Tiedonhaku täytyy olla päällä ja asetukset tallennettuna, ennen kuin tiedostoja voidaan ladata Tietoihin.",
"com_assistants_knowledge_info": "Jos lataat tiedostoja Tietoihin, Avustajasi kanssa käytyihin keskusteluihin voi tulla niiden sisältöä.",
"com_assistants_max_starters_reached": "Maksimimäärä keskustelun aloituksia lisätty",
"com_assistants_name_placeholder": "Valinnainen: Avustajan nimi",
"com_assistants_non_retrieval_model": "Tiedostohaku ei ole käytössä tässä mallissa. Valitse toinen malli.",
"com_assistants_retrieval": "Tiedonhaku",
"com_assistants_running_action": "Suoritetaan toimintoa",
"com_assistants_running_var": "Suoritetaan {{0}}",
"com_assistants_search_name": "Hae Avustajia nimen perusteella",
"com_assistants_update_actions_error": "Toiminnon luomisessa tai päivittämisessä tapahtui virhe.",
"com_assistants_update_actions_success": "Toiminto luotiiin tai päivitettiin onnistuneesti",
"com_assistants_update_error": "Avustajan päivittämisessä tapahtui virhe.",
"com_assistants_update_success": "Päivitys onnistui",
"com_auth_already_have_account": "Käyttäjätilisi on jo luotu?",
"com_auth_apple_login": "Kirjaudu Apple-tunnuksella",
"com_auth_back_to_login": "Palaa kirjautumiseen",
"com_auth_click": "Napauta",
"com_auth_click_here": "Napauta tästä",
@@ -93,7 +58,6 @@
"com_auth_email_verification_redirecting": "Uudelleenohjataan {{0}} sekunnissa...",
"com_auth_email_verification_resend_prompt": "Sähköposti ei saapunut perille?",
"com_auth_email_verification_success": "Sähköposti varmennettu",
"com_auth_email_verifying_ellipsis": "Varmistetaan...",
"com_auth_error_create": "Tilin rekisteröinnissä tapahtui virhe. Yritä uudestaan.",
"com_auth_error_invalid_reset_token": "Tämä salasanan uusimistunniste ei ole enää voimassa.",
"com_auth_error_login": "Kirjautuminen annetuilla tiedoilla ei onnistunut. Tarkista kirjautumistiedot, ja yritä uudestaan.",
@@ -125,7 +89,6 @@
"com_auth_reset_password_if_email_exists": "Jos kyseiselle sähköpostiosoitteelle löytyy käyttäjätili, siihen lähetetään sähköposti joka sisältää ohjeet salasanan uusimiseen. Tarkistathan myös roskapostikansion.",
"com_auth_reset_password_link_sent": "Sähköposti lähetetty",
"com_auth_reset_password_success": "Salasanan asettaminen onnistui",
"com_auth_saml_login": "Jatka SAML-kirjautumisella",
"com_auth_sign_in": "Kirjaudu",
"com_auth_sign_up": "Rekisteröidy",
"com_auth_submit_registration": "Lähetä rekisteröityminen",
@@ -134,20 +97,11 @@
"com_auth_username": "Käyttäjänimi (valinnainen)",
"com_auth_username_max_length": "Käyttäjänimi voi olla enintään 20 merkkiä pitkä",
"com_auth_username_min_length": "Käyttäjänimessä on oltava vähintään 2 merkkiä",
"com_auth_verify_your_identity": "Varmista identiteettisi",
"com_auth_welcome_back": "Tervetuloa takaisin",
"com_citation_more_details": "Lisätietoa: {{label}}",
"com_citation_source": "Lähde",
"com_click_to_download": "(lataa napauttamalla tästä)",
"com_download_expired": "(lataus on vanhentunut)",
"com_download_expires": "(lataa napauttamalla tätä - vanhenee {{0}})",
"com_endpoint": "Päätepiste",
"com_endpoint_agent": "Agentti",
"com_endpoint_agent_model": "Agenttimalli (Suositus: GPT-3.5)",
"com_endpoint_agent_placeholder": "Valitse agentti",
"com_endpoint_ai": "Tekoäly",
"com_endpoint_anthropic_maxoutputtokens": "Vastauksen maksimi-tokenmäärä. valitse pienempi arvo, jos haluat lyhyempiä vastauksia, ja korkeampi arvo, jos haluat pidempiä vastauksia.",
"com_endpoint_anthropic_prompt_cache": "Tallentamalla syötteet välimuistiin suurta määrää kontekstia tai ohjeita voidaan käyttää useiden API-kutsujen, mikä vähentää kuluja ja nopeuttaa käsittelyä.",
"com_endpoint_anthropic_temp": "Vaihteluväli on 0 - 1. Käytä lähempänä nollaa olevaa lämpötilaa analyyttisiin tai monivalintatehtäviin, ja lähempänä yhtä luoviin ja generatiivisiin tehtäviin. Suosittelemme, että muokkaat tätä tai Top P:tä, mutta ei molempia.",
"com_endpoint_anthropic_topk": "Top-k vaikuttaa siihen, miten malli valitsee tokeineita tulokseen. Jos Top-k on 1, valitaan se token, joka on kaikkien todennäköisen mallin sanastossa (tunnetaan myös nimellä ahne dekoodaus), kun taas top-k 3 tarkoittaisi, että seuraavat token valitaan 3 todennäköisimmän tokenin joukosta, lämpötilaa hyödyntäen.",
"com_endpoint_anthropic_topp": "Top-P vaikuttaa siihen kuinka malli valitsee tokeneita tulokseen. Tokenit valitaan top-k:sta (ks. Top-k -parametri) todennäköisimmistä vähiten todennäköseen, kunnes niiden todennäköisyyksien summa ylittää Top-P -arvon.",
@@ -259,16 +213,11 @@
"com_files_filter": "Suodata tiedostoja...",
"com_files_no_results": "Ei tuloksia.",
"com_files_number_selected": "{{0}}/{{1}} tiedostoa valittu",
"com_generated_files": "Luodut tiedostot:",
"com_hide_examples": "Piilota esimerkit",
"com_info_heic_converting": "Muunnetaan kuvaa HEIC:istä JPEG:iksi...",
"com_nav_2fa": "Kaksivaiheinen tunnistautuminen (2FA)",
"com_nav_account_settings": "Tilin asetukset",
"com_nav_always_make_prod": "Tee aina uudet versiot tuotantoon",
"com_nav_archive_created_at": "Arkistointipäivä",
"com_nav_archive_name": "Nimi",
"com_nav_archived_chats": "Arkistoidut keskustelut",
"com_nav_at_command": "@-komento",
"com_nav_audio_play_error": "Virhe ääntä toistaessa: {{0}}",
"com_nav_audio_process_error": "Virhe ääntä käsitellessä: {{0}}",
"com_nav_auto_scroll": "Vieritä automaattisesti viimeisimpään viestiin keskustelua avatessa",
@@ -307,12 +256,6 @@
"com_nav_export_recursive_or_sequential": "Rekursiivisesti vai sarjassa?",
"com_nav_export_type": "Tyyppi",
"com_nav_external": "Ulkoinen",
"com_nav_font_size": "Viestien kirjainkoko",
"com_nav_font_size_base": "Keskikoko",
"com_nav_font_size_lg": "Suuri",
"com_nav_font_size_sm": "Pieni",
"com_nav_font_size_xl": "Ekstrasuuri",
"com_nav_font_size_xs": "Ekstrapieni",
"com_nav_help_faq": "Ohjeet & FAQ",
"com_nav_hide_panel": "Piilota oikeanpuoleinen sivupaneeli",
"com_nav_info_enter_to_send": "Jos tämä on päällä, Enter-näppäimen painaminen lähettää viestin. Kun asetus on pois päältä, Enter lisää rivinvaihdon, ja viestin lähettämiseksi on painettava CTRL + ENTER.",
@@ -365,6 +308,7 @@
"com_nav_search_placeholder": "Etsi keskusteluista",
"com_nav_send_message": "Lähetä viesti",
"com_nav_setting_account": "Käyttäjätili",
"com_nav_setting_beta": "Beta-toiminnot",
"com_nav_setting_data": "Datakontrollit",
"com_nav_setting_general": "Yleiset",
"com_nav_setting_speech": "Puhe",

View File

@@ -19,7 +19,7 @@
"com_agents_instructions_placeholder": "Les instructions système que l'agent utilise",
"com_agents_mcp_description_placeholder": "Décrivez ce qu'il fait en quelques mots",
"com_agents_mcp_icon_size": "Taille minimale de 128 x 128 pixels",
"com_agents_mcp_info": "Ajoutez des serveurs MCP à votre agent pour lui permettre d'accomplir des tâches et d'interagir avec des services externes",
"com_agents_mcp_info": "Ajoutez des serveurs MCP à vos agents pour lui permettre d'accomplir des tâches et d'interagir avec des services externes",
"com_agents_mcp_name_placeholder": "Outil personnalisé",
"com_agents_mcp_trust_subtext": "Connecteurs personnalisés non vérifiés par LibreChat",
"com_agents_mcps_disabled": "Vous devez créer un agent avant d'ajouter des MCP.",
@@ -160,7 +160,6 @@
"com_endpoint_anthropic_thinking_budget": "Détermine le nombre maximum de jetons que Claude est autorisé à utiliser pour son processus de raisonnement interne. Des budgets plus importants peuvent améliorer la qualité des réponses en permettant une analyse plus approfondie des problèmes complexes, bien que Claude puisse ne pas utiliser la totalité du budget alloué, en particulier dans les plages supérieures à 32K. Ce paramètre doit être inférieur à \"Max Output Tokens\".",
"com_endpoint_anthropic_topk": "Top-k change la façon dont le modèle sélectionne les jetons pour la sortie. Un top-k de 1 signifie que le jeton sélectionné est le plus probable parmi tous les jetons du vocabulaire du modèle (également appelé décodage glouton), tandis qu'un top-k de 3 signifie que le jeton suivant est sélectionné parmi les 3 jetons les plus probables (en utilisant la température).",
"com_endpoint_anthropic_topp": "Top-p change la façon dont le modèle sélectionne les jetons pour la sortie. Les jetons sont sélectionnés du plus K (voir le paramètre topK) probable au moins jusqu'à ce que la somme de leurs probabilités égale la valeur top-p.",
"com_endpoint_anthropic_use_web_search": "Activer l'utilisation des capacités de recherche de Anthropic pour faire des recherches web. Cela permet au modèle de chercher des informations récentes sur internet et de fournir des réponses plus précises.",
"com_endpoint_assistant": "Assistant de point de terminaison",
"com_endpoint_assistant_model": "Modèle d'assistant",
"com_endpoint_assistant_placeholder": "Veuillez sélectionner un assistant dans le panneau latéral droit",
@@ -235,7 +234,6 @@
"com_endpoint_openai_temp": "Des valeurs plus élevées = plus aléatoires, tandis que des valeurs plus faibles = plus concentrées et déterministes. Nous vous recommandons de modifier ceci ou Top P mais pas les deux.",
"com_endpoint_openai_topp": "Une alternative à l'échantillonnage avec température, appelée échantillonnage du noyau, où le modèle considère les résultats des jetons avec une masse de probabilité top_p. Ainsi, 0,1 signifie que seuls les jetons représentant les 10 % de masse de probabilité les plus élevés sont pris en compte. Nous vous recommandons de modifier ceci ou la température mais pas les deux.",
"com_endpoint_openai_use_responses_api": "Utilise l'API de Réponses plutôt que la completion de messages, ce qui offre plus de fonctionnalités auprès d'OpenAI. Obligatoire pour o1-pro, o3-pro et pour activer la synthèse du raisonnement",
"com_endpoint_openai_use_web_search": "Activer l'utilisation des capacités de recherche de OpenAI pour faire des recherches web. Cela permet au modèle de chercher des informations récentes sur internet et de fournir des réponses plus précises.",
"com_endpoint_output": "Sortie",
"com_endpoint_plug_image_detail": "Détail de l'image",
"com_endpoint_plug_resend_files": "Renvoyer les fichiers",
@@ -405,7 +403,6 @@
"com_nav_info_show_thinking": "Lorsque cette option est activée, le chat affiche les menus déroulants de réflexion ouverts par défaut, ce qui vous permet de voir le raisonnement de l'IA en temps réel. Lorsqu'ils sont désactivés, les menus déroulants de réflexion restent fermés par défaut, ce qui permet d'obtenir une interface plus propre et plus rationnelle.",
"com_nav_info_user_name_display": "Lorsqu'activé, le nom d'utilisateur de l'expéditeur sera affiché au-dessus de chaque message que vous envoyez. Lorsque désactivé, vous verrez seulement \"Vous\" au-dessus de vos messages.",
"com_nav_lang_arabic": "العربية",
"com_nav_lang_armenian": "Հայերեն",
"com_nav_lang_auto": "Détection automatique",
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
"com_nav_lang_catalan": "Catalan",
@@ -425,7 +422,6 @@
"com_nav_lang_italian": "Italiano",
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_latvian": "Latviski",
"com_nav_lang_persian": "Persan",
"com_nav_lang_polish": "Polski",
"com_nav_lang_portuguese": "Português",
@@ -435,7 +431,6 @@
"com_nav_lang_thai": "ไทย",
"com_nav_lang_traditional_chinese": "繁體中文",
"com_nav_lang_turkish": "Türkçe",
"com_nav_lang_uyghur": "Uyƣur tili",
"com_nav_lang_vietnamese": "Tiếng Việt",
"com_nav_language": "Langue",
"com_nav_latex_parsing": "Analyse LaTeX dans les messages (peut affecter les performances)",
@@ -464,6 +459,7 @@
"com_nav_send_message": "Envoyer un message",
"com_nav_setting_account": "Compte",
"com_nav_setting_balance": "Solde",
"com_nav_setting_beta": "Fonctionnalités bêta",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Contrôles des données",
"com_nav_setting_general": "Général",
@@ -501,6 +497,7 @@
"com_sidepanel_conversation_tags": "Signets",
"com_sidepanel_hide_panel": "Masquer le panneau",
"com_sidepanel_manage_files": "Gérer les fichiers",
"com_sidepanel_mcp_enter_value": "Saisissez la valeur de {{0}}",
"com_sidepanel_mcp_no_servers_with_vars": "Aucun serveur MCP dont les variables sont configurables.",
"com_sidepanel_mcp_variables_for": "Variables MCP de {{0}}",
"com_sidepanel_parameters": "Paramètres",
@@ -805,7 +802,6 @@
"com_ui_good_morning": "Bonjour",
"com_ui_happy_birthday": "C'est mon premier anniversaire !",
"com_ui_hide_image_details": "Cacher les informations de l'images",
"com_ui_hide_password": "Cacher le mot de passe",
"com_ui_hide_qr": "Cacher le QR code",
"com_ui_high": "Elevé",
"com_ui_host": "Hôte",
@@ -982,7 +978,6 @@
"com_ui_show": "Montrer",
"com_ui_show_all": "Tout afficher",
"com_ui_show_image_details": "Montrer les informations de l'image",
"com_ui_show_password": "Afficher le mot de passe",
"com_ui_show_qr": "Afficher le QR code",
"com_ui_sign_in_to_domain": "S'identifier à {{0}}",
"com_ui_simple": "Simple",
@@ -1056,7 +1051,6 @@
"com_ui_web_search_jina_key": "Entrez la clé API de Jina",
"com_ui_web_search_processing": "Traitement des résultats",
"com_ui_web_search_provider": "Fournisseur de recherches web",
"com_ui_web_search_provider_searxng": "SearXNG",
"com_ui_web_search_provider_serper": "API de Serper",
"com_ui_web_search_provider_serper_key": "Obtenez votre clé API pour Serper",
"com_ui_web_search_reading": "Lecture des résultats",
@@ -1068,7 +1062,6 @@
"com_ui_web_search_scraper": "Extracteur (scraper)",
"com_ui_web_search_scraper_firecrawl": "API de Firecrawl",
"com_ui_web_search_scraper_firecrawl_key": "Obtenez votre clé API pour Firecrawl",
"com_ui_web_search_searxng_instance_url": "Adresse URL de l'instance SearXNG",
"com_ui_web_searching": "Rechercher sur le web",
"com_ui_web_searching_again": "Rechercher à nouveau sur le web",
"com_ui_weekend_morning": "Joyeux week-end",

View File

@@ -160,7 +160,6 @@
"com_endpoint_anthropic_thinking_budget": "קובע את מספר הטוקנים המקסימלי שקלוד רשאי להשתמש בו עבור תהליך החשיבה הפנימי. תקציב גבוה יותר עשוי לשפר את איכות התשובה על ידי מתן אפשרות לניתוח מעמיק יותר של בעיות מורכבות, אם כי קלוד לא בהכרח ישתמש בכל התקציב שהוקצה, במיוחד בטווחים שמעל 32K. הגדרה זו חייבת להיות נמוכה מ'מקסימום טוקנים לפלט'.",
"com_endpoint_anthropic_topk": "Top-k משנה את האופן שבו המודל בוחר אסימונים לפלט. Top-k של 1 פירושו שהאסימון שנבחר הוא הסביר ביותר מבין כל האסימונים באוצר המילים של הדגם (נקרא גם פענוח חמדן), בעוד ש-top-k של 3 פירושו שהאסימון הבא נבחר מבין 3 הכי הרבה. אסימונים סבירים (באמצעות טמפרטורה).",
"com_endpoint_anthropic_topp": "Top-p משנה את האופן שבו המודל בוחר אסימונים לפלט. אסימונים נבחרים מבין רוב K (ראה פרמטר topK) הסביר לפחות עד שסכום ההסתברויות שלהם שווה לערך העליון-p.",
"com_endpoint_anthropic_use_web_search": "הפעל אפשרות חיפוש ברשת באמצעות יכולות החיפוש המובנות של קלוד. זה מאפשר למודל לחפש באינטרנט מידע עדכני ולספק תשובות מדויקות ועדכניות יותר.",
"com_endpoint_assistant": "סייען",
"com_endpoint_assistant_model": "מודל סייען",
"com_endpoint_assistant_placeholder": "אנא בחר סייען מלוח הצד הימני",
@@ -234,8 +233,6 @@
"com_endpoint_openai_stop": "עד 4 רצפים שבהם ה-API יפסיק לייצר טוקנים נוספים.",
"com_endpoint_openai_temp": "ערכים גבוהים יותר = יותר אקראיים, בעוד שערכים נמוכים יותר = יותר ממוקד ודטרמיניסטי. אנו ממליצים לשנות את זה או את Top P אבל לא את שניהם.",
"com_endpoint_openai_topp": "חלופה לדגימה עם טמפרטורה, הנקראת דגימת גרעין, שבה המודל מחשיב את תוצאות האסימונים עם מסת ההסתברות top_p. אז 0.1 אומר שרק האסימונים המהווים את מסת ההסתברות העליונה של 10% נחשבים. אנו ממליצים לשנות את זה או את הטמפרטורה אבל לא את שניהם.",
"com_endpoint_openai_use_responses_api": "השתמש ב-API של תגובות במקום השלמות צ'אט, אשר כולל תכונות מורחבות מ-OpenAI. נדרש עבור o1-pro, o3-pro, וכדי לאפשר סיכומי חשיבה.",
"com_endpoint_openai_use_web_search": "הפעל פונקציונליות חיפוש ברשת באמצעות יכולות החיפוש המובנות של OpenAI. זה מאפשר למודל לחפש ברשת מידע עדכני ולספק תשובות מדויקות ועדכניות יותר.",
"com_endpoint_output": "פלט",
"com_endpoint_plug_image_detail": "פרטי תמונה",
"com_endpoint_plug_resend_files": "שלח שוב את הקובץ",
@@ -266,7 +263,6 @@
"com_endpoint_prompt_prefix_assistants_placeholder": "הגדר הוראות נוספות או הקשר על גבי ההנחיות הראשיות של ה-סייען. התעלמו אם ריק.",
"com_endpoint_prompt_prefix_placeholder": "הגדר הוראות מותאמות אישית או הקשר. התעלמו אם ריק.",
"com_endpoint_reasoning_effort": "מאמץ בתהליך החשיבה",
"com_endpoint_reasoning_summary": "סיכום נימוקים",
"com_endpoint_save_as_preset": "שמור כתבנית",
"com_endpoint_search": "חפש נקודת קצה לפי שם",
"com_endpoint_search_endpoint_models": "חפש מודלים של {{0}}...",
@@ -282,7 +278,6 @@
"com_endpoint_top_k": "Top K",
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "השתמש ב-סייען פעיל",
"com_endpoint_use_search_grounding": "התבססות על חיפוש גוגל",
"com_error_expired_user_key": "המפתח שסופק עבור {{0}} פג ב-{{1}}. אנא ספק מפתח חדש ונסה שוב.",
"com_error_files_dupe": "זוהה קובץ כפול",
"com_error_files_empty": "אין אפשרות לקבצים ריקים",
@@ -291,7 +286,6 @@
"com_error_files_upload": "אירעה שגיאה בעת העלאת הקובץ",
"com_error_files_upload_canceled": "בקשת העלאת הקובץ בוטלה. הערה: ייתכן שהעלאת הקובץ עדיין בעיבוד ותצטרך למחוק אותו בצורה ידנית.",
"com_error_files_validation": "אירעה שגיאה במהלך אימות הקובץ.",
"com_error_google_tool_conflict": "השימוש בכלים המובנים של Google אינו נתמך עם כלים חיצוניים. אנא השבת את הכלים המובנים או את הכלים החיצוניים.",
"com_error_heic_conversion": "המרת התמונה בפורמט HEIC לפורמט JPEG נכשלה. אנא נסה להמיר את התמונה ידנית או להשתמש בפורמט אחר.",
"com_error_input_length": "מספר הטוקנים של ההודעות האחרונות גבוה מדי, והוא חורג ממגבלת האסימונים ({{0}} בהתאמה. אנא קצר את ההודעה שלך, שנה את גודל ההקשר המקסימלי בפרמטרי השיחה, או התחל שיחה חדשה.",
"com_error_invalid_agent_provider": "המודלים של \"{{0}}\" אינם זמינים לשימוש עם סוכנים. אנא עבור להגדרות הסוכן שלך ובחר ספק הזמין כרגע.",
@@ -457,6 +451,7 @@
"com_nav_send_message": "שלח הודעה",
"com_nav_setting_account": "חשבון",
"com_nav_setting_balance": "לאזן",
"com_nav_setting_beta": "תכונות ביטא",
"com_nav_setting_chat": "צ'אט",
"com_nav_setting_data": "בקרות נתונים",
"com_nav_setting_general": "כללי",
@@ -494,6 +489,7 @@
"com_sidepanel_conversation_tags": "סימניות",
"com_sidepanel_hide_panel": "הסתר פאנל",
"com_sidepanel_manage_files": "נהל קבצים",
"com_sidepanel_mcp_enter_value": "הזן ערך עבור {{0}}",
"com_sidepanel_mcp_no_servers_with_vars": "אין שרתי MCP עם משתנים הניתנים להגדרה.",
"com_sidepanel_mcp_variables_for": "משתני MCP עבור {{0}}",
"com_sidepanel_parameters": "פרמטרים",
@@ -567,7 +563,6 @@
"com_ui_archive_error": "אירעה שגיאה באירכוב השיחה",
"com_ui_artifact_click": "לחץ לפתיחה",
"com_ui_artifacts": "רכיבי תצוגה",
"com_ui_artifacts_options": "אפשרויות ארטיפקטים",
"com_ui_artifacts_toggle": "הפעל/כבה רכיבי תצוגה",
"com_ui_artifacts_toggle_agent": "אפשר רכיבי תצוגה",
"com_ui_ascending": "סדר עולה",
@@ -636,7 +631,6 @@
"com_ui_command_placeholder": "אופציונלי: הזן פקודה להנחיה (פרומפט), או שיעשה שימוש בשם",
"com_ui_command_usage_placeholder": "בחר הנחיה (פרומפט) לפי פקודה או שם",
"com_ui_complete_setup": "ההגדרה הושלמה",
"com_ui_concise": "תמציתי",
"com_ui_configure_mcp_variables_for": "הגדרת משתנים עבור {{0}}",
"com_ui_confirm_action": "אשר פעולה",
"com_ui_confirm_admin_use_change": "שינוי הגדרה זו יחסום גישה למנהלים, כולל אותך. האם אתה בטוח שברצונך להמשיך?",
@@ -693,19 +687,15 @@
"com_ui_delete_mcp_error": "מחיקת שרת MCP נכשלה",
"com_ui_delete_mcp_success": "שרת ה-MCP נמחק בהצלחה",
"com_ui_delete_memory": "מחק זיכרון",
"com_ui_delete_not_allowed": "פעולת המחיקה אינה אפשרית",
"com_ui_delete_prompt": "מחק הנחיה (פרומפט)",
"com_ui_delete_shared_link": "מחק קישור שיתוף",
"com_ui_delete_success": "נמחק בהצלחה",
"com_ui_delete_tool": "מחק כלי",
"com_ui_delete_tool_confirm": "האת אתה בטוח שאתה רוצה למחוק את הכלי הזה?",
"com_ui_deleted": "נמחק",
"com_ui_deleting_file": "מוחק קובץ...",
"com_ui_descending": "תיאור",
"com_ui_description": "תיאור",
"com_ui_description_placeholder": "אופציונלי: הזן תיאור שיוצג עבור ההנחיה (פרומפט)",
"com_ui_deselect_all": "בטל את הבחירה של הכל",
"com_ui_detailed": "מפורט",
"com_ui_disabling": "מבטל הפעלה...",
"com_ui_download": "הורדות",
"com_ui_download_artifact": "רכיב תצוגת הורדות",
@@ -767,7 +757,6 @@
"com_ui_fork_change_default": "הגדרות הסתעפויות ברירת מחדל",
"com_ui_fork_default": "השתמש בהגדרות הסתעפויות ברירת מחדל",
"com_ui_fork_error": "אירעה שגיאה בעת פיצול השיחה",
"com_ui_fork_error_rate_limit": "יותר מדי בקשות פיצול. אנא נסה שוב מאוחר יותר.",
"com_ui_fork_from_message": "בחר הגדרת הסתעפויות",
"com_ui_fork_info_1": "השתמש בהגדרה זו כדי ליצור הסתעפות של הודעות עם ההתנהגות הרצויה.",
"com_ui_fork_info_2": "\"הסתעפות\" מתייחסת ליצירת שיחה חדשה המתחילה/מסתיימת מהודעות ספציפיות בשיחה הנוכחית, תוך יצירת העתק בהתאם לאפשרויות שנבחרו.",
@@ -800,9 +789,7 @@
"com_ui_good_morning": "ערב טוב",
"com_ui_happy_birthday": "זה יום ההולדת הראשון שלי!",
"com_ui_hide_image_details": "הסתר פרטי תמונה",
"com_ui_hide_password": "הסתר סיסמה",
"com_ui_hide_qr": "הסתר קוד QR",
"com_ui_high": "גבוה",
"com_ui_host": "מארח",
"com_ui_icon": "אייקון",
"com_ui_idea": "רעיונות",
@@ -829,7 +816,6 @@
"com_ui_loading": "טוען...",
"com_ui_locked": "נעול",
"com_ui_logo": "\"לוגו {{0}}\"",
"com_ui_low": "נמוך",
"com_ui_manage": "נהל",
"com_ui_max_tags": "המספר המקסימלי המותר על פי הערכים העדכניים הוא {{0}}.",
"com_ui_mcp_dialog_desc": "אנא הזן למטה את המידע הדרוש",
@@ -837,7 +823,6 @@
"com_ui_mcp_server_not_found": "נשרת לא נמצא",
"com_ui_mcp_servers": "שרתי MCP",
"com_ui_mcp_url": "קישור לשרת ה-MCP",
"com_ui_medium": "בינוני",
"com_ui_memories": "זכרונות",
"com_ui_memories_allow_create": "אפשר יצירת זיכרונות",
"com_ui_memories_allow_opt_out": "אפשר למשתמשים לבטל את הזיכרונות",
@@ -846,17 +831,12 @@
"com_ui_memories_allow_use": "אפשר שימוש בזיכרונות",
"com_ui_memories_filter": "סינון זיכרונות...",
"com_ui_memory": "זכרון",
"com_ui_memory_already_exceeded": "אחסון הזיכרונות כבר מלא - קיימת חריגה ב-{{tokens}} אסימונים. מחק זיכרונות קיימים לפני הוספת חדשים.",
"com_ui_memory_created": "הזיכרון נוצר בהצלחה",
"com_ui_memory_deleted": "הזיכרון נמחק",
"com_ui_memory_deleted_items": "זכרונות שנמחקו",
"com_ui_memory_error": "שגיאת זיכרון",
"com_ui_memory_key_exists": "זיכרון עם מפתח זה כבר קיים. אנא השתמש במפתח אחר.",
"com_ui_memory_key_validation": "מפתח הזיכרון חייב להכיל רק אותיות קטנות או קו תחתון.",
"com_ui_memory_storage_full": "אחסון הזיכרונות מלא",
"com_ui_memory_updated": "זיכרון שמור מעודכן",
"com_ui_memory_updated_items": "זיכרונות מעודכנים",
"com_ui_memory_would_exceed": "לא ניתן לשמור - זה יעבור את המגבלה ב-{{tokens}} אסימונים. מחק זיכרונות קיימים כדי לפנות מקום לזיכרונות חדשים.",
"com_ui_mention": "ציין נקודת קצה, סייען, או הנחייה (פרופמט) כדי לעבור אליה במהירות",
"com_ui_min_tags": "לא ניתן למחוק ערכים נוספים, יש צורך במינימום {{0}} ערכים.",
"com_ui_misc": "כללי",
@@ -981,7 +961,6 @@
"com_ui_show": "הצג",
"com_ui_show_all": "הראה הכל",
"com_ui_show_image_details": "הצג את פרטי התמונה",
"com_ui_show_password": "הראה סיסמה",
"com_ui_show_qr": "הראה קוד QR",
"com_ui_sign_in_to_domain": "היכנס אל {{0}}",
"com_ui_simple": "פשוט",

View File

@@ -408,6 +408,7 @@
"com_nav_search_placeholder": "Üzenetek keresése",
"com_nav_send_message": "Üzenet küldése",
"com_nav_setting_account": "Fiók",
"com_nav_setting_beta": "Béta funkciók",
"com_nav_setting_chat": "Csevegés",
"com_nav_setting_data": "Adatvezérlők",
"com_nav_setting_general": "Általános",

View File

@@ -45,15 +45,11 @@
"com_auth_click_here": "Սեղմեք այստեղ",
"com_auth_continue": "Շարունակել",
"com_auth_create_account": "Ստեղծեք ձեր ակաունթը",
"com_auth_discord_login": "Շարունակել Discord-ով",
"com_auth_email": "Էլ․ հասցե",
"com_auth_email_address": "Էլ․ հասցե",
"com_auth_email_required": "Էլ․ հասցեն պարտադիր է",
"com_auth_email_resend_link": "Ուղարկել էլ․ հասցեն կրկին",
"com_auth_email_verification_failed": "Էլ․ փոստի հաստատումը ձախողվեց",
"com_auth_email_verification_failed_token_missing": "Ստուգումը ձախողվեց, token-ը բացակայում է",
"com_auth_email_verification_in_progress": "Ստուգում ենք ձեր էլ․ փոստը, խնդրում ենք սպասել",
"com_auth_email_verification_invalid": "Էլ․ փոստի հաստատումը անվավեր է",
"com_auth_email_verification_success": "\"Էլ․ հասցեն հաջողությամբ հաստատվեց",
"com_auth_email_verifying_ellipsis": "Ստուգվում է...",
"com_auth_facebook_login": "Շարունակել Facebook-ի միջոցով",
@@ -73,7 +69,6 @@
"com_auth_reset_password": "Վերականգնել գաղտնաբառը",
"com_auth_reset_password_link_sent": "Էլ․ նամակը ուղարկվել է",
"com_auth_reset_password_success": "Գաղտնաբառը հաջողությամբ վերականգնվեց",
"com_auth_saml_login": "Շարունակել SAML-ով",
"com_auth_sign_in": "Մուտք գործել",
"com_auth_sign_up": "Գրանցվել",
"com_auth_to_reset_your_password": "գաղտնաբառը վերականգնելու համար։",
@@ -108,7 +103,6 @@
"com_endpoint_config_key_never_expires": "Ձեր key-ը երբեք չի սպառվի",
"com_endpoint_custom_name": "Անհատական անուն",
"com_endpoint_deprecated": "Հնացած",
"com_endpoint_disable_streaming_label": "Անջատել սթրիմինգը",
"com_endpoint_examples": "Օրինակ",
"com_endpoint_export": "Արտահանել",
"com_endpoint_export_share": "Արտահանել/Կիսվել",
@@ -126,7 +120,6 @@
"com_endpoint_preset_selected_title": "Ակտիվ է։",
"com_endpoint_preset_title": "Պրեսեթ",
"com_endpoint_presets": "պրեսեթներ",
"com_endpoint_reasoning_summary": "Փաստարկման ամփոփում",
"com_endpoint_save_as_preset": "Պահպանել որպես պրեսեթ",
"com_endpoint_temperature": "Temperature",
"com_error_files_dupe": "Հայտնաբերվել է կրկնվող ֆայլ։",
@@ -169,33 +162,8 @@
"com_nav_lang_vietnamese": "Tiếng Việt",
"com_nav_language": "Լեզու",
"com_nav_log_out": "Ելք",
"com_nav_my_files": "Իմ ֆայլերը",
"com_nav_not_supported": "Չի աջակցվում",
"com_nav_open_sidebar": "Բացել կողային վահանակը",
"com_nav_plugin_install": "Տեղադրել",
"com_nav_plugin_uninstall": "Հեռացնել",
"com_nav_setting_account": "Ակաունթ",
"com_nav_setting_balance": "Հաշվեկշիռ",
"com_nav_setting_chat": "Զրույց",
"com_nav_settings": "Կարգավորումներ",
"com_nav_tool_remove": "Հեռացնել",
"com_sidepanel_conversation_tags": "Էջանիշեր",
"com_sidepanel_hide_panel": "Թաքցնել վահանակը",
"com_sidepanel_manage_files": "Կառավարել ֆայլերը",
"com_ui_2fa_enable": "Միացնել 2FA-ն",
"com_ui_admin_settings": "Ադմինի կարգավորումներ",
"com_ui_tools": "Գործիքներ",
"com_ui_travel": "Ճամփորդություն",
"com_ui_unknown": "Անհայտ",
"com_ui_untitled": "Անվերնագիր",
"com_ui_update": "Թարմացնել",
"com_ui_upload": "Ներբեռնել",
"com_ui_upload_files": "Ներբեռնել ֆայլեր",
"com_ui_upload_image": "Ներբեռնել պատկեր",
"com_ui_upload_image_input": "Ներբեռնել պատկեր",
"com_ui_upload_ocr_text": "Ներբեռնել որպես տեքստ",
"com_ui_upload_type": "Ընտրեք ներբեռնելու տիպը",
"com_ui_write": "Գրում է",
"com_ui_yes": "Այո",
"com_user_message": "Դու"
"com_ui_2fa_enable": "Միացնել 2FA-ն"
}

View File

@@ -206,6 +206,7 @@
"com_nav_search_placeholder": "Cari pesan",
"com_nav_send_message": "Kirim pesan",
"com_nav_setting_account": "Akun",
"com_nav_setting_beta": "Fitur beta",
"com_nav_setting_data": "Kontrol data",
"com_nav_setting_general": "Umum",
"com_nav_settings": "Pengaturan",

View File

@@ -15,7 +15,6 @@
"com_agents_file_search_disabled": "L'Agente deve essere creato prima di caricare file per la Ricerca File.",
"com_agents_file_search_info": "Quando abilitato, l'agente verrà informato dei nomi esatti dei file elencati di seguito, permettendogli di recuperare il contesto pertinente da questi file.",
"com_agents_instructions_placeholder": "Le istruzioni di sistema utilizzate dall'agente",
"com_agents_mcp_icon_size": "Dimensione minima 128 x 128 px",
"com_agents_missing_provider_model": "Seleziona un provider e un modello prima di creare un agente.",
"com_agents_name_placeholder": "Opzionale: Il nome dell'agente",
"com_agents_no_access": "Non hai l'autorizzazione per modificare questo agente.",
@@ -398,6 +397,7 @@
"com_nav_search_placeholder": "Cerca messaggi",
"com_nav_send_message": "Invia messaggio",
"com_nav_setting_account": "Account",
"com_nav_setting_beta": "Funzioni Beta",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Controlli dati",
"com_nav_setting_general": "Generale",
@@ -785,7 +785,7 @@
"com_ui_storage": "Archiviazione",
"com_ui_submit": "Invia",
"com_ui_teach_or_explain": "Istruzione",
"com_ui_temporary": "Chat temporanea",
"com_ui_temporary": "Temporanea",
"com_ui_terms_and_conditions": "Termini d'uso",
"com_ui_terms_of_service": "Termini di servizio",
"com_ui_thinking": "Pensando...",

View File

@@ -17,16 +17,9 @@
"com_agents_file_search_disabled": "ファイル検索用のファイルをアップロードする前に、エージェントを作成する必要があります。",
"com_agents_file_search_info": "有効にすると、エージェントは以下に表示されているファイル名を正確に認識し、それらのファイルから関連する情報を取得することができます。",
"com_agents_instructions_placeholder": "エージェントが使用するシステムの指示",
"com_agents_mcp_description_placeholder": "それが何をするのか簡単に説明してください",
"com_agents_mcp_icon_size": "最小サイズ 128 x 128 px",
"com_agents_mcp_info": "エージェントにMCPサーバーを追加し、タスクの実行や外部サービスとのやり取りを可能にする。",
"com_agents_mcp_name_placeholder": "カスタムツール",
"com_agents_mcp_trust_subtext": "カスタムコネクタはLibreChatによって検証されません。",
"com_agents_mcps_disabled": "MCPを追加する前にエージェントを作成する必要があります。",
"com_agents_missing_provider_model": "エージェントを作成する前に、プロバイダーとモデルを選択してください。",
"com_agents_name_placeholder": "オプション: エージェントの名前",
"com_agents_no_access": "このエージェントを編集する権限がありません。",
"com_agents_no_agent_id_error": "エージェントIDが見つかりません。エージェントが作成されていることを確認してください。",
"com_agents_not_available": "エージェントは利用できません",
"com_agents_search_info": "有効にすると、エージェントが最新の情報を取得するためにWEB検索を行うようになります。有効なAPIキーが必要です。",
"com_agents_search_name": "名前でエージェントを検索",
@@ -69,7 +62,6 @@
"com_assistants_non_retrieval_model": "このモデルではファイル検索機能は有効になっていません。別のモデルを選択してください。",
"com_assistants_retrieval": "検索",
"com_assistants_running_action": "アクションを実行",
"com_assistants_running_var": "実行中 {{0}}",
"com_assistants_search_name": "アシスタントの名前で検索",
"com_assistants_update_actions_error": "アクションの作成または更新中にエラーが発生しました。",
"com_assistants_update_actions_success": "アクションが作成または更新されました",
@@ -131,7 +123,6 @@
"com_auth_reset_password_if_email_exists": "そのメールアドレスのアカウントが存在する場合は、パスワードリセット手順が記載されたメールが送信されています。スパムフォルダを必ず確認してください。",
"com_auth_reset_password_link_sent": "メールを送信",
"com_auth_reset_password_success": "パスワードのリセットに成功しました",
"com_auth_saml_login": "SAMLで続行",
"com_auth_sign_in": "ログイン",
"com_auth_sign_up": "新規登録",
"com_auth_submit_registration": "登録をする",
@@ -143,8 +134,6 @@
"com_auth_username_min_length": "ユーザ名は最低2文字で入力してください",
"com_auth_verify_your_identity": "本人確認",
"com_auth_welcome_back": "おかえりなさい",
"com_citation_more_details": "詳細はこちら {{label}}",
"com_citation_source": "出典",
"com_click_to_download": "(ダウンロードするにはこちらをクリック)",
"com_download_expired": "ダウンロードの期限が切れています",
"com_download_expires": "ダウンロードはこちら(有効期限:{{0}}",
@@ -160,7 +149,6 @@
"com_endpoint_anthropic_thinking_budget": "Claude が内部推論プロセスで使用できるトークンの最大数を指定します。予算を大きくすると、複雑な問題に対するより徹底的な分析が可能になり、応答品質が向上します。ただし、特に 32K を超える範囲では、Claude は割り当てられた予算をすべて使用できない可能性があります。この設定は「最大出力トークン」よりも小さくする必要があります。",
"com_endpoint_anthropic_topk": "Top-k はモデルがトークンをどのように選択して出力するかを変更します。top-kが1の場合はモデルの語彙に含まれるすべてのトークンの中で最も確率が高い1つが選択されます(greedy decodingと呼ばれている)。top-kが3の場合は上位3つのトークンの中から選択されます。(temperatureを使用)",
"com_endpoint_anthropic_topp": "Top-p はモデルがトークンをどのように選択して出力するかを変更します。K(topKを参照)の確率の合計がtop-pの確率と等しくなるまでのトークンが選択されます。",
"com_endpoint_anthropic_use_web_search": "Anthropic の組み込み検索機能を使用して、Web 検索機能を有効にします。これにより、モデルは Web 上で最新情報を検索し、より正確で最新の回答を提供できるようになります。",
"com_endpoint_assistant": "アシスタント",
"com_endpoint_assistant_model": "アシスタント モデル",
"com_endpoint_assistant_placeholder": "右側のサイドパネルからアシスタントを選択してください",
@@ -206,11 +194,8 @@
"com_endpoint_google_custom_name_placeholder": "Googleのカスタム名を設定する",
"com_endpoint_google_maxoutputtokens": "レスポンスで生成できるトークンの最大数。短い応答には低い値を、長い応答には高い値を指定します。注:モデルはこの最大値に達する前に停止することがあります。",
"com_endpoint_google_temp": "大きい値 = ランダム性が増します。低い値 = より決定論的になります。この値を変更するか、Top P の変更をおすすめしますが、両方を変更はおすすめしません。",
"com_endpoint_google_thinking": "推論を有効または無効にします。この設定は特定のモデル2.5シリーズ)のみがサポートしています。古いモデルの場合、この設定は効果がない場合があります。",
"com_endpoint_google_thinking_budget": "モデルが使用する思考トークンの数を指示する。実際の使用量は、プロンプトによってこの値を上回ったり下回ったりする。\n\nこの設定は、特定のモデル2.5シリーズのみがサポートしています。Gemini 2.5 Proは12832,768トークンをサポートします。Gemini 2.5 Flashは、024,576トークンをサポートします。Gemini 2.5 Flash Liteは、51224,576トークンをサポートします。\n\n空白のままにするか、\"-1 \"に設定すると、いつ、どのくらい考えるかをモデルが自動的に決定します。デフォルトでは、Gemini 2.5 Flash Liteは思考しない。",
"com_endpoint_google_topk": "top-k は、モデルがトークンを選択して出力する方法を変更する。top-kが1の場合、選択されたトークンはモデルの語彙に含まれるすべてのトークンの中で最も確率の高いものである貪欲なデコーディングとも呼ばれることを意味し、top-kが3の場合、次のトークンは最も確率の高い3つのトークンの中から選択される温度を使用することを意味する。",
"com_endpoint_google_topp": "top-p は、モデルがトークンをどのように選択して出力するかを変更する。トークンは、確率の合計が top-p の値に等しくなるまで、確率の最も高い K 個topK パラメータを参照)から低い順に選択される。",
"com_endpoint_google_use_search_grounding": "Googleの検索グラウンディング機能を使用して、リアルタイムのウェブ検索結果で回答を強化します。これにより、モデルは最新の情報にアクセスし、より正確で最新の回答を提供することができます。",
"com_endpoint_instructions_assistants": "指示をオーバーライドする",
"com_endpoint_instructions_assistants_placeholder": "アシスタントの指示を上書きします。これは、実行ごとに動作を変更する場合に便利です。",
"com_endpoint_max_output_tokens": "最大出力トークン数",
@@ -228,14 +213,11 @@
"com_endpoint_openai_pres": "-2.0から2.0の値。正の値は入力すると、新規トークンの出現に基づいたペナルティを課し、新しいトピックについて話す可能性を高める。",
"com_endpoint_openai_prompt_prefix_placeholder": "システムメッセージに含める Custom Instructions。デフォルト: none",
"com_endpoint_openai_reasoning_effort": "o1 モデルのみ: 推論モデルの推論の努力を制限します。推論の努力を減らすと、応答が速くなり、応答で推論に使用されるトークンが少なくなります。",
"com_endpoint_openai_reasoning_summary": "Responses APIのみモデルが実行した推論の概要。これは、モデルの推論プロセスのデバッグや理解に役立ちます。none、auto、concise、detailedのいずれかに設定してください。",
"com_endpoint_openai_resend": "これまでに添付した画像を全て再送信します。注意:トークン数が大幅に増加したり、多くの画像を添付するとエラーが発生する可能性があります。",
"com_endpoint_openai_resend_files": "以前に添付されたすべてのファイルを再送信します。注意:これにより、トークンのコストが増加し、多くの添付ファイルでエラーが発生する可能性があります。",
"com_endpoint_openai_stop": "APIがさらにトークンを生成するのを止めるため、最大で4つのシーケンスを設定可能",
"com_endpoint_openai_temp": "大きい値 = ランダム性が増します。低い値 = より決定論的になります。この値を変更するか、Top P の変更をおすすめしますが、両方を変更はおすすめしません。",
"com_endpoint_openai_topp": "nucleus sampling と呼ばれるtemperatureを使用したサンプリングの代わりに、top_p確率質量のトークンの結果を考慮します。つまり、0.1とすると確率質量の上位10%を構成するトークンのみが考慮されます。この値かtemperatureの変更をおすすめしますが、両方を変更はおすすめしません。",
"com_endpoint_openai_use_responses_api": "Chat Completions の代わりに、OpenAI の拡張機能を含む Responses API を使用してください。o1-pro、o3-pro、および推論要約を有効にするために必要です。",
"com_endpoint_openai_use_web_search": "OpenAIの組み込み検索機能を使用して、ウェブ検索機能を有効にします。これにより、モデルは最新の情報をウェブで検索し、より正確で最新の回答を提供できるようになります。",
"com_endpoint_output": "出力",
"com_endpoint_plug_image_detail": "画像の詳細",
"com_endpoint_plug_resend_files": "ファイルを再送",
@@ -266,7 +248,6 @@
"com_endpoint_prompt_prefix_assistants_placeholder": "アシスタントの主な指示に加えて、追加の指示やコンテキストを設定します。空欄の場合は無視されます。",
"com_endpoint_prompt_prefix_placeholder": "custom instructions か context を設定する。空の場合は無視されます。",
"com_endpoint_reasoning_effort": "推論の努力",
"com_endpoint_reasoning_summary": "推論の要約",
"com_endpoint_save_as_preset": "プリセット保存",
"com_endpoint_search": "エンドポイントを名前で検索",
"com_endpoint_search_endpoint_models": "{{0}} モデルを検索...",
@@ -282,8 +263,6 @@
"com_endpoint_top_k": "Top K",
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "アクティブなアシスタントを使用",
"com_endpoint_use_responses_api": "レスポンスAPIの使用",
"com_endpoint_use_search_grounding": "Google検索でグラウンディング",
"com_error_expired_user_key": "{{0}}の提供されたキーは{{1}}で期限切れです。キーを入力して再試行してください。",
"com_error_files_dupe": "重複したファイルが検出されました。",
"com_error_files_empty": "空のファイルはアップロードできません",
@@ -292,8 +271,6 @@
"com_error_files_upload": "ファイルのアップロード中にエラーが発生しました。",
"com_error_files_upload_canceled": "ファイルのアップロードがキャンセルされました。注意:アップロード処理が継続している可能性があるため、手動でファイルを削除する必要があるかもしれません。",
"com_error_files_validation": "ファイルの検証中にエラーが発生しました。",
"com_error_google_tool_conflict": "外部ツールではGoogleの組み込みツールの使用はサポートされていません。組み込みツールまたは外部ツールのいずれかを無効にしてください。",
"com_error_heic_conversion": "HEIC画像をJPEGに変換できませんでした。画像を手動で変換するか、別の形式を使用してください。",
"com_error_input_length": "最新のメッセージトークン数が長すぎてトークン制限を超えているか、トークン制限パラメータの設定が間違っていてコンテキストウィンドウに悪影響を及ぼしています。詳細はこちら: {{0}}.メッセージを短くするか、会話パラメータから最大コンテキストサイズを調整するか、会話をフォークして続行してください。",
"com_error_invalid_agent_provider": "その {{0}} プロバイダーはエージェントでは使用できません。エージェントの設定で、現在利用可能なプロバイダを選択してください。",
"com_error_invalid_user_key": "無効なキーが提供されました。キーを入力して再試行してください。",
@@ -306,7 +283,6 @@
"com_files_table": "ここに何かを入れる必要があります。空でした",
"com_generated_files": "生成されたファイル:",
"com_hide_examples": "例を非表示",
"com_info_heic_converting": "HEIC画像をJPEGに変換...",
"com_nav_2fa": "二要素認証 (2FA)",
"com_nav_account_settings": "アカウント設定",
"com_nav_always_make_prod": "常に新しいバージョンを制作する",
@@ -324,26 +300,6 @@
"com_nav_auto_transcribe_audio": "オーディオを自動で書き起こす",
"com_nav_automatic_playback": "最新メッセージを自動再生",
"com_nav_balance": "バランス",
"com_nav_balance_auto_refill_error": "自動補充設定の読み込み中にエラーが発生しました。",
"com_nav_balance_auto_refill_settings": "自動補充設定",
"com_nav_balance_day": "日",
"com_nav_balance_days": "日数",
"com_nav_balance_every": "毎",
"com_nav_balance_hour": "時",
"com_nav_balance_hours": "時間",
"com_nav_balance_interval": "間隔:",
"com_nav_balance_last_refill": "最終補充:",
"com_nav_balance_minute": "分",
"com_nav_balance_minutes": "分",
"com_nav_balance_month": "月",
"com_nav_balance_months": "ヶ月",
"com_nav_balance_next_refill": "次の補充",
"com_nav_balance_next_refill_info": "前回の補充から指定された時間間隔が経過していること、プロンプトを送信すると残高がゼロ以下になること、この2つの条件が満たされた場合にのみ、次回の補充が自動的に行われます。",
"com_nav_balance_refill_amount": "補充量:",
"com_nav_balance_second": "秒",
"com_nav_balance_seconds": "秒",
"com_nav_balance_week": "週",
"com_nav_balance_weeks": "週間",
"com_nav_browser": "ブラウザ",
"com_nav_center_chat_input": "ようこそ画面の中央にチャット入力を配置",
"com_nav_change_picture": "画像を変更",
@@ -390,7 +346,6 @@
"com_nav_font_size_xs": "極小",
"com_nav_help_faq": "ヘルプ & FAQ",
"com_nav_hide_panel": "右側のパネルを非表示",
"com_nav_info_balance": "残高には、使用可能なトークン・クレジットの残数が表示されます。トークン・クレジットは金額に換算されます1000クレジット0.001米ドル)",
"com_nav_info_code_artifacts": "チャットの横に実験的なコード アーティファクトの表示を有効にします",
"com_nav_info_code_artifacts_agent": "このエージェントのコードアーティファクトの使用を有効にします。デフォルトでは、\"カスタムプロンプトモード\" が有効になっていない限り、アーティファクトの使用に特化した追加の指示が追加されます。",
"com_nav_info_custom_prompt_mode": "有効にすると、デフォルトのアーティファクト システム プロンプトは含まれません。このモードでは、アーティファクト生成指示をすべて手動で提供する必要があります。",
@@ -404,7 +359,6 @@
"com_nav_info_show_thinking": "有効にすると、チャットはデフォルトで思考ドロップダウンを開いて表示し、AIの推論をリアルタイムで見ることができます。無効にすると、思考ドロップダウンはデフォルトで閉じたままになり、よりすっきりとした合理的なインターフェイスになります。",
"com_nav_info_user_name_display": "有効になっている場合、送信者のユーザー名が送信するメッセージの上に表示されます。無効になっている場合、メッセージの上に「あなた」のみが表示されます。",
"com_nav_lang_arabic": "العربية",
"com_nav_lang_armenian": "ルール",
"com_nav_lang_auto": "自動検出",
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
"com_nav_lang_catalan": "カタルーニャ語",
@@ -424,7 +378,6 @@
"com_nav_lang_italian": "Italiano",
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_latvian": "ラトビスキー",
"com_nav_lang_persian": "ファラオ",
"com_nav_lang_polish": "Polski",
"com_nav_lang_portuguese": "Português",
@@ -434,15 +387,12 @@
"com_nav_lang_thai": "ไทย",
"com_nav_lang_traditional_chinese": "繁體中文",
"com_nav_lang_turkish": "Türkçe",
"com_nav_lang_uyghur": "ウラジオストク",
"com_nav_lang_vietnamese": "Tiếng Việt",
"com_nav_language": "言語",
"com_nav_latex_parsing": "メッセージ内の LaTeX の構文解析 (パフォーマンスに影響する可能性があります)",
"com_nav_log_out": "ログアウト",
"com_nav_long_audio_warning": "長いテキストの処理には時間がかかります。",
"com_nav_maximize_chat_space": "チャット画面を最大化",
"com_nav_mcp_vars_update_error": "MCP カスタムユーザ変数の更新エラー: {{0}}",
"com_nav_mcp_vars_updated": "MCP カスタムユーザー変数が正常に更新されました。",
"com_nav_modular_chat": "会話の途中でのエンドポイント切替を有効化",
"com_nav_my_files": "自分のファイル",
"com_nav_not_supported": "サポートされていません",
@@ -462,12 +412,10 @@
"com_nav_search_placeholder": "メッセージ検索",
"com_nav_send_message": "メッセージを送信する",
"com_nav_setting_account": "アカウント",
"com_nav_setting_balance": "残高",
"com_nav_setting_beta": "ベータ版の機能",
"com_nav_setting_chat": "チャット",
"com_nav_setting_data": "データ管理",
"com_nav_setting_general": "一般",
"com_nav_setting_mcp": "MCP設定",
"com_nav_setting_personalization": "パーソナライゼーション",
"com_nav_setting_speech": "スピーチ",
"com_nav_settings": "設定",
"com_nav_shared_links": "共有リンク",
@@ -500,15 +448,7 @@
"com_sidepanel_conversation_tags": "ブックマーク",
"com_sidepanel_hide_panel": "パネルを隠す",
"com_sidepanel_manage_files": "ファイルを管理",
"com_sidepanel_mcp_no_servers_with_vars": "設定可能な変数を持つMCPサーバーはありません。",
"com_sidepanel_mcp_variables_for": "{{0}}のMCP変数",
"com_sidepanel_parameters": "パラメータ",
"com_sources_image_alt": "検索結果画像",
"com_sources_more_sources": "+{{count}} ソース",
"com_sources_tab_all": "全て",
"com_sources_tab_images": "画像",
"com_sources_tab_news": "ニュース",
"com_sources_title": "出典",
"com_ui_2fa_account_security": "2要素認証はアカウントのセキュリティをさらに強化します",
"com_ui_2fa_disable": "2FAを無効にする",
"com_ui_2fa_disable_error": "2要素認証を無効にする際にエラーが発生しました",
@@ -520,13 +460,9 @@
"com_ui_2fa_setup": "二要素認証を設定",
"com_ui_2fa_verified": "2要素認証の認証に成功しました",
"com_ui_accept": "同意します",
"com_ui_action_button": "アクションボタン",
"com_ui_add": "追加",
"com_ui_add_mcp": "MCPの追加",
"com_ui_add_mcp_server": "MCPサーバーの追加",
"com_ui_add_model_preset": "追加の応答のためのモデルまたはプリセットを追加する",
"com_ui_add_multi_conversation": "複数のチャットを追加",
"com_ui_adding_details": "詳細を追加する",
"com_ui_admin": "管理者",
"com_ui_admin_access_warning": "管理者アクセスをこの機能で無効にすると、予期せぬUI上の問題が発生し、画面の再読み込みが必要になる場合があります。設定を保存した場合、元に戻すには librechat.yaml の設定ファイルを直接編集する必要があり、この変更はすべての権限に影響します。",
"com_ui_admin_settings": "管理者設定",
@@ -545,20 +481,6 @@
"com_ui_agent_recursion_limit_info": "エージェントが最終応答を返す前に実行できるステップ数を制限します。デフォルトは25ステップです。ステップとは、AI APIリクエストまたはツール使用ラウンドのいずれかです。例えば、基本的なツールインタラクションは、最初のリクエスト、ツール使用、そしてフォローアップリクエストの3ステップで構成されます。",
"com_ui_agent_shared_to_all": "ここに何かを入れる必要があります。空でした",
"com_ui_agent_var": "{{0}}エージェント",
"com_ui_agent_version": "バージョン",
"com_ui_agent_version_active": "アクティブバージョン",
"com_ui_agent_version_duplicate": "重複バージョンが検出されました。これにより、バージョン{{versionIndex}}と同一のバージョンが作成されます。",
"com_ui_agent_version_empty": "利用可能なバージョンはありません",
"com_ui_agent_version_error": "バージョン取得エラー",
"com_ui_agent_version_history": "バージョン履歴",
"com_ui_agent_version_no_agent": "エージェントが選択されていません。バージョン履歴を表示するには、エージェントを選択してください。",
"com_ui_agent_version_no_date": "日付は不明",
"com_ui_agent_version_restore": "復元する",
"com_ui_agent_version_restore_confirm": "本当にこのバージョンを復元しますか?",
"com_ui_agent_version_restore_error": "バージョンの復元に失敗しました",
"com_ui_agent_version_restore_success": "バージョンの復元に成功",
"com_ui_agent_version_title": "バージョン {{versionNumber}}",
"com_ui_agent_version_unknown_date": "日付不明",
"com_ui_agents": "エージェント",
"com_ui_agents_allow_create": "エージェントの作成を許可",
"com_ui_agents_allow_share_global": "全ユーザーとAgentsの共有を許可",
@@ -573,7 +495,6 @@
"com_ui_archive_error": "アーカイブに失敗しました。",
"com_ui_artifact_click": "クリックして開く",
"com_ui_artifacts": "アーティファクト",
"com_ui_artifacts_options": "アーティファクト・オプション",
"com_ui_artifacts_toggle": "アーティファクト UI の切替",
"com_ui_artifacts_toggle_agent": "アーティファクトを有効にする",
"com_ui_ascending": "昇順",
@@ -593,11 +514,8 @@
"com_ui_auth_url": "認証URL",
"com_ui_authentication": "認証",
"com_ui_authentication_type": "認証タイプ",
"com_ui_auto": "自動",
"com_ui_available_tools": "利用可能なツール",
"com_ui_avatar": "アバター",
"com_ui_azure": "Azure",
"com_ui_back": "戻る",
"com_ui_back_to_chat": "チャットに戻る",
"com_ui_back_to_prompts": "プロンプトに戻る",
"com_ui_backup_codes": "バックアップコード",
@@ -627,7 +545,6 @@
"com_ui_bulk_delete_error": "共有リンクの削除に失敗しました",
"com_ui_callback_url": "コールバックURL",
"com_ui_cancel": "キャンセル",
"com_ui_cancelled": "キャンセル",
"com_ui_category": "カテゴリ",
"com_ui_chat": "チャット",
"com_ui_chat_history": "チャット履歴",
@@ -637,14 +554,11 @@
"com_ui_client_secret": "クライアントシークレット",
"com_ui_close": "閉じる",
"com_ui_close_menu": "メニューを閉じる",
"com_ui_close_window": "ウィンドウを閉じる",
"com_ui_code": "コード",
"com_ui_collapse_chat": "チャットを折りたたむ",
"com_ui_command_placeholder": "オプション:プロンプトのコマンドまたは名前を入力",
"com_ui_command_usage_placeholder": "コマンドまたは名前でプロンプトを選択してください",
"com_ui_complete_setup": "セットアップ完了",
"com_ui_concise": "簡潔",
"com_ui_configure_mcp_variables_for": "{{0}}の変数を設定",
"com_ui_confirm_action": "実行する",
"com_ui_confirm_admin_use_change": "この設定を変更すると、あなた自身を含む管理者のアクセスがブロックされます。本当によろしいですか?",
"com_ui_confirm_change": "変更の確認",
@@ -659,10 +573,7 @@
"com_ui_copy_to_clipboard": "クリップボードへコピー",
"com_ui_create": "作成",
"com_ui_create_link": "リンクを作成する",
"com_ui_create_memory": "メモリを作成します",
"com_ui_create_prompt": "プロンプトを作成する",
"com_ui_creating_image": "画像を作成しています。しばらく時間がかかる場合があります",
"com_ui_current": "現在",
"com_ui_currently_production": "現在生産中",
"com_ui_custom": "カスタム",
"com_ui_custom_header_name": "カスタムヘッダー名",
@@ -695,24 +606,13 @@
"com_ui_delete_confirm": "このチャットは削除されます。",
"com_ui_delete_confirm_prompt_version_var": "これは、選択されたバージョンを \"{{0}}.\" から削除します。他のバージョンが存在しない場合、プロンプトが削除されます。",
"com_ui_delete_conversation": "チャットを削除しますか?",
"com_ui_delete_mcp": "MCPの削除",
"com_ui_delete_mcp_confirm": "この MCP サーバーを削除してもよろしいですか?",
"com_ui_delete_mcp_error": "MCPサーバーの削除に失敗しました",
"com_ui_delete_mcp_success": "MCPサーバーの削除に成功",
"com_ui_delete_memory": "メモリの削除",
"com_ui_delete_not_allowed": "削除操作は許可されていません",
"com_ui_delete_prompt": "プロンプトを消しますか?",
"com_ui_delete_shared_link": "共有リンクを削除しますか?",
"com_ui_delete_success": "削除に成功",
"com_ui_delete_tool": "ツールを削除",
"com_ui_delete_tool_confirm": "このツールを削除してもよろしいですか?",
"com_ui_deleted": "削除済み",
"com_ui_deleting_file": "ファイルの削除...",
"com_ui_descending": "降順",
"com_ui_description": "説明",
"com_ui_description_placeholder": "オプション:プロンプトを表示するときの説明を入力",
"com_ui_deselect_all": "すべて選択解除",
"com_ui_detailed": "詳細",
"com_ui_disabling": "無効化...",
"com_ui_download": "ダウンロード",
"com_ui_download_artifact": "アーティファクトをダウンロード",
@@ -727,46 +627,21 @@
"com_ui_duplication_processing": "会話を複製中...",
"com_ui_duplication_success": "会話の複製が完了しました",
"com_ui_edit": "編集",
"com_ui_edit_editing_image": "画像編集",
"com_ui_edit_mcp_server": "MCPサーバーの編集",
"com_ui_edit_memory": "メモリ編集",
"com_ui_empty_category": "-",
"com_ui_endpoint": "エンドポイント",
"com_ui_endpoint_menu": "LLMエンドポイントメニュー",
"com_ui_enter": "入力",
"com_ui_enter_api_key": "APIキーを入力",
"com_ui_enter_key": "キーを入力",
"com_ui_enter_openapi_schema": "OpenAPIスキーマを入力してください",
"com_ui_enter_value": "値を入力",
"com_ui_error": "エラー",
"com_ui_error_connection": "サーバーへの接続中にエラーが発生しました。ページを更新してください。",
"com_ui_error_save_admin_settings": "管理者設定の保存にエラーが発生しました。",
"com_ui_error_updating_preferences": "環境設定の更新エラー",
"com_ui_examples": "例",
"com_ui_expand_chat": "チャットを展開",
"com_ui_export_convo_modal": "エクスポート",
"com_ui_feedback_more": "もっと...",
"com_ui_feedback_more_information": "追加フィードバックの提供",
"com_ui_feedback_negative": "改善が必要",
"com_ui_feedback_placeholder": "その他、ご意見・ご感想がございましたら、こちらにご記入ください。",
"com_ui_feedback_positive": "スキ!",
"com_ui_feedback_tag_accurate_reliable": "正確で信頼できる",
"com_ui_feedback_tag_attention_to_detail": "細部へのこだわり",
"com_ui_feedback_tag_bad_style": "スタイルや口調が悪い",
"com_ui_feedback_tag_clear_well_written": "わかりやすく、よく書けている",
"com_ui_feedback_tag_creative_solution": "創造的な解決策",
"com_ui_feedback_tag_inaccurate": "不正確または間違った回答",
"com_ui_feedback_tag_missing_image": "期待されるイメージ",
"com_ui_feedback_tag_not_helpful": "有益な情報に欠ける",
"com_ui_feedback_tag_not_matched": "私の要求と一致しませんでした",
"com_ui_feedback_tag_other": "その他の問題",
"com_ui_feedback_tag_unjustified_refusal": "理由もなく拒否された",
"com_ui_field_required": "必須入力項目です",
"com_ui_file_size": "ファイルサイズ",
"com_ui_files": "ファイル",
"com_ui_filter_prompts": "フィルタープロンプト",
"com_ui_filter_prompts_name": "名前でプロンプトをフィルタ",
"com_ui_final_touch": "最後の仕上げ",
"com_ui_finance": "財務",
"com_ui_fork": "分岐",
"com_ui_fork_all_target": "すべてを対象に含める",
@@ -774,7 +649,6 @@
"com_ui_fork_change_default": "デフォルトの分岐オプション",
"com_ui_fork_default": "デフォルトの分岐オプションを使用する",
"com_ui_fork_error": "会話を分岐できませんでした。エラーが発生しました。",
"com_ui_fork_error_rate_limit": "フォーク要求が多すぎます。後で再試行してください。",
"com_ui_fork_from_message": "分岐オプションを選択する",
"com_ui_fork_info_1": "この設定を使うと、希望の動作でメッセージを分岐させることができます。",
"com_ui_fork_info_2": "「分岐」とは、現在の会話から特定のメッセージを開始/終了点として新しい会話を作成し、選択したオプションに従ってコピーを作成することを指します。",
@@ -797,8 +671,6 @@
"com_ui_generate_backup": "バックアップコードを生成する",
"com_ui_generate_qrcode": "QRコードを生成する",
"com_ui_generating": "生成中...",
"com_ui_generation_settings": "生成設定",
"com_ui_getting_started": "はじめに",
"com_ui_global_group": "ここに何かを入れる必要があります。空でした",
"com_ui_go_back": "戻る",
"com_ui_go_to_conversation": "会話に移動する",
@@ -806,16 +678,9 @@
"com_ui_good_evening": "こんばんは",
"com_ui_good_morning": "おはよう",
"com_ui_happy_birthday": "初めての誕生日です!",
"com_ui_hide_image_details": "画像の詳細を隠す",
"com_ui_hide_password": "パスワードを隠す",
"com_ui_hide_qr": "QRコードを非表示にする",
"com_ui_high": "高い",
"com_ui_host": "ホスト",
"com_ui_icon": "アイコン",
"com_ui_idea": "アイデア",
"com_ui_image_created": "画像を作成しました",
"com_ui_image_details": "画像詳細",
"com_ui_image_edited": "画像編集済み",
"com_ui_image_gen": "画像生成",
"com_ui_import": "読み込む",
"com_ui_import_conversation_error": "会話のインポート時にエラーが発生しました",
@@ -825,7 +690,6 @@
"com_ui_include_shadcnui": "shadcn/uiコンポーネントの指示を含める",
"com_ui_input": "入力",
"com_ui_instructions": "指示文",
"com_ui_key": "キー",
"com_ui_late_night": "遅い夜を楽しんで",
"com_ui_latest_footer": "Every AI for Everyone.",
"com_ui_latest_production_version": "最新の製品バージョン",
@@ -836,34 +700,9 @@
"com_ui_loading": "読み込み中...",
"com_ui_locked": "ロック",
"com_ui_logo": "{{0}}のロゴ",
"com_ui_low": "低い",
"com_ui_manage": "管理",
"com_ui_max_tags": "最新の値を使用した場合、許可される最大数は {{0}} です。",
"com_ui_mcp_dialog_desc": "以下に必要事項を入力してください。",
"com_ui_mcp_enter_var": "{{0}}の値を入力する。",
"com_ui_mcp_server_not_found": "サーバーが見つかりません。",
"com_ui_mcp_servers": "MCP サーバー",
"com_ui_mcp_url": "MCPサーバーURL",
"com_ui_medium": "中",
"com_ui_memories": "メモリ",
"com_ui_memories_allow_create": "メモリの作成を許可する",
"com_ui_memories_allow_opt_out": "ユーザーがメモリをオプトアウトできるようにする",
"com_ui_memories_allow_read": "メモリを読む",
"com_ui_memories_allow_update": "メモリの更新を許可する",
"com_ui_memories_allow_use": "メモリの使用を許可する",
"com_ui_memories_filter": "メモリをフィルタリング...",
"com_ui_memory": "メモリ",
"com_ui_memory_already_exceeded": "メモリ容量がすでにいっぱいです - 超過 {{tokens}} トークン。新しいメモリを追加する前に、既存のメモリを削除してください。",
"com_ui_memory_created": "メモリの作成に成功",
"com_ui_memory_deleted": "メモリが削除されました",
"com_ui_memory_deleted_items": "削除されたメモリ",
"com_ui_memory_error": "メモリエラー",
"com_ui_memory_key_exists": "このキーを持つメモリはすでに存在します。別のキーを使用してください。",
"com_ui_memory_key_validation": "メモリー・キーには小文字とアンダースコアのみを使用する。",
"com_ui_memory_storage_full": "メモリストレージがいっぱいです",
"com_ui_memory_updated": "保存されたメモリを更新しました",
"com_ui_memory_updated_items": "更新されたメモリ",
"com_ui_memory_would_exceed": "保存できません - 制限を超えています {{tokens}} トークン。既存のメモリを削除してスペースを確保します。",
"com_ui_mention": "エンドポイント、アシスタント、またはプリセットを素早く切り替えるには、それらを言及してください。",
"com_ui_min_tags": "これ以上の値を削除できません。少なくとも {{0}} が必要です。",
"com_ui_misc": "その他",
@@ -882,30 +721,17 @@
"com_ui_no_category": "カテゴリなし",
"com_ui_no_changes": "更新する変更はありません",
"com_ui_no_data": "ここに何かを入れる必要があります。空でした",
"com_ui_no_personalization_available": "現在、パーソナライズオプションはありません",
"com_ui_no_read_access": "メモリを見る権限がありません",
"com_ui_no_terms_content": "表示する利用規約の内容はありません",
"com_ui_no_valid_items": "ここに何かを入れる必要があります。空でした",
"com_ui_none": "なし",
"com_ui_not_used": "未使用",
"com_ui_nothing_found": "該当するものが見つかりませんでした",
"com_ui_oauth": "OAuth",
"com_ui_oauth_connected_to": "Connected to",
"com_ui_oauth_error_callback_failed": "認証コールバックに失敗しました。再試行してください。",
"com_ui_oauth_error_generic": "認証に失敗しました。もう一度お試しください。",
"com_ui_oauth_error_invalid_state": "無効な状態パラメータです。再試行してください。",
"com_ui_oauth_error_missing_code": "認証コードがありません。もう一度お試しください。",
"com_ui_oauth_error_missing_state": "Stateパラメータがありません。再試行してください。",
"com_ui_oauth_error_title": "認証に失敗しました",
"com_ui_oauth_success_description": "認証が完了しました。このウィンドウはあとで閉じます",
"com_ui_oauth_success_title": "認証成功",
"com_ui_of": "of",
"com_ui_off": "オフ",
"com_ui_on": "オン",
"com_ui_openai": "OpenAI",
"com_ui_optional": "(任意)",
"com_ui_page": "ページ",
"com_ui_preferences_updated": "環境設定が正常に更新されました",
"com_ui_prev": "前",
"com_ui_preview": "プレビュー",
"com_ui_privacy_policy": "プライバシーポリシー",
@@ -923,11 +749,8 @@
"com_ui_prompts_allow_share_global": "全ユーザーとプロンプトを共有することを許可",
"com_ui_prompts_allow_use": "プロンプトの使用を許可",
"com_ui_provider": "プロバイダ",
"com_ui_quality": "品質",
"com_ui_read_aloud": "読み上げる",
"com_ui_redirecting_to_provider": "{{0}}にリダイレクト、 お待ちください...",
"com_ui_reference_saved_memories": "保存されたメモリを参照",
"com_ui_reference_saved_memories_description": "アシスタントが応答する際に、保存したメモリを参照し、使用できるようにする。",
"com_ui_refresh_link": "リンクの更新",
"com_ui_regenerate": "再度 生成する",
"com_ui_regenerate_backup": "バックアップコードの再生成",
@@ -939,7 +762,6 @@
"com_ui_rename_prompt": "プロンプトの名前を変更します",
"com_ui_requires_auth": "認証が必要です",
"com_ui_reset_var": "{{0}}をリセット",
"com_ui_reset_zoom": "ズームをリセット",
"com_ui_result": "結果",
"com_ui_revoke": "無効にする",
"com_ui_revoke_info": "ユーザへ発行した認証情報をすべて無効にする。",
@@ -955,14 +777,11 @@
"com_ui_save_badge_changes": "バッジの変更を保存しますか?",
"com_ui_save_submit": "保存 & 送信",
"com_ui_saved": "保存しました!",
"com_ui_saving": "保存中...",
"com_ui_schema": "スキーマ",
"com_ui_scope": "範囲",
"com_ui_search": "検索",
"com_ui_seconds": "秒",
"com_ui_secret_key": "秘密鍵",
"com_ui_select": "選択",
"com_ui_select_all": "すべて選択",
"com_ui_select_file": "ファイルを選択",
"com_ui_select_model": "モデル選択",
"com_ui_select_provider": "プロバイダーを選択してください",
@@ -988,8 +807,6 @@
"com_ui_shop": "買い物",
"com_ui_show": "表示",
"com_ui_show_all": "すべて表示",
"com_ui_show_image_details": "画像の詳細を表示",
"com_ui_show_password": "パスワードを表示する",
"com_ui_show_qr": "QR コードを表示",
"com_ui_sign_in_to_domain": "{{0}}にサインインする",
"com_ui_simple": "シンプル",
@@ -1011,23 +828,15 @@
"com_ui_terms_of_service": "利用規約",
"com_ui_thinking": "考え中...",
"com_ui_thoughts": "推論",
"com_ui_token": "トークン",
"com_ui_token_exchange_method": "トークン交換方法",
"com_ui_token_url": "トークンURL",
"com_ui_tokens": "トークン",
"com_ui_tool_collection_prefix": "ツールのコレクション",
"com_ui_tool_info": "ツール情報",
"com_ui_tool_more_info": "このツールの詳細",
"com_ui_tools": "ツール",
"com_ui_travel": "旅行",
"com_ui_trust_app": "このアプリケーションを信頼している",
"com_ui_unarchive": "アーカイブ解除",
"com_ui_unarchive_error": "アーカイブ解除に失敗しました。",
"com_ui_unknown": "不明",
"com_ui_untitled": "無題",
"com_ui_update": "更新",
"com_ui_update_mcp_error": "MCPの作成または更新にエラーが発生しました。",
"com_ui_update_mcp_success": "MCPの作成または更新に成功",
"com_ui_upload": "アップロード",
"com_ui_upload_code_files": "コードインタープリター用にアップロード",
"com_ui_upload_delay": "ファイル \"{{0}}\"のアップロードに時間がかかっています。ファイルの検索のためのインデックス作成が完了するまでお待ちください。",
@@ -1042,43 +851,17 @@
"com_ui_upload_ocr_text": "テキストとしてアップロード",
"com_ui_upload_success": "アップロード成功",
"com_ui_upload_type": "アップロード種別を選択",
"com_ui_usage": "使用方法",
"com_ui_use_2fa_code": "代わりに2FAコードを使用する",
"com_ui_use_backup_code": "代わりにバックアップコードを使用する",
"com_ui_use_memory": "メモリを使用する",
"com_ui_use_micrphone": "マイクを使用する",
"com_ui_use_prompt": "プロンプトの利用",
"com_ui_used": "使用済み",
"com_ui_value": "値",
"com_ui_variables": "変数",
"com_ui_variables_info": "テキスト内で二重中括弧を使用して変数を定義します。例えば、`{{example variable}}`のようにすると、プロンプトを使用するときに後で値を埋め込むことができます。",
"com_ui_verify": "確認する",
"com_ui_version_var": "バージョン {{0}}",
"com_ui_versions": "バージョン",
"com_ui_view_memory": "メモリを表示",
"com_ui_view_source": "ソースチャットを表示",
"com_ui_web_search": "ウェブ検索",
"com_ui_web_search_cohere_key": "Cohere API キーの入力",
"com_ui_web_search_firecrawl_url": "Firecrawl API URL (オプション)",
"com_ui_web_search_jina_key": "Jina APIキーを入力",
"com_ui_web_search_processing": "処理結果",
"com_ui_web_search_provider": "検索プロバイダー",
"com_ui_web_search_provider_searxng": "SearXNG",
"com_ui_web_search_provider_serper": "Serper API",
"com_ui_web_search_provider_serper_key": "Serper APIキーを取得する",
"com_ui_web_search_reading": "読み取り結果",
"com_ui_web_search_reranker": "Reranker",
"com_ui_web_search_reranker_cohere": "Cohere",
"com_ui_web_search_reranker_cohere_key": "Cohere API キーを取得する",
"com_ui_web_search_reranker_jina": "Jina AI",
"com_ui_web_search_reranker_jina_key": "Jina APIキーを取得する",
"com_ui_web_search_scraper": "スクレーパー",
"com_ui_web_search_scraper_firecrawl": "Firecrawl API",
"com_ui_web_search_scraper_firecrawl_key": "FirecrawlのAPIキーを取得する",
"com_ui_web_search_searxng_api_key": "SearXNG APIキーを入力オプション",
"com_ui_web_search_searxng_instance_url": "SearXNGインスタンスURL",
"com_ui_web_searching": "ウェブ検索",
"com_ui_web_searching_again": "もう一度ウェブを検索します",
"com_ui_weekend_morning": "楽しい週末を",
"com_ui_write": "執筆",
"com_ui_x_selected": "{{0}}が選択された",

View File

@@ -421,6 +421,7 @@
"com_nav_send_message": "메시지 보내기",
"com_nav_setting_account": "계정",
"com_nav_setting_balance": "잔액",
"com_nav_setting_beta": "베타 기능",
"com_nav_setting_chat": "채팅",
"com_nav_setting_data": "데이터 제어",
"com_nav_setting_general": "일반",

View File

@@ -463,6 +463,7 @@
"com_nav_send_message": "Sūtīt ziņu",
"com_nav_setting_account": "Konts",
"com_nav_setting_balance": "Bilance",
"com_nav_setting_beta": "Beta eksperimentālās funkcijas",
"com_nav_setting_chat": "Saruna",
"com_nav_setting_data": "Datu kontrole",
"com_nav_setting_general": "Vispārīgi",
@@ -500,6 +501,7 @@
"com_sidepanel_conversation_tags": "Grāmatzīmes",
"com_sidepanel_hide_panel": "Slēpt paneli",
"com_sidepanel_manage_files": "Pārvaldīt failus",
"com_sidepanel_mcp_enter_value": "Ievadiet vērtību {{0}}",
"com_sidepanel_mcp_no_servers_with_vars": "Nav MCP serveru ar konfigurējamiem mainīgajiem.",
"com_sidepanel_mcp_variables_for": "MCP parametri {{0}}",
"com_sidepanel_parameters": "Parametri",

View File

@@ -371,6 +371,7 @@
"com_nav_search_placeholder": "Szukaj wiadomości",
"com_nav_send_message": "Wyślij wiadomość",
"com_nav_setting_account": "Konto",
"com_nav_setting_beta": "Funkcje beta",
"com_nav_setting_chat": "Czat",
"com_nav_setting_data": "Kontrola danych",
"com_nav_setting_general": "Ogólne",

View File

@@ -409,6 +409,7 @@
"com_nav_search_placeholder": "Buscar mensagens",
"com_nav_send_message": "Enviar mensagem",
"com_nav_setting_account": "Conta",
"com_nav_setting_beta": "Recursos beta",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Controles de dados",
"com_nav_setting_general": "Geral",

View File

@@ -411,6 +411,7 @@
"com_nav_search_placeholder": "Buscar mensagens",
"com_nav_send_message": "Enviar mensagem",
"com_nav_setting_account": "Conta",
"com_nav_setting_beta": "Recursos beta",
"com_nav_setting_chat": "Chat",
"com_nav_setting_data": "Controles de dados",
"com_nav_setting_general": "Geral",

View File

@@ -408,6 +408,7 @@
"com_nav_search_placeholder": "Поиск сообщений",
"com_nav_send_message": "Отправить сообщение",
"com_nav_setting_account": "Аккаунт",
"com_nav_setting_beta": "Бета-функции",
"com_nav_setting_chat": "Чат",
"com_nav_setting_data": "Управление данными",
"com_nav_setting_general": "Общие",

View File

@@ -1,5 +1,5 @@
{
"chat_direction_left_to_right": "Något måste fyllas i här, information saknas.",
"chat_direction_left_to_right": "något måste fyllas i här, saknades",
"chat_direction_right_to_left": "något måste fyllas i här. saknades",
"com_a11y_ai_composing": "AI:n komponerar fortfarande.",
"com_a11y_end": "AI har avslutat sitt svar.",
@@ -7,40 +7,40 @@
"com_agents_allow_editing": "Tillåt andra användare att redigera din agent",
"com_agents_by_librechat": "av LibreChat",
"com_agents_code_interpreter": "När den är aktiverad kan din agent utnyttja LibreChat API för kodtolkning för att köra genererad kod, inklusive filbehandling, på ett säkert sätt. Kräver en giltig API-nyckel.",
"com_agents_code_interpreter_title": "Kodtolk för API ",
"com_agents_code_interpreter_title": "API för kodtolkning",
"com_agents_create_error": "Det uppstod ett fel vid skapandet av din agent.",
"com_agents_description_placeholder": "Valfritt: Beskriv din agent här",
"com_agents_enable_file_search": "Aktivera filsökning",
"com_agents_file_context": "Filkontext (OCR)",
"com_agents_file_context_disabled": "Agent måste skapas innan filer laddas upp för filkontext.",
"com_agents_file_context_info": "Filer som laddas upp som kontext bearbetas med OCR för att extrahera text, som sedan läggs till i agentens instruktioner. Lämpligt för dokument, bilder med text eller PDF-filer där du behöver hela textinnehållet i en fil",
"com_agents_file_search_disabled": "Agenten måste skapas innan du laddar upp filer.",
"com_agents_file_search_info": "När detta är aktiverat kommer agenten se och hämta relevant information från filnamnen nedan. ",
"com_agents_instructions_placeholder": "De systeminstruktioner som agenten använder.",
"com_agents_mcp_description_placeholder": "Förklara vad den gör med några få ord.",
"com_agents_mcp_icon_size": "Minsta storlek är 128 x 128 px.",
"com_agents_mcp_info": "Lägg till MCP-servrar för att din agent ska kunna utföra uppgifter i och interagera med externa tjänster.",
"com_agents_file_search_disabled": "Agenten måste skapas innan du laddar upp filer för filsök",
"com_agents_file_search_info": "När detta är aktiverat kommer agenten se exakta filnamn som anges nedan för att hämta relevant information från dessa filer.",
"com_agents_instructions_placeholder": "De systeminstruktioner som agenten använder",
"com_agents_mcp_description_placeholder": "Förklara vad den gör med några få ord",
"com_agents_mcp_icon_size": "Minsta storlek 128 x 128 px",
"com_agents_mcp_info": "Lägg till MCP-servrar till din agent så att den kan utföra uppgifter och interagera med externa tjänster",
"com_agents_mcp_name_placeholder": "Anpassat verktyg",
"com_agents_mcp_trust_subtext": "Anpassade anslutningar verifieras inte av LibreChat.",
"com_agents_mcps_disabled": "Du måste skapa en agent innan du lägger till MCP:ar.",
"com_agents_mcp_trust_subtext": "Anpassade anslutningar verifieras inte av LibreChat",
"com_agents_mcps_disabled": "Du måste skapa en agent innan du lägger till MCP:er.",
"com_agents_missing_provider_model": "Välj leverantör och modell innan du skapar en agent.",
"com_agents_name_placeholder": "Valfritt: Namn på agenten",
"com_agents_no_access": "Du har inte behörighet att redigera den här agenten.",
"com_agents_no_access": "Du har inte tillgång till att redigera denna agent.",
"com_agents_no_agent_id_error": "Inget agent-ID hittades. Se till att agenten skapas först.",
"com_agents_not_available": "Agenten är inte tillgänglig.",
"com_agents_search_info": "När detta är aktiverat kan din agent söka på webben efter aktuell information. En giltig API-nyckel krävs. ",
"com_agents_search_name": "Sök agenter efter namn",
"com_agents_update_error": "Det uppstod ett fel när din agent uppdaterades.",
"com_agents_not_available": "Agenten är ej tillgänglig",
"com_agents_search_info": "När detta är aktiverat kan din agent söka på webben efter aktuell information. Kräver en giltig API-nyckel.",
"com_agents_search_name": "Sök efter agenter med namn",
"com_agents_update_error": "Det uppstod ett fel vid uppdateringen av din agent.",
"com_assistants_action_attempt": "Assistenten vill prata med {{0}}",
"com_assistants_actions": "Åtgärder",
"com_assistants_actions_disabled": "Du behöver skapa en assistent innan du kan lägga till åtgärder.",
"com_assistants_actions_disabled": "Du måste skapa en assistent innan du kan lägga till åtgärder.",
"com_assistants_actions_info": "Låt din assistent hämta information eller vidta åtgärder via API:er",
"com_assistants_add_actions": "Lägg till åtgärder",
"com_assistants_add_tools": "Lägg till verktyg",
"com_assistants_allow_sites_you_trust": "Tillåt bara webbplatser du litar på.",
"com_assistants_append_date": "Lägg till aktuellt datum och tid",
"com_assistants_append_date_tooltip": "När detta är aktiverat kommer aktuellt datum och tid hos klienten läggas till i instruktionerna för hjälpsystemet.",
"com_assistants_attempt_info": "Assistenten vill skicka följande:",
"com_assistants_attempt_info": "Assistent vill skicka följande:",
"com_assistants_available_actions": "Tillgängliga åtgärder",
"com_assistants_capabilities": "Förmågor",
"com_assistants_code_interpreter": "Kodtolkare",
@@ -61,7 +61,7 @@
"com_assistants_function_use": "Assistent använde {{0}}",
"com_assistants_instructions_placeholder": "Systeminstruktioner som assistent ska använda",
"com_assistants_knowledge": "Kunskap",
"com_assistants_max_starters_reached": "Max antal konversationsstartare har uppnåtts.",
"com_assistants_max_starters_reached": "Max antal konversationsstartare uppnått",
"com_assistants_name_placeholder": "Valfritt: Assistentens namn",
"com_assistants_non_retrieval_model": "Filsökning är inte aktiverad på den här modellen. Vänligen välj en annan modell.",
"com_assistants_retrieval": "Återvinning",
@@ -100,7 +100,6 @@
"com_auth_error_create": "Det uppstod ett fel när du försökte registrera ditt konto. Vänligen försök igen.",
"com_auth_error_invalid_reset_token": "Detta lösenordsåterställningsnyckel är inte längre giltigt.",
"com_auth_error_login": "Kunde inte logga in med den angivna informationen. Kontrollera dina uppgifter och försök igen.",
"com_auth_error_login_ban": "Ditt konto har tillfälligt stängts av på grund av överträdelser av vår tjänst.",
"com_auth_error_login_rl": "För många inloggningsförsök från den här IP-adressen på kort tid. Vänligen försök igen senare.",
"com_auth_error_login_server": "Det uppstod ett internt serverfel. Vänligen vänta en stund och försök igen.",
"com_auth_facebook_login": "Logga in med Facebook",
@@ -156,7 +155,6 @@
"com_endpoint_completion": "Komplettering",
"com_endpoint_completion_model": "Kompletteringsmodell (Rekommenderad: GPT-4)",
"com_endpoint_config_click_here": "Klicka här",
"com_endpoint_config_google_gemini_api": "(Gemini API)",
"com_endpoint_config_key": "Ange API-nyckel",
"com_endpoint_config_key_encryption": "Din nyckel kommer att krypteras och raderas vid",
"com_endpoint_config_key_for": "Ange API-nyckel för",
@@ -232,53 +230,16 @@
"com_endpoint_thinking": "Tänkande",
"com_endpoint_top_k": "Top K",
"com_endpoint_top_p": "Top P",
"com_error_files_empty": "Tomma filer är inte tillåtna",
"com_error_files_process": "Ett fel inträffade under bearbetningen av filen.",
"com_error_no_base_url": "Ingen bas-URL hittades. Vänligen ange en URL och försök igen.",
"com_error_no_user_key": "Ingen nyckel hittades. Vänligen ange en nyckel och försök igen.",
"com_files_filter": "Filtrera filer...",
"com_files_no_results": "Inga resultat.",
"com_files_number_selected": "{{0}} av {{1}} valda artiklar",
"com_generated_files": "Genererade filer:",
"com_hide_examples": "Dölj exempel",
"com_info_heic_converting": "Konverterar HEIC-bild till JPEG...",
"com_nav_2fa": "Tvåfaktorsautentisering (2FA)",
"com_nav_account_settings": "Kontoinställningar",
"com_nav_archive_created_at": "Skapad",
"com_nav_archive_name": "Namn",
"com_nav_archived_chats": "Arkiverade chattar",
"com_nav_at_command": "@-Kommando",
"com_nav_balance": "Balans",
"com_nav_balance_day": "dag",
"com_nav_balance_days": "dagar",
"com_nav_balance_every": "Varje",
"com_nav_balance_hour": "timme",
"com_nav_balance_hours": "timmar",
"com_nav_balance_interval": "Intervall:",
"com_nav_balance_minute": "minut",
"com_nav_balance_minutes": "minuter",
"com_nav_balance_month": "månad",
"com_nav_balance_months": "månader",
"com_nav_balance_second": "sekund",
"com_nav_balance_seconds": "sekunder",
"com_nav_balance_week": "vecka",
"com_nav_balance_weeks": "veckor",
"com_nav_browser": "Webbläsare",
"com_nav_change_picture": "Ändra bild",
"com_nav_chat_commands": "Chattkommandon",
"com_nav_chat_direction": "Chattriktning",
"com_nav_clear_all_chats": "Rensa alla chattar",
"com_nav_clear_conversation": "Rensa konversationer",
"com_nav_clear_conversation_confirm_message": "Är du säker på att du vill rensa alla konversationer? Detta går inte att ångra.",
"com_nav_close_sidebar": "Stäng sidofält",
"com_nav_commands": "Kommandon",
"com_nav_confirm_clear": "Bekräfta rensning",
"com_nav_conversation_mode": "Konversationsläge",
"com_nav_convo_menu_options": "Konversationsmenyalternativ",
"com_nav_db_sensitivity": "Decibelkänslighet",
"com_nav_delete_account": "Radera konto",
"com_nav_enabled": "Aktiverad",
"com_nav_engine": "Motor",
"com_nav_export": "Exportera",
"com_nav_export_all_message_branches": "Exportera alla grenar för meddelanden",
"com_nav_export_conversation": "Exportera konversation",
@@ -288,7 +249,6 @@
"com_nav_export_recursive": "Rekursiv",
"com_nav_export_recursive_or_sequential": "Rekursiv eller sekventiell?",
"com_nav_export_type": "Typ",
"com_nav_external": "Extern",
"com_nav_font_size": "Textstorlek",
"com_nav_help_faq": "Hjälp & Vanliga frågor",
"com_nav_lang_arabic": "العربية",
@@ -318,45 +278,23 @@
"com_nav_lang_vietnamese": "Tiếng Việt",
"com_nav_language": "Språk",
"com_nav_log_out": "Logga ut",
"com_nav_my_files": "Mina filer",
"com_nav_not_supported": "Stöds ej",
"com_nav_open_sidebar": "Öppna sidofält",
"com_nav_plugin_auth_error": "Det uppstod ett fel när försöket att autentisera denna plugin gjordes. Försök igen.",
"com_nav_plugin_install": "Installera",
"com_nav_plugin_search": "Sök efter plugins",
"com_nav_plugin_store": "Pluginbutik",
"com_nav_plugin_uninstall": "Avinstallera",
"com_nav_plus_command": "+-Kommando",
"com_nav_profile_picture": "Profilbild",
"com_nav_search_placeholder": "Sök meddelanden",
"com_nav_send_message": "Skicka meddelande",
"com_nav_setting_account": "Konto",
"com_nav_setting_chat": "Chatt",
"com_nav_setting_data": "Datakontroller",
"com_nav_setting_general": "Allmänt",
"com_nav_settings": "Inställningar",
"com_nav_shared_links": "Delade länkar",
"com_nav_speech_to_text": "Tal till text",
"com_nav_text_to_speech": "Text till tal",
"com_nav_theme": "Tema",
"com_nav_theme_dark": "Mörkt",
"com_nav_theme_light": "Ljust",
"com_nav_theme_system": "System",
"com_nav_tool_remove": "Ta bort",
"com_nav_tool_search": "Sökverktyg",
"com_nav_user": "ANVÄNDARE",
"com_nav_voice_select": "Röst",
"com_sidepanel_attach_files": "Bifoga filer",
"com_sidepanel_conversation_tags": "Bokmärken",
"com_sidepanel_hide_panel": "Dölj panelen",
"com_sidepanel_manage_files": "Hantera filer",
"com_sidepanel_parameters": "Parametrar",
"com_sources_tab_all": "Alla",
"com_sources_tab_images": "Bilder",
"com_sources_tab_news": "Nyheter",
"com_sources_title": "Källor",
"com_ui_accept": "Jag accepterar",
"com_ui_add": "Lägg till",
"com_ui_agents": "Agenter",
"com_ui_agents_allow_create": "Tillåt att skapa agenter",
"com_ui_agents_allow_use": "Tillåt användning av agenter",

View File

@@ -389,6 +389,7 @@
"com_nav_search_placeholder": "ค้นหาข้อความ",
"com_nav_send_message": "ส่งข้อความ",
"com_nav_setting_account": "บัญชี",
"com_nav_setting_beta": "คุณสมบัติเบต้า",
"com_nav_setting_chat": "แชท",
"com_nav_setting_data": "การควบคุมข้อมูล",
"com_nav_setting_general": "ทั่วไป",

View File

@@ -377,6 +377,7 @@
"com_nav_search_placeholder": "Mesajları ara",
"com_nav_send_message": "Mesajı gönder",
"com_nav_setting_account": "Hesap",
"com_nav_setting_beta": "Beta özellikleri",
"com_nav_setting_chat": "Sohbet",
"com_nav_setting_data": "Veri kontrolleri",
"com_nav_setting_general": "Genel",

View File

@@ -1,36 +1,29 @@
{
"chat_direction_left_to_right": "这里需要放点东西,当前是空的",
"chat_direction_right_to_left": "这里需要放点东西,当前是空的",
"chat_direction_left_to_right": "something needs to go here. was empty",
"chat_direction_right_to_left": "something needs to go here. was empty",
"com_a11y_ai_composing": "AI 仍在撰写中。",
"com_a11y_end": "AI 已完成回复。",
"com_a11y_start": "AI 已开始回复。",
"com_agents_allow_editing": "允许其他用户编辑您的智能体",
"com_agents_allow_editing": "允许其他用户编辑您的代理",
"com_agents_by_librechat": "由 LibreChat 提供",
"com_agents_code_interpreter": "启用后,您的智能体可以安全地使用 LibreChat 代码解释器 API 来运行生成的代码,包括文件处理功能。需要有效的 API 密钥。",
"com_agents_code_interpreter": "启用后,您的代理可以安全地使用 LibreChat 代码解释器 API 来运行生成的代码,包括文件处理功能。需要有效的 API 密钥。",
"com_agents_code_interpreter_title": "代码解释器 API",
"com_agents_create_error": "更新智能体时出现错误。",
"com_agents_description_placeholder": "可选:在此描述您的智能体",
"com_agents_create_error": "更新代理时出现错误。",
"com_agents_description_placeholder": "可选:在此描述您的代理",
"com_agents_enable_file_search": "启用文件搜索",
"com_agents_file_context": "文件上下文OCR",
"com_agents_file_context_disabled": "必须先创建智能体,才能上传文件用于文件上下文。",
"com_agents_file_context_disabled": "必须先创建代理,才能上传文件用于文件上下文。",
"com_agents_file_context_info": "作为”上下文“上传的文件会通过 OCR 处理以提取文本,然后将其添加到代理的指令中。这非常适合文档、带有文本的图像或 PDF 文件等需要文件完整文本内容的场景。",
"com_agents_file_search_disabled": "必须先创建智能体,才能上传文件用于文件搜索。",
"com_agents_file_search_disabled": "必须先创建代理,才能上传文件用于文件搜索。",
"com_agents_file_search_info": "启用后,系统会告知代理以下列出的具体文件名,使其能够从这些文件中检索相关内容。",
"com_agents_instructions_placeholder": "智能体使用的系统指令",
"com_agents_mcp_description_placeholder": "简要解释它的作用",
"com_agents_instructions_placeholder": "代理使用的系统指令",
"com_agents_mcp_icon_size": "最小尺寸 128 × 128 像素",
"com_agents_mcp_info": "将 MCP 服务器添加到您的智能体,使其能够执行任务并与外部服务交互",
"com_agents_mcp_name_placeholder": "自定义工具",
"com_agents_mcp_trust_subtext": "自定义连接器未经 LibreChat 验证",
"com_agents_mcps_disabled": "在添加 MCP 之前,您需要先创建一个智能体。",
"com_agents_missing_provider_model": "请在创建智能体前选择提供商和模型",
"com_agents_name_placeholder": "可选:智能体名称",
"com_agents_no_access": "您没有权限编辑此智能体。",
"com_agents_no_agent_id_error": "未找到智能体 ID。请首先确保智能体已创建。",
"com_agents_not_available": "智能体不可用",
"com_agents_search_info": "启用后,允许您的智能体搜索网络以获取最新信息。需要有效的 API 密钥。",
"com_agents_search_name": "根据名称搜索智能体",
"com_agents_update_error": "更新智能体时出现错误。",
"com_agents_missing_provider_model": "请在创建代理前选择提供商和模型",
"com_agents_name_placeholder": "可选:代理名称",
"com_agents_no_access": "您没有权限编辑此代理。",
"com_agents_not_available": "代理不可用",
"com_agents_search_name": "根据名称搜索代理",
"com_agents_update_error": "更新代理时出现错误。",
"com_assistants_action_attempt": "助理想要和 {{0}} 对话",
"com_assistants_actions": "操作",
"com_assistants_actions_disabled": "您需要先创建助手,然后才能添加操作。",
@@ -69,7 +62,6 @@
"com_assistants_non_retrieval_model": "此模型未启用文件搜索功能。请选择其他模型。",
"com_assistants_retrieval": "检索",
"com_assistants_running_action": "正在运行操作",
"com_assistants_running_var": "正在运行 {{0}}",
"com_assistants_search_name": "根据名称搜索助手",
"com_assistants_update_actions_error": "创建或更新操作时出现错误。",
"com_assistants_update_actions_success": "已成功创建或更新操作",
@@ -143,23 +135,21 @@
"com_auth_username_min_length": "用户名至少 2 个字符",
"com_auth_verify_your_identity": "验证您的身份",
"com_auth_welcome_back": "欢迎",
"com_citation_source": "来源",
"com_click_to_download": "(点击此处下载)",
"com_download_expired": "下载已过期",
"com_download_expires": "(点击此处下载 - {{0}} 后过期)",
"com_endpoint": "端点",
"com_endpoint_agent": "智能体",
"com_endpoint_agent_model": "智能体模型(推荐GPT-3.5",
"com_endpoint_agent_placeholder": "请选择智能体",
"com_endpoint_agent": "代理",
"com_endpoint_agent_model": "代理模型(推荐: GPT-3.5",
"com_endpoint_agent_placeholder": "请选择代理",
"com_endpoint_ai": "人工智能",
"com_endpoint_anthropic_maxoutputtokens": "响应中可以生成的最大词元数。指定较低的值以获得较短的响应,指定较高的值以获得较长的响应。注意:模型可能会在达到此最大值之前停止。",
"com_endpoint_anthropic_maxoutputtokens": "响应中可以生成的最大令牌数。指定较低的值以获得较短的响应,指定较高的值以获得较长的响应。注意:模型可能会在达到此最大值之前停止。",
"com_endpoint_anthropic_prompt_cache": "提示词缓存允许在 API 调用中复用大型上下文或指令,从而降低成本和延迟",
"com_endpoint_anthropic_temp": "值介于 0 到 1 之间。对于分析性/选择性任务,值应更接近 0对于创造性和生成性任务值应更接近 1。我们建议更改该参数或 Top P但不要同时更改这两个参数。",
"com_endpoint_anthropic_thinking": "为受支持的 Claude 模型3.7 Sonnet启用内部推理。注意需要设置“思考预算”且低于“最大输出词元数”",
"com_endpoint_anthropic_thinking_budget": "决定 Claude 内部推理过程允许使用的最大词元数。尽管 Claude 可能不会使用分配的全部预算,尤其是在超过 32K 的范围内,但较大的预算可以对复杂问题进行更深入的分析,从而提高响应质量。此设置必须小于 \"最大输出词元数\"。",
"com_endpoint_anthropic_topk": "top-k 会改变模型选择输出词元的方式。top-k 为 1 意味着所选词是模型词汇中概率最大的(也称为贪心解码),而 top-k 为 3 意味着下一个词是从 3 个概率最大的词中选出的(使用随机性)。",
"com_endpoint_anthropic_topp": "top-p核采样会改变模型选择输出词元的方式。从概率最大的 K参见 topK 参数)向最小的 K 选择,直到它们的概率之和等于 top-p 值。",
"com_endpoint_anthropic_use_web_search": "使用 Anthropic 内置的搜索能力启用网络搜索功能。这使得模型能够搜索网络上的最新信息,提供更准确、更及时的响应。",
"com_endpoint_assistant": "助手",
"com_endpoint_assistant_model": "助手模型",
"com_endpoint_assistant_placeholder": "请从右侧面板中选择助手",
@@ -167,7 +157,7 @@
"com_endpoint_completion_model": "补全模型(推荐: GPT-4",
"com_endpoint_config_click_here": "点击此处",
"com_endpoint_config_google_api_info": "获取您的生成式语言 API 密钥Gemini",
"com_endpoint_config_google_api_key": "Google API Key",
"com_endpoint_config_google_api_key": "Google API KEY",
"com_endpoint_config_google_cloud_platform": "(从 Google Cloud 平台)",
"com_endpoint_config_google_gemini_api": "Gemini API",
"com_endpoint_config_google_service_key": "Google 服务账户密钥",
@@ -195,19 +185,16 @@
"com_endpoint_default_empty": "初始值:空",
"com_endpoint_default_with_num": "初始值:{{0}}",
"com_endpoint_deprecated": "已弃用",
"com_endpoint_deprecated_info": "此端点已被弃用并可能在未来的版本中删除,请改用智能体端点",
"com_endpoint_deprecated_info_a11y": "此插件端点已被弃用并可能在未来的版本中删除,请改用智能体端点",
"com_endpoint_disable_streaming": "禁用流式响应并一次性接收完整的响应。对于像 o3 这样需要进行组织验证才能进行流式响应的模型非常有用。",
"com_endpoint_disable_streaming_label": "禁用流式响应",
"com_endpoint_deprecated_info": "此端点已被弃用并可能在未来的版本中删除,请改用代理端点",
"com_endpoint_deprecated_info_a11y": "此插件端点已被弃用并可能在未来的版本中删除,请改用代理端点",
"com_endpoint_examples": " 预设",
"com_endpoint_export": "导出",
"com_endpoint_export_share": "导出/共享",
"com_endpoint_frequency_penalty": "频率惩罚度",
"com_endpoint_func_hover": "将插件作为 OpenAI 函数使用",
"com_endpoint_google_custom_name_placeholder": "为 Google 设置一个名称",
"com_endpoint_google_maxoutputtokens": "响应中可以生成的最大词元数。指定较低的值以获得较短的响应,指定较高的值以获得较长的响应。注意:模型可能会在达到此最大值之前停止。",
"com_endpoint_google_maxoutputtokens": "响应中可以生成的最大令牌数。指定较低的值以获得较短的响应,指定较高的值以获得较长的响应。注意:模型可能会在达到此最大值之前停止。",
"com_endpoint_google_temp": "值越高表示输出越随机,值越低表示输出越确定。建议不要同时改变此值和 Top-p。",
"com_endpoint_google_thinking": "启用或禁用推理。此设置仅支持某些模型2.5 系列)。对于更老的模型,此设置可能没有效果。",
"com_endpoint_google_topk": "top-k 会改变模型选择输出词元的方式。top-k 为 1 意味着所选词是模型词汇中概率最大的(也称为贪心解码),而 top-k 为 3 意味着下一个词是从 3 个概率最大的词中选出的(使用随机性)。",
"com_endpoint_google_topp": "top-p核采样会改变模型选择输出词的方式。从概率最大的 K参见 topK 参数)向最小的 K 选择,直到它们的概率之和等于 top-p 值。",
"com_endpoint_google_use_search_grounding": "使用 Google 的基础搜索特性,通过实时网络搜索结果优化响应。这使得模型能够访问当前信息,提供更准确、更及时的答案。",
@@ -221,15 +208,15 @@
"com_endpoint_no_presets": "暂无预设,使用设置按钮创建一个",
"com_endpoint_open_menu": "打开菜单",
"com_endpoint_openai_custom_name_placeholder": "为 AI 设置一个名称",
"com_endpoint_openai_detail": "发送给 Vision 的图分辨率。 “低” 更便宜且更快,“高” 更详细但更昂贵,“自动” 将基于图分辨率自动在两者之间进行选择。",
"com_endpoint_openai_detail": "发送给 Vision 的图分辨率。 “低” 更便宜且更快,“高” 更详细但更昂贵,“自动” 将基于图分辨率自动在两者之间进行选择。",
"com_endpoint_openai_freq": "值介于 -2.0 到 2.0 之间。正值将惩罚当前已频繁使用的词元,从而降低重复用词的可能性。",
"com_endpoint_openai_max": "最大生成词元数。输入词元长度由模型的上下文长度决定。",
"com_endpoint_openai_max_tokens": "可选的 'max_tokens' 字段,表示在对话补全中可生成的最大词元数量。输入词元和生成词元的总长度受模型上下文长度的限制。如果该数值超过最大上下文词元数,您可能会遇到错误。",
"com_endpoint_openai_pres": "值介于 -2.0 到 2.0 之间。正值将惩罚当前已经使用的词元,从而增加讨论新话题的可能性。",
"com_endpoint_openai_prompt_prefix_placeholder": "在系统消息中添加自定义指令,默认为空",
"com_endpoint_openai_reasoning_effort": "仅限 o1 和 o3 模型:限制推理模型的推理工作量。减少推理工作量可以获取更快的响应并在响应中使用更少的词元进行推理。",
"com_endpoint_openai_reasoning_summary": "仅限 Responses API模型执行推理的摘要。这对于调试和理解模型的推理过程非常有帮助。可以设置为无、自动、简洁或详细。",
"com_endpoint_openai_resend": "重新发送所有先前附加的图。注意:这会显着增加词元成本,并且可能会遇到很多关于图附件的错误。",
"com_endpoint_openai_reasoning_summary": "仅限 ",
"com_endpoint_openai_resend": "重新发送所有先前附加的图。注意:这会显着增加词元成本,并且可能会遇到很多关于图附件的错误。",
"com_endpoint_openai_resend_files": "重新发送所有先前附加的文件。注意:这会显着增加词元成本,并且可能会遇到很多关于图像附件的错误。",
"com_endpoint_openai_stop": "最多 4 个序列API 将停止生成更多词元。",
"com_endpoint_openai_temp": "值越高表示输出越随机,值越低表示输出越确定。建议不要同时改变此值和 Top P。",
@@ -244,7 +231,6 @@
"com_endpoint_plug_use_functions": "使用函数",
"com_endpoint_presence_penalty": "话题新鲜度",
"com_endpoint_preset": "预设",
"com_endpoint_preset_custom_name_placeholder": "这里需要放点东西,当前是空的",
"com_endpoint_preset_default": "现在是默认预设。",
"com_endpoint_preset_default_item": "默认:",
"com_endpoint_preset_default_none": "无默认预设可用。",
@@ -266,7 +252,7 @@
"com_endpoint_prompt_prefix_assistants_placeholder": "在助手的主要指令之上设置额外的指令或上下文。如果为空,则忽略。",
"com_endpoint_prompt_prefix_placeholder": "自定义指令和上下文,默认为空。",
"com_endpoint_reasoning_effort": "推理强度",
"com_endpoint_reasoning_summary": "推理摘要",
"com_endpoint_reasoning_summary": "推理总结",
"com_endpoint_save_as_preset": "保存为预设",
"com_endpoint_search": "按名称搜索端点",
"com_endpoint_search_endpoint_models": "搜索 {{0}} 模型...",
@@ -283,7 +269,6 @@
"com_endpoint_top_p": "Top P",
"com_endpoint_use_active_assistant": "使用激活的助手",
"com_endpoint_use_responses_api": "使用 Responses API",
"com_endpoint_use_search_grounding": "以 Google 搜索为基础",
"com_error_expired_user_key": "您提供的 {{0}} 密钥已于 {{1}} 过期。请提供新的密钥并重试。",
"com_error_files_dupe": "检测到重复文件",
"com_error_files_empty": "不允许上传空文件",
@@ -292,21 +277,17 @@
"com_error_files_upload": "上传文件时发生错误",
"com_error_files_upload_canceled": "文件上传请求已取消。注意:文件上传可能仍在进行中,需要手动删除。",
"com_error_files_validation": "验证文件时出错。",
"com_error_google_tool_conflict": "内置的 Google 工具与外部工具不兼容。请禁用内置工具或外部工具。",
"com_error_heic_conversion": "将 HEIC 图片转换为 JPEG 失败。请尝试手动转换图像或使用其他格式。",
"com_error_input_length": "最新消息的令牌数过长,超出了令牌限制,或者您的令牌限制参数配置错误,对上下文窗口产生不利影响。更多信息:{{0}}。请缩短您的消息、调整对话参数中的最大上下文大小,或分叉对话以继续。",
"com_error_invalid_agent_provider": "提供商 ”{{0}}“ 不适用于智能体。请转到智能体设置并选择当前可用的提供商。",
"com_error_invalid_agent_provider": "提供商 ”{{0}}“ 不适用于代理。请转到代理设置并选择当前可用的提供商。",
"com_error_invalid_user_key": "提供的密钥无效。请提供有效的密钥后重试。",
"com_error_moderation": "很抱歉,您提交的内容被我们的审核系统标记为不符合社区指引。我们无法就此特定主题继续交流。如果您有任何其他问题或想探讨的话题,请编辑您的消息或开启新的对话。",
"com_error_no_base_url": "未找到基础 URL请提供一个后重试。",
"com_error_no_user_key": "找到密钥。请提供密钥后重试。",
"com_error_no_user_key": "没有找到密钥。请提供密钥后重试。",
"com_files_filter": "筛选文件...",
"com_files_no_results": "无结果。",
"com_files_number_selected": "已选择 {{0}} 个文件(共 {{1}} 个文件)",
"com_files_table": "这里需要放点东西,当前是空的",
"com_generated_files": "生成的文件",
"com_hide_examples": "隐藏示例",
"com_info_heic_converting": "正在将 HEIC 图片转换为 JPEG ...",
"com_nav_2fa": "双重身份验证2FA",
"com_nav_account_settings": "账户设置",
"com_nav_always_make_prod": "始终使用新版本",
@@ -324,27 +305,6 @@
"com_nav_auto_transcribe_audio": "自动转录音频",
"com_nav_automatic_playback": "自动播放最新消息",
"com_nav_balance": "余额",
"com_nav_balance_auto_refill_disabled": "自动充值已禁用。",
"com_nav_balance_auto_refill_error": "加载自动充值设置时发生错误。",
"com_nav_balance_auto_refill_settings": "自动充值设置",
"com_nav_balance_day": "天",
"com_nav_balance_days": "天",
"com_nav_balance_every": "每",
"com_nav_balance_hour": "小时",
"com_nav_balance_hours": "小时",
"com_nav_balance_interval": "间隔:",
"com_nav_balance_last_refill": "上次充值:",
"com_nav_balance_minute": "分钟",
"com_nav_balance_minutes": "分钟",
"com_nav_balance_month": "个月",
"com_nav_balance_months": "个月",
"com_nav_balance_next_refill": "下次充值:",
"com_nav_balance_next_refill_info": "只有同时满足两个条件时,才会自动进行下一次充值:自上次充值以来已经过了指定的时间间隔,并且发送提示词会导致您的余额降至零以下。",
"com_nav_balance_refill_amount": "充值金额:",
"com_nav_balance_second": "秒",
"com_nav_balance_seconds": "秒",
"com_nav_balance_week": "周",
"com_nav_balance_weeks": "周",
"com_nav_browser": "浏览器",
"com_nav_center_chat_input": "在欢迎屏幕上居中对话输入",
"com_nav_change_picture": "修改头像",
@@ -391,10 +351,9 @@
"com_nav_font_size_xs": "超小号",
"com_nav_help_faq": "帮助",
"com_nav_hide_panel": "隐藏最右侧面板",
"com_nav_info_balance": "余额显示您剩余的词元额度。词元额度对应一定的货币价值例如1000 额度 = 0.001 美元)。",
"com_nav_info_code_artifacts": "启用在对话旁显示实验性代码工件",
"com_nav_info_code_artifacts": "启用在对话旁显示的实验性代码工件",
"com_nav_info_code_artifacts_agent": "使该代理能够使用代码附件。默认情况下,除非启用“自定义提示模式”,否则会添加与附件使用相关的额外说明。",
"com_nav_info_custom_prompt_mode": "启用后,默认的 Artifacts 系统提示将不会包含在内。在此模式下,必须手动提供所有生成工件的指令。",
"com_nav_info_custom_prompt_mode": "启用后,默认的工件系统提示将不会包含在内。在此模式下,必须手动提供所有生成工件的指令。",
"com_nav_info_enter_to_send": "启用后,按下 `ENTER` 将发送您的消息。禁用后,按下 `ENTER` 将添加新行,您需要按下 `CTRL + ENTER` / `⌘ + ENTER` 来发送消息。",
"com_nav_info_fork_change_default": "`仅可见消息` 仅包含到所选消息的直接路径,`包含相关分支` 添加路径上的分支,`包含所有目标` 包括所有连接的消息和分支。",
"com_nav_info_fork_split_target_setting": "启用后,将根据选择的行为,从目标消息开始到对话中的最新消息进行分叉。",
@@ -405,13 +364,9 @@
"com_nav_info_show_thinking": "启用后,对话界面将默认展开深度思考下拉框,让您能够实时查看 AI 的推理过程。禁用后,深度思考下拉框将默认保持关闭状态,以提供更简洁、更流畅的界面。",
"com_nav_info_user_name_display": "启用后,发送者的用户名将显示在您发送的每条消息上方。禁用后,您只会在自己的消息上方看到 “您”。",
"com_nav_lang_arabic": "العربية",
"com_nav_lang_armenian": "Հայերեն",
"com_nav_lang_auto": "自动检测语言",
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
"com_nav_lang_catalan": "Català",
"com_nav_lang_chinese": "中文",
"com_nav_lang_czech": "Čeština",
"com_nav_lang_danish": "Dansk",
"com_nav_lang_dutch": "Nederlands",
"com_nav_lang_english": "English",
"com_nav_lang_estonian": "Eesti keel",
@@ -425,7 +380,6 @@
"com_nav_lang_italian": "Italiano",
"com_nav_lang_japanese": "日本語",
"com_nav_lang_korean": "한국어",
"com_nav_lang_latvian": "Latviski",
"com_nav_lang_persian": "فارسی",
"com_nav_lang_polish": "Polski",
"com_nav_lang_portuguese": "Português",
@@ -435,17 +389,12 @@
"com_nav_lang_thai": "ไทย",
"com_nav_lang_traditional_chinese": "繁體中文",
"com_nav_lang_turkish": "Türkçe",
"com_nav_lang_uyghur": "Uyƣur tili",
"com_nav_lang_vietnamese": "Tiếng Việt",
"com_nav_language": "语言",
"com_nav_latex_parsing": "解析消息中的 LaTeX可能会影响性能",
"com_nav_log_out": "注销",
"com_nav_long_audio_warning": "较长的文本将需要更长时间来处理。",
"com_nav_maximize_chat_space": "最大化对话窗口",
"com_nav_mcp_configure_server": "配置 {{0}}",
"com_nav_mcp_status_connecting": "{{0}} - 连接中",
"com_nav_mcp_vars_update_error": "更新 MCP 自定义用户变量时发生错误:{{0}}",
"com_nav_mcp_vars_updated": "MCP 自定义用户变量更新成功。",
"com_nav_modular_chat": "启用在对话中切换端点",
"com_nav_my_files": "我的文件",
"com_nav_not_supported": "未支持",
@@ -465,11 +414,10 @@
"com_nav_search_placeholder": "搜索消息",
"com_nav_send_message": "发送消息",
"com_nav_setting_account": "账户",
"com_nav_setting_balance": "余额",
"com_nav_setting_beta": "实验特性",
"com_nav_setting_chat": "对话",
"com_nav_setting_data": "数据管理",
"com_nav_setting_general": "通用",
"com_nav_setting_mcp": "MCP 设置",
"com_nav_setting_personalization": "个性化",
"com_nav_setting_speech": "语音",
"com_nav_settings": "设置",
@@ -486,7 +434,7 @@
"com_nav_theme_light": "亮色主题",
"com_nav_theme_system": "系统设置",
"com_nav_tool_dialog": "助手工具",
"com_nav_tool_dialog_agents": "智能体工具",
"com_nav_tool_dialog_agents": "代理工具",
"com_nav_tool_dialog_description": "必须保存助手才能保留工具选择。",
"com_nav_tool_remove": "移除",
"com_nav_tool_search": "搜索工具",
@@ -494,24 +442,16 @@
"com_nav_user_msg_markdown": "以 Markdown 格式显示用户消息",
"com_nav_user_name_display": "在消息中显示用户名",
"com_nav_voice_select": "语音",
"com_show_agent_settings": "显示智能体设置",
"com_show_agent_settings": "显示代理设置",
"com_show_completion_settings": "显示补全设置",
"com_show_examples": "显示示例",
"com_sidepanel_agent_builder": "智能体构建器",
"com_sidepanel_agent_builder": "代理构建器",
"com_sidepanel_assistant_builder": "助手生成器",
"com_sidepanel_attach_files": "附加文件",
"com_sidepanel_conversation_tags": "书签",
"com_sidepanel_hide_panel": "隐藏侧边栏",
"com_sidepanel_manage_files": "管理文件",
"com_sidepanel_mcp_no_servers_with_vars": "没有支持可配置变量的 MCP 服务器。",
"com_sidepanel_mcp_variables_for": "MCP 变量:{{0}}",
"com_sidepanel_parameters": "参数",
"com_sources_image_alt": "搜索结果图片",
"com_sources_more_sources": "+{{count}} 个来源",
"com_sources_tab_all": "全部",
"com_sources_tab_images": "图片",
"com_sources_tab_news": "新闻",
"com_sources_title": "来源",
"com_ui_2fa_account_security": "双重身份验证为您的账户提供了额外的安全保护",
"com_ui_2fa_disable": "关闭双重身份验证",
"com_ui_2fa_disable_error": "禁用双重身份验证时出现错误",
@@ -523,11 +463,7 @@
"com_ui_2fa_setup": "设置双重身份验证",
"com_ui_2fa_verified": "成功验证双重身份验证",
"com_ui_accept": "我接受",
"com_ui_action_button": "操作按钮",
"com_ui_active": "活动",
"com_ui_add": "添加",
"com_ui_add_mcp": "添加 MCP",
"com_ui_add_mcp_server": "添加 MCP 服务器",
"com_ui_add_model_preset": "添加一个模型或预设以获得额外的回复",
"com_ui_add_multi_conversation": "添加多个对话",
"com_ui_admin": "管理",
@@ -535,37 +471,29 @@
"com_ui_admin_settings": "管理员设置",
"com_ui_advanced": "进阶",
"com_ui_advanced_settings": "进阶设置",
"com_ui_agent": "智能体",
"com_ui_agent_chain": "智能体Mixture-of-Agents",
"com_ui_agent_chain_info": "启用创建智能体序列。每个智能体都可以访问智能体链中先前智能体的输出。基于 “Mixture-of-Agents” 架构,智能体使用先前的输出作为辅助信息。",
"com_ui_agent_chain_max": "您已达到智能体 {{0}} 的最大值。",
"com_ui_agent_delete_error": "删除智能体时出现错误",
"com_ui_agent_deleted": "智能体已成功删除",
"com_ui_agent_duplicate_error": "复制智能体时发生错误",
"com_ui_agent_duplicated": "智能体复制成功",
"com_ui_agent_editing_allowed": "其他用户已可以编辑此智能体",
"com_ui_agent_recursion_limit": "最大智能体步骤数",
"com_ui_agent_recursion_limit_info": "限制智能体在给出最终响应之前可执行的步骤数。默认为 25 个步骤。步骤可以是一次 AI API 请求或一次工具使用。例如,一个基本的工具交互需要 3 个步骤:初始请求、工具使用和后续请求。",
"com_ui_agent_shared_to_all": "这里需要放点东西,当前是空的",
"com_ui_agent_var": "{{0}} 智能体",
"com_ui_agent": "代理",
"com_ui_agent_chain": "代理Mixture-of-Agents",
"com_ui_agent_chain_info": "启用创建代理序列。每个代理都可以访问代理链中先前代理的输出。基于“Mixture-of-Agents”架构代理使用先前的输出作为辅助信息。",
"com_ui_agent_chain_max": "您已达到代理 {{0}} 的最大值。",
"com_ui_agent_delete_error": "删除代理时出现错误",
"com_ui_agent_deleted": "代理已成功删除",
"com_ui_agent_duplicate_error": "复制代理时发生错误",
"com_ui_agent_duplicated": "代理复制成功",
"com_ui_agent_editing_allowed": "其他用户已可以编辑此代理",
"com_ui_agent_recursion_limit": "最大代理步骤数",
"com_ui_agent_recursion_limit_info": "限制代理在给出最终响应之前可执行的步骤数。默认为 25 个步骤。步骤可以是一次 AI API 请求或一次工具使用。例如,一个基本的工具交互需要 3 个步骤:初始请求、工具使用和后续请求。",
"com_ui_agent_var": "{{0}} 代理",
"com_ui_agent_version": "版本",
"com_ui_agent_version_active": "活动版本",
"com_ui_agent_version_duplicate": "检测到重复版本。这将创建与版本 {{versionIndex}} 完全相同的版本。",
"com_ui_agent_version_empty": "无可用版本",
"com_ui_agent_version_error": "获取版本时发生错误",
"com_ui_agent_version_history": "版本历史",
"com_ui_agent_version_no_agent": "未选择智能体。请选择智能体以查看版本历史。",
"com_ui_agent_version_no_date": "日期不可用",
"com_ui_agent_version_restore": "还原",
"com_ui_agent_version_restore_confirm": "您确定要还原此版本吗?",
"com_ui_agent_version_restore_error": "还原版本失败",
"com_ui_agent_version_restore_success": "版本还原成功",
"com_ui_agent_version_title": "版本 {{versionNumber}}",
"com_ui_agent_version_unknown_date": "未知日期",
"com_ui_agents": "智能体",
"com_ui_agents_allow_create": "允许创建智能体",
"com_ui_agents_allow_share_global": "允许与所有用户共享智能体",
"com_ui_agents_allow_use": "允许使用智能体",
"com_ui_agents": "代理",
"com_ui_agents_allow_create": "允许创建代理",
"com_ui_agents_allow_share_global": "允许与所有用户共享代理",
"com_ui_agents_allow_use": "允许使用代理",
"com_ui_all": "所有",
"com_ui_all_proper": "所有",
"com_ui_analyzing": "正在分析",
@@ -576,9 +504,8 @@
"com_ui_archive_error": "归档对话失败",
"com_ui_artifact_click": "点击以打开",
"com_ui_artifacts": "Artifacts",
"com_ui_artifacts_options": "Artifacts 选项",
"com_ui_artifacts_toggle": "切换 Artifacts UI",
"com_ui_artifacts_toggle_agent": "启用 Artifacts",
"com_ui_artifacts_toggle": "切换至 Artifacts UI",
"com_ui_artifacts_toggle_agent": "启用附件",
"com_ui_ascending": "升序",
"com_ui_assistant": "助手",
"com_ui_assistant_delete_error": "删除助手时出现错误",
@@ -594,14 +521,11 @@
"com_ui_attachment": "附件",
"com_ui_auth_type": "认证类型",
"com_ui_auth_url": "认证 URL",
"com_ui_authenticate": "认证",
"com_ui_authentication": "认证",
"com_ui_authentication_type": "认证类型",
"com_ui_auto": "自动",
"com_ui_available_tools": "可用工具",
"com_ui_avatar": "头像",
"com_ui_azure": "Azure",
"com_ui_back": "后退",
"com_ui_back_to_chat": "返回对话",
"com_ui_back_to_prompts": "返回提示词",
"com_ui_backup_codes": "备份代码",
@@ -631,7 +555,6 @@
"com_ui_bulk_delete_error": "删除分享链接失败",
"com_ui_callback_url": "回调 URL",
"com_ui_cancel": "取消",
"com_ui_cancelled": "已取消",
"com_ui_category": "类别",
"com_ui_chat": "对话",
"com_ui_chat_history": "对话历史",
@@ -641,21 +564,17 @@
"com_ui_client_secret": "Client Secret",
"com_ui_close": "关闭",
"com_ui_close_menu": "关闭菜单",
"com_ui_close_window": "关闭窗口",
"com_ui_code": "代码",
"com_ui_collapse_chat": "收起对话",
"com_ui_command_placeholder": "可选:输入提示词的命令,否则将使用名称",
"com_ui_command_usage_placeholder": "通过命令或名称选择提示词",
"com_ui_complete_setup": "完成设置",
"com_ui_concise": "简洁",
"com_ui_configure_mcp_variables_for": "配置变量:{{0}}",
"com_ui_confirm_action": "确认执行",
"com_ui_confirm_admin_use_change": "更改此设置将阻止包括您的在内的所有管理员的权限。您确定要继续吗?",
"com_ui_confirm_change": "确认更改",
"com_ui_connecting": "连接中",
"com_ui_context": "上下文",
"com_ui_continue": "继续",
"com_ui_continue_oauth": "使用 OAuth 登录",
"com_ui_controls": "管理",
"com_ui_convo_delete_error": "删除对话失败",
"com_ui_copied": "已复制!",
@@ -667,8 +586,6 @@
"com_ui_create_link": "创建链接",
"com_ui_create_memory": "创建记忆",
"com_ui_create_prompt": "创建提示词",
"com_ui_creating_image": "图片创建中。可能需要一点时间。",
"com_ui_current": "当前",
"com_ui_currently_production": "目前正在使用中",
"com_ui_custom": "自定义",
"com_ui_custom_header_name": "自定义 Header 名称",
@@ -696,28 +613,22 @@
"com_ui_delete": "删除",
"com_ui_delete_action": "删除操作",
"com_ui_delete_action_confirm": "您确定要删除此操作吗?",
"com_ui_delete_agent_confirm": "您确定要删除此智能体吗?",
"com_ui_delete_agent_confirm": "您确定要删除此代理吗?",
"com_ui_delete_assistant_confirm": "您确定要删除此助手吗?该操作无法撤销。",
"com_ui_delete_confirm": "这将删除",
"com_ui_delete_confirm_prompt_version_var": "这将删除选中版本的 “{{0}}”。如果没有其他版本,该提示词将被删除。",
"com_ui_delete_conversation": "删除对话?",
"com_ui_delete_mcp": "删除 MCP",
"com_ui_delete_mcp_confirm": "您确定要删除此 MCP 服务器吗?",
"com_ui_delete_mcp_error": "删除 MCP 服务器失败",
"com_ui_delete_mcp_success": "MCP 服务器删除成功",
"com_ui_delete_memory": "删除记忆",
"com_ui_delete_not_allowed": "不允许删除操作",
"com_ui_delete_prompt": "删除提示词?",
"com_ui_delete_shared_link": "删除分享链接?",
"com_ui_delete_success": "已成功删除",
"com_ui_delete_tool": "删除工具",
"com_ui_delete_tool_confirm": "您确定要删除此工具吗?",
"com_ui_deleted": "已删除",
"com_ui_deleting_file": "删除文件中...",
"com_ui_descending": "降序",
"com_ui_description": "描述",
"com_ui_description_placeholder": "可选:输入要显示的提示词描述",
"com_ui_deselect_all": "取消全选",
"com_ui_detailed": "详细",
"com_ui_disabling": "停用中...",
"com_ui_download": "下载",
@@ -725,7 +636,7 @@
"com_ui_download_backup": "下载备份代码",
"com_ui_download_backup_tooltip": "在继续之前,请下载备份代码。如果您丢失了身份验证设备,您将需要该代码来重新获得访问权限",
"com_ui_download_error": "下载文件时出现错误,该文件可能已被删除。",
"com_ui_drag_drop": "这里需要放点东西,当前是空的",
"com_ui_drag_drop": "something needs to go here. was empty",
"com_ui_dropdown_variables": "下拉变量:",
"com_ui_dropdown_variables_info": "为您的提示词创建自定义下拉菜单:`{{variable_name:option1|option2|option3}}`",
"com_ui_duplicate": "复制",
@@ -733,46 +644,23 @@
"com_ui_duplication_processing": "正在复制对话...",
"com_ui_duplication_success": "成功复制对话",
"com_ui_edit": "编辑",
"com_ui_edit_editing_image": "编辑图片",
"com_ui_edit_mcp_server": "编辑 MCP 服务器",
"com_ui_edit_memory": "编辑记忆",
"com_ui_empty_category": "-",
"com_ui_endpoint": "端点",
"com_ui_endpoint_menu": "LLM 端点菜单",
"com_ui_enter": "进入",
"com_ui_enter_api_key": "输入 API 密钥",
"com_ui_enter_key": "输入键",
"com_ui_enter_openapi_schema": "请在此输入 OpenAPI 架构",
"com_ui_enter_value": "输入值",
"com_ui_enter_openapi_schema": "请在此输入OpenAPI架构",
"com_ui_error": "错误",
"com_ui_error_connection": "连接服务器时发生错误,请尝试刷新页面。",
"com_ui_error_connection": "连接服务器出现错误,请尝试刷新页面。",
"com_ui_error_save_admin_settings": "保存您的管理员设置时出现错误。",
"com_ui_error_updating_preferences": "更新偏好设置时发生错误",
"com_ui_examples": "示例",
"com_ui_expand_chat": "展开对话",
"com_ui_export_convo_modal": "导出对话窗口",
"com_ui_feedback_more": "更多...",
"com_ui_feedback_more_information": "提供更多反馈",
"com_ui_feedback_negative": "需改进",
"com_ui_feedback_placeholder": "请在此处提供任何其他反馈",
"com_ui_feedback_positive": "喜欢",
"com_ui_feedback_tag_accurate_reliable": "准确且可靠",
"com_ui_feedback_tag_attention_to_detail": "注重细节",
"com_ui_feedback_tag_bad_style": "风格或语气不佳",
"com_ui_feedback_tag_clear_well_written": "清晰且表达流畅",
"com_ui_feedback_tag_creative_solution": "创造性方案",
"com_ui_feedback_tag_inaccurate": "不准确或错误的回答",
"com_ui_feedback_tag_missing_image": "预期生成图片",
"com_ui_feedback_tag_not_helpful": "缺乏有用的信息",
"com_ui_feedback_tag_not_matched": "不符合我的要求",
"com_ui_feedback_tag_other": "其他问题",
"com_ui_feedback_tag_unjustified_refusal": "无故拒绝回答",
"com_ui_field_required": "此字段为必填项",
"com_ui_file_size": "文件大小",
"com_ui_files": "文件",
"com_ui_filter_prompts": "筛选提示词",
"com_ui_filter_prompts": "过滤 Prompts",
"com_ui_filter_prompts_name": "根据名称筛选提示词",
"com_ui_final_touch": "最后润色",
"com_ui_finance": "财务",
"com_ui_fork": "分叉",
"com_ui_fork_all_target": "包含所有目标",
@@ -780,19 +668,15 @@
"com_ui_fork_change_default": "默认分叉选项",
"com_ui_fork_default": "使用默认分叉选项",
"com_ui_fork_error": "分叉对话时出现错误",
"com_ui_fork_error_rate_limit": "分叉请求过多,请稍后再试。",
"com_ui_fork_from_message": "选择分叉选项",
"com_ui_fork_info_1": "使用此设置可以分叉消息,以获得所需的行为。",
"com_ui_fork_info_2": "“分叉” 是指从当前对话中选择特定消息作为起点/终点,根据所选选项创建一个新的对话副本。",
"com_ui_fork_info_3": "“目标消息” 是指此弹出窗口所打开的消息,或者如果您选中 “{{0}}”,则是对话中最新的消息。",
"com_ui_fork_info_branches": "此选项会分叉可见消息及相关分支;换句话说,包括沿路径的直接路线到目标消息,以及路径上的分支。",
"com_ui_fork_info_button_label": "查看有关分叉对话的信息",
"com_ui_fork_info_remember": "选中此项可记住您的选择,以便下次分叉对话时更快捷地使用您偏好的选项。",
"com_ui_fork_info_start": "如果勾选,则根据上述选择的行为,从此消息开始到对话中最新的消息将被分叉。",
"com_ui_fork_info_target": "此选项会分叉所有导向目标消息的消息分支,包括其相邻消息;换句话说,无论是否可见或在同一路径上,所有消息分支都会被包含在内。",
"com_ui_fork_info_visible": "此选项仅分叉可见的消息;换句话说,是直接路径到目标消息,没有任何分支。",
"com_ui_fork_more_details_about": "查看有关 \"{{0}}\" 分叉选项的更多信息和详情",
"com_ui_fork_more_info_options": "查看所有分叉选项及其行为的详细说明",
"com_ui_fork_processing": "正在分叉对话...",
"com_ui_fork_remember": "记住",
"com_ui_fork_remember_checked": "您的选择将在使用后被记住。您可以随时在设置中更改。",
@@ -803,25 +687,17 @@
"com_ui_generate_backup": "生成备份代码",
"com_ui_generate_qrcode": "生成二维码",
"com_ui_generating": "生成中...",
"com_ui_generation_settings": "生成设置",
"com_ui_getting_started": "已经开始",
"com_ui_global_group": "这里需要放点东西,当前是空的",
"com_ui_global_group": "something needs to go here. was empty",
"com_ui_go_back": "返回",
"com_ui_go_to_conversation": "转到对话",
"com_ui_good_afternoon": "下午好",
"com_ui_good_evening": "晚上好",
"com_ui_good_morning": "早上好",
"com_ui_happy_birthday": "这是我的第一个生日!",
"com_ui_hide_image_details": "隐藏图片详情",
"com_ui_hide_password": "隐藏密码",
"com_ui_hide_qr": "隐藏二维码",
"com_ui_high": "高",
"com_ui_host": "主机",
"com_ui_icon": "图标",
"com_ui_idea": "灵感",
"com_ui_image_created": "图片已创建",
"com_ui_image_details": "图片详情",
"com_ui_image_edited": "图片已编辑",
"com_ui_image_gen": "图片生成",
"com_ui_import": "导入",
"com_ui_import_conversation_error": "导入对话时发生错误",
@@ -831,7 +707,6 @@
"com_ui_include_shadcnui": "包含 shadcn/ui 组件指令",
"com_ui_input": "输入",
"com_ui_instructions": "指令",
"com_ui_key": "键",
"com_ui_late_night": "夜深了",
"com_ui_latest_footer": "Every AI for Everyone.",
"com_ui_latest_production_version": "最新在用版本",
@@ -842,44 +717,19 @@
"com_ui_loading": "加载中...",
"com_ui_locked": "已锁定",
"com_ui_logo": "{{0}} 标识",
"com_ui_low": "低",
"com_ui_manage": "管理",
"com_ui_max_tags": "最多允许 {{0}} 个,用最新值。",
"com_ui_mcp_authenticated_success": "MCP 服务器 “{{0}}” 认证成功",
"com_ui_mcp_dialog_desc": "请在下方输入必要的信息。",
"com_ui_mcp_enter_var": "输入值:{{0}}",
"com_ui_mcp_init_failed": "初始化 MCP 服务器失败",
"com_ui_mcp_initialize": "初始化",
"com_ui_mcp_initialized_success": "MCP 服务器 “{{0}}” 初始化成功",
"com_ui_mcp_not_authenticated": "{{0}} 未认证(需要 OAuth",
"com_ui_mcp_not_initialized": "{{0}} 未初始化",
"com_ui_mcp_oauth_cancelled": "{{0}} OAuth 登录已取消",
"com_ui_mcp_oauth_no_url": "需要 OAuth 认证,但未提供 URL",
"com_ui_mcp_oauth_timeout": "{{0}} OAuth 登录超时",
"com_ui_mcp_server_not_found": "未找到服务器。",
"com_ui_mcp_servers": "MCP 服务器",
"com_ui_mcp_update_var": "更新 {{0}}",
"com_ui_mcp_url": "MCP 服务器 URL",
"com_ui_medium": "中",
"com_ui_memories": "记忆",
"com_ui_memories_allow_create": "允许创建记忆",
"com_ui_memories_allow_opt_out": "允许用户选择退出记忆",
"com_ui_memories_allow_read": "允许读取记忆",
"com_ui_memories_allow_update": "允许更新记忆",
"com_ui_memories_allow_use": "允许使用记忆",
"com_ui_memories_filter": "筛选记忆...",
"com_ui_memory": "记忆",
"com_ui_memory_already_exceeded": "记忆存储已满 - 超过 {{tokens}} 词元。添加新记忆前,请删除现有记忆。",
"com_ui_memory_created": "记忆创建成功",
"com_ui_memory_deleted": "记忆已删除",
"com_ui_memory_deleted_items": "已删除的记忆",
"com_ui_memory_error": "记忆错误",
"com_ui_memory_key_exists": "已存在使用此键的记忆。请使用其他键。",
"com_ui_memory_key_validation": "记忆键只能包含小写字母和下划线。",
"com_ui_memory_storage_full": "记忆存储已满",
"com_ui_memory_updated": "已更新保存的记忆",
"com_ui_memory_updated_items": "已更新的记忆",
"com_ui_memory_would_exceed": "无法保存 - 将超过 {{tokens}} 词元限制。删除现有记忆以释放空间。",
"com_ui_mention": "提及一个端点、助手或预设以快速切换到它",
"com_ui_min_tags": "无法再移除更多值,至少需要保留 {{0}} 个。",
"com_ui_misc": "杂项",
@@ -897,31 +747,20 @@
"com_ui_no_bookmarks": "似乎您还没有书签。点击一个对话并添加一个新的书签",
"com_ui_no_category": "无类别",
"com_ui_no_changes": "无需更新",
"com_ui_no_data": "这里需要放点东西,当前是空的",
"com_ui_no_data": "something needs to go here. was empty",
"com_ui_no_personalization_available": "当前没有可用的个性化选项",
"com_ui_no_read_access": "您没有权限查看记忆",
"com_ui_no_terms_content": "没有可显示的条款和条件内容",
"com_ui_no_valid_items": "这里需要放点东西,当前是空的",
"com_ui_no_valid_items": "something needs to go here. was empty",
"com_ui_none": "无",
"com_ui_not_used": "未使用",
"com_ui_nothing_found": "未找到任何内容",
"com_ui_oauth": "OAuth",
"com_ui_oauth_connected_to": "连接到",
"com_ui_oauth_error_callback_failed": "认证回调失败。请重试。",
"com_ui_oauth_error_generic": "认证失败。请重试。",
"com_ui_oauth_error_invalid_state": "状态参数无效。请重试。",
"com_ui_oauth_error_missing_code": "缺少身份验证代码。请重试。",
"com_ui_oauth_error_missing_state": "缺少状态参数。请重试。",
"com_ui_oauth_error_title": "认证失败",
"com_ui_oauth_flow_desc": "在新窗口中完成 OAuth 流程,然后返回此处。",
"com_ui_oauth_success_description": "您的身份验证成功。此窗口将在以下时间后关闭:",
"com_ui_oauth_success_title": "认证成功",
"com_ui_of": "/",
"com_ui_off": "关闭",
"com_ui_offline": "离线",
"com_ui_on": "开启",
"com_ui_openai": "OpenAI",
"com_ui_optional": "(可选)",
"com_ui_page": "页面",
"com_ui_preferences_updated": "偏好设置更新成功",
"com_ui_prev": "上一页",
@@ -941,7 +780,6 @@
"com_ui_prompts_allow_share_global": "允许向所有用户共享提示词",
"com_ui_prompts_allow_use": "允许使用提示词",
"com_ui_provider": "提供商",
"com_ui_quality": "质量",
"com_ui_read_aloud": "大声朗读",
"com_ui_redirecting_to_provider": "正在重定向到 {{0}},请稍候...",
"com_ui_reference_saved_memories": "参考保存的记忆",
@@ -951,14 +789,12 @@
"com_ui_regenerate_backup": "重新生成备份代码",
"com_ui_regenerating": "重新生成中...",
"com_ui_region": "区域",
"com_ui_reinitialize": "重新初始化",
"com_ui_rename": "重命名",
"com_ui_rename_conversation": "重命名对话",
"com_ui_rename_failed": "重命名对话失败",
"com_ui_rename_prompt": "重命名 Prompt",
"com_ui_requires_auth": "需要认证",
"com_ui_reset_var": "重置 {{0}}",
"com_ui_reset_zoom": "重置缩放",
"com_ui_result": "结果",
"com_ui_revoke": "撤销",
"com_ui_revoke_info": "撤销所有用户提供的凭据",
@@ -974,14 +810,11 @@
"com_ui_save_badge_changes": "保存徽章更改?",
"com_ui_save_submit": "保存并提交",
"com_ui_saved": "保存成功!",
"com_ui_saving": "保存中...",
"com_ui_schema": "架构",
"com_ui_scope": "范围",
"com_ui_search": "搜索",
"com_ui_seconds": "秒",
"com_ui_secret_key": "Secret Key",
"com_ui_select": "选择",
"com_ui_select_all": "全选",
"com_ui_select_file": "选择文件",
"com_ui_select_model": "模型选择",
"com_ui_select_provider": "选择提供商",
@@ -991,12 +824,10 @@
"com_ui_select_search_plugin": "根据名称搜索插件",
"com_ui_select_search_provider": "以名称搜索服务商",
"com_ui_select_search_region": "以名称搜索区域",
"com_ui_set": "设置",
"com_ui_share": "共享",
"com_ui_share_create_message": "您的姓名以及您在共享后添加的任何消息将保持私密。",
"com_ui_share_delete_error": "删除共享链接时出现错误",
"com_ui_share_error": "共享对话链接时发生错误",
"com_ui_share_form_description": "这里需要放点东西,当前是空的",
"com_ui_share_link_to_chat": "共享链接到聊天",
"com_ui_share_to_all_users": "共享给所有用户",
"com_ui_share_update_message": "您的姓名、自定义指令以及您在共享后添加的任何消息将保持私密。",
@@ -1008,20 +839,12 @@
"com_ui_shop": "购物",
"com_ui_show": "显示",
"com_ui_show_all": "展开全部",
"com_ui_show_image_details": "显示图片详情",
"com_ui_show_password": "显示密码",
"com_ui_show_qr": "显示二维码",
"com_ui_sign_in_to_domain": "登录到 {{0}}",
"com_ui_simple": "基本",
"com_ui_size": "大小",
"com_ui_special_var_current_date": "当前日期",
"com_ui_special_var_current_datetime": "当前日期时间",
"com_ui_special_var_current_user": "当前用户",
"com_ui_special_var_iso_datetime": "UTC ISO 日期时间",
"com_ui_special_variables": "特殊变量:",
"com_ui_special_variables_more_info": "您可以从下拉菜单中选择特殊变量:`{{current_date}}`(今天的日期和星期)、`{{current_datetime}}`(本地日期和时间)、`{{utc_iso_datetime}}`UTC ISO 格式的日期时间)以及`{{current_user}}`(您的账户名称)。",
"com_ui_speech_while_submitting": "正在生成回复时无法提交语音",
"com_ui_sr_actions_menu": "打开 \"{{0}}\" 的操作菜单",
"com_ui_stop": "停止",
"com_ui_storage": "存储",
"com_ui_submit": "提交",
@@ -1031,22 +854,15 @@
"com_ui_terms_of_service": "服务政策",
"com_ui_thinking": "思考中...",
"com_ui_thoughts": "思考内容",
"com_ui_token": "词元",
"com_ui_token_exchange_method": "Token Exchange Method",
"com_ui_token_url": "Token URL",
"com_ui_tokens": "词元",
"com_ui_tool_collection_prefix": "工具集来自",
"com_ui_tool_info": "工具信息",
"com_ui_tool_more_info": "有关此工具的更多信息",
"com_ui_tools": "工具",
"com_ui_travel": "旅行",
"com_ui_unarchive": "取消归档",
"com_ui_unarchive_error": "取消归档对话失败",
"com_ui_unknown": "未知",
"com_ui_unset": "取消设置",
"com_ui_untitled": "无标题",
"com_ui_update": "更新",
"com_ui_update_mcp_error": "创建或更新 MCP 时出现错误。",
"com_ui_update_mcp_success": "已成功创建或更新 MCP",
"com_ui_upload": "上传",
"com_ui_upload_code_files": "上传代码解释器文件",
@@ -1057,19 +873,17 @@
"com_ui_upload_files": "上传文件",
"com_ui_upload_image": "上传图片",
"com_ui_upload_image_input": "上传图片",
"com_ui_upload_invalid": "上传的文件无效。必须是图,且不得超过大小限制",
"com_ui_upload_invalid_var": "上传的文件无效。必须是图,且不得超过 {{0}} MB。",
"com_ui_upload_invalid": "上传的文件无效。必须是图,且不得超过大小限制",
"com_ui_upload_invalid_var": "上传的文件无效。必须是图,且不得超过 {{0}} MB。",
"com_ui_upload_ocr_text": "作为文本上传",
"com_ui_upload_success": "上传文件成功",
"com_ui_upload_type": "选择上传类型",
"com_ui_usage": "用量",
"com_ui_use_2fa_code": "使用 2FA 代码替代",
"com_ui_use_backup_code": "使用备份代码代替",
"com_ui_use_memory": "使用记忆",
"com_ui_use_micrphone": "使用麦克风",
"com_ui_use_prompt": "使用提示词",
"com_ui_used": "已使用",
"com_ui_value": "值",
"com_ui_variables": "变量",
"com_ui_variables_info": "在您的文本中使用双大括号创建变量,例如 `{{example variable}}`,以便在使用提示词时填充。",
"com_ui_verify": "验证",
@@ -1078,28 +892,6 @@
"com_ui_view_memory": "查看记忆",
"com_ui_view_source": "查看来源对话",
"com_ui_web_search": "网络搜索",
"com_ui_web_search_api_subtitle": "搜索网络以获取最新信息",
"com_ui_web_search_cohere_key": "输入 Cohere API Key",
"com_ui_web_search_firecrawl_url": "Firecrawl API URL可选",
"com_ui_web_search_jina_key": "输入 Jina API Key",
"com_ui_web_search_processing": "正在处理结果",
"com_ui_web_search_provider": "搜索提供商",
"com_ui_web_search_provider_searxng": "SearXNG",
"com_ui_web_search_provider_serper": "Serper API",
"com_ui_web_search_provider_serper_key": "获取您的 Serper API Key",
"com_ui_web_search_reading": "正在读取结果",
"com_ui_web_search_reranker": "重排序器",
"com_ui_web_search_reranker_cohere": "Cohere",
"com_ui_web_search_reranker_cohere_key": "获取您的 Cohere API Key",
"com_ui_web_search_reranker_jina": "Jina AI",
"com_ui_web_search_reranker_jina_key": "获取您的 Jina API Key",
"com_ui_web_search_scraper": "抓取器",
"com_ui_web_search_scraper_firecrawl": "Firecrawl API",
"com_ui_web_search_scraper_firecrawl_key": "获取您的 Firecrawl API Key",
"com_ui_web_search_searxng_api_key": "输入 SearXNG API Key可选",
"com_ui_web_search_searxng_instance_url": "SearXNG 实例 URL",
"com_ui_web_searching": "正在搜索网络",
"com_ui_web_searching_again": "正在重新搜索网络",
"com_ui_weekend_morning": "周末愉快",
"com_ui_write": "写作",
"com_ui_x_selected": "{{0}} 已选择",

View File

@@ -255,8 +255,6 @@
"com_files_number_selected": "已選取 {{0}} 個檔案,共 {{1}} 個檔案",
"com_generated_files": "已生成的檔案:",
"com_hide_examples": "隱藏範例",
"com_info_heic_converting": "正在將 HEIC 圖片轉換為 JPEG...",
"com_nav_2fa": "雙因子認證 (2FA)",
"com_nav_account_settings": "帳號設定",
"com_nav_always_make_prod": "永遠將新版本設為正式版",
"com_nav_archive_created_at": "建立時間",
@@ -274,7 +272,6 @@
"com_nav_automatic_playback": "自動播放最新訊息",
"com_nav_balance": "餘額",
"com_nav_browser": "瀏覽器",
"com_nav_center_chat_input": "在歡迎畫面置中顯示聊天輸入框",
"com_nav_change_picture": "更換圖片",
"com_nav_chat_commands": "對話指令",
"com_nav_chat_commands_info": "這些指令是透過在訊息開頭輸入特定字元來啟動的。每個指令都由其專屬的前綴觸發。如果您經常在訊息開頭使用這些字元,可以選擇停用這些指令。",
@@ -326,7 +323,6 @@
"com_nav_info_fork_split_target_setting": "啟用時,系統將根據所選的行為,從目標訊息開始分支到對話中的最新訊息。",
"com_nav_info_include_shadcnui": "啟用後,將包含使用 shadcn/ui 元件的相關說明。shadcn/ui 是一個使用 Radix UI 和 Tailwind CSS 建置的可重複使用元件集。注意:這些說明較為冗長,建議僅在需要告知 LLM 正確的匯入方式和元件使用方法時才啟用。若要了解這些元件的更多資訊請造訪https://ui.shadcn.com/",
"com_nav_info_latex_parsing": "啟用時,訊息中的 LaTeX 程式碼將會被轉換為數學方程式。如果您不需要 LaTeX 轉換功能,停用此選項可以改善效能。",
"com_nav_info_save_badges_state": "啟用此功能後,聊天徽章的狀態將會被儲存。這表示當您建立新的對話時,徽章會維持和上一個對話相同的狀態。如果停用此選項,每次建立新的對話時,徽章都會重設為預設狀態。",
"com_nav_info_save_draft": "啟用後,您在聊天表單中輸入的文字和附件將自動儲存為本地草稿。即使重新載入頁面或切換至其他對話,這些草稿仍會保留。草稿僅儲存在您的裝置上,並會在訊息送出後自動刪除。",
"com_nav_info_user_name_display": "啟用時,每則您發送的訊息上方都會顯示您的使用者名稱。停用時,您的訊息上方只會顯示「您」。",
"com_nav_lang_arabic": "العربية",
@@ -372,22 +368,18 @@
"com_nav_plus_command": "指令選項",
"com_nav_plus_command_description": "切換「+」指令以新增多重回應設定",
"com_nav_profile_picture": "個人頭像",
"com_nav_save_badges_state": "儲存徽章狀態",
"com_nav_save_drafts": "儲存本機草稿",
"com_nav_scroll_button": "滾動至底部按鈕",
"com_nav_search_placeholder": "搜尋訊息",
"com_nav_send_message": "傳送訊息",
"com_nav_setting_account": "帳號",
"com_nav_setting_beta": "測試功能",
"com_nav_setting_chat": "聊天",
"com_nav_setting_data": "資料控制",
"com_nav_setting_general": "一般",
"com_nav_setting_mcp": "MCP 設定",
"com_nav_setting_personalization": "個性化",
"com_nav_setting_speech": "語音",
"com_nav_settings": "設定",
"com_nav_shared_links": "共享連結",
"com_nav_show_code": "一律顯示使用程式碼解譯器時的程式碼",
"com_nav_show_thinking": "預設展開思考過程",
"com_nav_slash_command": "/指令",
"com_nav_slash_command_description": "使用鍵盤按下 \"/\" 快速選擇提示詞",
"com_nav_speech_to_text": "語音轉文字",
@@ -416,22 +408,6 @@
"com_sidepanel_hide_panel": "隱藏側邊選單",
"com_sidepanel_manage_files": "管理檔案",
"com_sidepanel_parameters": "參數",
"com_sources_image_alt": "搜尋結果圖片",
"com_sources_more_sources": "+{{count}} 個來源",
"com_sources_tab_all": "全部",
"com_sources_tab_images": "圖片",
"com_sources_tab_news": "新聞",
"com_sources_title": "來源",
"com_ui_2fa_account_security": "雙因子驗證為您的帳戶增添一層額外的安全保護",
"com_ui_2fa_disable": "停用雙因子驗證",
"com_ui_2fa_disable_error": "停用雙因子驗證時發生錯誤",
"com_ui_2fa_disabled": "已停用雙因子驗證",
"com_ui_2fa_enable": "啟用雙因子驗證",
"com_ui_2fa_enabled": "已啟用雙因子驗證",
"com_ui_2fa_generate_error": "產生雙因子驗證設定時發生錯誤",
"com_ui_2fa_invalid": "雙因子驗證碼無效",
"com_ui_2fa_setup": "設定雙因子驗證",
"com_ui_2fa_verified": "雙因子驗證成功",
"com_ui_accept": "我接受",
"com_ui_add": "新增",
"com_ui_add_model_preset": "新增模型或預設設定以取得額外回應",
@@ -512,7 +488,6 @@
"com_ui_copy_to_clipboard": "複製到剪貼簿",
"com_ui_create": "建立",
"com_ui_create_link": "建立連結",
"com_ui_create_memory": "建立記憶",
"com_ui_create_prompt": "建立提示",
"com_ui_custom_prompt_mode": "自訂提示模式",
"com_ui_dashboard": "儀表板",
@@ -591,8 +566,6 @@
"com_ui_fork_split_target_setting": "預設從目標訊息開始分支",
"com_ui_fork_success": "已成功分支對話",
"com_ui_fork_visible": "僅顯示分支訊息",
"com_ui_generate_qrcode": "產生 QR 碼",
"com_ui_generating": "產生中...",
"com_ui_go_to_conversation": "前往對話",
"com_ui_happy_birthday": "這是我的第一個生日!",
"com_ui_host": "主機",
@@ -612,23 +585,6 @@
"com_ui_logo": "{{0}} 標誌",
"com_ui_manage": "管理",
"com_ui_max_tags": "允許的最大數量為 {{0}},已使用最新值。",
"com_ui_mcp_dialog_desc": "請在下方輸入必要資訊。",
"com_ui_mcp_enter_var": "請輸入 {{0}} 的值",
"com_ui_mcp_server_not_found": "找不到伺服器。",
"com_ui_mcp_url": "MCP 伺服器",
"com_ui_memories": "記憶",
"com_ui_memories_allow_create": "允許建立記憶",
"com_ui_memories_allow_opt_out": "允許用戶選擇不使用記憶功能",
"com_ui_memories_allow_read": "允許讀取記憶",
"com_ui_memories_allow_update": "允許更新記憶",
"com_ui_memories_allow_use": "允許使用記憶",
"com_ui_memories_filter": "篩選記憶...",
"com_ui_memory_created": "記憶建立成功",
"com_ui_memory_deleted": "記憶已刪除",
"com_ui_memory_deleted_items": "已刪除的記憶",
"com_ui_memory_key_exists": "已存在具有此鍵值的記憶。請使用不同的鍵值。",
"com_ui_memory_updated": "已更新儲存的記憶",
"com_ui_memory_updated_items": "已更新的記憶",
"com_ui_mention": "提及端點、助理或預設設定以快速切換",
"com_ui_min_tags": "無法再移除更多值,至少需要 {{0}} 個。",
"com_ui_model": "模型",
@@ -666,9 +622,6 @@
"com_ui_prompts_allow_use": "允許使用提示",
"com_ui_provider": "提供者",
"com_ui_read_aloud": "朗讀",
"com_ui_redirecting_to_provider": "正在重新導向至 {{0}},請稍候...",
"com_ui_reference_saved_memories": "參考儲存的記憶",
"com_ui_reference_saved_memories_description": "允許助理在回應時使用記憶",
"com_ui_regenerate": "重新生成",
"com_ui_region": "地區",
"com_ui_rename": "重新命名",
@@ -734,8 +687,6 @@
"com_ui_upload_invalid_var": "上傳的檔案無效。必須是不超過 {{0}} MB 的圖片檔案",
"com_ui_upload_success": "檔案上傳成功",
"com_ui_upload_type": "選擇上傳類型",
"com_ui_usage": "使用率",
"com_ui_use_memory": "使用記憶",
"com_ui_use_micrphone": "使用麥克風",
"com_ui_use_prompt": "使用提示",
"com_ui_variables": "變數",

View File

@@ -1,39 +0,0 @@
import debounce from 'lodash/debounce';
import { LocalStorageKeys } from 'librechat-data-provider';
export const clearDraft = debounce((id?: string | null) => {
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id ?? ''}`);
}, 2500);
export const encodeBase64 = (plainText: string): string => {
try {
const textBytes = new TextEncoder().encode(plainText);
return btoa(String.fromCharCode(...textBytes));
} catch {
return '';
}
};
export const decodeBase64 = (base64String: string): string => {
try {
const bytes = atob(base64String);
const uint8Array = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
uint8Array[i] = bytes.charCodeAt(i);
}
return new TextDecoder().decode(uint8Array);
} catch {
return '';
}
};
export const setDraft = ({ id, value }: { id: string; value?: string }) => {
if (value && value.length > 1) {
localStorage.setItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`, encodeBase64(value));
return;
}
localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`);
};
export const getDraft = (id?: string): string | null =>
decodeBase64((localStorage.getItem(`${LocalStorageKeys.TEXT_DRAFT}${id ?? ''}`) ?? '') || '');

View File

@@ -6,7 +6,6 @@ export * from './files';
export * from './latex';
export * from './theme';
export * from './forms';
export * from './drafts';
export * from './convos';
export * from './presets';
export * from './prompts';

View File

@@ -1,3 +1,3 @@
// v0.7.9
// v0.7.9-rc1
// See .env.test.example for an example of the '.env.test' file.
require('dotenv').config({ path: './e2e/.env.test' });

View File

@@ -22,7 +22,7 @@ version: 1.8.9
# It is recommended to use it with quotes.
# renovate: image=ghcr.io/danny-avila/librechat
appVersion: "v0.7.9"
appVersion: "v0.7.9-rc1"
home: https://www.librechat.ai

View File

@@ -16,10 +16,6 @@ interface:
# MCP Servers UI configuration
mcpServers:
placeholder: 'MCP Servers'
# Enable/disable file search as a chatarea selection (default: true)
# Note: This setting does not disable the Agents File Search Capability.
# To disable the Agents Capability, see the Agents Endpoint configuration instead.
fileSearch: true
# Privacy policy settings
privacyPolicy:
externalUrl: 'https://librechat.ai/privacy-policy'

4415
package-lock.json generated

File diff suppressed because it is too large Load Diff

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