Compare commits
64 Commits
fix/mcp-va
...
feat/add-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d0442531 | ||
|
|
274987712c | ||
|
|
888e3a31cf | ||
|
|
21e00168b1 | ||
|
|
7ea23c5a7d | ||
|
|
f4833b6b25 | ||
|
|
d37db43e29 | ||
|
|
eec10bf745 | ||
|
|
3508839d6d | ||
|
|
a8babbcebf | ||
|
|
da3730b7d6 | ||
|
|
770c766d50 | ||
|
|
5eb6926464 | ||
|
|
e478ae1c28 | ||
|
|
0c9284c8ae | ||
|
|
4eeadddfe6 | ||
|
|
9ca1847535 | ||
|
|
5d0bc95193 | ||
|
|
e7d6100fe4 | ||
|
|
01a95229f2 | ||
|
|
0939250f07 | ||
|
|
7147bce3c3 | ||
|
|
486fe34a2b | ||
|
|
922f43f520 | ||
|
|
e6fa01d514 | ||
|
|
8238fb49e0 | ||
|
|
430557676d | ||
|
|
8a5047c456 | ||
|
|
c787515894 | ||
|
|
d95d8032cc | ||
|
|
b9f72f4869 | ||
|
|
429bb6653a | ||
|
|
47caafa8f8 | ||
|
|
8530594f37 | ||
|
|
0b071c06f6 | ||
|
|
1092392ed8 | ||
|
|
36c8947029 | ||
|
|
4175a3ea19 | ||
|
|
02dc71f4b7 | ||
|
|
a6c99a3267 | ||
|
|
fcefc6eedf | ||
|
|
dfdafdbd09 | ||
|
|
33834cd484 | ||
|
|
7ef2c626e2 | ||
|
|
bc43423f58 | ||
|
|
863401bcdf | ||
|
|
33c8b87edd | ||
|
|
077248a8a7 | ||
|
|
c6fb4686ef | ||
|
|
f1c6e4d55e | ||
|
|
e192c99c7d | ||
|
|
056172f007 | ||
|
|
5eed5009e9 | ||
|
|
6fc9abd4ad | ||
|
|
03a924eaca | ||
|
|
25c993d93e | ||
|
|
09659c1040 | ||
|
|
19a8f5c545 | ||
|
|
1050346915 | ||
|
|
8a1a38f346 | ||
|
|
32081245da | ||
|
|
6fd3b569ac | ||
|
|
6671fcb714 | ||
|
|
c4677ab3fb |
@@ -442,6 +442,8 @@ OPENID_REQUIRED_ROLE_PARAMETER_PATH=
|
||||
OPENID_USERNAME_CLAIM=
|
||||
# Set to determine which user info property returned from OpenID Provider to store as the User's name
|
||||
OPENID_NAME_CLAIM=
|
||||
# Optional audience parameter for OpenID authorization requests
|
||||
OPENID_AUDIENCE=
|
||||
|
||||
OPENID_BUTTON_LABEL=
|
||||
OPENID_IMAGE_URL=
|
||||
|
||||
2
.github/workflows/locize-i18n-sync.yml
vendored
2
.github/workflows/locize-i18n-sync.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
# 2. Download translation files from locize.
|
||||
- name: Download Translations from locize
|
||||
uses: locize/download@v1
|
||||
uses: locize/download@v2
|
||||
with:
|
||||
project-id: ${{ secrets.LOCIZE_PROJECT_ID }}
|
||||
path: "client/src/locales"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,6 +13,9 @@ pids
|
||||
*.seed
|
||||
.git
|
||||
|
||||
# CI/CD data
|
||||
test-image*
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# v0.7.9
|
||||
# v0.8.0-rc1
|
||||
|
||||
# Base node image
|
||||
FROM node:20-alpine AS node
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Dockerfile.multi
|
||||
# v0.7.9
|
||||
# v0.8.0-rc1
|
||||
|
||||
# Base for all builds
|
||||
FROM node:20-alpine AS base-min
|
||||
|
||||
@@ -653,8 +653,10 @@ class OpenAIClient extends BaseClient {
|
||||
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
|
||||
configOptions.baseOptions = {
|
||||
headers: resolveHeaders({
|
||||
...headers,
|
||||
...configOptions?.baseOptions?.headers,
|
||||
headers: {
|
||||
...headers,
|
||||
...configOptions?.baseOptions?.headers,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -749,7 +751,7 @@ class OpenAIClient extends BaseClient {
|
||||
groupMap,
|
||||
});
|
||||
|
||||
this.options.headers = resolveHeaders(headers);
|
||||
this.options.headers = resolveHeaders({ headers });
|
||||
this.options.reverseProxyUrl = baseURL ?? null;
|
||||
this.langchainProxy = extractBaseURL(this.options.reverseProxyUrl);
|
||||
this.apiKey = azureOptions.azureOpenAIApiKey;
|
||||
@@ -1181,7 +1183,7 @@ ${convo}
|
||||
modelGroupMap,
|
||||
groupMap,
|
||||
});
|
||||
opts.defaultHeaders = resolveHeaders(headers);
|
||||
opts.defaultHeaders = resolveHeaders({ headers });
|
||||
this.langchainProxy = extractBaseURL(baseURL);
|
||||
this.apiKey = azureOptions.azureOpenAIApiKey;
|
||||
|
||||
@@ -1222,7 +1224,9 @@ ${convo}
|
||||
}
|
||||
|
||||
if (this.isOmni === true && modelOptions.max_tokens != null) {
|
||||
modelOptions.max_completion_tokens = modelOptions.max_tokens;
|
||||
const paramName =
|
||||
modelOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
|
||||
modelOptions[paramName] = modelOptions.max_tokens;
|
||||
delete modelOptions.max_tokens;
|
||||
}
|
||||
if (this.isOmni === true && modelOptions.temperature != null) {
|
||||
|
||||
@@ -316,17 +316,10 @@ const updateAgent = async (searchParameter, updateData, options = {}) => {
|
||||
if (shouldCreateVersion) {
|
||||
const duplicateVersion = isDuplicateVersion(updateData, versionData, versions, actionsHash);
|
||||
if (duplicateVersion && !forceVersion) {
|
||||
const error = new Error(
|
||||
'Duplicate version: This would create a version identical to an existing one',
|
||||
);
|
||||
error.statusCode = 409;
|
||||
error.details = {
|
||||
duplicateVersion,
|
||||
versionIndex: versions.findIndex(
|
||||
(v) => JSON.stringify(duplicateVersion) === JSON.stringify(v),
|
||||
),
|
||||
};
|
||||
throw error;
|
||||
// No changes detected, return the current agent without creating a new version
|
||||
const agentObj = currentAgent.toObject();
|
||||
agentObj.version = versions.length;
|
||||
return agentObj;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -879,45 +879,31 @@ describe('models/Agent', () => {
|
||||
expect(emptyParamsAgent.model_parameters).toEqual({});
|
||||
});
|
||||
|
||||
test('should detect duplicate versions and reject updates', async () => {
|
||||
const originalConsoleError = console.error;
|
||||
console.error = jest.fn();
|
||||
test('should not create new version for duplicate updates', async () => {
|
||||
const authorId = new mongoose.Types.ObjectId();
|
||||
const testCases = generateVersionTestCases();
|
||||
|
||||
try {
|
||||
const authorId = new mongoose.Types.ObjectId();
|
||||
const testCases = generateVersionTestCases();
|
||||
for (const testCase of testCases) {
|
||||
const testAgentId = `agent_${uuidv4()}`;
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const testAgentId = `agent_${uuidv4()}`;
|
||||
await createAgent({
|
||||
id: testAgentId,
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
author: authorId,
|
||||
...testCase.initial,
|
||||
});
|
||||
|
||||
await createAgent({
|
||||
id: testAgentId,
|
||||
provider: 'test',
|
||||
model: 'test-model',
|
||||
author: authorId,
|
||||
...testCase.initial,
|
||||
});
|
||||
const updatedAgent = await updateAgent({ id: testAgentId }, testCase.update);
|
||||
expect(updatedAgent.versions).toHaveLength(2); // No new version created
|
||||
|
||||
await updateAgent({ id: testAgentId }, testCase.update);
|
||||
// Update with duplicate data should succeed but not create a new version
|
||||
const duplicateUpdate = await updateAgent({ id: testAgentId }, testCase.duplicate);
|
||||
|
||||
let error;
|
||||
try {
|
||||
await updateAgent({ id: testAgentId }, testCase.duplicate);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(duplicateUpdate.versions).toHaveLength(2); // No new version created
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toContain('Duplicate version');
|
||||
expect(error.statusCode).toBe(409);
|
||||
expect(error.details).toBeDefined();
|
||||
expect(error.details.duplicateVersion).toBeDefined();
|
||||
|
||||
const agent = await getAgent({ id: testAgentId });
|
||||
expect(agent.versions).toHaveLength(2);
|
||||
}
|
||||
} finally {
|
||||
console.error = originalConsoleError;
|
||||
const agent = await getAgent({ id: testAgentId });
|
||||
expect(agent.versions).toHaveLength(2);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1093,20 +1079,13 @@ describe('models/Agent', () => {
|
||||
expect(secondUpdate.versions).toHaveLength(3);
|
||||
|
||||
// Update without forceVersion and no changes should not create a version
|
||||
let error;
|
||||
try {
|
||||
await updateAgent(
|
||||
{ id: agentId },
|
||||
{ tools: ['listEvents_action_test.com', 'createEvent_action_test.com'] },
|
||||
{ updatingUserId: authorId.toString(), forceVersion: false },
|
||||
);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
const duplicateUpdate = await updateAgent(
|
||||
{ id: agentId },
|
||||
{ tools: ['listEvents_action_test.com', 'createEvent_action_test.com'] },
|
||||
{ updatingUserId: authorId.toString(), forceVersion: false },
|
||||
);
|
||||
|
||||
expect(error).toBeDefined();
|
||||
expect(error.message).toContain('Duplicate version');
|
||||
expect(error.statusCode).toBe(409);
|
||||
expect(duplicateUpdate.versions).toHaveLength(3); // No new version created
|
||||
});
|
||||
|
||||
test('should handle isDuplicateVersion with arrays containing null/undefined values', async () => {
|
||||
@@ -2400,11 +2379,18 @@ describe('models/Agent', () => {
|
||||
agent_ids: ['agent1', 'agent2'],
|
||||
});
|
||||
|
||||
await updateAgent({ id: agentId }, { agent_ids: ['agent1', 'agent2', 'agent3'] });
|
||||
const updatedAgent = await updateAgent(
|
||||
{ id: agentId },
|
||||
{ agent_ids: ['agent1', 'agent2', 'agent3'] },
|
||||
);
|
||||
expect(updatedAgent.versions).toHaveLength(2);
|
||||
|
||||
await expect(
|
||||
updateAgent({ id: agentId }, { agent_ids: ['agent1', 'agent2', 'agent3'] }),
|
||||
).rejects.toThrow('Duplicate version');
|
||||
// Update with same agent_ids should succeed but not create a new version
|
||||
const duplicateUpdate = await updateAgent(
|
||||
{ id: agentId },
|
||||
{ agent_ids: ['agent1', 'agent2', 'agent3'] },
|
||||
);
|
||||
expect(duplicateUpdate.versions).toHaveLength(2); // No new version created
|
||||
});
|
||||
|
||||
test('should handle agent_ids field alongside other fields', async () => {
|
||||
@@ -2543,9 +2529,10 @@ describe('models/Agent', () => {
|
||||
expect(updated.versions).toHaveLength(2);
|
||||
expect(updated.agent_ids).toEqual([]);
|
||||
|
||||
await expect(updateAgent({ id: agentId }, { agent_ids: [] })).rejects.toThrow(
|
||||
'Duplicate version',
|
||||
);
|
||||
// Update with same empty agent_ids should succeed but not create a new version
|
||||
const duplicateUpdate = await updateAgent({ id: agentId }, { agent_ids: [] });
|
||||
expect(duplicateUpdate.versions).toHaveLength(2); // No new version created
|
||||
expect(duplicateUpdate.agent_ids).toEqual([]);
|
||||
});
|
||||
|
||||
test('should handle agent without agent_ids field', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { matchModelName } = require('../utils');
|
||||
const { matchModelName } = require('../utils/tokens');
|
||||
const defaultRate = 6;
|
||||
|
||||
/**
|
||||
@@ -87,6 +87,9 @@ const tokenValues = Object.assign(
|
||||
'gpt-4.1': { prompt: 2, completion: 8 },
|
||||
'gpt-4.5': { prompt: 75, completion: 150 },
|
||||
'gpt-4o-mini': { prompt: 0.15, completion: 0.6 },
|
||||
'gpt-5': { prompt: 1.25, completion: 10 },
|
||||
'gpt-5-mini': { prompt: 0.25, completion: 2 },
|
||||
'gpt-5-nano': { prompt: 0.05, completion: 0.4 },
|
||||
'gpt-4o': { prompt: 2.5, completion: 10 },
|
||||
'gpt-4o-2024-05-13': { prompt: 5, completion: 15 },
|
||||
'gpt-4-1106': { prompt: 10, completion: 30 },
|
||||
@@ -147,6 +150,9 @@ const tokenValues = Object.assign(
|
||||
codestral: { prompt: 0.3, completion: 0.9 },
|
||||
'ministral-8b': { prompt: 0.1, completion: 0.1 },
|
||||
'ministral-3b': { prompt: 0.04, completion: 0.04 },
|
||||
// GPT-OSS models
|
||||
'gpt-oss-20b': { prompt: 0.05, completion: 0.2 },
|
||||
'gpt-oss-120b': { prompt: 0.15, completion: 0.6 },
|
||||
},
|
||||
bedrockValues,
|
||||
);
|
||||
@@ -214,6 +220,12 @@ const getValueKey = (model, endpoint) => {
|
||||
return 'gpt-4.1';
|
||||
} else if (modelName.includes('gpt-4o-2024-05-13')) {
|
||||
return 'gpt-4o-2024-05-13';
|
||||
} else if (modelName.includes('gpt-5-nano')) {
|
||||
return 'gpt-5-nano';
|
||||
} else if (modelName.includes('gpt-5-mini')) {
|
||||
return 'gpt-5-mini';
|
||||
} else if (modelName.includes('gpt-5')) {
|
||||
return 'gpt-5';
|
||||
} else if (modelName.includes('gpt-4o-mini')) {
|
||||
return 'gpt-4o-mini';
|
||||
} else if (modelName.includes('gpt-4o')) {
|
||||
|
||||
@@ -25,8 +25,14 @@ describe('getValueKey', () => {
|
||||
expect(getValueKey('gpt-4-some-other-info')).toBe('8k');
|
||||
});
|
||||
|
||||
it('should return undefined for model names that do not match any known patterns', () => {
|
||||
expect(getValueKey('gpt-5-some-other-info')).toBeUndefined();
|
||||
it('should return "gpt-5" for model name containing "gpt-5"', () => {
|
||||
expect(getValueKey('gpt-5-some-other-info')).toBe('gpt-5');
|
||||
expect(getValueKey('gpt-5-2025-01-30')).toBe('gpt-5');
|
||||
expect(getValueKey('gpt-5-2025-01-30-0130')).toBe('gpt-5');
|
||||
expect(getValueKey('openai/gpt-5')).toBe('gpt-5');
|
||||
expect(getValueKey('openai/gpt-5-2025-01-30')).toBe('gpt-5');
|
||||
expect(getValueKey('gpt-5-turbo')).toBe('gpt-5');
|
||||
expect(getValueKey('gpt-5-0130')).toBe('gpt-5');
|
||||
});
|
||||
|
||||
it('should return "gpt-3.5-turbo-1106" for model name containing "gpt-3.5-turbo-1106"', () => {
|
||||
@@ -84,6 +90,29 @@ describe('getValueKey', () => {
|
||||
expect(getValueKey('gpt-4.1-nano-0125')).toBe('gpt-4.1-nano');
|
||||
});
|
||||
|
||||
it('should return "gpt-5" for model type of "gpt-5"', () => {
|
||||
expect(getValueKey('gpt-5-2025-01-30')).toBe('gpt-5');
|
||||
expect(getValueKey('gpt-5-2025-01-30-0130')).toBe('gpt-5');
|
||||
expect(getValueKey('openai/gpt-5')).toBe('gpt-5');
|
||||
expect(getValueKey('openai/gpt-5-2025-01-30')).toBe('gpt-5');
|
||||
expect(getValueKey('gpt-5-turbo')).toBe('gpt-5');
|
||||
expect(getValueKey('gpt-5-0130')).toBe('gpt-5');
|
||||
});
|
||||
|
||||
it('should return "gpt-5-mini" for model type of "gpt-5-mini"', () => {
|
||||
expect(getValueKey('gpt-5-mini-2025-01-30')).toBe('gpt-5-mini');
|
||||
expect(getValueKey('openai/gpt-5-mini')).toBe('gpt-5-mini');
|
||||
expect(getValueKey('gpt-5-mini-0130')).toBe('gpt-5-mini');
|
||||
expect(getValueKey('gpt-5-mini-2025-01-30-0130')).toBe('gpt-5-mini');
|
||||
});
|
||||
|
||||
it('should return "gpt-5-nano" for model type of "gpt-5-nano"', () => {
|
||||
expect(getValueKey('gpt-5-nano-2025-01-30')).toBe('gpt-5-nano');
|
||||
expect(getValueKey('openai/gpt-5-nano')).toBe('gpt-5-nano');
|
||||
expect(getValueKey('gpt-5-nano-0130')).toBe('gpt-5-nano');
|
||||
expect(getValueKey('gpt-5-nano-2025-01-30-0130')).toBe('gpt-5-nano');
|
||||
});
|
||||
|
||||
it('should return "gpt-4o" for model type of "gpt-4o"', () => {
|
||||
expect(getValueKey('gpt-4o-2024-08-06')).toBe('gpt-4o');
|
||||
expect(getValueKey('gpt-4o-2024-08-06-0718')).toBe('gpt-4o');
|
||||
@@ -207,6 +236,48 @@ describe('getMultiplier', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct multiplier for gpt-5', () => {
|
||||
const valueKey = getValueKey('gpt-5-2025-01-30');
|
||||
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5'].prompt);
|
||||
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
||||
tokenValues['gpt-5'].completion,
|
||||
);
|
||||
expect(getMultiplier({ model: 'gpt-5-preview', tokenType: 'prompt' })).toBe(
|
||||
tokenValues['gpt-5'].prompt,
|
||||
);
|
||||
expect(getMultiplier({ model: 'openai/gpt-5', tokenType: 'completion' })).toBe(
|
||||
tokenValues['gpt-5'].completion,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct multiplier for gpt-5-mini', () => {
|
||||
const valueKey = getValueKey('gpt-5-mini-2025-01-30');
|
||||
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5-mini'].prompt);
|
||||
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
||||
tokenValues['gpt-5-mini'].completion,
|
||||
);
|
||||
expect(getMultiplier({ model: 'gpt-5-mini-preview', tokenType: 'prompt' })).toBe(
|
||||
tokenValues['gpt-5-mini'].prompt,
|
||||
);
|
||||
expect(getMultiplier({ model: 'openai/gpt-5-mini', tokenType: 'completion' })).toBe(
|
||||
tokenValues['gpt-5-mini'].completion,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct multiplier for gpt-5-nano', () => {
|
||||
const valueKey = getValueKey('gpt-5-nano-2025-01-30');
|
||||
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-5-nano'].prompt);
|
||||
expect(getMultiplier({ valueKey, tokenType: 'completion' })).toBe(
|
||||
tokenValues['gpt-5-nano'].completion,
|
||||
);
|
||||
expect(getMultiplier({ model: 'gpt-5-nano-preview', tokenType: 'prompt' })).toBe(
|
||||
tokenValues['gpt-5-nano'].prompt,
|
||||
);
|
||||
expect(getMultiplier({ model: 'openai/gpt-5-nano', tokenType: 'completion' })).toBe(
|
||||
tokenValues['gpt-5-nano'].completion,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the correct multiplier for gpt-4o', () => {
|
||||
const valueKey = getValueKey('gpt-4o-2024-08-06');
|
||||
expect(getMultiplier({ valueKey, tokenType: 'prompt' })).toBe(tokenValues['gpt-4o'].prompt);
|
||||
@@ -307,10 +378,22 @@ describe('getMultiplier', () => {
|
||||
});
|
||||
|
||||
it('should return defaultRate if derived valueKey does not match any known patterns', () => {
|
||||
expect(getMultiplier({ tokenType: 'prompt', model: 'gpt-5-some-other-info' })).toBe(
|
||||
expect(getMultiplier({ tokenType: 'prompt', model: 'gpt-10-some-other-info' })).toBe(
|
||||
defaultRate,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct multipliers for GPT-OSS models', () => {
|
||||
const models = ['gpt-oss-20b', 'gpt-oss-120b'];
|
||||
models.forEach((key) => {
|
||||
const expectedPrompt = tokenValues[key].prompt;
|
||||
const expectedCompletion = tokenValues[key].completion;
|
||||
expect(getMultiplier({ valueKey: key, tokenType: 'prompt' })).toBe(expectedPrompt);
|
||||
expect(getMultiplier({ valueKey: key, tokenType: 'completion' })).toBe(expectedCompletion);
|
||||
expect(getMultiplier({ model: key, tokenType: 'prompt' })).toBe(expectedPrompt);
|
||||
expect(getMultiplier({ model: key, tokenType: 'completion' })).toBe(expectedCompletion);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AWS Bedrock Model Tests', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/backend",
|
||||
"version": "v0.7.9",
|
||||
"version": "v0.8.0-rc1",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "echo 'please run this from the root directory'",
|
||||
@@ -49,10 +49,10 @@
|
||||
"@langchain/google-vertexai": "^0.2.13",
|
||||
"@langchain/openai": "^0.5.18",
|
||||
"@langchain/textsplitters": "^0.1.0",
|
||||
"@librechat/agents": "^2.4.68",
|
||||
"@librechat/agents": "^2.4.75",
|
||||
"@librechat/api": "*",
|
||||
"@librechat/data-schemas": "*",
|
||||
"@modelcontextprotocol/sdk": "^1.17.0",
|
||||
"@modelcontextprotocol/sdk": "^1.17.1",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||
"axios": "^1.8.2",
|
||||
|
||||
@@ -1,54 +1,16 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys, AuthType, Constants } = require('librechat-data-provider');
|
||||
const { CacheKeys, Constants } = require('librechat-data-provider');
|
||||
const {
|
||||
getToolkitKey,
|
||||
checkPluginAuth,
|
||||
filterUniquePlugins,
|
||||
convertMCPToolsToPlugins,
|
||||
} = require('@librechat/api');
|
||||
const { getCustomConfig, getCachedTools } = require('~/server/services/Config');
|
||||
const { getToolkitKey } = require('~/server/services/ToolService');
|
||||
const { availableTools, toolkits } = require('~/app/clients/tools');
|
||||
const { getMCPManager, getFlowStateManager } = require('~/config');
|
||||
const { availableTools } = require('~/app/clients/tools');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
/**
|
||||
* Filters out duplicate plugins from the list of plugins.
|
||||
*
|
||||
* @param {TPlugin[]} plugins The list of plugins to filter.
|
||||
* @returns {TPlugin[]} The list of plugins with duplicates removed.
|
||||
*/
|
||||
const filterUniquePlugins = (plugins) => {
|
||||
const seen = new Set();
|
||||
return plugins.filter((plugin) => {
|
||||
const duplicate = seen.has(plugin.pluginKey);
|
||||
seen.add(plugin.pluginKey);
|
||||
return !duplicate;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if a plugin is authenticated by checking if all required authentication fields have non-empty values.
|
||||
* Supports alternate authentication fields, allowing validation against multiple possible environment variables.
|
||||
*
|
||||
* @param {TPlugin} plugin The plugin object containing the authentication configuration.
|
||||
* @returns {boolean} True if the plugin is authenticated for all required fields, false otherwise.
|
||||
*/
|
||||
const checkPluginAuth = (plugin) => {
|
||||
if (!plugin.authConfig || plugin.authConfig.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return plugin.authConfig.every((authFieldObj) => {
|
||||
const authFieldOptions = authFieldObj.authField.split('||');
|
||||
let isFieldAuthenticated = false;
|
||||
|
||||
for (const fieldOption of authFieldOptions) {
|
||||
const envValue = process.env[fieldOption];
|
||||
if (envValue && envValue.trim() !== '' && envValue !== AuthType.USER_PROVIDED) {
|
||||
isFieldAuthenticated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isFieldAuthenticated;
|
||||
});
|
||||
};
|
||||
|
||||
const getAvailablePluginsController = async (req, res) => {
|
||||
try {
|
||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||
@@ -143,9 +105,9 @@ 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 = convertMCPToolsToPlugins({ functionTools: cachedUserTools, customConfig });
|
||||
|
||||
if (cachedToolsArray && userPlugins) {
|
||||
if (cachedToolsArray != null && userPlugins != null) {
|
||||
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
|
||||
res.status(200).json(dedupedTools);
|
||||
return;
|
||||
@@ -185,7 +147,9 @@ const getAvailableTools = async (req, res) => {
|
||||
const isToolDefined = toolDefinitions[plugin.pluginKey] !== undefined;
|
||||
const isToolkit =
|
||||
plugin.toolkit === true &&
|
||||
Object.keys(toolDefinitions).some((key) => getToolkitKey(key) === plugin.pluginKey);
|
||||
Object.keys(toolDefinitions).some(
|
||||
(key) => getToolkitKey({ toolkits, toolName: key }) === plugin.pluginKey,
|
||||
);
|
||||
|
||||
if (!isToolDefined && !isToolkit) {
|
||||
continue;
|
||||
@@ -235,58 +199,6 @@ const getAvailableTools = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts MCP function format tools to plugin format
|
||||
* @param {Object} functionTools - Object with function format tools
|
||||
* @param {Object} customConfig - Custom configuration for MCP servers
|
||||
* @returns {Array} Array of plugin objects
|
||||
*/
|
||||
function convertMCPToolsToPlugins(functionTools, customConfig) {
|
||||
const plugins = [];
|
||||
|
||||
for (const [toolKey, toolData] of Object.entries(functionTools)) {
|
||||
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const functionData = toolData.function;
|
||||
const parts = toolKey.split(Constants.mcp_delimiter);
|
||||
const serverName = parts[parts.length - 1];
|
||||
|
||||
const serverConfig = customConfig?.mcpServers?.[serverName];
|
||||
|
||||
const plugin = {
|
||||
name: parts[0], // Use the tool name without server suffix
|
||||
pluginKey: toolKey,
|
||||
description: functionData.description || '',
|
||||
authenticated: true,
|
||||
icon: serverConfig?.iconPath,
|
||||
};
|
||||
|
||||
// Build authConfig for MCP tools
|
||||
if (!serverConfig?.customUserVars) {
|
||||
plugin.authConfig = [];
|
||||
plugins.push(plugin);
|
||||
continue;
|
||||
}
|
||||
|
||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
||||
if (customVarKeys.length === 0) {
|
||||
plugin.authConfig = [];
|
||||
} else {
|
||||
plugin.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
|
||||
authField: key,
|
||||
label: value.title || key,
|
||||
description: value.description || '',
|
||||
}));
|
||||
}
|
||||
|
||||
plugins.push(plugin);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAvailableTools,
|
||||
getAvailablePluginsController,
|
||||
|
||||
@@ -28,19 +28,211 @@ jest.mock('~/config', () => ({
|
||||
|
||||
jest.mock('~/app/clients/tools', () => ({
|
||||
availableTools: [],
|
||||
toolkits: [],
|
||||
}));
|
||||
|
||||
jest.mock('~/cache', () => ({
|
||||
getLogStores: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
getToolkitKey: jest.fn(),
|
||||
checkPluginAuth: jest.fn(),
|
||||
filterUniquePlugins: jest.fn(),
|
||||
convertMCPToolsToPlugins: jest.fn(),
|
||||
}));
|
||||
|
||||
// Import the actual module with the function we want to test
|
||||
const { getAvailableTools } = require('./PluginController');
|
||||
const { getAvailableTools, getAvailablePluginsController } = require('./PluginController');
|
||||
const {
|
||||
filterUniquePlugins,
|
||||
checkPluginAuth,
|
||||
convertMCPToolsToPlugins,
|
||||
getToolkitKey,
|
||||
} = require('@librechat/api');
|
||||
|
||||
describe('PluginController', () => {
|
||||
describe('plugin.icon behavior', () => {
|
||||
let mockReq, mockRes, mockCache;
|
||||
let mockReq, mockRes, mockCache;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
describe('getAvailablePluginsController', () => {
|
||||
beforeEach(() => {
|
||||
mockReq.app = { locals: { filteredTools: [], includedTools: [] } };
|
||||
});
|
||||
|
||||
it('should use filterUniquePlugins to remove duplicate plugins', async () => {
|
||||
const mockPlugins = [
|
||||
{ name: 'Plugin1', pluginKey: 'key1', description: 'First' },
|
||||
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
|
||||
];
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
filterUniquePlugins.mockReturnValue(mockPlugins);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
expect(filterUniquePlugins).toHaveBeenCalled();
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
// The response includes authenticated: true for each plugin when checkPluginAuth returns true
|
||||
expect(mockRes.json).toHaveBeenCalledWith([
|
||||
{ name: 'Plugin1', pluginKey: 'key1', description: 'First', authenticated: true },
|
||||
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second', authenticated: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use checkPluginAuth to verify plugin authentication', async () => {
|
||||
const mockPlugin = { name: 'Plugin1', pluginKey: 'key1', description: 'First' };
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
filterUniquePlugins.mockReturnValue([mockPlugin]);
|
||||
checkPluginAuth.mockReturnValueOnce(true);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
expect(checkPluginAuth).toHaveBeenCalledWith(mockPlugin);
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
expect(responseData[0].authenticated).toBe(true);
|
||||
});
|
||||
|
||||
it('should return cached plugins when available', async () => {
|
||||
const cachedPlugins = [
|
||||
{ name: 'CachedPlugin', pluginKey: 'cached', description: 'Cached plugin' },
|
||||
];
|
||||
|
||||
mockCache.get.mockResolvedValue(cachedPlugins);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
expect(filterUniquePlugins).not.toHaveBeenCalled();
|
||||
expect(checkPluginAuth).not.toHaveBeenCalled();
|
||||
expect(mockRes.json).toHaveBeenCalledWith(cachedPlugins);
|
||||
});
|
||||
|
||||
it('should filter plugins based on includedTools', async () => {
|
||||
const mockPlugins = [
|
||||
{ name: 'Plugin1', pluginKey: 'key1', description: 'First' },
|
||||
{ name: 'Plugin2', pluginKey: 'key2', description: 'Second' },
|
||||
];
|
||||
|
||||
mockReq.app.locals.includedTools = ['key1'];
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
filterUniquePlugins.mockReturnValue(mockPlugins);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
expect(responseData).toHaveLength(1);
|
||||
expect(responseData[0].pluginKey).toBe('key1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvailableTools', () => {
|
||||
it('should use convertMCPToolsToPlugins for user-specific MCP tools', async () => {
|
||||
const mockUserTools = {
|
||||
[`tool1${Constants.mcp_delimiter}server1`]: {
|
||||
function: { name: 'tool1', description: 'Tool 1' },
|
||||
},
|
||||
};
|
||||
const mockConvertedPlugins = [
|
||||
{
|
||||
name: 'tool1',
|
||||
pluginKey: `tool1${Constants.mcp_delimiter}server1`,
|
||||
description: 'Tool 1',
|
||||
},
|
||||
];
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||
convertMCPToolsToPlugins.mockReturnValue(mockConvertedPlugins);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||
functionTools: mockUserTools,
|
||||
customConfig: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use filterUniquePlugins to deduplicate combined tools', async () => {
|
||||
const mockUserPlugins = [
|
||||
{ name: 'UserTool', pluginKey: 'user-tool', description: 'User tool' },
|
||||
];
|
||||
const mockManifestPlugins = [
|
||||
{ name: 'ManifestTool', pluginKey: 'manifest-tool', description: 'Manifest tool' },
|
||||
];
|
||||
|
||||
mockCache.get.mockResolvedValue(mockManifestPlugins);
|
||||
getCachedTools.mockResolvedValueOnce({});
|
||||
convertMCPToolsToPlugins.mockReturnValue(mockUserPlugins);
|
||||
filterUniquePlugins.mockReturnValue([...mockUserPlugins, ...mockManifestPlugins]);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
// Should be called to deduplicate the combined array
|
||||
expect(filterUniquePlugins).toHaveBeenLastCalledWith([
|
||||
...mockUserPlugins,
|
||||
...mockManifestPlugins,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use checkPluginAuth to verify authentication status', async () => {
|
||||
const mockPlugin = { name: 'Tool1', pluginKey: 'tool1', description: 'Tool 1' };
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue({});
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
filterUniquePlugins.mockReturnValue([mockPlugin]);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock getCachedTools second call to return tool definitions
|
||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({ tool1: true });
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(checkPluginAuth).toHaveBeenCalledWith(mockPlugin);
|
||||
});
|
||||
|
||||
it('should use getToolkitKey for toolkit validation', async () => {
|
||||
const mockToolkit = {
|
||||
name: 'Toolkit1',
|
||||
pluginKey: 'toolkit1',
|
||||
description: 'Toolkit 1',
|
||||
toolkit: true,
|
||||
};
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue({});
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
filterUniquePlugins.mockReturnValue([mockToolkit]);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
getToolkitKey.mockReturnValue('toolkit1');
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock getCachedTools second call to return tool definitions
|
||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({
|
||||
toolkit1_function: true,
|
||||
});
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(getToolkitKey).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin.icon behavior', () => {
|
||||
const callGetAvailableToolsWithMCPServer = async (mcpServers) => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCustomConfig.mockResolvedValue({ mcpServers });
|
||||
@@ -50,7 +242,22 @@ describe('PluginController', () => {
|
||||
function: { name: 'test-tool', description: 'A test tool' },
|
||||
},
|
||||
};
|
||||
|
||||
const mockConvertedPlugin = {
|
||||
name: 'test-tool',
|
||||
pluginKey: `test-tool${Constants.mcp_delimiter}test-server`,
|
||||
description: 'A test tool',
|
||||
icon: mcpServers['test-server']?.iconPath,
|
||||
authenticated: true,
|
||||
authConfig: [],
|
||||
};
|
||||
|
||||
getCachedTools.mockResolvedValueOnce(functionTools);
|
||||
convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
getToolkitKey.mockReturnValue(undefined);
|
||||
|
||||
getCachedTools.mockResolvedValueOnce({
|
||||
[`test-tool${Constants.mcp_delimiter}test-server`]: true,
|
||||
});
|
||||
@@ -60,14 +267,6 @@ describe('PluginController', () => {
|
||||
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': {
|
||||
@@ -86,4 +285,236 @@ describe('PluginController', () => {
|
||||
expect(testTool.icon).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('helper function integration', () => {
|
||||
it('should properly handle MCP tools with custom user variables', async () => {
|
||||
const customConfig = {
|
||||
mcpServers: {
|
||||
'test-server': {
|
||||
customUserVars: {
|
||||
API_KEY: { title: 'API Key', description: 'Your API key' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// We need to test the actual flow where MCP manager tools are included
|
||||
const mcpManagerTools = [
|
||||
{
|
||||
name: 'tool1',
|
||||
pluginKey: `tool1${Constants.mcp_delimiter}test-server`,
|
||||
description: 'Tool 1',
|
||||
authenticated: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock the MCP manager to return tools
|
||||
const mockMCPManager = {
|
||||
loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools),
|
||||
};
|
||||
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCustomConfig.mockResolvedValue(customConfig);
|
||||
|
||||
// First call returns user tools (empty in this case)
|
||||
getCachedTools.mockResolvedValueOnce({});
|
||||
|
||||
// Mock convertMCPToolsToPlugins to return empty array for user tools
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
|
||||
// Mock filterUniquePlugins to pass through
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||
|
||||
// Mock checkPluginAuth
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
|
||||
// Second call returns tool definitions
|
||||
getCachedTools.mockResolvedValueOnce({
|
||||
[`tool1${Constants.mcp_delimiter}test-server`]: true,
|
||||
});
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
|
||||
// Find the MCP tool in the response
|
||||
const mcpTool = responseData.find(
|
||||
(tool) => tool.pluginKey === `tool1${Constants.mcp_delimiter}test-server`,
|
||||
);
|
||||
|
||||
// The actual implementation adds authConfig and sets authenticated to false when customUserVars exist
|
||||
expect(mcpTool).toBeDefined();
|
||||
expect(mcpTool.authConfig).toEqual([
|
||||
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
||||
]);
|
||||
expect(mcpTool.authenticated).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle error cases gracefully', async () => {
|
||||
mockCache.get.mockRejectedValue(new Error('Cache error'));
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Cache error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases with undefined/null values', () => {
|
||||
it('should handle undefined cache gracefully', async () => {
|
||||
getLogStores.mockReturnValue(undefined);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||
});
|
||||
|
||||
it('should handle null cachedTools and cachedUserTools', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue(null);
|
||||
convertMCPToolsToPlugins.mockReturnValue(undefined);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||
functionTools: null,
|
||||
customConfig: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle when getCachedTools returns undefined', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue(undefined);
|
||||
convertMCPToolsToPlugins.mockReturnValue(undefined);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
|
||||
// Mock getCachedTools to return undefined for both calls
|
||||
getCachedTools.mockReset();
|
||||
getCachedTools.mockResolvedValueOnce(undefined).mockResolvedValueOnce(undefined);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||
functionTools: undefined,
|
||||
customConfig: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle cachedToolsArray and userPlugins both being defined', async () => {
|
||||
const cachedTools = [{ name: 'CachedTool', pluginKey: 'cached-tool', description: 'Cached' }];
|
||||
const userTools = {
|
||||
'user-tool': { function: { name: 'user-tool', description: 'User tool' } },
|
||||
};
|
||||
const userPlugins = [{ name: 'UserTool', pluginKey: 'user-tool', description: 'User tool' }];
|
||||
|
||||
mockCache.get.mockResolvedValue(cachedTools);
|
||||
getCachedTools.mockResolvedValue(userTools);
|
||||
convertMCPToolsToPlugins.mockReturnValue(userPlugins);
|
||||
filterUniquePlugins.mockReturnValue([...userPlugins, ...cachedTools]);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith([...userPlugins, ...cachedTools]);
|
||||
});
|
||||
|
||||
it('should handle empty toolDefinitions object', async () => {
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce({});
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins || []);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
// With empty tool definitions, no tools should be in the final output
|
||||
expect(mockRes.json).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should handle MCP tools without customUserVars', async () => {
|
||||
const customConfig = {
|
||||
mcpServers: {
|
||||
'test-server': {
|
||||
// No customUserVars defined
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockUserTools = {
|
||||
[`tool1${Constants.mcp_delimiter}test-server`]: {
|
||||
function: { name: 'tool1', description: 'Tool 1' },
|
||||
},
|
||||
};
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCustomConfig.mockResolvedValue(customConfig);
|
||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||
|
||||
const mockPlugin = {
|
||||
name: 'tool1',
|
||||
pluginKey: `tool1${Constants.mcp_delimiter}test-server`,
|
||||
description: 'Tool 1',
|
||||
authenticated: true,
|
||||
authConfig: [],
|
||||
};
|
||||
|
||||
convertMCPToolsToPlugins.mockReturnValue([mockPlugin]);
|
||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||
checkPluginAuth.mockReturnValue(true);
|
||||
|
||||
getCachedTools.mockResolvedValueOnce({
|
||||
[`tool1${Constants.mcp_delimiter}test-server`]: true,
|
||||
});
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
const responseData = mockRes.json.mock.calls[0][0];
|
||||
expect(responseData[0].authenticated).toBe(true);
|
||||
// The actual implementation doesn't set authConfig on tools without customUserVars
|
||||
expect(responseData[0].authConfig).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle req.app.locals with undefined filteredTools and includedTools', async () => {
|
||||
mockReq.app = { locals: {} };
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
filterUniquePlugins.mockReturnValue([]);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
|
||||
await getAvailablePluginsController(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
expect(mockRes.json).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should handle toolkit with undefined toolDefinitions keys', async () => {
|
||||
const mockToolkit = {
|
||||
name: 'Toolkit1',
|
||||
pluginKey: 'toolkit1',
|
||||
description: 'Toolkit 1',
|
||||
toolkit: true,
|
||||
};
|
||||
|
||||
mockCache.get.mockResolvedValue(null);
|
||||
getCachedTools.mockResolvedValue({});
|
||||
convertMCPToolsToPlugins.mockReturnValue([]);
|
||||
filterUniquePlugins.mockReturnValue([mockToolkit]);
|
||||
checkPluginAuth.mockReturnValue(false);
|
||||
getToolkitKey.mockReturnValue(undefined);
|
||||
getCustomConfig.mockResolvedValue(null);
|
||||
|
||||
// Mock getCachedTools second call to return null
|
||||
getCachedTools.mockResolvedValueOnce({}).mockResolvedValueOnce(null);
|
||||
|
||||
await getAvailableTools(mockReq, mockRes);
|
||||
|
||||
// Should handle null toolDefinitions gracefully
|
||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { webSearchKeys, extractWebSearchEnvVars } = require('@librechat/api');
|
||||
const { webSearchKeys, extractWebSearchEnvVars, normalizeHttpError } = require('@librechat/api');
|
||||
const {
|
||||
getFiles,
|
||||
updateUser,
|
||||
@@ -89,8 +89,8 @@ const updateUserPluginsController = async (req, res) => {
|
||||
|
||||
if (userPluginsService instanceof Error) {
|
||||
logger.error('[userPluginsService]', userPluginsService);
|
||||
const { status, message } = userPluginsService;
|
||||
res.status(status).send({ message });
|
||||
const { status, message } = normalizeHttpError(userPluginsService);
|
||||
return res.status(status).send({ message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ const updateUserPluginsController = async (req, res) => {
|
||||
authService = await updateUserPluginAuth(user.id, keys[i], pluginKey, values[i]);
|
||||
if (authService instanceof Error) {
|
||||
logger.error('[authService]', authService);
|
||||
({ status, message } = authService);
|
||||
({ status, message } = normalizeHttpError(authService));
|
||||
}
|
||||
}
|
||||
} else if (action === 'uninstall') {
|
||||
@@ -151,7 +151,7 @@ const updateUserPluginsController = async (req, res) => {
|
||||
`[authService] Error deleting all auth for MCP tool ${pluginKey}:`,
|
||||
authService,
|
||||
);
|
||||
({ status, message } = authService);
|
||||
({ status, message } = normalizeHttpError(authService));
|
||||
}
|
||||
} else {
|
||||
// This handles:
|
||||
@@ -163,7 +163,7 @@ const updateUserPluginsController = async (req, res) => {
|
||||
authService = await deleteUserPluginAuth(user.id, keys[i]); // Deletes by authField name
|
||||
if (authService instanceof Error) {
|
||||
logger.error('[authService] Error deleting specific auth key:', authService);
|
||||
({ status, message } = authService);
|
||||
({ status, message } = normalizeHttpError(authService));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +193,8 @@ const updateUserPluginsController = async (req, res) => {
|
||||
return res.status(status).send();
|
||||
}
|
||||
|
||||
res.status(status).send({ message });
|
||||
const normalized = normalizeHttpError({ status, message });
|
||||
return res.status(normalized.status).send({ message: normalized.message });
|
||||
} catch (err) {
|
||||
logger.error('[updateUserPluginsController]', err);
|
||||
return res.status(500).json({ message: 'Something went wrong.' });
|
||||
|
||||
@@ -402,6 +402,34 @@ class AgentClient extends BaseClient {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promise that resolves with the memory promise result or undefined after a timeout
|
||||
* @param {Promise<(TAttachment | null)[] | undefined>} memoryPromise - The memory promise to await
|
||||
* @param {number} timeoutMs - Timeout in milliseconds (default: 3000)
|
||||
* @returns {Promise<(TAttachment | null)[] | undefined>}
|
||||
*/
|
||||
async awaitMemoryWithTimeout(memoryPromise, timeoutMs = 3000) {
|
||||
if (!memoryPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Memory processing timeout')), timeoutMs),
|
||||
);
|
||||
|
||||
const attachments = await Promise.race([memoryPromise, timeoutPromise]);
|
||||
return attachments;
|
||||
} catch (error) {
|
||||
if (error.message === 'Memory processing timeout') {
|
||||
logger.warn('[AgentClient] Memory processing timed out after 3 seconds');
|
||||
} else {
|
||||
logger.error('[AgentClient] Error processing memory:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string | undefined>}
|
||||
*/
|
||||
@@ -512,6 +540,39 @@ class AgentClient extends BaseClient {
|
||||
return withoutKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out image URLs from message content
|
||||
* @param {BaseMessage} message - The message to filter
|
||||
* @returns {BaseMessage} - A new message with image URLs removed
|
||||
*/
|
||||
filterImageUrls(message) {
|
||||
if (!message.content || typeof message.content === 'string') {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const filteredContent = message.content.filter(
|
||||
(part) => part.type !== ContentTypes.IMAGE_URL,
|
||||
);
|
||||
|
||||
if (filteredContent.length === 1 && filteredContent[0].type === ContentTypes.TEXT) {
|
||||
const MessageClass = message.constructor;
|
||||
return new MessageClass({
|
||||
content: filteredContent[0].text,
|
||||
additional_kwargs: message.additional_kwargs,
|
||||
});
|
||||
}
|
||||
|
||||
const MessageClass = message.constructor;
|
||||
return new MessageClass({
|
||||
content: filteredContent,
|
||||
additional_kwargs: message.additional_kwargs,
|
||||
});
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BaseMessage[]} messages
|
||||
* @returns {Promise<void | (TAttachment | null)[]>}
|
||||
@@ -540,7 +601,8 @@ class AgentClient extends BaseClient {
|
||||
}
|
||||
}
|
||||
|
||||
const bufferString = getBufferString(messagesToProcess);
|
||||
const filteredMessages = messagesToProcess.map((msg) => this.filterImageUrls(msg));
|
||||
const bufferString = getBufferString(filteredMessages);
|
||||
const bufferMessage = new HumanMessage(`# Current Chat:\n\n${bufferString}`);
|
||||
return await this.processMemory([bufferMessage]);
|
||||
} catch (error) {
|
||||
@@ -968,11 +1030,9 @@ class AgentClient extends BaseClient {
|
||||
});
|
||||
|
||||
try {
|
||||
if (memoryPromise) {
|
||||
const attachments = await memoryPromise;
|
||||
if (attachments && attachments.length > 0) {
|
||||
this.artifactPromises.push(...attachments);
|
||||
}
|
||||
const attachments = await this.awaitMemoryWithTimeout(memoryPromise);
|
||||
if (attachments && attachments.length > 0) {
|
||||
this.artifactPromises.push(...attachments);
|
||||
}
|
||||
await this.recordCollectedUsage({ context: 'message' });
|
||||
} catch (err) {
|
||||
@@ -982,11 +1042,9 @@ class AgentClient extends BaseClient {
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (memoryPromise) {
|
||||
const attachments = await memoryPromise;
|
||||
if (attachments && attachments.length > 0) {
|
||||
this.artifactPromises.push(...attachments);
|
||||
}
|
||||
const attachments = await this.awaitMemoryWithTimeout(memoryPromise);
|
||||
if (attachments && attachments.length > 0) {
|
||||
this.artifactPromises.push(...attachments);
|
||||
}
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #sendCompletion] Operation aborted',
|
||||
@@ -1088,11 +1146,16 @@ class AgentClient extends BaseClient {
|
||||
clientOptions.configuration = options.configOptions;
|
||||
}
|
||||
|
||||
// Ensure maxTokens is set for non-o1 models
|
||||
if (!/\b(o\d)\b/i.test(clientOptions.model) && !clientOptions.maxTokens) {
|
||||
clientOptions.maxTokens = 75;
|
||||
} else if (/\b(o\d)\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
|
||||
const shouldRemoveMaxTokens = /\b(o\d|gpt-[5-9])\b/i.test(clientOptions.model);
|
||||
if (shouldRemoveMaxTokens && clientOptions.maxTokens != null) {
|
||||
delete clientOptions.maxTokens;
|
||||
} else if (!shouldRemoveMaxTokens && !clientOptions.maxTokens) {
|
||||
clientOptions.maxTokens = 75;
|
||||
}
|
||||
if (shouldRemoveMaxTokens && clientOptions?.modelKwargs?.max_completion_tokens != null) {
|
||||
delete clientOptions.modelKwargs.max_completion_tokens;
|
||||
} else if (shouldRemoveMaxTokens && clientOptions?.modelKwargs?.max_output_tokens != null) {
|
||||
delete clientOptions.modelKwargs.max_output_tokens;
|
||||
}
|
||||
|
||||
clientOptions = Object.assign(
|
||||
|
||||
@@ -727,4 +727,464 @@ describe('AgentClient - titleConvo', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptions method - GPT-5+ model handling', () => {
|
||||
let mockReq;
|
||||
let mockRes;
|
||||
let mockAgent;
|
||||
let mockOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockAgent = {
|
||||
id: 'agent-123',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
provider: EModelEndpoint.openAI,
|
||||
model_parameters: {
|
||||
model: 'gpt-5',
|
||||
},
|
||||
};
|
||||
|
||||
mockReq = {
|
||||
app: {
|
||||
locals: {},
|
||||
},
|
||||
user: {
|
||||
id: 'user-123',
|
||||
},
|
||||
};
|
||||
|
||||
mockRes = {};
|
||||
|
||||
mockOptions = {
|
||||
req: mockReq,
|
||||
res: mockRes,
|
||||
agent: mockAgent,
|
||||
};
|
||||
|
||||
client = new AgentClient(mockOptions);
|
||||
});
|
||||
|
||||
it('should move maxTokens to modelKwargs.max_completion_tokens for GPT-5 models', () => {
|
||||
const clientOptions = {
|
||||
model: 'gpt-5',
|
||||
maxTokens: 2048,
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
// Simulate the getOptions logic that handles GPT-5+ models
|
||||
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
|
||||
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
|
||||
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
}
|
||||
|
||||
expect(clientOptions.maxTokens).toBeUndefined();
|
||||
expect(clientOptions.modelKwargs).toBeDefined();
|
||||
expect(clientOptions.modelKwargs.max_completion_tokens).toBe(2048);
|
||||
expect(clientOptions.temperature).toBe(0.7); // Other options should remain
|
||||
});
|
||||
|
||||
it('should move maxTokens to modelKwargs.max_output_tokens for GPT-5 models with useResponsesApi', () => {
|
||||
const clientOptions = {
|
||||
model: 'gpt-5',
|
||||
maxTokens: 2048,
|
||||
temperature: 0.7,
|
||||
useResponsesApi: true,
|
||||
};
|
||||
|
||||
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
|
||||
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
|
||||
const paramName =
|
||||
clientOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
|
||||
clientOptions.modelKwargs[paramName] = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
}
|
||||
|
||||
expect(clientOptions.maxTokens).toBeUndefined();
|
||||
expect(clientOptions.modelKwargs).toBeDefined();
|
||||
expect(clientOptions.modelKwargs.max_output_tokens).toBe(2048);
|
||||
expect(clientOptions.temperature).toBe(0.7); // Other options should remain
|
||||
});
|
||||
|
||||
it('should handle GPT-5+ models with existing modelKwargs', () => {
|
||||
const clientOptions = {
|
||||
model: 'gpt-6',
|
||||
maxTokens: 1500,
|
||||
temperature: 0.8,
|
||||
modelKwargs: {
|
||||
customParam: 'value',
|
||||
},
|
||||
};
|
||||
|
||||
// Simulate the getOptions logic
|
||||
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
|
||||
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
|
||||
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
}
|
||||
|
||||
expect(clientOptions.maxTokens).toBeUndefined();
|
||||
expect(clientOptions.modelKwargs).toEqual({
|
||||
customParam: 'value',
|
||||
max_completion_tokens: 1500,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify maxTokens for non-GPT-5+ models', () => {
|
||||
const clientOptions = {
|
||||
model: 'gpt-4',
|
||||
maxTokens: 2048,
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
// Simulate the getOptions logic
|
||||
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
|
||||
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
|
||||
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
}
|
||||
|
||||
// Should not be modified since it's GPT-4
|
||||
expect(clientOptions.maxTokens).toBe(2048);
|
||||
expect(clientOptions.modelKwargs).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle various GPT-5+ model formats', () => {
|
||||
const testCases = [
|
||||
{ model: 'gpt-5', shouldTransform: true },
|
||||
{ model: 'gpt-5-turbo', shouldTransform: true },
|
||||
{ model: 'gpt-6', shouldTransform: true },
|
||||
{ model: 'gpt-7-preview', shouldTransform: true },
|
||||
{ model: 'gpt-8', shouldTransform: true },
|
||||
{ model: 'gpt-9-mini', shouldTransform: true },
|
||||
{ model: 'gpt-4', shouldTransform: false },
|
||||
{ model: 'gpt-4o', shouldTransform: false },
|
||||
{ model: 'gpt-3.5-turbo', shouldTransform: false },
|
||||
{ model: 'claude-3', shouldTransform: false },
|
||||
];
|
||||
|
||||
testCases.forEach(({ model, shouldTransform }) => {
|
||||
const clientOptions = {
|
||||
model,
|
||||
maxTokens: 1000,
|
||||
};
|
||||
|
||||
// Simulate the getOptions logic
|
||||
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
|
||||
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
|
||||
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
}
|
||||
|
||||
if (shouldTransform) {
|
||||
expect(clientOptions.maxTokens).toBeUndefined();
|
||||
expect(clientOptions.modelKwargs?.max_completion_tokens).toBe(1000);
|
||||
} else {
|
||||
expect(clientOptions.maxTokens).toBe(1000);
|
||||
expect(clientOptions.modelKwargs).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not swap max token param for older models when using useResponsesApi', () => {
|
||||
const testCases = [
|
||||
{ model: 'gpt-5', shouldTransform: true },
|
||||
{ model: 'gpt-5-turbo', shouldTransform: true },
|
||||
{ model: 'gpt-6', shouldTransform: true },
|
||||
{ model: 'gpt-7-preview', shouldTransform: true },
|
||||
{ model: 'gpt-8', shouldTransform: true },
|
||||
{ model: 'gpt-9-mini', shouldTransform: true },
|
||||
{ model: 'gpt-4', shouldTransform: false },
|
||||
{ model: 'gpt-4o', shouldTransform: false },
|
||||
{ model: 'gpt-3.5-turbo', shouldTransform: false },
|
||||
{ model: 'claude-3', shouldTransform: false },
|
||||
];
|
||||
|
||||
testCases.forEach(({ model, shouldTransform }) => {
|
||||
const clientOptions = {
|
||||
model,
|
||||
maxTokens: 1000,
|
||||
useResponsesApi: true,
|
||||
};
|
||||
|
||||
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
|
||||
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
|
||||
const paramName =
|
||||
clientOptions.useResponsesApi === true ? 'max_output_tokens' : 'max_completion_tokens';
|
||||
clientOptions.modelKwargs[paramName] = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
}
|
||||
|
||||
if (shouldTransform) {
|
||||
expect(clientOptions.maxTokens).toBeUndefined();
|
||||
expect(clientOptions.modelKwargs?.max_output_tokens).toBe(1000);
|
||||
} else {
|
||||
expect(clientOptions.maxTokens).toBe(1000);
|
||||
expect(clientOptions.modelKwargs).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not transform if maxTokens is null or undefined', () => {
|
||||
const testCases = [
|
||||
{ model: 'gpt-5', maxTokens: null },
|
||||
{ model: 'gpt-5', maxTokens: undefined },
|
||||
{ model: 'gpt-6', maxTokens: 0 }, // Should transform even if 0
|
||||
];
|
||||
|
||||
testCases.forEach(({ model, maxTokens }, index) => {
|
||||
const clientOptions = {
|
||||
model,
|
||||
maxTokens,
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
// Simulate the getOptions logic
|
||||
if (/\bgpt-[5-9]\b/i.test(clientOptions.model) && clientOptions.maxTokens != null) {
|
||||
clientOptions.modelKwargs = clientOptions.modelKwargs ?? {};
|
||||
clientOptions.modelKwargs.max_completion_tokens = clientOptions.maxTokens;
|
||||
delete clientOptions.maxTokens;
|
||||
}
|
||||
|
||||
if (index < 2) {
|
||||
// null or undefined cases
|
||||
expect(clientOptions.maxTokens).toBe(maxTokens);
|
||||
expect(clientOptions.modelKwargs).toBeUndefined();
|
||||
} else {
|
||||
// 0 case - should transform
|
||||
expect(clientOptions.maxTokens).toBeUndefined();
|
||||
expect(clientOptions.modelKwargs?.max_completion_tokens).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('runMemory method', () => {
|
||||
let client;
|
||||
let mockReq;
|
||||
let mockRes;
|
||||
let mockAgent;
|
||||
let mockOptions;
|
||||
let mockProcessMemory;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockAgent = {
|
||||
id: 'agent-123',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
provider: EModelEndpoint.openAI,
|
||||
model_parameters: {
|
||||
model: 'gpt-4',
|
||||
},
|
||||
};
|
||||
|
||||
mockReq = {
|
||||
app: {
|
||||
locals: {
|
||||
memory: {
|
||||
messageWindowSize: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
id: 'user-123',
|
||||
personalization: {
|
||||
memories: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockRes = {};
|
||||
|
||||
mockOptions = {
|
||||
req: mockReq,
|
||||
res: mockRes,
|
||||
agent: mockAgent,
|
||||
};
|
||||
|
||||
mockProcessMemory = jest.fn().mockResolvedValue([]);
|
||||
|
||||
client = new AgentClient(mockOptions);
|
||||
client.processMemory = mockProcessMemory;
|
||||
client.conversationId = 'convo-123';
|
||||
client.responseMessageId = 'response-123';
|
||||
});
|
||||
|
||||
it('should filter out image URLs from message content', async () => {
|
||||
const { HumanMessage, AIMessage } = require('@langchain/core/messages');
|
||||
const messages = [
|
||||
new HumanMessage({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'What is in this image?',
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
|
||||
detail: 'auto',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
new AIMessage('I can see a small red pixel in the image.'),
|
||||
new HumanMessage({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'What about this one?',
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/',
|
||||
detail: 'high',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
await client.runMemory(messages);
|
||||
|
||||
expect(mockProcessMemory).toHaveBeenCalledTimes(1);
|
||||
const processedMessage = mockProcessMemory.mock.calls[0][0][0];
|
||||
|
||||
// Verify the buffer message was created
|
||||
expect(processedMessage.constructor.name).toBe('HumanMessage');
|
||||
expect(processedMessage.content).toContain('# Current Chat:');
|
||||
|
||||
// Verify that image URLs are not in the buffer string
|
||||
expect(processedMessage.content).not.toContain('image_url');
|
||||
expect(processedMessage.content).not.toContain('data:image');
|
||||
expect(processedMessage.content).not.toContain('base64');
|
||||
|
||||
// Verify text content is preserved
|
||||
expect(processedMessage.content).toContain('What is in this image?');
|
||||
expect(processedMessage.content).toContain('I can see a small red pixel in the image.');
|
||||
expect(processedMessage.content).toContain('What about this one?');
|
||||
});
|
||||
|
||||
it('should handle messages with only text content', async () => {
|
||||
const { HumanMessage, AIMessage } = require('@langchain/core/messages');
|
||||
const messages = [
|
||||
new HumanMessage('Hello, how are you?'),
|
||||
new AIMessage('I am doing well, thank you!'),
|
||||
new HumanMessage('That is great to hear.'),
|
||||
];
|
||||
|
||||
await client.runMemory(messages);
|
||||
|
||||
expect(mockProcessMemory).toHaveBeenCalledTimes(1);
|
||||
const processedMessage = mockProcessMemory.mock.calls[0][0][0];
|
||||
|
||||
expect(processedMessage.content).toContain('Hello, how are you?');
|
||||
expect(processedMessage.content).toContain('I am doing well, thank you!');
|
||||
expect(processedMessage.content).toContain('That is great to hear.');
|
||||
});
|
||||
|
||||
it('should handle mixed content types correctly', async () => {
|
||||
const { HumanMessage } = require('@langchain/core/messages');
|
||||
const { ContentTypes } = require('librechat-data-provider');
|
||||
|
||||
const messages = [
|
||||
new HumanMessage({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Here is some text',
|
||||
},
|
||||
{
|
||||
type: ContentTypes.IMAGE_URL,
|
||||
image_url: {
|
||||
url: 'https://example.com/image.png',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: ' and more text',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
await client.runMemory(messages);
|
||||
|
||||
expect(mockProcessMemory).toHaveBeenCalledTimes(1);
|
||||
const processedMessage = mockProcessMemory.mock.calls[0][0][0];
|
||||
|
||||
// Should contain text parts but not image URLs
|
||||
expect(processedMessage.content).toContain('Here is some text');
|
||||
expect(processedMessage.content).toContain('and more text');
|
||||
expect(processedMessage.content).not.toContain('example.com/image.png');
|
||||
expect(processedMessage.content).not.toContain('IMAGE_URL');
|
||||
});
|
||||
|
||||
it('should preserve original messages without mutation', async () => {
|
||||
const { HumanMessage } = require('@langchain/core/messages');
|
||||
const originalContent = [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Original text',
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: 'data:image/png;base64,ABC123',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const messages = [
|
||||
new HumanMessage({
|
||||
content: [...originalContent],
|
||||
}),
|
||||
];
|
||||
|
||||
await client.runMemory(messages);
|
||||
|
||||
// Verify original message wasn't mutated
|
||||
expect(messages[0].content).toHaveLength(2);
|
||||
expect(messages[0].content[1].type).toBe('image_url');
|
||||
expect(messages[0].content[1].image_url.url).toBe('data:image/png;base64,ABC123');
|
||||
});
|
||||
|
||||
it('should handle message window size correctly', async () => {
|
||||
const { HumanMessage, AIMessage } = require('@langchain/core/messages');
|
||||
const messages = [
|
||||
new HumanMessage('Message 1'),
|
||||
new AIMessage('Response 1'),
|
||||
new HumanMessage('Message 2'),
|
||||
new AIMessage('Response 2'),
|
||||
new HumanMessage('Message 3'),
|
||||
new AIMessage('Response 3'),
|
||||
];
|
||||
|
||||
// Window size is set to 3 in mockReq
|
||||
await client.runMemory(messages);
|
||||
|
||||
expect(mockProcessMemory).toHaveBeenCalledTimes(1);
|
||||
const processedMessage = mockProcessMemory.mock.calls[0][0][0];
|
||||
|
||||
// Should only include last 3 messages due to window size
|
||||
expect(processedMessage.content).toContain('Message 3');
|
||||
expect(processedMessage.content).toContain('Response 3');
|
||||
expect(processedMessage.content).not.toContain('Message 1');
|
||||
expect(processedMessage.content).not.toContain('Response 1');
|
||||
});
|
||||
|
||||
it('should return early if processMemory is not set', async () => {
|
||||
const { HumanMessage } = require('@langchain/core/messages');
|
||||
client.processMemory = null;
|
||||
|
||||
const result = await client.runMemory([new HumanMessage('Test')]);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockProcessMemory).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,8 +105,6 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
|
||||
return res.end();
|
||||
}
|
||||
await cache.delete(cacheKey);
|
||||
// const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
|
||||
// logger.debug(`[${originPath}] Cancelled run:`, cancelledRun);
|
||||
} catch (error) {
|
||||
logger.error(`[${originPath}] Error cancelling run`, error);
|
||||
}
|
||||
@@ -115,7 +113,6 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
|
||||
|
||||
let run;
|
||||
try {
|
||||
// run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
|
||||
await recordUsage({
|
||||
...run.usage,
|
||||
model: run.model,
|
||||
@@ -128,18 +125,9 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
|
||||
|
||||
let finalEvent;
|
||||
try {
|
||||
// const errorContentPart = {
|
||||
// text: {
|
||||
// value:
|
||||
// error?.message ?? 'There was an error processing your request. Please try again later.',
|
||||
// },
|
||||
// type: ContentTypes.ERROR,
|
||||
// };
|
||||
|
||||
finalEvent = {
|
||||
final: true,
|
||||
conversation: await getConvo(req.user.id, conversationId),
|
||||
// runMessages,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`[${originPath}] Error finalizing error process`, error);
|
||||
|
||||
@@ -233,6 +233,26 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Edge case: sendMessage completed but abort happened during sendCompletion
|
||||
// We need to ensure a final event is sent
|
||||
else if (!res.headersSent && !res.finished) {
|
||||
logger.debug(
|
||||
'[AgentController] Handling edge case: `sendMessage` completed but aborted during `sendCompletion`',
|
||||
);
|
||||
|
||||
const finalResponse = { ...response };
|
||||
finalResponse.error = true;
|
||||
|
||||
sendEvent(res, {
|
||||
final: true,
|
||||
conversation,
|
||||
title: conversation.title,
|
||||
requestMessage: userMessage,
|
||||
responseMessage: finalResponse,
|
||||
error: { message: 'Request was aborted during completion' },
|
||||
});
|
||||
res.end();
|
||||
}
|
||||
|
||||
// Save user message if needed
|
||||
if (!client.skipSaveUserMessage) {
|
||||
|
||||
@@ -194,6 +194,9 @@ const updateAgentHandler = async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Add version count to the response
|
||||
updatedAgent.version = updatedAgent.versions ? updatedAgent.versions.length : 0;
|
||||
|
||||
if (updatedAgent.author) {
|
||||
updatedAgent.author = updatedAgent.author.toString();
|
||||
}
|
||||
|
||||
@@ -498,6 +498,28 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
||||
expect(mockRes.json).toHaveBeenCalledWith({ error: 'Agent not found' });
|
||||
});
|
||||
|
||||
test('should include version field in update response', async () => {
|
||||
mockReq.user.id = existingAgentAuthorId.toString();
|
||||
mockReq.params.id = existingAgentId;
|
||||
mockReq.body = {
|
||||
name: 'Updated with Version Check',
|
||||
};
|
||||
|
||||
await updateAgentHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.json).toHaveBeenCalled();
|
||||
const updatedAgent = mockRes.json.mock.calls[0][0];
|
||||
|
||||
// Verify version field is included and is a number
|
||||
expect(updatedAgent).toHaveProperty('version');
|
||||
expect(typeof updatedAgent.version).toBe('number');
|
||||
expect(updatedAgent.version).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Verify in database
|
||||
const agentInDb = await Agent.findOne({ id: existingAgentId });
|
||||
expect(updatedAgent.version).toBe(agentInDb.versions.length);
|
||||
});
|
||||
|
||||
test('should handle validation errors properly', async () => {
|
||||
mockReq.user.id = existingAgentAuthorId.toString();
|
||||
mockReq.params.id = existingAgentId;
|
||||
|
||||
@@ -152,7 +152,7 @@ const chatV1 = async (req, res) => {
|
||||
return res.end();
|
||||
}
|
||||
await cache.delete(cacheKey);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(run_id, { thread_id });
|
||||
logger.debug('[/assistants/chat/] Cancelled run:', cancelledRun);
|
||||
} catch (error) {
|
||||
logger.error('[/assistants/chat/] Error cancelling run', error);
|
||||
@@ -162,7 +162,7 @@ const chatV1 = async (req, res) => {
|
||||
|
||||
let run;
|
||||
try {
|
||||
run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
|
||||
run = await openai.beta.threads.runs.retrieve(run_id, { thread_id });
|
||||
await recordUsage({
|
||||
...run.usage,
|
||||
model: run.model,
|
||||
@@ -623,7 +623,7 @@ const chatV1 = async (req, res) => {
|
||||
|
||||
if (!response.run.usage) {
|
||||
await sleep(3000);
|
||||
completedRun = await openai.beta.threads.runs.retrieve(thread_id, response.run.id);
|
||||
completedRun = await openai.beta.threads.runs.retrieve(response.run.id, { thread_id });
|
||||
if (completedRun.usage) {
|
||||
await recordUsage({
|
||||
...completedRun.usage,
|
||||
|
||||
@@ -467,7 +467,7 @@ const chatV2 = async (req, res) => {
|
||||
|
||||
if (!response.run.usage) {
|
||||
await sleep(3000);
|
||||
completedRun = await openai.beta.threads.runs.retrieve(thread_id, response.run.id);
|
||||
completedRun = await openai.beta.threads.runs.retrieve(response.run.id, { thread_id });
|
||||
if (completedRun.usage) {
|
||||
await recordUsage({
|
||||
...completedRun.usage,
|
||||
|
||||
@@ -108,7 +108,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
|
||||
return res.end();
|
||||
}
|
||||
await cache.delete(cacheKey);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(run_id, { thread_id });
|
||||
logger.debug(`[${originPath}] Cancelled run:`, cancelledRun);
|
||||
} catch (error) {
|
||||
logger.error(`[${originPath}] Error cancelling run`, error);
|
||||
@@ -118,7 +118,7 @@ const createErrorHandler = ({ req, res, getContext, originPath = '/assistants/ch
|
||||
|
||||
let run;
|
||||
try {
|
||||
run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
|
||||
run = await openai.beta.threads.runs.retrieve(run_id, { thread_id });
|
||||
await recordUsage({
|
||||
...run.usage,
|
||||
model: run.model,
|
||||
|
||||
@@ -173,6 +173,16 @@ const listAssistantsForAzure = async ({ req, res, version, azureConfig = {}, que
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes the OpenAI client.
|
||||
* @param {object} params - The parameters object.
|
||||
* @param {ServerRequest} params.req - The request object.
|
||||
* @param {ServerResponse} params.res - The response object.
|
||||
* @param {TEndpointOption} params.endpointOption - The endpoint options.
|
||||
* @param {boolean} params.initAppClient - Whether to initialize the app client.
|
||||
* @param {string} params.overrideEndpoint - The endpoint to override.
|
||||
* @returns {Promise<{ openai: OpenAIClient, openAIApiKey: string; client: import('~/app/clients/OpenAIClient') }>} - The initialized OpenAI client.
|
||||
*/
|
||||
async function getOpenAIClient({ req, res, endpointOption, initAppClient, overrideEndpoint }) {
|
||||
let endpoint = overrideEndpoint ?? req.body.endpoint ?? req.query.endpoint;
|
||||
const version = await getCurrentVersion(req, endpoint);
|
||||
|
||||
@@ -197,7 +197,7 @@ const deleteAssistant = async (req, res) => {
|
||||
await validateAuthor({ req, openai });
|
||||
|
||||
const assistant_id = req.params.id;
|
||||
const deletionStatus = await openai.beta.assistants.del(assistant_id);
|
||||
const deletionStatus = await openai.beta.assistants.delete(assistant_id);
|
||||
if (deletionStatus?.deleted) {
|
||||
await deleteAssistantActions({ req, assistant_id });
|
||||
}
|
||||
@@ -365,7 +365,7 @@ const uploadAssistantAvatar = async (req, res) => {
|
||||
try {
|
||||
await fs.unlink(req.file.path);
|
||||
logger.debug('[/:agent_id/avatar] Temp. image upload file deleted');
|
||||
} catch (error) {
|
||||
} catch {
|
||||
logger.debug('[/:agent_id/avatar] Temp. image upload file already deleted');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ async function abortRun(req, res) {
|
||||
|
||||
try {
|
||||
await cache.set(cacheKey, 'cancelled', three_minutes);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(thread_id, run_id);
|
||||
const cancelledRun = await openai.beta.threads.runs.cancel(run_id, { thread_id });
|
||||
logger.debug('[abortRun] Cancelled run:', cancelledRun);
|
||||
} catch (error) {
|
||||
logger.error('[abortRun] Error cancelling run', error);
|
||||
@@ -60,7 +60,7 @@ async function abortRun(req, res) {
|
||||
}
|
||||
|
||||
try {
|
||||
const run = await openai.beta.threads.runs.retrieve(thread_id, run_id);
|
||||
const run = await openai.beta.threads.runs.retrieve(run_id, { thread_id });
|
||||
await recordUsage({
|
||||
...run.usage,
|
||||
model: run.model,
|
||||
|
||||
1259
api/server/routes/__tests__/mcp.spec.js
Normal file
1259
api/server/routes/__tests__/mcp.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
||||
const express = require('express');
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { CacheKeys, defaultSocialLogins, Constants } = require('librechat-data-provider');
|
||||
const { getCustomConfig } = require('~/server/services/Config/getCustomConfig');
|
||||
const { getLdapConfig } = require('~/server/services/Config/ldap');
|
||||
const { getProjectByName } = require('~/models/Project');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { getMCPManager } = require('~/config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
|
||||
const router = express.Router();
|
||||
@@ -102,11 +103,16 @@ router.get('/', async function (req, res) {
|
||||
payload.mcpServers = {};
|
||||
const config = await getCustomConfig();
|
||||
if (config?.mcpServers != null) {
|
||||
const mcpManager = getMCPManager();
|
||||
const oauthServers = mcpManager.getOAuthServers();
|
||||
|
||||
for (const serverName in config.mcpServers) {
|
||||
const serverConfig = config.mcpServers[serverName];
|
||||
payload.mcpServers[serverName] = {
|
||||
customUserVars: serverConfig?.customUserVars || {},
|
||||
chatMenu: serverConfig?.chatMenu,
|
||||
isOAuth: oauthServers.has(serverName),
|
||||
startup: serverConfig?.startup,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ router.delete('/', async (req, res) => {
|
||||
/** @type {{ openai: OpenAI }} */
|
||||
const { openai } = await assistantClients[endpoint].initializeClient({ req, res });
|
||||
try {
|
||||
const response = await openai.beta.threads.del(thread_id);
|
||||
const response = await openai.beta.threads.delete(thread_id);
|
||||
logger.debug('Deleted OpenAI thread:', response);
|
||||
} catch (error) {
|
||||
logger.error('Error deleting OpenAI thread:', error);
|
||||
|
||||
@@ -413,13 +413,15 @@ router.post('/', async (req, res) => {
|
||||
logger.error('[/files] Error deleting file:', error);
|
||||
}
|
||||
res.status(500).json({ message });
|
||||
}
|
||||
|
||||
if (cleanup) {
|
||||
try {
|
||||
await fs.unlink(req.file.path);
|
||||
} catch (error) {
|
||||
logger.error('[/files] Error deleting file after file processing:', error);
|
||||
} finally {
|
||||
if (cleanup) {
|
||||
try {
|
||||
await fs.unlink(req.file.path);
|
||||
} catch (error) {
|
||||
logger.error('[/files] Error deleting file after file processing:', error);
|
||||
}
|
||||
} else {
|
||||
logger.debug('[/files] File processing completed without cleanup');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -93,7 +93,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
return res.redirect('/oauth/error?error=missing_state');
|
||||
}
|
||||
|
||||
// Extract flow ID from state
|
||||
const flowId = state;
|
||||
logger.debug('[MCP OAuth] Using flow ID from state', { flowId });
|
||||
|
||||
@@ -116,22 +115,17 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
hasCodeVerifier: !!flowState.codeVerifier,
|
||||
});
|
||||
|
||||
// Complete the OAuth flow
|
||||
logger.debug('[MCP OAuth] Completing OAuth flow');
|
||||
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,
|
||||
@@ -148,10 +142,8 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
`[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}`)) {
|
||||
@@ -159,7 +151,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
@@ -173,7 +164,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Save the updated user tool cache
|
||||
await setCachedTools(userTools, { userId: flowState.userId });
|
||||
|
||||
logger.debug(
|
||||
@@ -183,7 +173,6 @@ router.get('/:serverName/oauth/callback', async (req, res) => {
|
||||
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,
|
||||
@@ -219,7 +208,6 @@ router.get('/oauth/tokens/:flowId', requireJwtAuth, async (req, res) => {
|
||||
return res.status(401).json({ error: 'User not authenticated' });
|
||||
}
|
||||
|
||||
// Allow system flows or user-owned flows
|
||||
if (!flowId.startsWith(`${user.id}:`) && !flowId.startsWith('system:')) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
@@ -287,11 +275,7 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => {
|
||||
|
||||
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) {
|
||||
@@ -302,8 +286,7 @@ router.post('/oauth/cancel/:serverName', requireJwtAuth, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Cancel the flow by marking it as failed
|
||||
await flowManager.completeFlow(flowId, 'mcp_oauth', null, 'User cancelled OAuth flow');
|
||||
await flowManager.failFlow(flowId, 'mcp_oauth', 'User cancelled OAuth flow');
|
||||
|
||||
logger.info(`[MCP OAuth Cancel] Successfully cancelled OAuth flow for ${serverName}`);
|
||||
|
||||
@@ -354,9 +337,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
for (const varName of Object.keys(serverConfig.customUserVars)) {
|
||||
try {
|
||||
const value = await getUserPluginAuthValue(user.id, varName, false);
|
||||
if (value) {
|
||||
customUserVars[varName] = value;
|
||||
}
|
||||
customUserVars[varName] = value;
|
||||
} catch (err) {
|
||||
logger.error(`[MCP Reinitialize] Error fetching ${varName} for user ${user.id}:`, err);
|
||||
}
|
||||
@@ -379,8 +360,7 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
createToken,
|
||||
deleteTokens,
|
||||
},
|
||||
returnOnOAuth: true, // Return immediately when OAuth is initiated
|
||||
// Add OAuth handlers to capture the OAuth URL when needed
|
||||
returnOnOAuth: true,
|
||||
oauthStart: async (authURL) => {
|
||||
logger.info(`[MCP Reinitialize] OAuth URL received: ${authURL}`);
|
||||
oauthUrl = authURL;
|
||||
@@ -395,7 +375,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
`[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') ||
|
||||
@@ -408,7 +387,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
`[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:`,
|
||||
@@ -418,11 +396,9 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 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}`)) {
|
||||
@@ -430,7 +406,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
@@ -444,7 +419,6 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Save the updated user tool cache
|
||||
await setCachedTools(userTools, { userId: user.id });
|
||||
}
|
||||
|
||||
@@ -452,11 +426,19 @@ router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||
`[MCP Reinitialize] Sending response for ${serverName} - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
||||
);
|
||||
|
||||
const getResponseMessage = () => {
|
||||
if (oauthRequired) {
|
||||
return `MCP server '${serverName}' ready for OAuth authentication`;
|
||||
}
|
||||
if (userConnection) {
|
||||
return `MCP server '${serverName}' reinitialized successfully`;
|
||||
}
|
||||
return `Failed to reinitialize MCP server '${serverName}'`;
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: oauthRequired
|
||||
? `MCP server '${serverName}' ready for OAuth authentication`
|
||||
: `MCP server '${serverName}' reinitialized successfully`,
|
||||
success: (userConnection && !oauthRequired) || (oauthRequired && oauthUrl),
|
||||
message: getResponseMessage(),
|
||||
serverName,
|
||||
oauthRequired,
|
||||
oauthUrl,
|
||||
@@ -520,10 +502,6 @@ router.get('/connection/status/:serverName', requireJwtAuth, async (req, res) =>
|
||||
return res.status(401).json({ error: 'User not authenticated' });
|
||||
}
|
||||
|
||||
if (!serverName) {
|
||||
return res.status(400).json({ error: 'Server name is required' });
|
||||
}
|
||||
|
||||
const { mcpConfig, appConnections, userConnections, oauthServers } = await getMCPSetupData(
|
||||
user.id,
|
||||
);
|
||||
@@ -585,19 +563,16 @@ router.get('/:serverName/auth-values', requireJwtAuth, async (req, res) => {
|
||||
const pluginKey = `${Constants.mcp_prefix}${serverName}`;
|
||||
const authValueFlags = {};
|
||||
|
||||
// Check existence of saved values for each custom user variable (don't fetch actual values)
|
||||
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);
|
||||
// Only store boolean flag indicating if value exists
|
||||
authValueFlags[varName] = !!(value && value.length > 0);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[MCP Auth Value Flags] Error checking ${varName} for user ${user.id}:`,
|
||||
err,
|
||||
);
|
||||
// Default to false if we can't check
|
||||
authValueFlags[varName] = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,14 @@ const replaceArtifactContent = (originalText, artifact, original, updated) => {
|
||||
|
||||
// Find boundaries between ARTIFACT_START and ARTIFACT_END
|
||||
const contentStart = artifactContent.indexOf('\n', artifactContent.indexOf(ARTIFACT_START)) + 1;
|
||||
const contentEnd = artifactContent.lastIndexOf(ARTIFACT_END);
|
||||
let contentEnd = artifactContent.lastIndexOf(ARTIFACT_END);
|
||||
|
||||
// Special case: if contentEnd is 0, it means the only ::: found is at the start of :::artifact
|
||||
// This indicates an incomplete artifact (no closing :::)
|
||||
// We need to check that it's exactly at position 0 (the beginning of artifactContent)
|
||||
if (contentEnd === 0 && artifactContent.indexOf(ARTIFACT_START) === 0) {
|
||||
contentEnd = artifactContent.length;
|
||||
}
|
||||
|
||||
if (contentStart === -1 || contentEnd === -1) {
|
||||
return null;
|
||||
@@ -72,12 +79,20 @@ const replaceArtifactContent = (originalText, artifact, original, updated) => {
|
||||
|
||||
// Determine where to look for the original content
|
||||
let searchStart, searchEnd;
|
||||
if (codeBlockStart !== -1 && codeBlockEnd !== -1) {
|
||||
// If code blocks exist, search between them
|
||||
if (codeBlockStart !== -1) {
|
||||
// Code block starts
|
||||
searchStart = codeBlockStart + 4; // after ```\n
|
||||
searchEnd = codeBlockEnd;
|
||||
|
||||
if (codeBlockEnd !== -1 && codeBlockEnd > codeBlockStart) {
|
||||
// Code block has proper ending
|
||||
searchEnd = codeBlockEnd;
|
||||
} else {
|
||||
// No closing backticks found or they're before the opening (shouldn't happen)
|
||||
// This might be an incomplete artifact - search to contentEnd
|
||||
searchEnd = contentEnd;
|
||||
}
|
||||
} else {
|
||||
// Otherwise search in the whole artifact content
|
||||
// No code blocks at all
|
||||
searchStart = contentStart;
|
||||
searchEnd = contentEnd;
|
||||
}
|
||||
|
||||
@@ -89,9 +89,9 @@ describe('replaceArtifactContent', () => {
|
||||
};
|
||||
|
||||
test('should replace content within artifact boundaries', () => {
|
||||
const original = 'console.log(\'hello\')';
|
||||
const original = "console.log('hello')";
|
||||
const artifact = createTestArtifact(original);
|
||||
const updated = 'console.log(\'updated\')';
|
||||
const updated = "console.log('updated')";
|
||||
|
||||
const result = replaceArtifactContent(artifact.text, artifact, original, updated);
|
||||
expect(result).toContain(updated);
|
||||
@@ -317,4 +317,182 @@ console.log(greeting);`;
|
||||
expect(result).not.toContain('\n\n```');
|
||||
expect(result).not.toContain('```\n\n');
|
||||
});
|
||||
|
||||
describe('incomplete artifacts', () => {
|
||||
test('should handle incomplete artifacts (missing closing ::: and ```)', () => {
|
||||
const original = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Pomodoro</title>
|
||||
<meta name="description" content="A single-file Pomodoro timer with logs, charts, sounds, and dark mode." />
|
||||
<style>
|
||||
:root{`;
|
||||
|
||||
const prefix = `Awesome idea! I'll deliver a complete single-file HTML app called "Pomodoro" with:
|
||||
- Custom session/break durations
|
||||
|
||||
You can save this as pomodoro.html and open it directly in your browser.
|
||||
|
||||
`;
|
||||
|
||||
// This simulates the real incomplete artifact case - no closing ``` or :::
|
||||
const incompleteArtifact = `${ARTIFACT_START}{identifier="pomodoro-single-file-app" type="text/html" title="Pomodoro — Single File App"}
|
||||
\`\`\`
|
||||
${original}`;
|
||||
|
||||
const fullText = prefix + incompleteArtifact;
|
||||
const message = { text: fullText };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
expect(artifacts).toHaveLength(1);
|
||||
expect(artifacts[0].end).toBe(fullText.length);
|
||||
|
||||
const updated = original.replace('Pomodoro</title>', 'Pomodoro</title>UPDATED');
|
||||
const result = replaceArtifactContent(fullText, artifacts[0], original, updated);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('UPDATED');
|
||||
expect(result).toContain(prefix);
|
||||
// Should not have added closing markers
|
||||
expect(result).not.toMatch(/:::\s*$/);
|
||||
});
|
||||
|
||||
test('should handle incomplete artifacts with only opening code block', () => {
|
||||
const original = 'function hello() { console.log("world"); }';
|
||||
const incompleteArtifact = `${ARTIFACT_START}{id="test"}\n\`\`\`\n${original}`;
|
||||
|
||||
const message = { text: incompleteArtifact };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
expect(artifacts).toHaveLength(1);
|
||||
|
||||
const updated = 'function hello() { console.log("UPDATED"); }';
|
||||
const result = replaceArtifactContent(incompleteArtifact, artifacts[0], original, updated);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('UPDATED');
|
||||
});
|
||||
|
||||
test('should handle incomplete artifacts without code blocks', () => {
|
||||
const original = 'Some plain text content';
|
||||
const incompleteArtifact = `${ARTIFACT_START}{id="test"}\n${original}`;
|
||||
|
||||
const message = { text: incompleteArtifact };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
expect(artifacts).toHaveLength(1);
|
||||
|
||||
const updated = 'Some UPDATED text content';
|
||||
const result = replaceArtifactContent(incompleteArtifact, artifacts[0], original, updated);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('UPDATED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('regression tests for edge cases', () => {
|
||||
test('should still handle complete artifacts correctly', () => {
|
||||
// Ensure we didn't break normal artifact handling
|
||||
const original = 'console.log("test");';
|
||||
const artifact = createArtifactText({ content: original });
|
||||
|
||||
const message = { text: artifact };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
expect(artifacts).toHaveLength(1);
|
||||
|
||||
const updated = 'console.log("updated");';
|
||||
const result = replaceArtifactContent(artifact, artifacts[0], original, updated);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain(updated);
|
||||
expect(result).toContain(ARTIFACT_END);
|
||||
expect(result).toMatch(/```\nconsole\.log\("updated"\);\n```/);
|
||||
});
|
||||
|
||||
test('should handle multiple complete artifacts', () => {
|
||||
// Ensure multiple artifacts still work
|
||||
const content1 = 'First artifact';
|
||||
const content2 = 'Second artifact';
|
||||
const text = `${createArtifactText({ content: content1 })}\n\n${createArtifactText({ content: content2 })}`;
|
||||
|
||||
const message = { text };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
expect(artifacts).toHaveLength(2);
|
||||
|
||||
// Update first artifact
|
||||
const result1 = replaceArtifactContent(text, artifacts[0], content1, 'First UPDATED');
|
||||
expect(result1).not.toBeNull();
|
||||
expect(result1).toContain('First UPDATED');
|
||||
expect(result1).toContain(content2);
|
||||
|
||||
// Update second artifact
|
||||
const result2 = replaceArtifactContent(text, artifacts[1], content2, 'Second UPDATED');
|
||||
expect(result2).not.toBeNull();
|
||||
expect(result2).toContain(content1);
|
||||
expect(result2).toContain('Second UPDATED');
|
||||
});
|
||||
|
||||
test('should not mistake ::: at position 0 for artifact end in complete artifacts', () => {
|
||||
// This tests the specific fix - ensuring contentEnd=0 doesn't break complete artifacts
|
||||
const original = 'test content';
|
||||
// Create an artifact that will have ::: at position 0 when substring'd
|
||||
const artifact = `${ARTIFACT_START}\n\`\`\`\n${original}\n\`\`\`\n${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifact };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
expect(artifacts).toHaveLength(1);
|
||||
|
||||
const updated = 'updated content';
|
||||
const result = replaceArtifactContent(artifact, artifacts[0], original, updated);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain(updated);
|
||||
expect(result).toContain(ARTIFACT_END);
|
||||
});
|
||||
|
||||
test('should handle empty artifacts', () => {
|
||||
// Edge case: empty artifact
|
||||
const artifact = `${ARTIFACT_START}\n${ARTIFACT_END}`;
|
||||
|
||||
const message = { text: artifact };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
expect(artifacts).toHaveLength(1);
|
||||
|
||||
// Trying to replace non-existent content should return null
|
||||
const result = replaceArtifactContent(artifact, artifacts[0], 'something', 'updated');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('should preserve whitespace and formatting in complete artifacts', () => {
|
||||
const original = ` function test() {
|
||||
return {
|
||||
value: 42
|
||||
};
|
||||
}`;
|
||||
const artifact = createArtifactText({ content: original });
|
||||
|
||||
const message = { text: artifact };
|
||||
const artifacts = findAllArtifacts(message);
|
||||
|
||||
const updated = ` function test() {
|
||||
return {
|
||||
value: 100
|
||||
};
|
||||
}`;
|
||||
const result = replaceArtifactContent(artifact, artifacts[0], original, updated);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toContain('value: 100');
|
||||
// Should preserve exact formatting
|
||||
expect(result).toMatch(
|
||||
/```\n {2}function test\(\) \{\n {4}return \{\n {6}value: 100\n {4}\};\n {2}\}\n```/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -281,7 +281,7 @@ function createInProgressHandler(openai, thread_id, messages) {
|
||||
|
||||
openai.seenCompletedMessages.add(message_id);
|
||||
|
||||
const message = await openai.beta.threads.messages.retrieve(thread_id, message_id);
|
||||
const message = await openai.beta.threads.messages.retrieve(message_id, { thread_id });
|
||||
if (!message?.content?.length) {
|
||||
return;
|
||||
}
|
||||
@@ -435,9 +435,11 @@ async function runAssistant({
|
||||
};
|
||||
});
|
||||
|
||||
const outputs = await processRequiredActions(openai, actions);
|
||||
|
||||
const toolRun = await openai.beta.threads.runs.submitToolOutputs(run.thread_id, run.id, outputs);
|
||||
const tool_outputs = await processRequiredActions(openai, actions);
|
||||
const toolRun = await openai.beta.threads.runs.submitToolOutputs(run.id, {
|
||||
thread_id: run.thread_id,
|
||||
tool_outputs,
|
||||
});
|
||||
|
||||
// Recursive call with accumulated steps and messages
|
||||
return await runAssistant({
|
||||
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
getUserKeyExpiry,
|
||||
checkUserKeyExpiry,
|
||||
} = require('~/server/services/UserService');
|
||||
const OpenAIClient = require('~/app/clients/OpenAIClient');
|
||||
const OAIClient = require('~/app/clients/OpenAIClient');
|
||||
const { isUserProvided } = require('~/server/utils');
|
||||
|
||||
const initializeClient = async ({ req, res, endpointOption, version, initAppClient = false }) => {
|
||||
@@ -79,7 +79,7 @@ const initializeClient = async ({ req, res, endpointOption, version, initAppClie
|
||||
openai.res = res;
|
||||
|
||||
if (endpointOption && initAppClient) {
|
||||
const client = new OpenAIClient(apiKey, clientOptions);
|
||||
const client = new OAIClient(apiKey, clientOptions);
|
||||
return {
|
||||
client,
|
||||
openai,
|
||||
|
||||
@@ -3,11 +3,11 @@ const { ProxyAgent } = require('undici');
|
||||
const { constructAzureURL, isUserProvided, resolveHeaders } = require('@librechat/api');
|
||||
const { ErrorTypes, EModelEndpoint, mapModelToAzureConfig } = require('librechat-data-provider');
|
||||
const {
|
||||
checkUserKeyExpiry,
|
||||
getUserKeyValues,
|
||||
getUserKeyExpiry,
|
||||
checkUserKeyExpiry,
|
||||
} = require('~/server/services/UserService');
|
||||
const OpenAIClient = require('~/app/clients/OpenAIClient');
|
||||
const OAIClient = require('~/app/clients/OpenAIClient');
|
||||
|
||||
class Files {
|
||||
constructor(client) {
|
||||
@@ -109,14 +109,15 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
|
||||
|
||||
apiKey = azureOptions.azureOpenAIApiKey;
|
||||
opts.defaultQuery = { 'api-version': azureOptions.azureOpenAIApiVersion };
|
||||
opts.defaultHeaders = resolveHeaders(
|
||||
{
|
||||
opts.defaultHeaders = resolveHeaders({
|
||||
headers: {
|
||||
...headers,
|
||||
'api-key': apiKey,
|
||||
'OpenAI-Beta': `assistants=${version}`,
|
||||
},
|
||||
req.user,
|
||||
);
|
||||
user: req.user,
|
||||
body: req.body,
|
||||
});
|
||||
opts.model = azureOptions.azureOpenAIApiDeploymentName;
|
||||
|
||||
if (initAppClient) {
|
||||
@@ -184,7 +185,7 @@ const initializeClient = async ({ req, res, version, endpointOption, initAppClie
|
||||
}
|
||||
|
||||
if (endpointOption && initAppClient) {
|
||||
const client = new OpenAIClient(apiKey, clientOptions);
|
||||
const client = new OAIClient(apiKey, clientOptions);
|
||||
return {
|
||||
client,
|
||||
openai,
|
||||
|
||||
@@ -28,7 +28,11 @@ const initializeClient = async ({ req, res, endpointOption, optionsOnly, overrid
|
||||
const CUSTOM_API_KEY = extractEnvVariable(endpointConfig.apiKey);
|
||||
const CUSTOM_BASE_URL = extractEnvVariable(endpointConfig.baseURL);
|
||||
|
||||
let resolvedHeaders = resolveHeaders(endpointConfig.headers, req.user);
|
||||
let resolvedHeaders = resolveHeaders({
|
||||
headers: endpointConfig.headers,
|
||||
user: req.user,
|
||||
body: req.body,
|
||||
});
|
||||
|
||||
if (CUSTOM_API_KEY.match(envVarRegex)) {
|
||||
throw new Error(`Missing API Key for ${endpoint}.`);
|
||||
|
||||
@@ -64,13 +64,14 @@ describe('custom/initializeClient', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls resolveHeaders with headers and user', async () => {
|
||||
it('calls resolveHeaders with headers, user, and body for body placeholder support', async () => {
|
||||
const { resolveHeaders } = require('@librechat/api');
|
||||
await initializeClient({ req: mockRequest, res: mockResponse, optionsOnly: true });
|
||||
expect(resolveHeaders).toHaveBeenCalledWith(
|
||||
{ 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||
{ id: 'user-123', email: 'test@example.com' },
|
||||
);
|
||||
expect(resolveHeaders).toHaveBeenCalledWith({
|
||||
headers: { 'x-user': '{{LIBRECHAT_USER_ID}}', 'x-email': '{{LIBRECHAT_USER_EMAIL}}' },
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
body: { endpoint: 'test-endpoint' }, // body - supports {{LIBRECHAT_BODY_*}} placeholders
|
||||
});
|
||||
});
|
||||
|
||||
it('throws if endpoint config is missing', async () => {
|
||||
|
||||
@@ -81,10 +81,11 @@ const initializeClient = async ({
|
||||
serverless = _serverless;
|
||||
|
||||
clientOptions.reverseProxyUrl = baseURL ?? clientOptions.reverseProxyUrl;
|
||||
clientOptions.headers = resolveHeaders(
|
||||
{ ...headers, ...(clientOptions.headers ?? {}) },
|
||||
req.user,
|
||||
);
|
||||
clientOptions.headers = resolveHeaders({
|
||||
headers: { ...headers, ...(clientOptions.headers ?? {}) },
|
||||
user: req.user,
|
||||
body: req.body,
|
||||
});
|
||||
|
||||
clientOptions.titleConvo = azureConfig.titleConvo;
|
||||
clientOptions.titleModel = azureConfig.titleModel;
|
||||
|
||||
@@ -189,6 +189,7 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) {
|
||||
},
|
||||
oauthStart,
|
||||
oauthEnd,
|
||||
body: req.body,
|
||||
});
|
||||
|
||||
if (isAssistantsEndpoint(provider) && Array.isArray(result)) {
|
||||
@@ -287,14 +288,26 @@ async function checkOAuthFlowStatus(userId, serverName) {
|
||||
const flowTTL = flowState.ttl || 180000; // Default 3 minutes
|
||||
|
||||
if (flowState.status === 'FAILED' || flowAge > flowTTL) {
|
||||
logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, {
|
||||
flowId,
|
||||
status: flowState.status,
|
||||
flowAge,
|
||||
flowTTL,
|
||||
timedOut: flowAge > flowTTL,
|
||||
});
|
||||
return { hasActiveFlow: false, hasFailedFlow: true };
|
||||
const wasCancelled = flowState.error && flowState.error.includes('cancelled');
|
||||
|
||||
if (wasCancelled) {
|
||||
logger.debug(`[MCP Connection Status] Found cancelled OAuth flow for ${serverName}`, {
|
||||
flowId,
|
||||
status: flowState.status,
|
||||
error: flowState.error,
|
||||
});
|
||||
return { hasActiveFlow: false, hasFailedFlow: false };
|
||||
} else {
|
||||
logger.debug(`[MCP Connection Status] Found failed OAuth flow for ${serverName}`, {
|
||||
flowId,
|
||||
status: flowState.status,
|
||||
flowAge,
|
||||
flowTTL,
|
||||
timedOut: flowAge > flowTTL,
|
||||
error: flowState.error,
|
||||
});
|
||||
return { hasActiveFlow: false, hasFailedFlow: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (flowState.status === 'PENDING') {
|
||||
|
||||
@@ -91,11 +91,10 @@ class RunManager {
|
||||
* @param {boolean} [params.final] - The end of the run polling loop, due to `requires_action`, `cancelling`, `cancelled`, `failed`, `completed`, or `expired` statuses.
|
||||
*/
|
||||
async fetchRunSteps({ openai, thread_id, run_id, runStatus, final = false }) {
|
||||
// const { data: steps, first_id, last_id, has_more } = await openai.beta.threads.runs.steps.list(thread_id, run_id);
|
||||
// const { data: steps, first_id, last_id, has_more } = await openai.beta.threads.runs.steps.list(run_id, { thread_id });
|
||||
const { data: _steps } = await openai.beta.threads.runs.steps.list(
|
||||
thread_id,
|
||||
run_id,
|
||||
{},
|
||||
{ thread_id },
|
||||
{
|
||||
timeout: 3000,
|
||||
maxRetries: 5,
|
||||
|
||||
@@ -573,9 +573,9 @@ class StreamRunManager {
|
||||
let toolRun;
|
||||
try {
|
||||
toolRun = this.openai.beta.threads.runs.submitToolOutputsStream(
|
||||
run.thread_id,
|
||||
run.id,
|
||||
{
|
||||
thread_id: run.thread_id,
|
||||
tool_outputs,
|
||||
stream: true,
|
||||
},
|
||||
|
||||
@@ -179,7 +179,7 @@ async function waitForRun({
|
||||
* @return {Promise<RunStep[]>} A promise that resolves to an array of RunStep objects.
|
||||
*/
|
||||
async function _retrieveRunSteps({ openai, thread_id, run_id }) {
|
||||
const runSteps = await openai.beta.threads.runs.steps.list(thread_id, run_id);
|
||||
const runSteps = await openai.beta.threads.runs.steps.list(run_id, { thread_id });
|
||||
return runSteps;
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,8 @@ async function addThreadMetadata({ openai, thread_id, messageId, messages }) {
|
||||
const promises = [];
|
||||
for (const message of messages) {
|
||||
promises.push(
|
||||
openai.beta.threads.messages.update(thread_id, message.id, {
|
||||
openai.beta.threads.messages.update(message.id, {
|
||||
thread_id,
|
||||
metadata: {
|
||||
messageId,
|
||||
},
|
||||
@@ -263,7 +264,8 @@ async function syncMessages({
|
||||
}
|
||||
|
||||
modifyPromises.push(
|
||||
openai.beta.threads.messages.update(thread_id, apiMessage.id, {
|
||||
openai.beta.threads.messages.update(apiMessage.id, {
|
||||
thread_id,
|
||||
metadata: {
|
||||
messageId: dbMessage.messageId,
|
||||
},
|
||||
@@ -413,7 +415,7 @@ async function checkMessageGaps({
|
||||
}) {
|
||||
const promises = [];
|
||||
promises.push(openai.beta.threads.messages.list(thread_id, defaultOrderQuery));
|
||||
promises.push(openai.beta.threads.runs.steps.list(thread_id, run_id));
|
||||
promises.push(openai.beta.threads.runs.steps.list(run_id, { thread_id }));
|
||||
/** @type {[{ data: ThreadMessage[] }, { data: RunStep[] }]} */
|
||||
const [response, stepsResponse] = await Promise.all(promises);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { sleep } = require('@librechat/agents');
|
||||
const { getToolkitKey } = require('@librechat/api');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { zodToJsonSchema } = require('zod-to-json-schema');
|
||||
const { Calculator } = require('@langchain/community/tools/calculator');
|
||||
@@ -11,7 +12,6 @@ const {
|
||||
ErrorTypes,
|
||||
ContentTypes,
|
||||
imageGenTools,
|
||||
EToolResources,
|
||||
EModelEndpoint,
|
||||
actionDelimiter,
|
||||
ImageVisionTool,
|
||||
@@ -40,30 +40,6 @@ const { recordUsage } = require('~/server/services/Threads');
|
||||
const { loadTools } = require('~/app/clients/tools/util');
|
||||
const { redactMessage } = require('~/config/parsers');
|
||||
|
||||
/**
|
||||
* @param {string} toolName
|
||||
* @returns {string | undefined} toolKey
|
||||
*/
|
||||
function getToolkitKey(toolName) {
|
||||
/** @type {string|undefined} */
|
||||
let toolkitKey;
|
||||
for (const toolkit of toolkits) {
|
||||
if (toolName.startsWith(EToolResources.image_edit)) {
|
||||
const splitMatches = toolkit.pluginKey.split('_');
|
||||
const suffix = splitMatches[splitMatches.length - 1];
|
||||
if (toolName.endsWith(suffix)) {
|
||||
toolkitKey = toolkit.pluginKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (toolName.startsWith(toolkit.pluginKey)) {
|
||||
toolkitKey = toolkit.pluginKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return toolkitKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and formats tools from the specified tool directory.
|
||||
*
|
||||
@@ -145,7 +121,7 @@ function loadAndFormatTools({ directory, adminFilter = [], adminIncluded = [] })
|
||||
for (const toolInstance of basicToolInstances) {
|
||||
const formattedTool = formatToOpenAIAssistantTool(toolInstance);
|
||||
let toolName = formattedTool[Tools.function].name;
|
||||
toolName = getToolkitKey(toolName) ?? toolName;
|
||||
toolName = getToolkitKey({ toolkits, toolName }) ?? toolName;
|
||||
if (filter.has(toolName) && included.size === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,14 @@ class CustomOpenIDStrategy extends OpenIDStrategy {
|
||||
if (options?.state && !params.has('state')) {
|
||||
params.set('state', options.state);
|
||||
}
|
||||
|
||||
if (process.env.OPENID_AUDIENCE) {
|
||||
params.set('audience', process.env.OPENID_AUDIENCE);
|
||||
logger.debug(
|
||||
`[openidStrategy] Adding audience to authorization request: ${process.env.OPENID_AUDIENCE}`,
|
||||
);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
}
|
||||
@@ -353,7 +361,7 @@ async function setupOpenId() {
|
||||
username = userinfo[process.env.OPENID_USERNAME_CLAIM];
|
||||
} else {
|
||||
username = convertToUsername(
|
||||
userinfo.username || userinfo.given_name || userinfo.email,
|
||||
userinfo.preferred_username || userinfo.username || userinfo.email,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,7 @@ jest.mock('openid-client', () => {
|
||||
}),
|
||||
fetchUserInfo: jest.fn().mockImplementation((config, accessToken, sub) => {
|
||||
// Only return additional properties, but don't override any claims
|
||||
return Promise.resolve({
|
||||
preferred_username: 'preferred_username',
|
||||
});
|
||||
return Promise.resolve({});
|
||||
}),
|
||||
customFetch: Symbol('customFetch'),
|
||||
};
|
||||
@@ -104,6 +102,7 @@ describe('setupOpenId', () => {
|
||||
given_name: 'First',
|
||||
family_name: 'Last',
|
||||
name: 'My Full',
|
||||
preferred_username: 'testusername',
|
||||
username: 'flast',
|
||||
picture: 'https://example.com/avatar.png',
|
||||
}),
|
||||
@@ -156,20 +155,20 @@ describe('setupOpenId', () => {
|
||||
verifyCallback = require('openid-client/passport').__getVerifyCallback();
|
||||
});
|
||||
|
||||
it('should create a new user with correct username when username claim exists', async () => {
|
||||
// Arrange – our userinfo already has username 'flast'
|
||||
it('should create a new user with correct username when preferred_username claim exists', async () => {
|
||||
// Arrange – our userinfo already has preferred_username 'testusername'
|
||||
const userinfo = tokenset.claims();
|
||||
|
||||
// Act
|
||||
const { user } = await validate(tokenset);
|
||||
|
||||
// Assert
|
||||
expect(user.username).toBe(userinfo.username);
|
||||
expect(user.username).toBe(userinfo.preferred_username);
|
||||
expect(createUser).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: 'openid',
|
||||
openidId: userinfo.sub,
|
||||
username: userinfo.username,
|
||||
username: userinfo.preferred_username,
|
||||
email: userinfo.email,
|
||||
name: `${userinfo.given_name} ${userinfo.family_name}`,
|
||||
}),
|
||||
@@ -179,12 +178,12 @@ describe('setupOpenId', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should use given_name as username when username claim is missing', async () => {
|
||||
// Arrange – remove username from userinfo
|
||||
it('should use username as username when preferred_username claim is missing', async () => {
|
||||
// Arrange – remove preferred_username from userinfo
|
||||
const userinfo = { ...tokenset.claims() };
|
||||
delete userinfo.username;
|
||||
// Expect the username to be the given name (unchanged case)
|
||||
const expectUsername = userinfo.given_name;
|
||||
delete userinfo.preferred_username;
|
||||
// Expect the username to be the "username"
|
||||
const expectUsername = userinfo.username;
|
||||
|
||||
// Act
|
||||
const { user } = await validate({ ...tokenset, claims: () => userinfo });
|
||||
@@ -199,11 +198,11 @@ describe('setupOpenId', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should use email as username when username and given_name are missing', async () => {
|
||||
// Arrange – remove username and given_name
|
||||
it('should use email as username when username and preferred_username are missing', async () => {
|
||||
// Arrange – remove username and preferred_username
|
||||
const userinfo = { ...tokenset.claims() };
|
||||
delete userinfo.username;
|
||||
delete userinfo.given_name;
|
||||
delete userinfo.preferred_username;
|
||||
const expectUsername = userinfo.email;
|
||||
|
||||
// Act
|
||||
@@ -289,7 +288,7 @@ describe('setupOpenId', () => {
|
||||
expect.objectContaining({
|
||||
provider: 'openid',
|
||||
openidId: userinfo.sub,
|
||||
username: userinfo.username,
|
||||
username: userinfo.preferred_username,
|
||||
name: `${userinfo.given_name} ${userinfo.family_name}`,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -19,6 +19,9 @@ const openAIModels = {
|
||||
'gpt-4.1': 1047576,
|
||||
'gpt-4.1-mini': 1047576,
|
||||
'gpt-4.1-nano': 1047576,
|
||||
'gpt-5': 400000,
|
||||
'gpt-5-mini': 400000,
|
||||
'gpt-5-nano': 400000,
|
||||
'gpt-4o': 127500, // -500 from max
|
||||
'gpt-4o-mini': 127500, // -500 from max
|
||||
'gpt-4o-2024-05-13': 127500, // -500 from max
|
||||
@@ -234,6 +237,9 @@ const aggregateModels = {
|
||||
...xAIModels,
|
||||
// misc.
|
||||
kimi: 131000,
|
||||
// GPT-OSS
|
||||
'gpt-oss-20b': 131000,
|
||||
'gpt-oss-120b': 131000,
|
||||
};
|
||||
|
||||
const maxTokensMap = {
|
||||
@@ -250,6 +256,11 @@ const modelMaxOutputs = {
|
||||
o1: 32268, // -500 from max: 32,768
|
||||
'o1-mini': 65136, // -500 from max: 65,536
|
||||
'o1-preview': 32268, // -500 from max: 32,768
|
||||
'gpt-5': 128000,
|
||||
'gpt-5-mini': 128000,
|
||||
'gpt-5-nano': 128000,
|
||||
'gpt-oss-20b': 131000,
|
||||
'gpt-oss-120b': 131000,
|
||||
system_default: 1024,
|
||||
};
|
||||
|
||||
@@ -468,10 +479,11 @@ const tiktokenModels = new Set([
|
||||
]);
|
||||
|
||||
module.exports = {
|
||||
tiktokenModels,
|
||||
maxTokensMap,
|
||||
inputSchema,
|
||||
modelSchema,
|
||||
maxTokensMap,
|
||||
tiktokenModels,
|
||||
maxOutputTokensMap,
|
||||
matchModelName,
|
||||
processModelData,
|
||||
getModelMaxTokens,
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
const { EModelEndpoint } = require('librechat-data-provider');
|
||||
const { getModelMaxTokens, processModelData, matchModelName, maxTokensMap } = require('./tokens');
|
||||
const {
|
||||
maxOutputTokensMap,
|
||||
getModelMaxTokens,
|
||||
processModelData,
|
||||
matchModelName,
|
||||
maxTokensMap,
|
||||
} = require('./tokens');
|
||||
|
||||
describe('getModelMaxTokens', () => {
|
||||
test('should return correct tokens for exact match', () => {
|
||||
@@ -150,6 +156,35 @@ describe('getModelMaxTokens', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for gpt-5 matches', () => {
|
||||
expect(getModelMaxTokens('gpt-5')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5']);
|
||||
expect(getModelMaxTokens('gpt-5-preview')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5']);
|
||||
expect(getModelMaxTokens('openai/gpt-5')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5']);
|
||||
expect(getModelMaxTokens('gpt-5-2025-01-30')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for gpt-5-mini matches', () => {
|
||||
expect(getModelMaxTokens('gpt-5-mini')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5-mini']);
|
||||
expect(getModelMaxTokens('gpt-5-mini-preview')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5-mini'],
|
||||
);
|
||||
expect(getModelMaxTokens('openai/gpt-5-mini')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5-mini'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for gpt-5-nano matches', () => {
|
||||
expect(getModelMaxTokens('gpt-5-nano')).toBe(maxTokensMap[EModelEndpoint.openAI]['gpt-5-nano']);
|
||||
expect(getModelMaxTokens('gpt-5-nano-preview')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5-nano'],
|
||||
);
|
||||
expect(getModelMaxTokens('openai/gpt-5-nano')).toBe(
|
||||
maxTokensMap[EModelEndpoint.openAI]['gpt-5-nano'],
|
||||
);
|
||||
});
|
||||
|
||||
test('should return correct tokens for Anthropic models', () => {
|
||||
const models = [
|
||||
'claude-2.1',
|
||||
@@ -349,6 +384,39 @@ describe('getModelMaxTokens', () => {
|
||||
expect(getModelMaxTokens('o3')).toBe(o3Tokens);
|
||||
expect(getModelMaxTokens('openai/o3')).toBe(o3Tokens);
|
||||
});
|
||||
|
||||
test('should return correct tokens for GPT-OSS models', () => {
|
||||
const expected = maxTokensMap[EModelEndpoint.openAI]['gpt-oss-20b'];
|
||||
['gpt-oss-20b', 'gpt-oss-120b', 'openai/gpt-oss-20b', 'openai/gpt-oss-120b'].forEach((name) => {
|
||||
expect(getModelMaxTokens(name)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return correct max output tokens for GPT-5 models', () => {
|
||||
const { getModelMaxOutputTokens } = require('./tokens');
|
||||
['gpt-5', 'gpt-5-mini', 'gpt-5-nano'].forEach((model) => {
|
||||
expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]);
|
||||
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(
|
||||
maxOutputTokensMap[EModelEndpoint.openAI][model],
|
||||
);
|
||||
expect(getModelMaxOutputTokens(model, EModelEndpoint.azureOpenAI)).toBe(
|
||||
maxOutputTokensMap[EModelEndpoint.azureOpenAI][model],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return correct max output tokens for GPT-OSS models', () => {
|
||||
const { getModelMaxOutputTokens } = require('./tokens');
|
||||
['gpt-oss-20b', 'gpt-oss-120b'].forEach((model) => {
|
||||
expect(getModelMaxOutputTokens(model)).toBe(maxOutputTokensMap[EModelEndpoint.openAI][model]);
|
||||
expect(getModelMaxOutputTokens(model, EModelEndpoint.openAI)).toBe(
|
||||
maxOutputTokensMap[EModelEndpoint.openAI][model],
|
||||
);
|
||||
expect(getModelMaxOutputTokens(model, EModelEndpoint.azureOpenAI)).toBe(
|
||||
maxOutputTokensMap[EModelEndpoint.azureOpenAI][model],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchModelName', () => {
|
||||
@@ -420,6 +488,25 @@ describe('matchModelName', () => {
|
||||
expect(matchModelName('gpt-4.1-nano-2024-08-06')).toBe('gpt-4.1-nano');
|
||||
});
|
||||
|
||||
it('should return the closest matching key for gpt-5 matches', () => {
|
||||
expect(matchModelName('openai/gpt-5')).toBe('gpt-5');
|
||||
expect(matchModelName('gpt-5-preview')).toBe('gpt-5');
|
||||
expect(matchModelName('gpt-5-2025-01-30')).toBe('gpt-5');
|
||||
expect(matchModelName('gpt-5-2025-01-30-0130')).toBe('gpt-5');
|
||||
});
|
||||
|
||||
it('should return the closest matching key for gpt-5-mini matches', () => {
|
||||
expect(matchModelName('openai/gpt-5-mini')).toBe('gpt-5-mini');
|
||||
expect(matchModelName('gpt-5-mini-preview')).toBe('gpt-5-mini');
|
||||
expect(matchModelName('gpt-5-mini-2025-01-30')).toBe('gpt-5-mini');
|
||||
});
|
||||
|
||||
it('should return the closest matching key for gpt-5-nano matches', () => {
|
||||
expect(matchModelName('openai/gpt-5-nano')).toBe('gpt-5-nano');
|
||||
expect(matchModelName('gpt-5-nano-preview')).toBe('gpt-5-nano');
|
||||
expect(matchModelName('gpt-5-nano-2025-01-30')).toBe('gpt-5-nano');
|
||||
});
|
||||
|
||||
// Tests for Google models
|
||||
it('should return the exact model name if it exists in maxTokensMap - Google models', () => {
|
||||
expect(matchModelName('text-bison-32k', EModelEndpoint.google)).toBe('text-bison-32k');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@librechat/frontend",
|
||||
"version": "v0.7.9",
|
||||
"version": "v0.8.0-rc1",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
46
client/src/Providers/ArtifactsContext.tsx
Normal file
46
client/src/Providers/ArtifactsContext.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import type { TMessage } from 'librechat-data-provider';
|
||||
import { useChatContext } from './ChatContext';
|
||||
import { getLatestText } from '~/utils';
|
||||
|
||||
interface ArtifactsContextValue {
|
||||
isSubmitting: boolean;
|
||||
latestMessageId: string | null;
|
||||
latestMessageText: string;
|
||||
conversationId: string | null;
|
||||
}
|
||||
|
||||
const ArtifactsContext = createContext<ArtifactsContextValue | undefined>(undefined);
|
||||
|
||||
export function ArtifactsProvider({ children }: { children: React.ReactNode }) {
|
||||
const { isSubmitting, latestMessage, conversation } = useChatContext();
|
||||
|
||||
const latestMessageText = useMemo(() => {
|
||||
return getLatestText({
|
||||
messageId: latestMessage?.messageId ?? null,
|
||||
text: latestMessage?.text ?? null,
|
||||
content: latestMessage?.content ?? null,
|
||||
} as TMessage);
|
||||
}, [latestMessage?.messageId, latestMessage?.text, latestMessage?.content]);
|
||||
|
||||
/** Context value only created when relevant values change */
|
||||
const contextValue = useMemo<ArtifactsContextValue>(
|
||||
() => ({
|
||||
isSubmitting,
|
||||
latestMessageText,
|
||||
latestMessageId: latestMessage?.messageId ?? null,
|
||||
conversationId: conversation?.conversationId ?? null,
|
||||
}),
|
||||
[isSubmitting, latestMessage?.messageId, latestMessageText, conversation?.conversationId],
|
||||
);
|
||||
|
||||
return <ArtifactsContext.Provider value={contextValue}>{children}</ArtifactsContext.Provider>;
|
||||
}
|
||||
|
||||
export function useArtifactsContext() {
|
||||
const context = useContext(ArtifactsContext);
|
||||
if (!context) {
|
||||
throw new Error('useArtifactsContext must be used within ArtifactsProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -23,4 +23,5 @@ export * from './SetConvoContext';
|
||||
export * from './SearchContext';
|
||||
export * from './BadgeRowContext';
|
||||
export * from './SidePanelContext';
|
||||
export * from './ArtifactsContext';
|
||||
export { default as BadgeRowProvider } from './BadgeRowContext';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { memo, useEffect, useMemo, useCallback } from 'react';
|
||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
useSandpack,
|
||||
SandpackCodeEditor,
|
||||
@@ -10,8 +10,8 @@ import type { SandpackBundlerFile } from '@codesandbox/sandpack-client';
|
||||
import type { CodeEditorRef } from '@codesandbox/sandpack-react';
|
||||
import type { ArtifactFiles, Artifact } from '~/common';
|
||||
import { useEditArtifact, useGetStartupConfig } from '~/data-provider';
|
||||
import { useEditorContext, useArtifactsContext } from '~/Providers';
|
||||
import { sharedFiles, sharedOptions } from '~/utils/artifacts';
|
||||
import { useEditorContext } from '~/Providers';
|
||||
|
||||
const createDebouncedMutation = (
|
||||
callback: (params: {
|
||||
@@ -29,18 +29,21 @@ const CodeEditor = ({
|
||||
editorRef,
|
||||
}: {
|
||||
fileKey: string;
|
||||
readOnly: boolean;
|
||||
readOnly?: boolean;
|
||||
artifact: Artifact;
|
||||
editorRef: React.MutableRefObject<CodeEditorRef>;
|
||||
}) => {
|
||||
const { sandpack } = useSandpack();
|
||||
const [currentUpdate, setCurrentUpdate] = useState<string | null>(null);
|
||||
const { isMutating, setIsMutating, setCurrentCode } = useEditorContext();
|
||||
const editArtifact = useEditArtifact({
|
||||
onMutate: () => {
|
||||
onMutate: (vars) => {
|
||||
setIsMutating(true);
|
||||
setCurrentUpdate(vars.updated);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setIsMutating(false);
|
||||
setCurrentUpdate(null);
|
||||
},
|
||||
onError: () => {
|
||||
setIsMutating(false);
|
||||
@@ -71,8 +74,14 @@ const CodeEditor = ({
|
||||
}
|
||||
|
||||
const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code;
|
||||
const isNotOriginal =
|
||||
currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim();
|
||||
const isNotRepeated =
|
||||
currentUpdate == null
|
||||
? true
|
||||
: currentCode != null && currentCode.trim() !== currentUpdate.trim();
|
||||
|
||||
if (currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim()) {
|
||||
if (artifact.content && isNotOriginal && isNotRepeated) {
|
||||
setCurrentCode(currentCode);
|
||||
debouncedMutation({
|
||||
index: artifact.index,
|
||||
@@ -92,8 +101,9 @@ const CodeEditor = ({
|
||||
artifact.messageId,
|
||||
readOnly,
|
||||
isMutating,
|
||||
sandpack.files,
|
||||
currentUpdate,
|
||||
setIsMutating,
|
||||
sandpack.files,
|
||||
setCurrentCode,
|
||||
debouncedMutation,
|
||||
]);
|
||||
@@ -102,33 +112,32 @@ const CodeEditor = ({
|
||||
<SandpackCodeEditor
|
||||
ref={editorRef}
|
||||
showTabs={false}
|
||||
readOnly={readOnly}
|
||||
showRunButton={false}
|
||||
showLineNumbers={true}
|
||||
showInlineErrors={true}
|
||||
readOnly={readOnly === true}
|
||||
className="hljs language-javascript bg-black"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ArtifactCodeEditor = memo(function ({
|
||||
export const ArtifactCodeEditor = function ({
|
||||
files,
|
||||
fileKey,
|
||||
template,
|
||||
artifact,
|
||||
editorRef,
|
||||
sharedProps,
|
||||
isSubmitting,
|
||||
}: {
|
||||
fileKey: string;
|
||||
artifact: Artifact;
|
||||
files: ArtifactFiles;
|
||||
isSubmitting: boolean;
|
||||
template: SandpackProviderProps['template'];
|
||||
sharedProps: Partial<SandpackProviderProps>;
|
||||
editorRef: React.MutableRefObject<CodeEditorRef>;
|
||||
}) {
|
||||
const { data: config } = useGetStartupConfig();
|
||||
const { isSubmitting } = useArtifactsContext();
|
||||
const options: typeof sharedOptions = useMemo(() => {
|
||||
if (!config) {
|
||||
return sharedOptions;
|
||||
@@ -138,6 +147,10 @@ export const ArtifactCodeEditor = memo(function ({
|
||||
bundlerURL: template === 'static' ? config.staticBundlerURL : config.bundlerURL,
|
||||
};
|
||||
}, [config, template]);
|
||||
const [readOnly, setReadOnly] = useState(isSubmitting ?? false);
|
||||
useEffect(() => {
|
||||
setReadOnly(isSubmitting ?? false);
|
||||
}, [isSubmitting]);
|
||||
|
||||
if (Object.keys(files).length === 0) {
|
||||
return null;
|
||||
@@ -154,12 +167,7 @@ export const ArtifactCodeEditor = memo(function ({
|
||||
{...sharedProps}
|
||||
template={template}
|
||||
>
|
||||
<CodeEditor
|
||||
editorRef={editorRef}
|
||||
fileKey={fileKey}
|
||||
readOnly={isSubmitting}
|
||||
artifact={artifact}
|
||||
/>
|
||||
<CodeEditor fileKey={fileKey} artifact={artifact} editorRef={editorRef} readOnly={readOnly} />
|
||||
</StyledProvider>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,12 +2,12 @@ import { useRef, useEffect } from 'react';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import type { SandpackPreviewRef, CodeEditorRef } from '@codesandbox/sandpack-react';
|
||||
import type { Artifact } from '~/common';
|
||||
import { useEditorContext, useArtifactsContext } from '~/Providers';
|
||||
import useArtifactProps from '~/hooks/Artifacts/useArtifactProps';
|
||||
import { useAutoScroll } from '~/hooks/Artifacts/useAutoScroll';
|
||||
import { ArtifactCodeEditor } from './ArtifactCodeEditor';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import { ArtifactPreview } from './ArtifactPreview';
|
||||
import { useEditorContext } from '~/Providers';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function ArtifactTabs({
|
||||
@@ -15,14 +15,13 @@ export default function ArtifactTabs({
|
||||
isMermaid,
|
||||
editorRef,
|
||||
previewRef,
|
||||
isSubmitting,
|
||||
}: {
|
||||
artifact: Artifact;
|
||||
isMermaid: boolean;
|
||||
isSubmitting: boolean;
|
||||
editorRef: React.MutableRefObject<CodeEditorRef>;
|
||||
previewRef: React.MutableRefObject<SandpackPreviewRef>;
|
||||
}) {
|
||||
const { isSubmitting } = useArtifactsContext();
|
||||
const { currentCode, setCurrentCode } = useEditorContext();
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const lastIdRef = useRef<string | null>(null);
|
||||
@@ -52,7 +51,6 @@ export default function ArtifactTabs({
|
||||
artifact={artifact}
|
||||
editorRef={editorRef}
|
||||
sharedProps={sharedProps}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
|
||||
@@ -29,7 +29,6 @@ export default function Artifacts() {
|
||||
isMermaid,
|
||||
setActiveTab,
|
||||
currentIndex,
|
||||
isSubmitting,
|
||||
cycleArtifact,
|
||||
currentArtifact,
|
||||
orderedArtifactIds,
|
||||
@@ -116,7 +115,6 @@ export default function Artifacts() {
|
||||
<ArtifactTabs
|
||||
isMermaid={isMermaid}
|
||||
artifact={currentArtifact}
|
||||
isSubmitting={isSubmitting}
|
||||
editorRef={editorRef as React.MutableRefObject<CodeEditorRef>}
|
||||
previewRef={previewRef as React.MutableRefObject<SandpackPreviewRef>}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ function MCPSelect() {
|
||||
batchToggleServers,
|
||||
getServerStatusIconProps,
|
||||
getConfigDialogProps,
|
||||
isInitializing,
|
||||
localize,
|
||||
} = useMCPServerManager();
|
||||
|
||||
@@ -32,14 +33,20 @@ function MCPSelect() {
|
||||
const renderItemContent = useCallback(
|
||||
(serverName: string, defaultContent: React.ReactNode) => {
|
||||
const statusIconProps = getServerStatusIconProps(serverName);
|
||||
const isServerInitializing = isInitializing(serverName);
|
||||
|
||||
// Common wrapper for the main content (check mark + text)
|
||||
// Ensures Check & Text are adjacent and the group takes available space.
|
||||
/**
|
||||
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"
|
||||
className={`flex flex-grow items-center rounded bg-transparent p-0 text-left transition-colors focus:outline-none ${
|
||||
isServerInitializing ? 'opacity-50' : ''
|
||||
}`}
|
||||
tabIndex={0}
|
||||
disabled={isServerInitializing}
|
||||
>
|
||||
{defaultContent}
|
||||
</button>
|
||||
@@ -58,15 +65,13 @@ function MCPSelect() {
|
||||
|
||||
return mainContentWrapper;
|
||||
},
|
||||
[getServerStatusIconProps],
|
||||
[getServerStatusIconProps, isInitializing],
|
||||
);
|
||||
|
||||
// Don't render if no servers are selected and not pinned
|
||||
if ((!mcpValues || mcpValues.length === 0) && !isPinned) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't render if no MCP servers are configured
|
||||
if (!configuredServers || configuredServers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
|
||||
toggleServerSelection,
|
||||
getServerStatusIconProps,
|
||||
getConfigDialogProps,
|
||||
isInitializing,
|
||||
} = useMCPServerManager();
|
||||
|
||||
const menuStore = Ariakit.useMenuStore({
|
||||
@@ -86,6 +87,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
|
||||
{configuredServers.map((serverName) => {
|
||||
const statusIconProps = getServerStatusIconProps(serverName);
|
||||
const isSelected = mcpValues?.includes(serverName) ?? false;
|
||||
const isServerInitializing = isInitializing(serverName);
|
||||
|
||||
const statusIcon = statusIconProps && <MCPServerStatusIcon {...statusIconProps} />;
|
||||
|
||||
@@ -96,12 +98,15 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
|
||||
event.preventDefault();
|
||||
toggleServerSelection(serverName);
|
||||
}}
|
||||
disabled={isServerInitializing}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg px-2 py-1.5 text-text-primary hover:cursor-pointer',
|
||||
'scroll-m-1 outline-none transition-colors',
|
||||
'hover:bg-black/[0.075] dark:hover:bg-white/10',
|
||||
'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10',
|
||||
'w-full min-w-0 justify-between text-sm',
|
||||
isServerInitializing &&
|
||||
'opacity-50 hover:bg-transparent dark:hover:bg-transparent',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-grow items-center gap-2">
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { EditorProvider, SidePanelProvider, ArtifactsProvider } from '~/Providers';
|
||||
import Artifacts from '~/components/Artifacts/Artifacts';
|
||||
import { SidePanelGroup } from '~/components/SidePanel';
|
||||
import { useSetFilesToDelete } from '~/hooks';
|
||||
@@ -66,9 +66,11 @@ export default function Presentation({ children }: { children: React.ReactNode }
|
||||
defaultCollapsed={defaultCollapsed}
|
||||
artifacts={
|
||||
artifactsVisibility === true && Object.keys(artifacts ?? {}).length > 0 ? (
|
||||
<EditorProvider>
|
||||
<Artifacts />
|
||||
</EditorProvider>
|
||||
<ArtifactsProvider>
|
||||
<EditorProvider>
|
||||
<Artifacts />
|
||||
</EditorProvider>
|
||||
</ArtifactsProvider>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
|
||||
@@ -25,7 +25,7 @@ type EndpointIcon = {
|
||||
|
||||
function getOpenAIColor(_model: string | null | undefined) {
|
||||
const model = _model?.toLowerCase() ?? '';
|
||||
if (model && /\b(o\d)\b/i.test(model)) {
|
||||
if (model && (/\b(o\d)\b/i.test(model) || /\bgpt-[5-9]\b/i.test(model))) {
|
||||
return '#000000';
|
||||
}
|
||||
return model.includes('gpt-4') ? '#AB68FF' : '#19C37D';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { Input, Label, Button } from '@librechat/client';
|
||||
import { Input, Label, Button, TooltipAnchor, CircleHelpIcon } from '@librechat/client';
|
||||
import { useMCPAuthValuesQuery } from '~/data-provider/Tools/queries';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
@@ -31,16 +31,25 @@ function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps)
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor={name} className="text-sm font-medium">
|
||||
{config.title}
|
||||
</Label>
|
||||
<TooltipAnchor
|
||||
enableHTML={true}
|
||||
description={config.description || ''}
|
||||
render={
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor={name} className="text-sm font-medium">
|
||||
{config.title}
|
||||
</Label>
|
||||
<CircleHelpIcon className="h-6 w-6 cursor-help text-text-secondary transition-colors hover:text-text-primary" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{hasValue ? (
|
||||
<div className="flex min-w-fit items-center gap-2 whitespace-nowrap rounded-full border border-border-medium px-2 py-0.5 text-xs font-medium text-text-secondary">
|
||||
<div className="flex min-w-fit items-center gap-2 whitespace-nowrap rounded-full border border-border-light px-2 py-0.5 text-xs font-medium text-text-secondary">
|
||||
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||
<span>{localize('com_ui_set')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-w-fit items-center gap-2 whitespace-nowrap rounded-full border border-border-medium px-2 py-0.5 text-xs font-medium text-text-secondary">
|
||||
<div className="flex min-w-fit items-center gap-2 whitespace-nowrap rounded-full border border-border-light px-2 py-0.5 text-xs font-medium text-text-secondary">
|
||||
<div className="h-1.5 w-1.5 rounded-full border border-border-medium" />
|
||||
<span>{localize('com_ui_unset')}</span>
|
||||
</div>
|
||||
@@ -60,16 +69,10 @@ function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps)
|
||||
? localize('com_ui_mcp_update_var', { 0: config.title })
|
||||
: 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"
|
||||
className="w-full shadow-sm sm:text-sm"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{config.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: config.description }}
|
||||
/>
|
||||
)}
|
||||
{errors[name] && <p className="text-xs text-red-500">{errors[name]?.message}</p>}
|
||||
</div>
|
||||
);
|
||||
@@ -110,17 +113,15 @@ export default function CustomUserVarsSection({
|
||||
|
||||
const handleRevokeClick = () => {
|
||||
onRevoke();
|
||||
// Reset form after revoke
|
||||
reset();
|
||||
};
|
||||
|
||||
// Don't render if no fields to configure
|
||||
if (!fields || Object.keys(fields).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex-1 space-y-4">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
|
||||
{Object.entries(fields).map(([key, config]) => {
|
||||
const hasValue = authValuesData?.authValueFlags?.[key] || false;
|
||||
@@ -138,21 +139,11 @@ export default function CustomUserVarsSection({
|
||||
})}
|
||||
</form>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button
|
||||
onClick={handleRevokeClick}
|
||||
className="bg-red-600 text-white hover:bg-red-700 dark:hover:bg-red-800"
|
||||
disabled={isSubmitting}
|
||||
size="sm"
|
||||
>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button onClick={handleRevokeClick} variant="destructive" disabled={isSubmitting}>
|
||||
{localize('com_ui_revoke')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
className="bg-green-500 text-white hover:bg-green-600"
|
||||
disabled={isSubmitting}
|
||||
size="sm"
|
||||
>
|
||||
<Button onClick={handleSubmit(onFormSubmit)} variant="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? localize('com_ui_saving') : localize('com_ui_save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Loader2, KeyRound, PlugZap, AlertTriangle } from 'lucide-react';
|
||||
import { KeyRound, PlugZap, AlertTriangle } from 'lucide-react';
|
||||
import {
|
||||
Spinner,
|
||||
OGDialog,
|
||||
OGDialogTitle,
|
||||
OGDialogHeader,
|
||||
OGDialogContent,
|
||||
OGDialogDescription,
|
||||
} from '@librechat/client';
|
||||
import type { MCPServerStatus } from 'librechat-data-provider';
|
||||
import ServerInitializationSection from './ServerInitializationSection';
|
||||
@@ -45,9 +45,6 @@ export default function MCPConfigDialog({
|
||||
const dialogTitle = hasFields
|
||||
? localize('com_ui_configure_mcp_variables_for', { 0: serverName })
|
||||
: `${serverName} MCP Server`;
|
||||
const dialogDescription = hasFields
|
||||
? 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 = () => {
|
||||
@@ -60,7 +57,7 @@ export default function MCPConfigDialog({
|
||||
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" />
|
||||
<Spinner className="h-3 w-3" />
|
||||
<span>{localize('com_ui_connecting')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -107,31 +104,30 @@ export default function MCPConfigDialog({
|
||||
|
||||
return (
|
||||
<OGDialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<OGDialogContent className="flex max-h-[90vh] w-full max-w-md flex-col">
|
||||
<OGDialogContent className="flex max-h-screen w-11/12 max-w-lg flex-col space-y-2">
|
||||
<OGDialogHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<OGDialogTitle>{dialogTitle}</OGDialogTitle>
|
||||
<OGDialogTitle className="text-xl">
|
||||
{dialogTitle.charAt(0).toUpperCase() + dialogTitle.slice(1)}
|
||||
</OGDialogTitle>
|
||||
{renderStatusBadge()}
|
||||
</div>
|
||||
<OGDialogDescription>{dialogDescription}</OGDialogDescription>
|
||||
</OGDialogHeader>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{/* Custom User Variables Section */}
|
||||
<CustomUserVarsSection
|
||||
serverName={serverName}
|
||||
fields={fieldsSchema}
|
||||
onSave={onSave}
|
||||
onRevoke={onRevoke || (() => {})}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
{/* Custom User Variables Section */}
|
||||
<CustomUserVarsSection
|
||||
serverName={serverName}
|
||||
fields={fieldsSchema}
|
||||
onSave={onSave}
|
||||
onRevoke={onRevoke || (() => {})}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
|
||||
{/* Server Initialization Section */}
|
||||
<ServerInitializationSection
|
||||
serverName={serverName}
|
||||
requiresOAuth={serverStatus?.requiresOAuth || false}
|
||||
hasCustomUserVars={fieldsSchema && Object.keys(fieldsSchema).length > 0}
|
||||
/>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { SettingsIcon, AlertTriangle, Loader2, KeyRound, PlugZap, X } from 'lucide-react';
|
||||
import { Spinner } from '@librechat/client';
|
||||
import { SettingsIcon, AlertTriangle, KeyRound, PlugZap, X } from 'lucide-react';
|
||||
import type { MCPServerStatus, TPlugin } from 'librechat-data-provider';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
@@ -96,12 +97,12 @@ function InitializingStatusIcon({ serverName, onCancel, canCancel }: Initializin
|
||||
<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"
|
||||
className="group 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" />
|
||||
<div className="relative h-4 w-4">
|
||||
<Spinner className="h-4 w-4 group-hover:opacity-0" />
|
||||
<X className="absolute inset-0 h-4 w-4 text-red-500 opacity-0 group-hover:opacity-100" />
|
||||
</div>
|
||||
</button>
|
||||
@@ -110,8 +111,8 @@ function InitializingStatusIcon({ serverName, onCancel, canCancel }: Initializin
|
||||
|
||||
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"
|
||||
<Spinner
|
||||
className="h-4 w-4"
|
||||
aria-label={localize('com_nav_mcp_status_connecting', { 0: serverName })}
|
||||
/>
|
||||
</div>
|
||||
@@ -121,8 +122,8 @@ function InitializingStatusIcon({ serverName, onCancel, canCancel }: Initializin
|
||||
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"
|
||||
<Spinner
|
||||
className="h-4 w-4"
|
||||
aria-label={localize('com_nav_mcp_status_connecting', { 0: serverName })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Button } from '@librechat/client';
|
||||
import { RefreshCw, Link } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { Button, Spinner } from '@librechat/client';
|
||||
import { useMCPServerManager } from '~/hooks/MCP/useMCPServerManager';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface ServerInitializationSectionProps {
|
||||
sidePanel?: boolean;
|
||||
serverName: string;
|
||||
requiresOAuth: boolean;
|
||||
hasCustomUserVars?: boolean;
|
||||
}
|
||||
|
||||
export default function ServerInitializationSection({
|
||||
sidePanel = false,
|
||||
serverName,
|
||||
requiresOAuth,
|
||||
hasCustomUserVars = false,
|
||||
}: ServerInitializationSectionProps) {
|
||||
const localize = useLocalize();
|
||||
|
||||
// Use the centralized server manager instead of the old initialization hook so we can handle multiple oauth flows at once
|
||||
const {
|
||||
initializeServer,
|
||||
connectionStatus,
|
||||
@@ -31,96 +34,66 @@ export default function ServerInitializationSection({
|
||||
const isServerInitializing = isInitializing(serverName);
|
||||
const serverOAuthUrl = getOAuthUrl(serverName);
|
||||
|
||||
const handleInitializeClick = useCallback(() => {
|
||||
initializeServer(serverName);
|
||||
}, [initializeServer, serverName]);
|
||||
const shouldShowReinit = isConnected && (requiresOAuth || hasCustomUserVars);
|
||||
const shouldShowInit = !isConnected && !serverOAuthUrl;
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
cancelOAuthFlow(serverName);
|
||||
}, [cancelOAuthFlow, serverName]);
|
||||
if (!shouldShowReinit && !shouldShowInit && !serverOAuthUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show subtle reinitialize option if connected
|
||||
if (isConnected) {
|
||||
if (serverOAuthUrl) {
|
||||
return (
|
||||
<div className="flex justify-start">
|
||||
<button
|
||||
onClick={handleInitializeClick}
|
||||
disabled={isServerInitializing}
|
||||
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 ${isServerInitializing ? 'animate-spin' : ''}`} />
|
||||
{isServerInitializing ? localize('com_ui_loading') : localize('com_ui_reinitialize')}
|
||||
</button>
|
||||
</div>
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => cancelOAuthFlow(serverName)}
|
||||
disabled={!canCancel}
|
||||
variant="outline"
|
||||
title={!canCancel ? 'disabled' : undefined}
|
||||
>
|
||||
{localize('com_ui_cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="submit"
|
||||
onClick={() => window.open(serverOAuthUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="flex-1"
|
||||
>
|
||||
{localize('com_ui_continue_oauth')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-[#991b1b] bg-[#2C1315] p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<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 })}
|
||||
</span>
|
||||
</div>
|
||||
{/* Only show authenticate button when OAuth URL is not present */}
|
||||
{!serverOAuthUrl && (
|
||||
<Button
|
||||
onClick={handleInitializeClick}
|
||||
disabled={isServerInitializing}
|
||||
className="flex items-center gap-2 bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 dark:hover:bg-blue-800"
|
||||
>
|
||||
{isServerInitializing ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
{localize('com_ui_loading')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
{requiresOAuth
|
||||
? localize('com_ui_authenticate')
|
||||
: localize('com_ui_mcp_initialize')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
// Unified button rendering
|
||||
const isReinit = shouldShowReinit;
|
||||
const outerClass = isReinit ? 'flex justify-start' : 'flex justify-end';
|
||||
const buttonVariant = isReinit ? undefined : 'default';
|
||||
const buttonText = isServerInitializing
|
||||
? localize('com_ui_loading')
|
||||
: isReinit
|
||||
? localize('com_ui_reinitialize')
|
||||
: requiresOAuth
|
||||
? localize('com_ui_authenticate')
|
||||
: localize('com_ui_mcp_initialize');
|
||||
const icon = isServerInitializing ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
);
|
||||
|
||||
{/* OAuth URL display */}
|
||||
{serverOAuthUrl && (
|
||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/20">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-blue-500">
|
||||
<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')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => window.open(serverOAuthUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="flex-1 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')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div className={outerClass}>
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
onClick={() => initializeServer(serverName, false)}
|
||||
disabled={isServerInitializing}
|
||||
size={sidePanel ? 'sm' : 'default'}
|
||||
className="w-full"
|
||||
>
|
||||
{icon}
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function ExportModal({
|
||||
|
||||
const { exportConversation } = useExportConversation({
|
||||
conversation,
|
||||
filename,
|
||||
filename: filenamify(filename),
|
||||
type,
|
||||
includeOptions,
|
||||
exportBranches,
|
||||
@@ -95,7 +95,7 @@ export default function ExportModal({
|
||||
<Input
|
||||
id="filename"
|
||||
value={filename}
|
||||
onChange={(e) => setFileName(filenamify(e.target.value || ''))}
|
||||
onChange={(e) => setFileName(e.target.value || '')}
|
||||
placeholder={localize('com_nav_export_filename_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,8 @@ export const LangSelector = ({
|
||||
{ value: 'nl-NL', label: localize('com_nav_lang_dutch') },
|
||||
{ value: 'id-ID', label: localize('com_nav_lang_indonesia') },
|
||||
{ value: 'fi-FI', label: localize('com_nav_lang_finnish') },
|
||||
{ value: 'bo', label: localize('com_nav_lang_tibetan') },
|
||||
{ value: 'uk-UA', label: localize('com_nav_lang_ukrainian') },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
380
client/src/components/SidePanel/Agents/AgentPanel.test.tsx
Normal file
380
client/src/components/SidePanel/Agents/AgentPanel.test.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { render, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { Agent } from 'librechat-data-provider';
|
||||
|
||||
// Mock toast context - define this after all mocks
|
||||
let mockShowToast: jest.Mock;
|
||||
|
||||
// Mock notification severity enum before other imports
|
||||
jest.mock('~/common/types', () => ({
|
||||
NotificationSeverity: {
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
INFO: 'info',
|
||||
WARNING: 'warning',
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock store to prevent import errors
|
||||
jest.mock('~/store/toast', () => ({
|
||||
default: () => ({
|
||||
showToast: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/store', () => {});
|
||||
|
||||
// Mock the data service to control network responses
|
||||
jest.mock('librechat-data-provider', () => {
|
||||
const actualModule = jest.requireActual('librechat-data-provider') as any;
|
||||
return {
|
||||
...actualModule,
|
||||
dataService: {
|
||||
updateAgent: jest.fn(),
|
||||
},
|
||||
Tools: {
|
||||
execute_code: 'execute_code',
|
||||
file_search: 'file_search',
|
||||
web_search: 'web_search',
|
||||
},
|
||||
Constants: {
|
||||
EPHEMERAL_AGENT_ID: 'ephemeral',
|
||||
},
|
||||
SystemRoles: {
|
||||
ADMIN: 'ADMIN',
|
||||
},
|
||||
EModelEndpoint: {
|
||||
agents: 'agents',
|
||||
chatGPTBrowser: 'chatGPTBrowser',
|
||||
gptPlugins: 'gptPlugins',
|
||||
},
|
||||
isAssistantsEndpoint: jest.fn(() => false),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@librechat/client', () => ({
|
||||
Button: ({ children, onClick, ...props }: any) => (
|
||||
<button onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
useToastContext: () => ({
|
||||
get showToast() {
|
||||
return mockShowToast || jest.fn();
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock other dependencies
|
||||
jest.mock('librechat-data-provider/react-query', () => ({
|
||||
useGetModelsQuery: () => ({ data: {} }),
|
||||
}));
|
||||
|
||||
jest.mock('~/utils', () => ({
|
||||
createProviderOption: jest.fn((provider: string) => ({ value: provider, label: provider })),
|
||||
getDefaultAgentFormValues: jest.fn(() => ({
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
model: '',
|
||||
provider: '',
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useSelectAgent: () => ({ onSelect: jest.fn() }),
|
||||
useLocalize: () => (key: string) => key,
|
||||
useAuthContext: () => ({ user: { id: 'user-123', role: 'USER' } }),
|
||||
}));
|
||||
|
||||
jest.mock('~/Providers/AgentPanelContext', () => ({
|
||||
useAgentPanelContext: () => ({
|
||||
activePanel: 'builder',
|
||||
agentsConfig: { allowedProviders: [] },
|
||||
setActivePanel: jest.fn(),
|
||||
endpointsConfig: {},
|
||||
setCurrentAgentId: jest.fn(),
|
||||
agent_id: 'agent-123',
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/common', () => ({
|
||||
Panel: {
|
||||
model: 'model',
|
||||
builder: 'builder',
|
||||
advanced: 'advanced',
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock child components to simplify testing
|
||||
jest.mock('./AgentPanelSkeleton', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>{`Loading...`}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('./Advanced/AdvancedPanel', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>{`Advanced Panel`}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('./AgentConfig', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>{`Agent Config`}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('./AgentSelect', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>{`Agent Select`}</div>,
|
||||
}));
|
||||
|
||||
jest.mock('./ModelPanel', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>{`Model Panel`}</div>,
|
||||
}));
|
||||
|
||||
// Mock AgentFooter to provide a save button
|
||||
jest.mock('./AgentFooter', () => ({
|
||||
__esModule: true,
|
||||
default: () => (
|
||||
<button type="submit" data-testid="save-agent-button">
|
||||
{`Save Agent`}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock react-hook-form to capture form submission
|
||||
let mockFormSubmitHandler: (() => void) | null = null;
|
||||
|
||||
jest.mock('react-hook-form', () => {
|
||||
const actual = jest.requireActual('react-hook-form') as any;
|
||||
return {
|
||||
...actual,
|
||||
useForm: () => {
|
||||
const methods = actual.useForm({
|
||||
defaultValues: {
|
||||
id: 'agent-123',
|
||||
name: 'Test Agent',
|
||||
description: 'Test description',
|
||||
model: 'gpt-4',
|
||||
provider: 'openai',
|
||||
tools: [],
|
||||
execute_code: false,
|
||||
file_search: false,
|
||||
web_search: false,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...methods,
|
||||
handleSubmit: (onSubmit: any) => (e?: any) => {
|
||||
e?.preventDefault?.();
|
||||
mockFormSubmitHandler = () => onSubmit(methods.getValues());
|
||||
return mockFormSubmitHandler;
|
||||
},
|
||||
};
|
||||
},
|
||||
FormProvider: ({ children }: any) => children,
|
||||
useWatch: () => 'agent-123',
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mocks
|
||||
import { dataService } from 'librechat-data-provider';
|
||||
import { useGetAgentByIdQuery } from '~/data-provider';
|
||||
import AgentPanel from './AgentPanel';
|
||||
|
||||
// Mock useGetAgentByIdQuery
|
||||
jest.mock('~/data-provider', () => {
|
||||
const actual = jest.requireActual('~/data-provider') as any;
|
||||
return {
|
||||
...actual,
|
||||
useGetAgentByIdQuery: jest.fn(),
|
||||
useUpdateAgentMutation: actual.useUpdateAgentMutation,
|
||||
};
|
||||
});
|
||||
|
||||
// Test wrapper with QueryClient
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// Test helpers
|
||||
const setupMocks = () => {
|
||||
const mockUseGetAgentByIdQuery = useGetAgentByIdQuery as jest.MockedFunction<
|
||||
typeof useGetAgentByIdQuery
|
||||
>;
|
||||
const mockUpdateAgent = dataService.updateAgent as jest.MockedFunction<
|
||||
typeof dataService.updateAgent
|
||||
>;
|
||||
|
||||
return { mockUseGetAgentByIdQuery, mockUpdateAgent };
|
||||
};
|
||||
|
||||
const mockAgentQuery = (
|
||||
mockUseGetAgentByIdQuery: jest.MockedFunction<typeof useGetAgentByIdQuery>,
|
||||
agent: Partial<Agent>,
|
||||
) => {
|
||||
mockUseGetAgentByIdQuery.mockReturnValue({
|
||||
data: {
|
||||
id: 'agent-123',
|
||||
author: 'user-123',
|
||||
isCollaborative: false,
|
||||
...agent,
|
||||
} as Agent,
|
||||
isInitialLoading: false,
|
||||
} as any);
|
||||
};
|
||||
|
||||
const createMockAgent = (overrides: Partial<Agent> = {}): Agent =>
|
||||
({
|
||||
id: 'agent-123',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
...overrides,
|
||||
}) as Agent;
|
||||
|
||||
const renderAndSubmitForm = async () => {
|
||||
const Wrapper = createWrapper();
|
||||
const { container, rerender } = render(<AgentPanel />, { wrapper: Wrapper });
|
||||
|
||||
const form = container.querySelector('form');
|
||||
expect(form).toBeTruthy();
|
||||
|
||||
fireEvent.submit(form!);
|
||||
|
||||
if (mockFormSubmitHandler) {
|
||||
mockFormSubmitHandler();
|
||||
}
|
||||
|
||||
return { container, rerender, form };
|
||||
};
|
||||
|
||||
describe('AgentPanel - Update Agent Toast Messages', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockShowToast = jest.fn();
|
||||
mockFormSubmitHandler = null;
|
||||
});
|
||||
|
||||
describe('AgentPanel', () => {
|
||||
it('should show "no changes" toast when version does not change', async () => {
|
||||
const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks();
|
||||
|
||||
// Mock the agent query with version 2
|
||||
mockAgentQuery(mockUseGetAgentByIdQuery, {
|
||||
name: 'Test Agent',
|
||||
version: 2,
|
||||
});
|
||||
|
||||
// Mock network response - same version
|
||||
mockUpdateAgent.mockResolvedValue(createMockAgent({ name: 'Test Agent', version: 2 }));
|
||||
|
||||
await renderAndSubmitForm();
|
||||
|
||||
// Wait for the toast to be shown
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
message: 'com_ui_no_changes',
|
||||
status: 'info',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show "update success" toast when version changes', async () => {
|
||||
const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks();
|
||||
|
||||
// Mock the agent query with version 2
|
||||
mockAgentQuery(mockUseGetAgentByIdQuery, {
|
||||
name: 'Test Agent',
|
||||
version: 2,
|
||||
});
|
||||
|
||||
// Mock network response - different version
|
||||
mockUpdateAgent.mockResolvedValue(createMockAgent({ name: 'Test Agent', version: 3 }));
|
||||
|
||||
await renderAndSubmitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
message: 'com_assistants_update_success Test Agent',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show "update success" with default name when agent has no name', async () => {
|
||||
const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks();
|
||||
|
||||
// Mock the agent query without name
|
||||
mockAgentQuery(mockUseGetAgentByIdQuery, {
|
||||
version: 1,
|
||||
});
|
||||
|
||||
// Mock network response - no name
|
||||
mockUpdateAgent.mockResolvedValue(createMockAgent({ version: 2 }));
|
||||
|
||||
await renderAndSubmitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
message: 'com_assistants_update_success com_ui_agent',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show "update success" when agent query has no version (undefined)', async () => {
|
||||
const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks();
|
||||
|
||||
// Mock the agent query with no version data
|
||||
mockAgentQuery(mockUseGetAgentByIdQuery, {
|
||||
name: 'Test Agent',
|
||||
// No version property
|
||||
});
|
||||
|
||||
mockUpdateAgent.mockResolvedValue(createMockAgent({ name: 'Test Agent', version: 1 }));
|
||||
|
||||
await renderAndSubmitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
message: 'com_assistants_update_success Test Agent',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast on update failure', async () => {
|
||||
const { mockUseGetAgentByIdQuery, mockUpdateAgent } = setupMocks();
|
||||
|
||||
// Mock the agent query
|
||||
mockAgentQuery(mockUseGetAgentByIdQuery, {
|
||||
name: 'Test Agent',
|
||||
version: 1,
|
||||
});
|
||||
|
||||
// Mock network error
|
||||
mockUpdateAgent.mockRejectedValue(new Error('Update failed'));
|
||||
|
||||
await renderAndSubmitForm();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockShowToast).toHaveBeenCalledWith({
|
||||
message: 'com_agents_update_error com_ui_error: Update failed',
|
||||
status: 'error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Plus } from 'lucide-react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import React, { useMemo, useCallback, useRef } from 'react';
|
||||
import { Button, useToastContext } from '@librechat/client';
|
||||
import { useWatch, useForm, FormProvider } from 'react-hook-form';
|
||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||
@@ -54,6 +54,7 @@ export default function AgentPanel() {
|
||||
|
||||
const { control, handleSubmit, reset } = methods;
|
||||
const agent_id = useWatch({ control, name: 'id' });
|
||||
const previousVersionRef = useRef<number | undefined>();
|
||||
|
||||
const allowedProviders = useMemo(
|
||||
() => new Set(agentsConfig?.allowedProviders),
|
||||
@@ -77,50 +78,29 @@ export default function AgentPanel() {
|
||||
|
||||
/* Mutations */
|
||||
const update = useUpdateAgentMutation({
|
||||
onMutate: () => {
|
||||
// Store the current version before mutation
|
||||
previousVersionRef.current = agentQuery.data?.version;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
showToast({
|
||||
message: `${localize('com_assistants_update_success')} ${
|
||||
data.name ?? localize('com_ui_agent')
|
||||
}`,
|
||||
});
|
||||
// Check if agent version is the same (no changes were made)
|
||||
if (previousVersionRef.current !== undefined && data.version === previousVersionRef.current) {
|
||||
showToast({
|
||||
message: localize('com_ui_no_changes'),
|
||||
status: 'info',
|
||||
});
|
||||
} else {
|
||||
showToast({
|
||||
message: `${localize('com_assistants_update_success')} ${
|
||||
data.name ?? localize('com_ui_agent')
|
||||
}`,
|
||||
});
|
||||
}
|
||||
// Clear the ref after use
|
||||
previousVersionRef.current = undefined;
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = err as Error & {
|
||||
statusCode?: number;
|
||||
details?: { duplicateVersion?: any; versionIndex?: number };
|
||||
response?: { status?: number; data?: any };
|
||||
};
|
||||
|
||||
const isDuplicateVersionError =
|
||||
(error.statusCode === 409 && error.details?.duplicateVersion) ||
|
||||
(error.response?.status === 409 && error.response?.data?.details?.duplicateVersion);
|
||||
|
||||
if (isDuplicateVersionError) {
|
||||
let versionIndex: number | undefined = undefined;
|
||||
|
||||
if (error.details?.versionIndex !== undefined) {
|
||||
versionIndex = error.details.versionIndex;
|
||||
} else if (error.response?.data?.details?.versionIndex !== undefined) {
|
||||
versionIndex = error.response.data.details.versionIndex;
|
||||
}
|
||||
|
||||
if (versionIndex === undefined || versionIndex < 0) {
|
||||
showToast({
|
||||
message: localize('com_agents_update_error'),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
});
|
||||
} else {
|
||||
showToast({
|
||||
message: localize('com_ui_agent_version_duplicate', { versionIndex: versionIndex + 1 }),
|
||||
status: 'error',
|
||||
duration: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const error = err as Error;
|
||||
showToast({
|
||||
message: `${localize('com_agents_update_error')}${
|
||||
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Constants, QueryKeys } from 'librechat-data-provider';
|
||||
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
||||
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
||||
import ServerInitializationSection from '~/components/MCP/ServerInitializationSection';
|
||||
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
||||
import BadgeRowProvider from '~/Providers/BadgeRowContext';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import MCPPanelSkeleton from './MCPPanelSkeleton';
|
||||
@@ -127,50 +127,45 @@ function MCPPanelContent() {
|
||||
const serverStatus = connectionStatus[selectedServerNameForEditing];
|
||||
|
||||
return (
|
||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleGoBackToList}
|
||||
className="mb-3 flex items-center px-3 py-2 text-sm"
|
||||
>
|
||||
<div className="h-auto max-w-full space-y-4 overflow-x-hidden py-2">
|
||||
<Button variant="outline" onClick={handleGoBackToList} size="sm">
|
||||
<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
|
||||
<CustomUserVarsSection
|
||||
serverName={selectedServerNameForEditing}
|
||||
requiresOAuth={serverStatus?.requiresOAuth || false}
|
||||
fields={serverBeingEdited.config.customUserVars}
|
||||
onSave={(authData) => {
|
||||
if (selectedServerNameForEditing) {
|
||||
handleConfigSave(selectedServerNameForEditing, authData);
|
||||
}
|
||||
}}
|
||||
onRevoke={() => {
|
||||
if (selectedServerNameForEditing) {
|
||||
handleConfigRevoke(selectedServerNameForEditing);
|
||||
}
|
||||
}}
|
||||
isSubmitting={updateUserPluginsMutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Custom User Variables Section */}
|
||||
<CustomUserVarsSection
|
||||
<ServerInitializationSection
|
||||
sidePanel={true}
|
||||
serverName={selectedServerNameForEditing}
|
||||
fields={serverBeingEdited.config.customUserVars}
|
||||
onSave={(authData) => {
|
||||
if (selectedServerNameForEditing) {
|
||||
handleConfigSave(selectedServerNameForEditing, authData);
|
||||
}
|
||||
}}
|
||||
onRevoke={() => {
|
||||
if (selectedServerNameForEditing) {
|
||||
handleConfigRevoke(selectedServerNameForEditing);
|
||||
}
|
||||
}}
|
||||
isSubmitting={updateUserPluginsMutation.isLoading}
|
||||
requiresOAuth={serverStatus?.requiresOAuth || false}
|
||||
hasCustomUserVars={
|
||||
serverBeingEdited.config.customUserVars &&
|
||||
Object.keys(serverBeingEdited.config.customUserVars).length > 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// Server List View
|
||||
return (
|
||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||
<div className="h-auto max-w-full overflow-x-hidden py-2">
|
||||
<div className="space-y-2">
|
||||
{mcpServerDefinitions.map((server) => {
|
||||
const serverStatus = connectionStatus[server.serverName];
|
||||
@@ -187,7 +182,7 @@ function MCPPanelContent() {
|
||||
<span>{server.serverName}</span>
|
||||
{serverStatus && (
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs ${
|
||||
className={`rounded-xl 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'
|
||||
|
||||
@@ -43,11 +43,7 @@ export const useCreateAgentMutation = (
|
||||
*/
|
||||
export const useUpdateAgentMutation = (
|
||||
options?: t.UpdateAgentMutationOptions,
|
||||
): UseMutationResult<
|
||||
t.Agent,
|
||||
t.DuplicateVersionError,
|
||||
{ agent_id: string; data: t.AgentUpdateParams }
|
||||
> => {
|
||||
): UseMutationResult<t.Agent, Error, { agent_id: string; data: t.AgentUpdateParams }> => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
({ agent_id, data }: { agent_id: string; data: t.AgentUpdateParams }) => {
|
||||
@@ -59,8 +55,7 @@ export const useUpdateAgentMutation = (
|
||||
{
|
||||
onMutate: (variables) => options?.onMutate?.(variables),
|
||||
onError: (error, variables, context) => {
|
||||
const typedError = error as t.DuplicateVersionError;
|
||||
return options?.onError?.(typedError, variables, context);
|
||||
return options?.onError?.(error, variables, context);
|
||||
},
|
||||
onSuccess: (updatedAgent, variables, context) => {
|
||||
const listRes = queryClient.getQueryData<t.AgentListResponse>([
|
||||
|
||||
@@ -55,9 +55,10 @@ export const useUpdateConversationMutation = (
|
||||
return useMutation(
|
||||
(payload: t.TUpdateConversationRequest) => dataService.updateConversation(payload),
|
||||
{
|
||||
onSuccess: (updatedConvo) => {
|
||||
queryClient.setQueryData([QueryKeys.conversation, id], updatedConvo);
|
||||
updateConvoInAllQueries(queryClient, id, () => updatedConvo);
|
||||
onSuccess: (updatedConvo, payload) => {
|
||||
const targetId = payload.conversationId || id;
|
||||
queryClient.setQueryData([QueryKeys.conversation, targetId], updatedConvo);
|
||||
updateConvoInAllQueries(queryClient, targetId, () => updatedConvo);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import { Constants } from 'librechat-data-provider';
|
||||
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { getLatestText, logger } from '~/utils';
|
||||
import { useChatContext } from '~/Providers';
|
||||
import { logger } from '~/utils';
|
||||
import { useArtifactsContext } from '~/Providers';
|
||||
import { getKey } from '~/utils/artifacts';
|
||||
import store from '~/store';
|
||||
|
||||
export default function useArtifacts() {
|
||||
const [activeTab, setActiveTab] = useState('preview');
|
||||
const { isSubmitting, latestMessage, conversation } = useChatContext();
|
||||
const { isSubmitting, latestMessageId, latestMessageText, conversationId } =
|
||||
useArtifactsContext();
|
||||
|
||||
const artifacts = useRecoilValue(store.artifactsState);
|
||||
const resetArtifacts = useResetRecoilState(store.artifactsState);
|
||||
@@ -31,26 +32,23 @@ export default function useArtifacts() {
|
||||
const resetState = () => {
|
||||
resetArtifacts();
|
||||
resetCurrentArtifactId();
|
||||
prevConversationIdRef.current = conversation?.conversationId ?? null;
|
||||
prevConversationIdRef.current = conversationId;
|
||||
lastRunMessageIdRef.current = null;
|
||||
lastContentRef.current = null;
|
||||
hasEnclosedArtifactRef.current = false;
|
||||
};
|
||||
if (
|
||||
conversation?.conversationId !== prevConversationIdRef.current &&
|
||||
prevConversationIdRef.current != null
|
||||
) {
|
||||
if (conversationId !== prevConversationIdRef.current && prevConversationIdRef.current != null) {
|
||||
resetState();
|
||||
} else if (conversation?.conversationId === Constants.NEW_CONVO) {
|
||||
} else if (conversationId === Constants.NEW_CONVO) {
|
||||
resetState();
|
||||
}
|
||||
prevConversationIdRef.current = conversation?.conversationId ?? null;
|
||||
prevConversationIdRef.current = conversationId;
|
||||
/** Resets artifacts when unmounting */
|
||||
return () => {
|
||||
logger.log('artifacts_visibility', 'Unmounting artifacts');
|
||||
resetState();
|
||||
};
|
||||
}, [conversation?.conversationId, resetArtifacts, resetCurrentArtifactId]);
|
||||
}, [conversationId, resetArtifacts, resetCurrentArtifactId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (orderedArtifactIds.length > 0) {
|
||||
@@ -66,7 +64,7 @@ export default function useArtifacts() {
|
||||
if (orderedArtifactIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (latestMessage == null) {
|
||||
if (latestMessageId == null) {
|
||||
return;
|
||||
}
|
||||
const latestArtifactId = orderedArtifactIds[orderedArtifactIds.length - 1];
|
||||
@@ -78,7 +76,6 @@ export default function useArtifacts() {
|
||||
setCurrentArtifactId(latestArtifactId);
|
||||
lastContentRef.current = latestArtifact?.content ?? null;
|
||||
|
||||
const latestMessageText = getLatestText(latestMessage);
|
||||
const hasEnclosedArtifact =
|
||||
/:::artifact(?:\{[^}]*\})?(?:\s|\n)*(?:```[\s\S]*?```(?:\s|\n)*)?:::/m.test(
|
||||
latestMessageText.trim(),
|
||||
@@ -95,15 +92,22 @@ export default function useArtifacts() {
|
||||
hasAutoSwitchedToCodeRef.current = true;
|
||||
}
|
||||
}
|
||||
}, [setCurrentArtifactId, isSubmitting, orderedArtifactIds, artifacts, latestMessage]);
|
||||
}, [
|
||||
artifacts,
|
||||
isSubmitting,
|
||||
latestMessageId,
|
||||
latestMessageText,
|
||||
orderedArtifactIds,
|
||||
setCurrentArtifactId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (latestMessage?.messageId !== lastRunMessageIdRef.current) {
|
||||
lastRunMessageIdRef.current = latestMessage?.messageId ?? null;
|
||||
if (latestMessageId !== lastRunMessageIdRef.current) {
|
||||
lastRunMessageIdRef.current = latestMessageId;
|
||||
hasEnclosedArtifactRef.current = false;
|
||||
hasAutoSwitchedToCodeRef.current = false;
|
||||
}
|
||||
}, [latestMessage]);
|
||||
}, [latestMessageId]);
|
||||
|
||||
const currentArtifact = currentArtifactId != null ? artifacts?.[currentArtifactId] : null;
|
||||
|
||||
@@ -131,7 +135,6 @@ export default function useArtifacts() {
|
||||
isMermaid,
|
||||
setActiveTab,
|
||||
currentIndex,
|
||||
isSubmitting,
|
||||
cycleArtifact,
|
||||
currentArtifact,
|
||||
orderedArtifactIds,
|
||||
|
||||
@@ -3,15 +3,15 @@ import { useToastContext } from '@librechat/client';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Constants, QueryKeys } from 'librechat-data-provider';
|
||||
import {
|
||||
useCancelMCPOAuthMutation,
|
||||
useUpdateUserPluginsMutation,
|
||||
useReinitializeMCPServerMutation,
|
||||
useCancelMCPOAuthMutation,
|
||||
} from 'librechat-data-provider/react-query';
|
||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
|
||||
import type { ConfigFieldDetail } from '~/components/MCP/MCPConfigDialog';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { useMCPConnectionStatusQuery } from '~/data-provider/Tools/queries';
|
||||
import { useBadgeRowContext } from '~/Providers';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface ServerState {
|
||||
isInitializing: boolean;
|
||||
@@ -81,7 +81,9 @@ export function useMCPServerManager() {
|
||||
return initialStates;
|
||||
});
|
||||
|
||||
const { data: connectionStatusData } = useMCPConnectionStatusQuery();
|
||||
const { data: connectionStatusData } = useMCPConnectionStatusQuery({
|
||||
enabled: !!startupConfig?.mcpServers && Object.keys(startupConfig.mcpServers).length > 0,
|
||||
});
|
||||
const connectionStatus = useMemo(
|
||||
() => connectionStatusData?.connectionStatus || {},
|
||||
[connectionStatusData?.connectionStatus],
|
||||
@@ -158,6 +160,8 @@ export function useMCPServerManager() {
|
||||
setMCPValues([...currentValues, serverName]);
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries([QueryKeys.tools]);
|
||||
|
||||
// This delay is to ensure UI has updated with new connection status before cleanup
|
||||
// Otherwise servers will show as disconnected for a second after OAuth flow completes
|
||||
setTimeout(() => {
|
||||
@@ -171,6 +175,7 @@ export function useMCPServerManager() {
|
||||
message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
clearInterval(pollInterval);
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
@@ -180,10 +185,15 @@ export function useMCPServerManager() {
|
||||
message: localize('com_ui_mcp_init_failed'),
|
||||
status: 'error',
|
||||
});
|
||||
clearInterval(pollInterval);
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[MCP Manager] Error polling server ${serverName}:`, error);
|
||||
clearInterval(pollInterval);
|
||||
cleanupServerState(serverName);
|
||||
return;
|
||||
}
|
||||
}, 3500);
|
||||
|
||||
@@ -201,7 +211,7 @@ export function useMCPServerManager() {
|
||||
);
|
||||
|
||||
const initializeServer = useCallback(
|
||||
async (serverName: string) => {
|
||||
async (serverName: string, autoOpenOAuth: boolean = true) => {
|
||||
updateServerState(serverName, { isInitializing: true });
|
||||
|
||||
try {
|
||||
@@ -216,7 +226,9 @@ export function useMCPServerManager() {
|
||||
isInitializing: true,
|
||||
});
|
||||
|
||||
window.open(response.oauthUrl, '_blank', 'noopener,noreferrer');
|
||||
if (autoOpenOAuth) {
|
||||
window.open(response.oauthUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
startServerPolling(serverName);
|
||||
} else {
|
||||
@@ -234,6 +246,12 @@ export function useMCPServerManager() {
|
||||
|
||||
cleanupServerState(serverName);
|
||||
}
|
||||
} else {
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
cleanupServerState(serverName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[MCP Manager] Failed to initialize ${serverName}:`, error);
|
||||
@@ -259,13 +277,23 @@ export function useMCPServerManager() {
|
||||
|
||||
const cancelOAuthFlow = useCallback(
|
||||
(serverName: string) => {
|
||||
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||
cleanupServerState(serverName);
|
||||
cancelOAuthMutation.mutate(serverName);
|
||||
cancelOAuthMutation.mutate(serverName, {
|
||||
onSuccess: () => {
|
||||
cleanupServerState(serverName);
|
||||
queryClient.invalidateQueries([QueryKeys.mcpConnectionStatus]);
|
||||
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }),
|
||||
status: 'warning',
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_oauth_cancelled', { 0: serverName }),
|
||||
status: 'warning',
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(`[MCP Manager] Failed to cancel OAuth for ${serverName}:`, error);
|
||||
showToast({
|
||||
message: localize('com_ui_mcp_init_failed', { 0: serverName }),
|
||||
status: 'error',
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[queryClient, cleanupServerState, showToast, localize, cancelOAuthMutation],
|
||||
@@ -303,6 +331,10 @@ export function useMCPServerManager() {
|
||||
const disconnectedServers: string[] = [];
|
||||
|
||||
serverNames.forEach((serverName) => {
|
||||
if (isInitializing(serverName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverStatus = connectionStatus[serverName];
|
||||
if (serverStatus?.connectionState === 'connected') {
|
||||
connectedServers.push(serverName);
|
||||
@@ -317,11 +349,15 @@ export function useMCPServerManager() {
|
||||
initializeServer(serverName);
|
||||
});
|
||||
},
|
||||
[connectionStatus, setMCPValues, initializeServer],
|
||||
[connectionStatus, setMCPValues, initializeServer, isInitializing],
|
||||
);
|
||||
|
||||
const toggleServerSelection = useCallback(
|
||||
(serverName: string) => {
|
||||
if (isInitializing(serverName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValues = mcpValues ?? [];
|
||||
const isCurrentlySelected = currentValues.includes(serverName);
|
||||
|
||||
@@ -337,7 +373,7 @@ export function useMCPServerManager() {
|
||||
}
|
||||
}
|
||||
},
|
||||
[mcpValues, setMCPValues, connectionStatus, initializeServer],
|
||||
[mcpValues, setMCPValues, connectionStatus, initializeServer, isInitializing],
|
||||
);
|
||||
|
||||
const handleConfigSave = useCallback(
|
||||
|
||||
@@ -155,7 +155,10 @@ export default function useSideNavLinks({
|
||||
if (
|
||||
startupConfig?.mcpServers &&
|
||||
Object.values(startupConfig.mcpServers).some(
|
||||
(server) => server.customUserVars && Object.keys(server.customUserVars).length > 0,
|
||||
(server: any) =>
|
||||
(server.customUserVars && Object.keys(server.customUserVars).length > 0) ||
|
||||
server.isOAuth ||
|
||||
server.startup === false,
|
||||
)
|
||||
) {
|
||||
links.push({
|
||||
|
||||
135
client/src/locales/README.md
Normal file
135
client/src/locales/README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# LibreChat Localization Guide
|
||||
|
||||
This guide explains how to add new languages to LibreChat's localization system.
|
||||
|
||||
## Adding a New Language
|
||||
|
||||
To add a new language to LibreChat, follow these steps:
|
||||
|
||||
### 1. Add the Language to Locize Project
|
||||
|
||||
- Navigate to the [LibreChat locize project](https://www.locize.app/cat/62uyy7c9),
|
||||
- Click the "ADD LANGUAGE" button, typically found within the "..." menu of the "Start to translate" card on the project overview page.
|
||||
|
||||
### 2. Update the Language Selector Component
|
||||
|
||||
Edit `client/src/components/Nav/SettingsTabs/General/General.tsx` and add your new language option to the `languageOptions` array:
|
||||
|
||||
```typescript
|
||||
{ value: 'language-code', label: localize('com_nav_lang_language_name') },
|
||||
```
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
{ value: 'bo', label: localize('com_nav_lang_tibetan') },
|
||||
{ value: 'uk-UA', label: localize('com_nav_lang_ukrainian') },
|
||||
```
|
||||
|
||||
**Note:** Use the appropriate language code format:
|
||||
- Use simple codes (e.g., `bo`) for languages without regional variants
|
||||
- Use region-specific codes (e.g., `uk-UA`) when needed
|
||||
|
||||
### 3. Add Localization Keys
|
||||
|
||||
In `client/src/locales/en/translation.json`, add the corresponding localization key for your language label:
|
||||
|
||||
```json
|
||||
"com_nav_lang_language_name": "Native Language Name",
|
||||
```
|
||||
|
||||
Example:
|
||||
```json
|
||||
"com_nav_lang_tibetan": "བོད་སྐད་",
|
||||
"com_nav_lang_ukrainian": "Українська",
|
||||
```
|
||||
|
||||
**Best Practice:** Use the native language name as the value.
|
||||
|
||||
### 4. Create the Translation File
|
||||
|
||||
Create a new directory and translation file:
|
||||
|
||||
```bash
|
||||
mkdir -p client/src/locales/[language-code]
|
||||
```
|
||||
|
||||
Create `client/src/locales/[language-code]/translation.json` with an empty JSON object:
|
||||
|
||||
```json
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
Example:
|
||||
- `client/src/locales/bo/translation.json`
|
||||
- `client/src/locales/uk/translation.json`
|
||||
|
||||
### 5. Configure i18n
|
||||
|
||||
Update `client/src/locales/i18n.ts`:
|
||||
|
||||
1. Import the new translation file:
|
||||
```typescript
|
||||
import translationLanguageCode from './language-code/translation.json';
|
||||
```
|
||||
|
||||
2. Add it to the `resources` object:
|
||||
```typescript
|
||||
export const resources = {
|
||||
// ... existing languages
|
||||
'language-code': { translation: translationLanguageCode },
|
||||
} as const;
|
||||
```
|
||||
|
||||
Example:
|
||||
```typescript
|
||||
import translationBo from './bo/translation.json';
|
||||
import translationUk from './uk/translation.json';
|
||||
|
||||
export const resources = {
|
||||
// ... existing languages
|
||||
bo: { translation: translationBo },
|
||||
uk: { translation: translationUk },
|
||||
} as const;
|
||||
```
|
||||
|
||||
### 6. Handle Fallback Languages (Optional)
|
||||
|
||||
If your language should fall back to a specific language when translations are missing, update the `fallbackLng` configuration in `i18n.ts`:
|
||||
|
||||
```typescript
|
||||
fallbackLng: {
|
||||
'language-variant': ['fallback-language', 'en'],
|
||||
// ... existing fallbacks
|
||||
},
|
||||
```
|
||||
|
||||
## Translation Process
|
||||
|
||||
After adding a new language:
|
||||
|
||||
1. The empty translation file will be populated through LibreChat's automated translation platform
|
||||
2. Only the English (`en`) translation file should be manually updated
|
||||
3. Other language translations are managed externally
|
||||
|
||||
## Language Code Standards
|
||||
|
||||
- Use ISO 639-1 codes for most languages (e.g., `en`, `fr`, `de`)
|
||||
- Use ISO 639-1 with region codes when needed (e.g., `pt-BR`, `zh-Hans`)
|
||||
- Tibetan uses `bo` (Bodic)
|
||||
- Ukrainian uses `uk` or `uk-UA` with region
|
||||
|
||||
## Testing
|
||||
|
||||
After adding a new language:
|
||||
|
||||
1. Restart the development server
|
||||
2. Navigate to Settings > General
|
||||
3. Verify your language appears in the dropdown
|
||||
4. Select it to ensure it changes the UI language code
|
||||
|
||||
## Notes
|
||||
|
||||
- Keep language options alphabetically sorted in the dropdown for better UX
|
||||
- Always use native script for language names in the dropdown
|
||||
- The system will use English as fallback for any missing translations
|
||||
@@ -722,7 +722,6 @@
|
||||
"com_ui_upload_success": "تم تحميل الملف بنجاح",
|
||||
"com_ui_upload_type": "اختر نوع التحميل",
|
||||
"com_ui_use_micrphone": "استخدام الميكروفون",
|
||||
"com_ui_use_prompt": "استخدم الأمر",
|
||||
"com_ui_variables": "متغيرات",
|
||||
"com_ui_variables_info": "استخدم أقواس مزدوجة في نصك لإنشاء متغيرات، مثل `{{متغير كمثال}}`، لملئها لاحقاً عند استخدام النص البرمجي.",
|
||||
"com_ui_version_var": "الإصدار {{0}}",
|
||||
@@ -730,4 +729,4 @@
|
||||
"com_ui_yes": "نعم",
|
||||
"com_ui_zoom": "تكبير",
|
||||
"com_user_message": "أنت"
|
||||
}
|
||||
}
|
||||
|
||||
1
client/src/locales/bo/translation.json
Normal file
1
client/src/locales/bo/translation.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -61,6 +61,7 @@
|
||||
"com_assistants_non_retrieval_model": "La cerca de fitxers no està habilitada en aquest model. Selecciona un altre model.",
|
||||
"com_assistants_retrieval": "Recuperació",
|
||||
"com_assistants_running_action": "Executant acció",
|
||||
"com_assistants_running_var": "Executant {{0}}",
|
||||
"com_assistants_search_name": "Cerca assistents per nom",
|
||||
"com_assistants_update_actions_error": "S'ha produït un error en crear o actualitzar l'acció.",
|
||||
"com_assistants_update_actions_success": "Acció creada o actualitzada amb èxit",
|
||||
@@ -122,6 +123,7 @@
|
||||
"com_auth_reset_password_if_email_exists": "Si existeix un compte amb aquest correu, s'ha enviat un correu amb instruccions per restablir la contrasenya. Comprova la carpeta de correu brossa.",
|
||||
"com_auth_reset_password_link_sent": "Correu enviat",
|
||||
"com_auth_reset_password_success": "Contrasenya restablerta amb èxit",
|
||||
"com_auth_saml_login": "Continuar amb SAML",
|
||||
"com_auth_sign_in": "Inicia sessió",
|
||||
"com_auth_sign_up": "Registra't",
|
||||
"com_auth_submit_registration": "Envia el registre",
|
||||
@@ -133,6 +135,8 @@
|
||||
"com_auth_username_min_length": "El nom d'usuari ha de tenir almenys 2 caràcters",
|
||||
"com_auth_verify_your_identity": "Verifica la teva identitat",
|
||||
"com_auth_welcome_back": "Benvingut/da de nou",
|
||||
"com_citation_more_details": "Més detalls sobre {{label}}",
|
||||
"com_citation_source": "Font",
|
||||
"com_click_to_download": "(fes clic aquí per descarregar)",
|
||||
"com_download_expired": "(descàrrega caducada)",
|
||||
"com_download_expires": "(fes clic aquí per descarregar - caduca en {{0}})",
|
||||
@@ -299,6 +303,18 @@
|
||||
"com_nav_auto_transcribe_audio": "Transcriu àudio automàticament",
|
||||
"com_nav_automatic_playback": "Reprodueix automàticament el darrer missatge",
|
||||
"com_nav_balance": "Balanç",
|
||||
"com_nav_balance_day": "dia",
|
||||
"com_nav_balance_days": "dies",
|
||||
"com_nav_balance_hour": "hora",
|
||||
"com_nav_balance_hours": "hores",
|
||||
"com_nav_balance_minute": "minut",
|
||||
"com_nav_balance_minutes": "minuts",
|
||||
"com_nav_balance_month": "mes",
|
||||
"com_nav_balance_months": "mesos",
|
||||
"com_nav_balance_second": "segon",
|
||||
"com_nav_balance_seconds": "segons",
|
||||
"com_nav_balance_week": "setmana",
|
||||
"com_nav_balance_weeks": "setmanes",
|
||||
"com_nav_browser": "Navegador",
|
||||
"com_nav_center_chat_input": "Centra la entrada del xat a la pantalla de benvinguda",
|
||||
"com_nav_change_picture": "Canvia la imatge",
|
||||
@@ -560,8 +576,10 @@
|
||||
"com_ui_confirm_action": "Confirma l'acció",
|
||||
"com_ui_confirm_admin_use_change": "Canviar aquesta opció bloquejarà l'accés als administradors, inclòs tu mateix. Segur que vols continuar?",
|
||||
"com_ui_confirm_change": "Confirma el canvi",
|
||||
"com_ui_connecting": "Connectant",
|
||||
"com_ui_context": "Context",
|
||||
"com_ui_continue": "Continua",
|
||||
"com_ui_continue_oauth": "Continuar amb OAuth",
|
||||
"com_ui_controls": "Controls",
|
||||
"com_ui_convo_delete_error": "No s'ha pogut eliminar la conversa",
|
||||
"com_ui_copied": "Copiat!",
|
||||
@@ -625,6 +643,7 @@
|
||||
"com_ui_duplication_processing": "Duplicant conversa...",
|
||||
"com_ui_duplication_success": "Conversa duplicada amb èxit",
|
||||
"com_ui_edit": "Edita",
|
||||
"com_ui_edit_memory": "Editar memòria",
|
||||
"com_ui_empty_category": "-",
|
||||
"com_ui_endpoint": "Extrem",
|
||||
"com_ui_endpoint_menu": "Menú d'extrem LLM",
|
||||
@@ -701,6 +720,8 @@
|
||||
"com_ui_manage": "Gestiona",
|
||||
"com_ui_max_tags": "El màxim permès és {{0}}, s'utilitzen els últims valors.",
|
||||
"com_ui_mcp_servers": "Servidors MCP",
|
||||
"com_ui_memories": "Memòries",
|
||||
"com_ui_memory": "Memòria",
|
||||
"com_ui_mention": "Menciona un endpoint, assistent o predefinit per canviar-hi ràpidament",
|
||||
"com_ui_min_tags": "No es poden eliminar més valors, el mínim requerit és {{0}}.",
|
||||
"com_ui_misc": "Miscel·lània",
|
||||
@@ -729,6 +750,7 @@
|
||||
"com_ui_off": "Desactivat",
|
||||
"com_ui_on": "Activat",
|
||||
"com_ui_openai": "OpenAI",
|
||||
"com_ui_optional": "(opcional)",
|
||||
"com_ui_page": "Pàgina",
|
||||
"com_ui_prev": "Anterior",
|
||||
"com_ui_preview": "Previsualitza",
|
||||
@@ -778,6 +800,7 @@
|
||||
"com_ui_schema": "Esquema",
|
||||
"com_ui_scope": "Abast",
|
||||
"com_ui_search": "Cerca",
|
||||
"com_ui_seconds": "segons",
|
||||
"com_ui_secret_key": "Clau secreta",
|
||||
"com_ui_select": "Selecciona",
|
||||
"com_ui_select_file": "Selecciona un fitxer",
|
||||
@@ -852,7 +875,6 @@
|
||||
"com_ui_use_2fa_code": "Utilitza codi 2FA",
|
||||
"com_ui_use_backup_code": "Utilitza codi de recuperació",
|
||||
"com_ui_use_micrphone": "Utilitza el micròfon",
|
||||
"com_ui_use_prompt": "Utilitza prompt",
|
||||
"com_ui_used": "Utilitzat",
|
||||
"com_ui_variables": "Variables",
|
||||
"com_ui_variables_info": "Utilitza claus dobles per crear variables, per ex. `{{exemple variable}}`, per omplir-les quan utilitzis el prompt.",
|
||||
@@ -860,10 +882,11 @@
|
||||
"com_ui_version_var": "Versió {{0}}",
|
||||
"com_ui_versions": "Versions",
|
||||
"com_ui_view_source": "Mostra el xat original",
|
||||
"com_ui_web_search_processing": "Processant resultats",
|
||||
"com_ui_weekend_morning": "Bon cap de setmana",
|
||||
"com_ui_write": "Escriptura",
|
||||
"com_ui_x_selected": "{{0}} seleccionats",
|
||||
"com_ui_yes": "Sí",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "Tu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,7 +721,6 @@
|
||||
"com_ui_use_2fa_code": "Použít kód 2FA",
|
||||
"com_ui_use_backup_code": "Použít záložní kód",
|
||||
"com_ui_use_micrphone": "Použít mikrofon",
|
||||
"com_ui_use_prompt": "Použít výzvu",
|
||||
"com_ui_used": "Použito",
|
||||
"com_ui_variables": "Proměnné",
|
||||
"com_ui_variables_info": "Použijte dvojité složené závorky k vytvoření proměnných, např. `{{příklad proměnné}}`, které lze vyplnit při použití výzvy.",
|
||||
@@ -733,4 +732,4 @@
|
||||
"com_ui_yes": "Ano",
|
||||
"com_ui_zoom": "Přiblížit",
|
||||
"com_user_message": "Vy"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +516,6 @@
|
||||
"com_ui_agent_var": "{{0}} agent",
|
||||
"com_ui_agent_version": "Version",
|
||||
"com_ui_agent_version_active": "Aktiv version",
|
||||
"com_ui_agent_version_duplicate": "Duplikatversion fundet. Dette vil skabe en version, der er identisk med Version {{versionIndex}}.",
|
||||
"com_ui_agent_version_empty": "Ingen tilgængelige versioner",
|
||||
"com_ui_agent_version_error": "Fejl ved hentning af versioner",
|
||||
"com_ui_agent_version_history": "Versionshistorik",
|
||||
@@ -908,7 +907,6 @@
|
||||
"com_ui_use_2fa_code": "Brug 2FA-kode i stedet",
|
||||
"com_ui_use_backup_code": "Brug backup-koden i stedet",
|
||||
"com_ui_use_micrphone": "Brug mikrofon",
|
||||
"com_ui_use_prompt": "Brug prompt",
|
||||
"com_ui_used": "Brugt",
|
||||
"com_ui_variables": "Variabler",
|
||||
"com_ui_variables_info": "Brug dobbelte parenteser i din tekst til at oprette variabler, f.eks.{{example variable}}`, som senere skal udfyldes ved brug af prompten.",
|
||||
@@ -941,4 +939,4 @@
|
||||
"com_ui_yes": "Ja",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "Du"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,7 +503,6 @@
|
||||
"com_sidepanel_hide_panel": "Seitenleiste ausblenden",
|
||||
"com_sidepanel_manage_files": "Dateien verwalten",
|
||||
"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",
|
||||
"com_sources_image_alt": "Suchergebnis Bild\n",
|
||||
"com_sources_more_sources": "+{{count}} Quellen\n",
|
||||
@@ -549,7 +548,6 @@
|
||||
"com_ui_agent_var": "{{0}} Agent",
|
||||
"com_ui_agent_version": "Version",
|
||||
"com_ui_agent_version_active": "Aktive Version\n",
|
||||
"com_ui_agent_version_duplicate": "Doppelte Version entdeckt. Dies würde eine Version erzeugen, die identisch mit der Version {{versionIndex}} ist.",
|
||||
"com_ui_agent_version_empty": "Keine Versionen verfügbar\n",
|
||||
"com_ui_agent_version_error": "Fehler beim Abrufen der Versionen",
|
||||
"com_ui_agent_version_history": "Versionsgeschichte\n",
|
||||
@@ -840,7 +838,6 @@
|
||||
"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.",
|
||||
"com_ui_mcp_enter_var": "Geben Sie einen Wert für {{0}} ein",
|
||||
"com_ui_mcp_server_not_found": "Server nicht gefunden",
|
||||
"com_ui_mcp_servers": "MCP Server",
|
||||
@@ -1048,7 +1045,6 @@
|
||||
"com_ui_use_backup_code": "Stattdessen Backup-Code verwenden",
|
||||
"com_ui_use_memory": "Erinnerung nutzen",
|
||||
"com_ui_use_micrphone": "Mikrofon verwenden",
|
||||
"com_ui_use_prompt": "Prompt verwenden",
|
||||
"com_ui_used": "Verwendet",
|
||||
"com_ui_value": "Wert",
|
||||
"com_ui_variables": "Variablen",
|
||||
@@ -1086,4 +1082,4 @@
|
||||
"com_ui_yes": "Ja",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "Du"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
"com_endpoint_openai_max_tokens": "Optional 'max_tokens' field, representing the maximum number of tokens that can be generated in the chat completion. The total length of input tokens and generated tokens is limited by the models context length. You may experience errors if this number exceeds the max context tokens.",
|
||||
"com_endpoint_openai_pres": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.",
|
||||
"com_endpoint_openai_prompt_prefix_placeholder": "Set custom instructions to include in System Message. Default: none",
|
||||
"com_endpoint_openai_reasoning_effort": "o1 and o3 models only: constrains effort on reasoning for reasoning models. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response.",
|
||||
"com_endpoint_openai_reasoning_effort": "Reasoning models only: constrains effort on reasoning. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. 'Minimal' produces very few reasoning tokens for fastest time-to-first-token, especially well-suited for coding and instruction following.",
|
||||
"com_endpoint_openai_reasoning_summary": "Responses API only: A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. Set to none,auto, concise, or detailed.",
|
||||
"com_endpoint_openai_resend": "Resend all previously attached images. Note: this can significantly increase token cost and you may experience errors with many image attachments.",
|
||||
"com_endpoint_openai_resend_files": "Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.",
|
||||
@@ -238,6 +238,7 @@
|
||||
"com_endpoint_openai_topp": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.",
|
||||
"com_endpoint_openai_use_responses_api": "Use the Responses API instead of Chat Completions, which includes extended features from OpenAI. Required for o1-pro, o3-pro, and to enable reasoning summaries.",
|
||||
"com_endpoint_openai_use_web_search": "Enable web search functionality using OpenAI's built-in search capabilities. This allows the model to search the web for up-to-date information and provide more accurate, current responses.",
|
||||
"com_endpoint_openai_verbosity": "Constrains the verbosity of the model's response. Lower values will result in more concise responses, while higher values will result in more verbose responses. Currently supported values are low, medium, and high.",
|
||||
"com_endpoint_output": "Output",
|
||||
"com_endpoint_plug_image_detail": "Image Detail",
|
||||
"com_endpoint_plug_resend_files": "Resend Files",
|
||||
@@ -286,6 +287,7 @@
|
||||
"com_endpoint_use_active_assistant": "Use Active Assistant",
|
||||
"com_endpoint_use_responses_api": "Use Responses API",
|
||||
"com_endpoint_use_search_grounding": "Grounding with Google Search",
|
||||
"com_endpoint_verbosity": "Verbosity",
|
||||
"com_error_expired_user_key": "Provided key for {{0}} expired at {{1}}. Please provide a new key and try again.",
|
||||
"com_error_files_dupe": "Duplicate file detected.",
|
||||
"com_error_files_empty": "Empty files are not allowed.",
|
||||
@@ -435,8 +437,10 @@
|
||||
"com_nav_lang_spanish": "Español",
|
||||
"com_nav_lang_swedish": "Svenska",
|
||||
"com_nav_lang_thai": "ไทย",
|
||||
"com_nav_lang_tibetan": "བོད་སྐད་",
|
||||
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||
"com_nav_lang_turkish": "Türkçe",
|
||||
"com_nav_lang_ukrainian": "Українська",
|
||||
"com_nav_lang_uyghur": "Uyƣur tili",
|
||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||
"com_nav_language": "Language",
|
||||
@@ -446,7 +450,7 @@
|
||||
"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_update_error": "Error updating MCP custom user variables",
|
||||
"com_nav_mcp_vars_updated": "MCP custom user variables updated successfully.",
|
||||
"com_nav_modular_chat": "Enable switching Endpoints mid-conversation",
|
||||
"com_nav_my_files": "My Files",
|
||||
@@ -506,7 +510,6 @@
|
||||
"com_sidepanel_hide_panel": "Hide Panel",
|
||||
"com_sidepanel_manage_files": "Manage Files",
|
||||
"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",
|
||||
"com_sources_image_alt": "Search result image",
|
||||
"com_sources_more_sources": "+{{count}} sources",
|
||||
@@ -553,7 +556,6 @@
|
||||
"com_ui_agent_var": "{{0}} agent",
|
||||
"com_ui_agent_version": "Version",
|
||||
"com_ui_agent_version_active": "Active Version",
|
||||
"com_ui_agent_version_duplicate": "Duplicate version detected. This would create a version identical to Version {{versionIndex}}.",
|
||||
"com_ui_agent_version_empty": "No versions available",
|
||||
"com_ui_agent_version_error": "Error fetching versions",
|
||||
"com_ui_agent_version_history": "Version History",
|
||||
@@ -849,14 +851,10 @@
|
||||
"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_cancelled": "MCP server '{{0}}' initialization was cancelled due to simultaneous request",
|
||||
"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_timeout": "OAuth login timed out for {{0}}",
|
||||
"com_ui_mcp_server_not_found": "Server not found.",
|
||||
@@ -885,6 +883,7 @@
|
||||
"com_ui_memory_would_exceed": "Cannot save - would exceed limit by {{tokens}} tokens. Delete existing memories to make space.",
|
||||
"com_ui_mention": "Mention an endpoint, assistant, or preset to quickly switch to it",
|
||||
"com_ui_min_tags": "Cannot remove more values, a minimum of {{0}} are required.",
|
||||
"com_ui_minimal": "Minimal",
|
||||
"com_ui_misc": "Misc.",
|
||||
"com_ui_model": "Model",
|
||||
"com_ui_model_parameters": "Model Parameters",
|
||||
@@ -916,7 +915,6 @@
|
||||
"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",
|
||||
|
||||
@@ -743,7 +743,6 @@
|
||||
"com_ui_upload_success": "Archivo subido con éxito",
|
||||
"com_ui_upload_type": "Seleccionar tipo de carga",
|
||||
"com_ui_use_micrphone": "Usar micrófono",
|
||||
"com_ui_use_prompt": "Usar prompt",
|
||||
"com_ui_variables": "Variables",
|
||||
"com_ui_variables_info": "Utilice llaves dobles en su texto para crear variables, por ejemplo `{{variable de ejemplo}}`, para completarlas posteriormente al usar el prompt.",
|
||||
"com_ui_verify": "Verificar",
|
||||
@@ -755,4 +754,4 @@
|
||||
"com_ui_yes": "Sí",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "Usted"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,7 +517,6 @@
|
||||
"com_ui_agent_var": "{{0}} agent",
|
||||
"com_ui_agent_version": "Versioon",
|
||||
"com_ui_agent_version_active": "Aktiivne versioon",
|
||||
"com_ui_agent_version_duplicate": "Tuvastati duplikaatversioon. See looks versiooni, mis on identne versiooniga {{versionIndex}}.",
|
||||
"com_ui_agent_version_empty": "Versioone pole saadaval",
|
||||
"com_ui_agent_version_error": "Viga versioonide laadimisel",
|
||||
"com_ui_agent_version_history": "Versioonide ajalugu",
|
||||
@@ -930,7 +929,6 @@
|
||||
"com_ui_use_2fa_code": "Kasuta hoopis 2FA koodi",
|
||||
"com_ui_use_backup_code": "Kasuta hoopis varukoodi",
|
||||
"com_ui_use_micrphone": "Kasuta mikrofoni",
|
||||
"com_ui_use_prompt": "Kasuta sisendit",
|
||||
"com_ui_used": "Kasutatud",
|
||||
"com_ui_variables": "Muutujad",
|
||||
"com_ui_variables_info": "Kasuta oma tekstis topelt sulgusid, et luua muutujaid, nt `{{näidismuutuja}}`, et hiljem sisendi kasutamisel täita.",
|
||||
@@ -963,4 +961,4 @@
|
||||
"com_ui_yes": "Jah",
|
||||
"com_ui_zoom": "Suumi",
|
||||
"com_user_message": "Sina"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,7 +832,6 @@
|
||||
"com_ui_use_2fa_code": "به جای آن از کد 2FA استفاده کنید",
|
||||
"com_ui_use_backup_code": "به جای آن از کد پشتیبان استفاده کنید",
|
||||
"com_ui_use_micrphone": "از میکروفون استفاده کنید",
|
||||
"com_ui_use_prompt": "از prompt استفاده کنید",
|
||||
"com_ui_used": "استفاده می شود",
|
||||
"com_ui_variables": "متغیرها",
|
||||
"com_ui_variables_info": "از پرانتزهای دوتایی در متن خود برای ایجاد متغیرها استفاده کنید، به عنوان مثال. `{{example variable}}`، تا بعداً هنگام استفاده از درخواست پر شود.",
|
||||
@@ -845,4 +844,4 @@
|
||||
"com_ui_yes": "بله",
|
||||
"com_ui_zoom": "بزرگنمایی ضربه بزنید؛",
|
||||
"com_user_message": "شما"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -596,11 +596,10 @@
|
||||
"com_ui_upload_invalid_var": "Virheellinen ladattava tiedosto. Tiedoston täytyy olla enintään {{0}} MB kokoinen kuvatiedosto",
|
||||
"com_ui_upload_success": "Tiedoston lataus onnistui",
|
||||
"com_ui_use_micrphone": "Käytä mikrofonia",
|
||||
"com_ui_use_prompt": "Käytä syötettä",
|
||||
"com_ui_variables": "Muuttujat",
|
||||
"com_ui_variables_info": "Käytä kaksoisaaltosulkeita tekstissäsi muuttujien luomiseen, esim. {{esimerkkimuuttuja}}. Muuttujia voi täyttää myöhemmin syötettä käyttäessä.",
|
||||
"com_ui_version_var": "Versio {{0}}",
|
||||
"com_ui_versions": "Versiot",
|
||||
"com_ui_yes": "Kyllä",
|
||||
"com_user_message": "Sinä"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,7 +502,6 @@
|
||||
"com_sidepanel_hide_panel": "Masquer le panneau",
|
||||
"com_sidepanel_manage_files": "Gérer les fichiers",
|
||||
"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",
|
||||
"com_sources_image_alt": "Image de résultat de recherche",
|
||||
"com_sources_more_sources": "+{{count}} sources",
|
||||
@@ -548,7 +547,6 @@
|
||||
"com_ui_agent_var": "agent {{0}}",
|
||||
"com_ui_agent_version": "Version",
|
||||
"com_ui_agent_version_active": "Version active",
|
||||
"com_ui_agent_version_duplicate": "Duplicata de version détecté. Cela créerait une version identique à la version {{versionIndex}}",
|
||||
"com_ui_agent_version_empty": "Aucune version disponible",
|
||||
"com_ui_agent_version_error": "Erreur lors de la collecte des versions",
|
||||
"com_ui_agent_version_history": "Historique des versions",
|
||||
@@ -837,7 +835,6 @@
|
||||
"com_ui_low": "Faible",
|
||||
"com_ui_manage": "Gérer",
|
||||
"com_ui_max_tags": "Le nombre maximum autorisé est {{0}}, en utilisant les dernières valeurs.",
|
||||
"com_ui_mcp_dialog_desc": "Veuillez saisir les informations importantes ci-dessous.",
|
||||
"com_ui_mcp_enter_var": "Saisissez la valeur de {{0}}",
|
||||
"com_ui_mcp_server_not_found": "Le serveur n'a pas été trouvé.",
|
||||
"com_ui_mcp_servers": "Serveurs MCP",
|
||||
@@ -1021,18 +1018,18 @@
|
||||
"com_ui_update": "Mettre à jour",
|
||||
"com_ui_update_mcp_error": "Une erreur est survenue lors de la création ou l'actualisation du MCP.",
|
||||
"com_ui_update_mcp_success": "Création ou actualisation du MCP réussie",
|
||||
"com_ui_upload": "Téléverser",
|
||||
"com_ui_upload_code_files": "Téléverser pour l'Interpréteur de Code",
|
||||
"com_ui_upload": "Télécharger",
|
||||
"com_ui_upload_code_files": "Télécharger pour l'Interpréteur de Code",
|
||||
"com_ui_upload_delay": "Le téléversement de \"{{0}}\" prend plus de temps que prévu. Veuillez patienter pendant que le fichier termine son indexation pour la récupération.",
|
||||
"com_ui_upload_error": "Une erreur s'est produite lors du téléversement de votre fichier",
|
||||
"com_ui_upload_file_context": "Téléverser le contexte du fichier",
|
||||
"com_ui_upload_file_search": "Téléverser pour la recherche de fichiers",
|
||||
"com_ui_upload_files": "Téléverser des fichiers",
|
||||
"com_ui_upload_image": "Téléverser une image",
|
||||
"com_ui_upload_file_context": "Télécharger le contexte du fichier",
|
||||
"com_ui_upload_file_search": "Télécharger pour la recherche dans un fichier",
|
||||
"com_ui_upload_files": "Télécharger des fichiers",
|
||||
"com_ui_upload_image": "Télécharger une image",
|
||||
"com_ui_upload_image_input": "Téléverser une image",
|
||||
"com_ui_upload_invalid": "Fichier non valide pour le téléchargement. L'image ne doit pas dépasser la limite",
|
||||
"com_ui_upload_invalid_var": "Fichier non valide pour le téléchargement. L'image ne doit pas dépasser {{0}} Mo",
|
||||
"com_ui_upload_ocr_text": "Téléverser en tant que texte",
|
||||
"com_ui_upload_ocr_text": "Téléchager en tant que texte",
|
||||
"com_ui_upload_success": "Fichier téléversé avec succès",
|
||||
"com_ui_upload_type": "Sélectionner le type de téléversement",
|
||||
"com_ui_usage": "Utilisation",
|
||||
@@ -1040,7 +1037,6 @@
|
||||
"com_ui_use_backup_code": "Utiliser un code de sauvegarde à la place",
|
||||
"com_ui_use_memory": "Utiliser le Souvenir",
|
||||
"com_ui_use_micrphone": "Utiliser le microphone",
|
||||
"com_ui_use_prompt": "Utiliser le prompt",
|
||||
"com_ui_used": "Déjà utilisé",
|
||||
"com_ui_value": "Valeur",
|
||||
"com_ui_variables": "Variables",
|
||||
@@ -1077,4 +1073,4 @@
|
||||
"com_ui_yes": "Oui",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "Vous"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +495,6 @@
|
||||
"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}}} מקורות",
|
||||
@@ -541,7 +540,6 @@
|
||||
"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": "היסטוריית גרסאות",
|
||||
@@ -832,7 +830,6 @@
|
||||
"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",
|
||||
@@ -1039,7 +1036,6 @@
|
||||
"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": "משתנים",
|
||||
@@ -1069,4 +1065,4 @@
|
||||
"com_ui_yes": "כן",
|
||||
"com_ui_zoom": "זום",
|
||||
"com_user_message": "אתה"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,7 +832,6 @@
|
||||
"com_ui_use_2fa_code": "2FA kód használata helyette",
|
||||
"com_ui_use_backup_code": "Biztonsági mentési kód használata helyette",
|
||||
"com_ui_use_micrphone": "Mikrofon használata",
|
||||
"com_ui_use_prompt": "Prompt használata",
|
||||
"com_ui_used": "Használt",
|
||||
"com_ui_variables": "Változók",
|
||||
"com_ui_variables_info": "Használjon dupla kapcsos zárójeleket a szövegben változók létrehozásához, pl. `{{példa változó}}`, amelyeket később a prompt használatakor kitölthet.",
|
||||
@@ -845,4 +844,4 @@
|
||||
"com_ui_yes": "Igen",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "Ön"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,4 +198,4 @@
|
||||
"com_ui_write": "Գրում է",
|
||||
"com_ui_yes": "Այո",
|
||||
"com_user_message": "Դու"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ import translationHy from './hy/translation.json';
|
||||
import translationFi from './fi/translation.json';
|
||||
import translationZh_Hans from './zh-Hans/translation.json';
|
||||
import translationZh_Hant from './zh-Hant/translation.json';
|
||||
import translationBo from './bo/translation.json';
|
||||
import translationUk from './uk/translation.json';
|
||||
|
||||
export const defaultNS = 'translation';
|
||||
|
||||
@@ -71,6 +73,8 @@ export const resources = {
|
||||
hu: { translation: translationHu },
|
||||
hy: { translation: translationHy },
|
||||
fi: { translation: translationFi },
|
||||
bo: { translation: translationBo },
|
||||
uk: { translation: translationUk },
|
||||
} as const;
|
||||
|
||||
i18n
|
||||
|
||||
@@ -287,6 +287,5 @@
|
||||
"com_ui_upload": "Unggah",
|
||||
"com_ui_upload_error": "Ada kesalahan saat mengunggah file Anda",
|
||||
"com_ui_upload_success": "Berhasil mengunggah file",
|
||||
"com_ui_use_prompt": "Gunakan petunjuk",
|
||||
"com_user_message": "Kamu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -815,7 +815,6 @@
|
||||
"com_ui_use_2fa_code": "Usa invece il codice 2FA",
|
||||
"com_ui_use_backup_code": "Usa invece il codice di backup",
|
||||
"com_ui_use_micrphone": "Usa microfono",
|
||||
"com_ui_use_prompt": "Usa prompt",
|
||||
"com_ui_used": "Usato",
|
||||
"com_ui_variables": "Variabili",
|
||||
"com_ui_variables_info": "Usa le doppie parentesi graffe nel testo per creare variabili, ad esempio `{{variabile esempio}}`, da compilare successivamente quando utilizzi il prompt.",
|
||||
@@ -828,4 +827,4 @@
|
||||
"com_ui_yes": "Sì",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "Mostra nome utente nei messaggi"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,8 @@
|
||||
"com_endpoint_deprecated": "非推奨",
|
||||
"com_endpoint_deprecated_info": "このエンドポイントは非推奨であり、将来のバージョンで削除される可能性があります。",
|
||||
"com_endpoint_deprecated_info_a11y": "プラグインエンドポイントは非推奨であり、将来のバージョンで削除される可能性があります。",
|
||||
"com_endpoint_disable_streaming": "ストリーミング応答を無効にし、完全な応答を一度に受信する。o3 のように、ストリーミングのための組織検証を必要とするモデルに便利です。",
|
||||
"com_endpoint_disable_streaming_label": "ストリーミングを無効にする",
|
||||
"com_endpoint_examples": " プリセット名",
|
||||
"com_endpoint_export": "エクスポート",
|
||||
"com_endpoint_export_share": "エクスポート/共有",
|
||||
@@ -227,7 +229,7 @@
|
||||
"com_endpoint_openai_max_tokens": "オプションの 'max_tokens' フィールドで、チャット補完時に生成可能な最大トークン数を設定します。入力トークンと生成されたトークンの合計長さは、モデルのコンテキスト長によって制限されています。この数値がコンテキストの最大トークン数を超えると、エラーが発生する可能性があります。",
|
||||
"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_effort": "推論モデルのみ:推論の努力を制限します。推論の努力を減らすことで、応答が速くなり、応答における推論に使用されるトークンが少なくなります。「最小限」は、特にコーディングや指示のフォローに適しており、最初のトークンまでの時間を最速にするためにごくわずかな推論トークンを生成します。",
|
||||
"com_endpoint_openai_reasoning_summary": "Responses APIのみ:モデルが実行した推論の概要。これは、モデルの推論プロセスのデバッグや理解に役立ちます。none、auto、concise、detailedのいずれかに設定してください。",
|
||||
"com_endpoint_openai_resend": "これまでに添付した画像を全て再送信します。注意:トークン数が大幅に増加したり、多くの画像を添付するとエラーが発生する可能性があります。",
|
||||
"com_endpoint_openai_resend_files": "以前に添付されたすべてのファイルを再送信します。注意:これにより、トークンのコストが増加し、多くの添付ファイルでエラーが発生する可能性があります。",
|
||||
@@ -236,6 +238,7 @@
|
||||
"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_openai_verbosity": "モデルの応答の冗長性を制限します。値が低いほど簡潔な応答となり、値が高いほど冗長な応答となります。現在サポートされている値はlow、medium、highです。",
|
||||
"com_endpoint_output": "出力",
|
||||
"com_endpoint_plug_image_detail": "画像の詳細",
|
||||
"com_endpoint_plug_resend_files": "ファイルを再送",
|
||||
@@ -284,6 +287,7 @@
|
||||
"com_endpoint_use_active_assistant": "アクティブなアシスタントを使用",
|
||||
"com_endpoint_use_responses_api": "レスポンスAPIの使用",
|
||||
"com_endpoint_use_search_grounding": "Google検索でグラウンディング",
|
||||
"com_endpoint_verbosity": "冗長性",
|
||||
"com_error_expired_user_key": "{{0}}の提供されたキーは{{1}}で期限切れです。キーを入力して再試行してください。",
|
||||
"com_error_files_dupe": "重複したファイルが検出されました。",
|
||||
"com_error_files_empty": "空のファイルはアップロードできません",
|
||||
@@ -432,8 +436,10 @@
|
||||
"com_nav_lang_spanish": "Español",
|
||||
"com_nav_lang_swedish": "Svenska",
|
||||
"com_nav_lang_thai": "ไทย",
|
||||
"com_nav_lang_tibetan": "བོད་ སྐད་",
|
||||
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||
"com_nav_lang_turkish": "Türkçe",
|
||||
"com_nav_lang_ukrainian": "ウクラエンスカ",
|
||||
"com_nav_lang_uyghur": "ウラジオストク",
|
||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||
"com_nav_language": "言語",
|
||||
@@ -441,6 +447,8 @@
|
||||
"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": "会話の途中でのエンドポイント切替を有効化",
|
||||
@@ -501,7 +509,6 @@
|
||||
"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}} ソース",
|
||||
@@ -521,6 +528,7 @@
|
||||
"com_ui_2fa_verified": "2要素認証の認証に成功しました",
|
||||
"com_ui_accept": "同意します",
|
||||
"com_ui_action_button": "アクションボタン",
|
||||
"com_ui_active": "有効化",
|
||||
"com_ui_add": "追加",
|
||||
"com_ui_add_mcp": "MCPの追加",
|
||||
"com_ui_add_mcp_server": "MCPサーバーの追加",
|
||||
@@ -547,7 +555,6 @@
|
||||
"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": "バージョン履歴",
|
||||
@@ -591,6 +598,7 @@
|
||||
"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": "自動",
|
||||
@@ -648,8 +656,10 @@
|
||||
"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": "コピーしました!",
|
||||
@@ -839,10 +849,16 @@
|
||||
"com_ui_low": "低い",
|
||||
"com_ui_manage": "管理",
|
||||
"com_ui_max_tags": "最新の値を使用した場合、許可される最大数は {{0}} です。",
|
||||
"com_ui_mcp_dialog_desc": "以下に必要事項を入力してください。",
|
||||
"com_ui_mcp_authenticated_success": "MCPサーバー{{0}}認証成功",
|
||||
"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_oauth_cancelled": "OAuthログインがキャンセルされた {{0}}",
|
||||
"com_ui_mcp_oauth_timeout": "OAuthログインがタイムアウトしました。 {{0}}",
|
||||
"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": "メモリ",
|
||||
@@ -866,6 +882,7 @@
|
||||
"com_ui_memory_would_exceed": "保存できません - 制限を超えています {{tokens}} トークン。既存のメモリを削除してスペースを確保します。",
|
||||
"com_ui_mention": "エンドポイント、アシスタント、またはプリセットを素早く切り替えるには、それらを言及してください。",
|
||||
"com_ui_min_tags": "これ以上の値を削除できません。少なくとも {{0}} が必要です。",
|
||||
"com_ui_minimal": "最小限",
|
||||
"com_ui_misc": "その他",
|
||||
"com_ui_model": "モデル",
|
||||
"com_ui_model_parameters": "モデルパラメータ",
|
||||
@@ -901,6 +918,7 @@
|
||||
"com_ui_oauth_success_title": "認証成功",
|
||||
"com_ui_of": "of",
|
||||
"com_ui_off": "オフ",
|
||||
"com_ui_offline": "オフライン",
|
||||
"com_ui_on": "オン",
|
||||
"com_ui_openai": "OpenAI",
|
||||
"com_ui_optional": "(任意)",
|
||||
@@ -933,6 +951,7 @@
|
||||
"com_ui_regenerate_backup": "バックアップコードの再生成",
|
||||
"com_ui_regenerating": "再生成中...",
|
||||
"com_ui_region": "地域",
|
||||
"com_ui_reinitialize": "再初期化",
|
||||
"com_ui_rename": "タイトル変更",
|
||||
"com_ui_rename_conversation": "会話の名前を変更する",
|
||||
"com_ui_rename_failed": "会話の名前を変更できませんでした",
|
||||
@@ -972,6 +991,7 @@
|
||||
"com_ui_select_search_plugin": "プラグイン名で検索",
|
||||
"com_ui_select_search_provider": "プロバイダー名で検索",
|
||||
"com_ui_select_search_region": "地域名で検索",
|
||||
"com_ui_set": "セット",
|
||||
"com_ui_share": "共有",
|
||||
"com_ui_share_create_message": "あなたの名前と共有リンクを作成した後のメッセージは、共有されません。",
|
||||
"com_ui_share_delete_error": "共有リンクの削除中にエラーが発生しました。",
|
||||
@@ -1024,6 +1044,7 @@
|
||||
"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の作成または更新にエラーが発生しました。",
|
||||
@@ -1047,7 +1068,6 @@
|
||||
"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": "変数",
|
||||
@@ -1085,4 +1105,4 @@
|
||||
"com_ui_yes": "はい",
|
||||
"com_ui_zoom": "ズーム",
|
||||
"com_user_message": "あなた"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,4 +66,4 @@
|
||||
"com_nav_lang_turkish": "თურქული",
|
||||
"com_nav_lang_vietnamese": "ვიეტნამური",
|
||||
"com_nav_language": "ენა"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"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": "이름으로 에이전트 검색",
|
||||
@@ -128,6 +129,7 @@
|
||||
"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": "등록하기",
|
||||
@@ -156,6 +158,7 @@
|
||||
"com_endpoint_anthropic_thinking_budget": "Claude의 내부 추론에 사용할 수 있는 최대 토큰 수를 결정합니다. 큰 예산은 복잡한 문제에 대해 더 철저한 분석을 가능하게 하여 응답 품질을 개선할 수 있지만, 32K 이상 범위에서는 Claude가 할당된 전체 예산을 모두 사용하지 않을 수도 있습니다. 이 설정은 \"최대 출력 토큰\"보다 낮아야 합니다.",
|
||||
"com_endpoint_anthropic_topk": "Top-k는 모델이 출력에 사용할 토큰을 선택하는 방식을 변경합니다. top-k가 1인 경우 모델의 어휘 중 가장 확률이 높은 토큰이 선택됩니다(greedy decoding). top-k가 3인 경우 다음 토큰은 가장 확률이 높은 3개의 토큰 중에서 선택됩니다(temperature 사용).",
|
||||
"com_endpoint_anthropic_topp": "Top-p는 모델이 출력에 사용할 토큰을 선택하는 방식을 변경합니다. 토큰은 가장 높은 확률부터 가장 낮은 확률까지 선택됩니다. 선택된 토큰의 확률의 합이 top-p 값과 같아질 때까지 선택됩니다.",
|
||||
"com_endpoint_anthropic_use_web_search": "Anthropic의 내장 검색 기능을 사용하여 웹 검색 기능을 활성화합니다. 모델이 최신 정보를 검색하여 더 정확하고 현재의 응답을 제공할 수 있게 합니다.",
|
||||
"com_endpoint_assistant": "어시스턴트",
|
||||
"com_endpoint_assistant_model": "에이전트 모델",
|
||||
"com_endpoint_assistant_placeholder": "오른쪽 사이드 패널에서 에이전트를 선택하세요",
|
||||
@@ -193,6 +196,8 @@
|
||||
"com_endpoint_deprecated": "단축됨",
|
||||
"com_endpoint_deprecated_info": "이 엔드포인트는 단축되었으며 향후 버전에서 제거될 수 있습니다. 대신 에이전트 엔드포인트를 사용하세요.",
|
||||
"com_endpoint_deprecated_info_a11y": "이 플러그인 엔드포인트는 단축되었으며 향후 버전에서 제거될 수 있습니다. 대신 에이전트 엔드포인트를 사용하세요.",
|
||||
"com_endpoint_disable_streaming": "스트리밍 응답을 비활성화하고 완전한 응답을 한 번에 받습니다. o3와 같이 스트리밍을 위해 조직 확인이 필요한 모델에 유용합니다",
|
||||
"com_endpoint_disable_streaming_label": "스트리밍 비활성화",
|
||||
"com_endpoint_examples": " 프리셋",
|
||||
"com_endpoint_export": "내보내기",
|
||||
"com_endpoint_export_share": "내보내기/공유",
|
||||
@@ -201,8 +206,11 @@
|
||||
"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는 128-32,768 토큰을 지원합니다. Gemini 2.5 Flash는 0-24,576 토큰을 지원합니다. Gemini 2.5 Flash Lite는 512-24,576 토큰을 지원합니다.\n\n비워두거나 \"-1\"로 설정하면 모델이 언제 얼마나 생각할지 자동으로 결정합니다. 기본적으로 Gemini 2.5 Flash Lite는 생각하지 않습니다.",
|
||||
"com_endpoint_google_topk": "Top-k는 모델이 출력에 사용할 토큰을 선택하는 방식을 변경합니다. top-k가 1인 경우 모델의 어휘 중 가장 확률이 높은 토큰이 선택됩니다(greedy decoding). top-k가 3인 경우 다음 토큰은 가장 확률이 높은 3개의 토큰 중에서 선택됩니다(temperature 사용).",
|
||||
"com_endpoint_google_topp": "Top-p는 모델이 출력에 사용할 토큰을 선택하는 방식을 변경합니다. 토큰은 가장 높은 확률부터 가장 낮은 확률까지 선택됩니다. 선택된 토큰의 확률의 합이 top-p 값과 같아질 때까지 선택됩니다.",
|
||||
"com_endpoint_google_use_search_grounding": "Google의 검색 그라운딩 기능을 사용하여 실시간 웹 검색 결과로 응답을 향상시킵니다. 모델이 현재 정보에 접근하여 더 정확하고 최신의 답변을 제공할 수 있게 합니다.",
|
||||
"com_endpoint_instructions_assistants": "에이전트 지침 재정의",
|
||||
"com_endpoint_instructions_assistants_placeholder": "어시스턴트의 지침을 재정의합니다. 이를 통해 실행마다 동작을 수정할 수 있습니다.",
|
||||
"com_endpoint_max_output_tokens": "최대 출력 토큰 수",
|
||||
@@ -220,11 +228,14 @@
|
||||
"com_endpoint_openai_pres": "텍스트에서 토큰이 나타나는지 여부에 따라 새로운 토큰에 패널티를 부여합니다. 이전에 나온 텍스트에 나타나는 토큰에 대한 패널티를 증가시켜 새로운 주제에 대해 이야기할 가능성을 높입니다.",
|
||||
"com_endpoint_openai_prompt_prefix_placeholder": "시스템 메시지에 포함할 사용자 정의 지시사항을 설정하세요. 기본값: 없음",
|
||||
"com_endpoint_openai_reasoning_effort": "o1 및 o3 모델 전용: 추론 모델의 추론 노력(reasoning effort)을 제한합니다. 추론 노력을 줄이면 응답 속도가 빨라지고, 응답에서 사용되는 추론 관련 토큰 수가 줄어들 수 있습니다.",
|
||||
"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": "온도를 사용한 샘플링 대신, top_p 확률 질량을 고려하는 nucleus 샘플링입니다. 따라서 0.1은 상위 10% 확률 질량을 구성하는 토큰만 고려합니다. 이 값을 변경하거나 온도를 변경하는 것을 권장하지만, 둘 다 변경하지는 마세요.",
|
||||
"com_endpoint_openai_use_responses_api": "OpenAI의 확장 기능이 포함된 Chat Completions 대신 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": "파일 재전송",
|
||||
@@ -255,6 +266,7 @@
|
||||
"com_endpoint_prompt_prefix_assistants_placeholder": "추가 지시사항 또는 컨텍스트를 Assistant의 기본 지시사항에 추가합니다. 비어 있으면 무시됩니다.",
|
||||
"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}} 모델 검색중...",
|
||||
@@ -270,6 +282,8 @@
|
||||
"com_endpoint_top_k": "Top K",
|
||||
"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": "빈 파일은 허용되지 않습니다",
|
||||
@@ -278,6 +292,8 @@
|
||||
"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}}. 메시지를 줄이거나, 대화 파라미터에서 최대 컨텍스트 크기를 조정하거나, 대화를 포크(fork)하여 계속 진행해 주세요.",
|
||||
"com_error_invalid_agent_provider": "\"{{0}}\" 제공자는 에이전트와 함께 사용할 수 없습니다. 에이전트 설정으로 이동하여 현재 사용 가능한 제공자를 선택하세요.",
|
||||
"com_error_invalid_user_key": "제공된 키가 유효하지 않습니다. 키를 제공하고 다시 시도해주세요.",
|
||||
@@ -290,6 +306,7 @@
|
||||
"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": "항상 새 버전을 프로덕션으로 설정",
|
||||
@@ -307,6 +324,26 @@
|
||||
"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_next_refill": "다음 충전:",
|
||||
"com_nav_balance_next_refill_info": "다음 충전은 두 조건이 모두 충족될 때만 자동으로 발생합니다: 마지막 충전 이후 지정된 시간 간격이 지났고, 프롬프트를 보내면 잔액이 0 아래로 떨어질 때입니다.",
|
||||
"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": "프로필 사진 변경",
|
||||
@@ -367,6 +404,7 @@
|
||||
"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": "카탈로니아어",
|
||||
@@ -386,6 +424,7 @@
|
||||
"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",
|
||||
@@ -395,12 +434,17 @@
|
||||
"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_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": "지원되지 않음",
|
||||
@@ -424,6 +468,8 @@
|
||||
"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": "공유 링크",
|
||||
@@ -456,6 +502,7 @@
|
||||
"com_sidepanel_conversation_tags": "북마크",
|
||||
"com_sidepanel_hide_panel": "패널 숨기기",
|
||||
"com_sidepanel_manage_files": "파일 관리",
|
||||
"com_sidepanel_mcp_no_servers_with_vars": "설정 가능한 변수가 있는 MCP 서버가 없습니다.",
|
||||
"com_sidepanel_parameters": "매개변수",
|
||||
"com_sources_image_alt": "검색 결과 이미지",
|
||||
"com_sources_more_sources": "+{{count}}개 소스",
|
||||
@@ -475,6 +522,7 @@
|
||||
"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 서버 추가",
|
||||
@@ -501,7 +549,6 @@
|
||||
"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": "버전 기록",
|
||||
@@ -527,6 +574,7 @@
|
||||
"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": "오름차순",
|
||||
@@ -544,11 +592,14 @@
|
||||
"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": "백업 코드",
|
||||
@@ -588,16 +639,21 @@
|
||||
"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": "변경 확인",
|
||||
"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": "복사됨",
|
||||
@@ -610,6 +666,7 @@
|
||||
"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": "사용자 지정 헤더 이름",
|
||||
@@ -647,15 +704,19 @@
|
||||
"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": "아티팩트 다운로드",
|
||||
@@ -697,6 +758,7 @@
|
||||
"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": "유용한 정보가 부족함",
|
||||
@@ -716,6 +778,7 @@
|
||||
"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": "\"포킹(Forking)\"은 현재 대화에서 특정 메시지를 시작/종료 지점으로 하여 새로운 대화를 생성하고, 선택한 옵션에 따라 복사본을 만드는 것을 의미합니다.",
|
||||
@@ -748,7 +811,9 @@
|
||||
"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": "아이디어",
|
||||
@@ -775,10 +840,21 @@
|
||||
"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_enter_var": "{{0}}의 값 입력",
|
||||
"com_ui_mcp_init_failed": "MCP 서버 초기화 실패",
|
||||
"com_ui_mcp_initialize": "초기화",
|
||||
"com_ui_mcp_initialized_success": "MCP 서버 '{{0}}'가 성공적으로 초기화되었습니다",
|
||||
"com_ui_mcp_oauth_cancelled": "{{0}}의 OAuth 로그인이 취소되었습니다",
|
||||
"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": "사용자가 메모리 기능을 비활성화할 수 있도록 허용",
|
||||
@@ -787,12 +863,17 @@
|
||||
"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": "기타",
|
||||
@@ -819,8 +900,17 @@
|
||||
"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_missing_code": "인증 코드가 누락되었습니다. 다시 시도하세요.",
|
||||
"com_ui_oauth_error_missing_state": "상태 파라미터가 누락되었습니다. 다시 시도하세요.",
|
||||
"com_ui_oauth_error_title": "인증 실패",
|
||||
"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": "(선택사항)",
|
||||
@@ -843,6 +933,7 @@
|
||||
"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": "저장된 메모리 참고",
|
||||
@@ -852,12 +943,14 @@
|
||||
"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": "프롬프트 이름 변경",
|
||||
"com_ui_requires_auth": "인증이 필요합니다",
|
||||
"com_ui_reset_var": "{{0}} 초기화",
|
||||
"com_ui_reset_zoom": "초기화",
|
||||
"com_ui_result": "결과",
|
||||
"com_ui_revoke": "취소",
|
||||
"com_ui_revoke_info": "사용자가 제공한 자격 증명을 모두 취소합니다.",
|
||||
@@ -873,9 +966,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": "모두 선택",
|
||||
@@ -888,6 +983,7 @@
|
||||
"com_ui_select_search_plugin": "이름으로 플러그인 검색",
|
||||
"com_ui_select_search_provider": "이름으로 공급자 검색",
|
||||
"com_ui_select_search_region": "이름으로 지역 검색",
|
||||
"com_ui_set": "설정",
|
||||
"com_ui_share": "공유하기",
|
||||
"com_ui_share_create_message": "이름과 공유 후에 추가하는 메시지는 비공개로 유지됩니다.",
|
||||
"com_ui_share_delete_error": "공유 링크를 삭제하는 중에 오류가 발생했습니다.",
|
||||
@@ -905,6 +1001,7 @@
|
||||
"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": "간단",
|
||||
@@ -930,12 +1027,16 @@
|
||||
"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_unset": "설정 해제",
|
||||
"com_ui_untitled": "제목 없음",
|
||||
"com_ui_update": "업데이트",
|
||||
"com_ui_update_mcp_error": " MCP 생성 혹은 업데이트 중 오류가 발생했습니다.",
|
||||
@@ -959,7 +1060,6 @@
|
||||
"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": "변수",
|
||||
@@ -975,6 +1075,7 @@
|
||||
"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": "결과 읽기 중",
|
||||
@@ -986,6 +1087,8 @@
|
||||
"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": "행복한 주말 되세요",
|
||||
@@ -994,4 +1097,4 @@
|
||||
"com_ui_yes": "네",
|
||||
"com_ui_zoom": "확대/축소",
|
||||
"com_user_message": "당신"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
"com_auth_email_verification_failed_token_missing": "Verifikācija neizdevās, trūkst tokena",
|
||||
"com_auth_email_verification_in_progress": "Jūsu e-pasta verifikācija, lūdzu, uzgaidiet",
|
||||
"com_auth_email_verification_invalid": "Nederīga e-pasta verifikācija",
|
||||
"com_auth_email_verification_redirecting": "Pārvirzīšana {{0}} sekundēs...",
|
||||
"com_auth_email_verification_redirecting": "Pārvirzu {{0}} sekundēs...",
|
||||
"com_auth_email_verification_resend_prompt": "Nesaņēmāt e-pastu?",
|
||||
"com_auth_email_verification_success": "E-pasts veiksmīgi pārbaudīts",
|
||||
"com_auth_email_verifying_ellipsis": "Pārbauda...",
|
||||
@@ -155,7 +155,7 @@
|
||||
"com_endpoint_ai": "Mākslīgais intelekts",
|
||||
"com_endpoint_anthropic_maxoutputtokens": "Maksimālais atbildē ģenerējamo tokenu skaits. Norādiet zemāku vērtību īsākām atbildēm un augstāku vērtību garākām atbildēm. Piezīme: modeļi var apstāties pirms šī maksimālā skaita sasniegšanas.",
|
||||
"com_endpoint_anthropic_prompt_cache": "Uzvednes kešatmiņa ļauj atkārtoti izmantot lielu kontekstu vai instrukcijas API izsaukumos, samazinot izmaksas un ābildes ātrumu.",
|
||||
"com_endpoint_anthropic_temp": "Diapazons no 0 līdz 1. Analītiskiem/atbilžu variantiem izmantojiet temp vērtību tuvāk 0, bet radošiem un ģeneratīviem uzdevumiem — tuvāk 1. Iesakām mainīt šo vai Top P, bet ne abus.",
|
||||
"com_endpoint_anthropic_temp": "Diapazons no 0 līdz 1. Analītiskiem/atbilžu variantiem izmantot temp vērtību tuvāk 0, bet radošiem un ģeneratīviem uzdevumiem — tuvāk 1. Iesakām mainīt šo vai Top P, bet ne abus.",
|
||||
"com_endpoint_anthropic_thinking": "Iespējo iekšējo domāšanu atbalstītajiem Claude modeļiem (3.7 Sonnet). Piezīme: nepieciešams iestatīt \"Domāšanas budžetu\", kam arī jābūt zemākam par \"Max Output Tokens\".",
|
||||
"com_endpoint_anthropic_thinking_budget": "Nosaka maksimālo žetonu skaitu, ko Claude drīkst izmantot savā iekšējā domāšanas procesā. Lielāki budžeti var uzlabot atbilžu kvalitāti, nodrošinot rūpīgāku analīzi sarežģītām problēmām, lai gan Claude var neizmantot visu piešķirto budžetu, īpaši diapazonos virs 32 000. Šim iestatījumam jābūt zemākam par \"Maksimālie izvades tokeni\".",
|
||||
"com_endpoint_anthropic_topk": "Top-k maina to, kā modelis atlasa marķierus izvadei. Ja top-k ir 1, tas nozīmē, ka atlasītais marķieris ir visticamākais starp visiem modeļa vārdu krājumā esošajiem marķieriem (to sauc arī par alkatīgo dekodēšanu), savukārt, ja top-k ir 3, tas nozīmē, ka nākamais marķieris tiek izvēlēts no 3 visticamākajiem marķieriem (izmantojot temperatūru).",
|
||||
@@ -188,7 +188,7 @@
|
||||
"com_endpoint_config_placeholder": "Iestatiet savu atslēgu galvenes izvēlnē, lai izveidotu sarunu.",
|
||||
"com_endpoint_config_value": "Ievadiet vērtību",
|
||||
"com_endpoint_context": "Konteksts",
|
||||
"com_endpoint_context_info": "Maksimālais tokenu skaits, ko var izmantot kontekstam. Izmantojiet to, lai kontrolētu, cik tokenu tiek nosūtīti katrā pieprasījumā. Ja tas nav norādīts, tiks izmantoti sistēmas noklusējuma iestatījumi, pamatojoties uz zināmo modeļu konteksta lielumu. Augstāku vērtību iestatīšana var izraisīt kļūdas un/vai augstākas tokenu izmaksas.",
|
||||
"com_endpoint_context_info": "Maksimālais tokenu skaits, ko var izmantot kontekstam. Izmanto to, lai kontrolētu, cik tokenu tiek nosūtīti katrā pieprasījumā. Ja tas nav norādīts, tiks izmantoti sistēmas noklusējuma iestatījumi, pamatojoties uz zināmo modeļu konteksta lielumu. Augstāku vērtību iestatīšana var izraisīt kļūdas un/vai augstākas tokenu izmaksas.",
|
||||
"com_endpoint_context_tokens": "Maksimālais konteksta tokenu skaits",
|
||||
"com_endpoint_custom_name": "Pielāgots nosaukums",
|
||||
"com_endpoint_default": "noklusējuma",
|
||||
@@ -212,7 +212,7 @@
|
||||
"com_endpoint_google_thinking_budget": "Norāda modeļa izmantoto domāšanas tokenu skaitu. Faktiskais skaits var pārsniegt vai būt mazāks par šo vērtību atkarībā no uzvednes.\n\nŠo iestatījumu atbalsta tikai noteikti modeļi (2.5 sērija). Gemini 2.5 Pro atbalsta 128–32 768 žetonus. Gemini 2.5 Flash atbalsta 0–24 576 žetonus. Gemini 2.5 Flash Lite atbalsta 512–24 576 žetonus.\n\nAtstājiet tukšu vai iestatiet uz \"-1\", lai modelis automātiski izlemtu, kad un cik daudz domāt. Pēc noklusējuma Gemini 2.5 Flash Lite nedomā.",
|
||||
"com_endpoint_google_topk": "Top-k maina to, kā modelis atlasa marķierus izvadei. Ja top-k ir 1, tas nozīmē, ka atlasītais marķieris ir visticamākais starp visiem modeļa vārdu krājumā esošajiem marķieriem (to sauc arī par alkatīgo dekodēšanu), savukārt, ja top-k ir 3, tas nozīmē, ka nākamais marķieris tiek izvēlēts no 3 visticamākajiem marķieriem (izmantojot temperatūru).",
|
||||
"com_endpoint_google_topp": "`Top-p` maina to, kā modelis atlasa tokenus izvadei. Marķieri tiek atlasīti no K (skatīt parametru topK) ticamākās līdz vismazāk ticamajai, līdz to varbūtību summa ir vienāda ar `top-p` vērtību.",
|
||||
"com_endpoint_google_use_search_grounding": "Izmantojiet Google meklēšanas pamatošanas funkciju, lai uzlabotu atbildes ar reāllaika tīmekļa meklēšanas rezultātiem. Tas ļauj modeļiem piekļūt aktuālajai informācijai un sniegt precīzākas, aktuālākas atbildes.",
|
||||
"com_endpoint_google_use_search_grounding": "Izmantot Google meklēšanas pamatošanas funkciju, lai uzlabotu atbildes ar reāllaika tīmekļa meklēšanas rezultātiem. Tas ļauj modeļiem piekļūt aktuālajai informācijai un sniegt precīzākas, aktuālākas atbildes.",
|
||||
"com_endpoint_instructions_assistants": "Pārrakstīt instrukcijas",
|
||||
"com_endpoint_instructions_assistants_placeholder": "Pārraksta asistenta norādījumus. Tas ir noderīgi, lai mainītu darbību katrā palaišanas reizē.",
|
||||
"com_endpoint_max_output_tokens": "Maksimālais izvades tokenu skaits",
|
||||
@@ -236,14 +236,14 @@
|
||||
"com_endpoint_openai_stop": "Līdz 4 secībām, kurās API pārtrauks turpmāku tokenu ģenerēšanu.",
|
||||
"com_endpoint_openai_temp": "Augstākas vērtības = nejaušāks, savukārt zemākas vērtības = fokusētāks un deterministiskāks. Iesakām mainīt šo vai Top P, bet ne abus.",
|
||||
"com_endpoint_openai_topp": "Alternatīva izlasei ar temperatūru, ko sauc par kodola izlasi, kur modelis ņem vērā tokenu rezultātus ar varbūtības masu top_p. Tātad 0,1 nozīmē, ka tiek ņemti vērā tikai tie tokeni, kas veido augšējo 10% varbūtības masu. Mēs iesakām mainīt šo vai temperatūru, bet ne abus.",
|
||||
"com_endpoint_openai_use_responses_api": "Izmantojiet Response API sarunas pabeigšanas vietā, kas ietver paplašinātas OpenAI funkcijas. Nepieciešams o1-pro, o3-pro un spriešanas kopsavilkumu iespējošanai.",
|
||||
"com_endpoint_openai_use_responses_api": "Izmantot Response API sarunas pabeigšanas vietā, kas ietver paplašinātas OpenAI funkcijas. Nepieciešams o1-pro, o3-pro un spriešanas kopsavilkumu iespējošanai.",
|
||||
"com_endpoint_openai_use_web_search": "Iespējojiet tīmekļa meklēšanas funkcionalitāti, izmantojot OpenAI iebūvētās meklēšanas iespējas. Tas ļauj modelim meklēt tīmeklī aktuālu informāciju un sniegt precīzākas, aktuālākas atbildes.",
|
||||
"com_endpoint_output": "Izvade",
|
||||
"com_endpoint_plug_image_detail": "Attēla detaļas",
|
||||
"com_endpoint_plug_resend_files": "Atkārtoti nosūtīt failus",
|
||||
"com_endpoint_plug_set_custom_instructions_for_gpt_placeholder": "Iestatiet pielāgotas instrukcijas, kas jāiekļauj sistēmas ziņā. Noklusējuma vērtība: nav",
|
||||
"com_endpoint_plug_skip_completion": "Izlaist pabeigšanu",
|
||||
"com_endpoint_plug_use_functions": "Izmant funkcijas",
|
||||
"com_endpoint_plug_use_functions": "Izmantot funkcijas",
|
||||
"com_endpoint_presence_penalty": "Klātbūtnes sods",
|
||||
"com_endpoint_preset": "iepriekš iestatīts",
|
||||
"com_endpoint_preset_custom_name_placeholder": "kaut kam šeit ir jānotiek. bija tukšs",
|
||||
@@ -308,15 +308,15 @@
|
||||
"com_files_table": "kaut kam šeit ir jānotiek. bija tukšs",
|
||||
"com_generated_files": "Ģenerētie faili:",
|
||||
"com_hide_examples": "Slēpt piemērus",
|
||||
"com_info_heic_converting": "HEIC attēla konvertēšana uz JPEG...",
|
||||
"com_info_heic_converting": "Konvertēju HEIC attēlu uz JPEG...",
|
||||
"com_nav_2fa": "Divfaktoru autentifikācija (2FA)",
|
||||
"com_nav_account_settings": "Konta iestatījumi",
|
||||
"com_nav_always_make_prod": "Vienmēr uzlieciet jaunas versijas produkcijā",
|
||||
"com_nav_archive_created_at": "Arhivēšanas datums",
|
||||
"com_nav_archive_name": "Vārds",
|
||||
"com_nav_archived_chats": "Arhivētas sarunas",
|
||||
"com_nav_archived_chats": "Arhivētās sarunas",
|
||||
"com_nav_at_command": "@-Komanda",
|
||||
"com_nav_at_command_description": "Pārslēgšanas komanda \"@\" galapunktu, modeļu, sākotnējo iestatījumu u. c. pārslēgšanai.",
|
||||
"com_nav_at_command_description": "Pārslēgšanas komanda \"@\" galapunktu, modeļu, sākotnējo iestatījumu u.c. pārslēgšanai.",
|
||||
"com_nav_audio_play_error": "Kļūda, atskaņojot audio: {{0}}",
|
||||
"com_nav_audio_process_error": "Kļūda, apstrādājot audio: {{0}}",
|
||||
"com_nav_auto_scroll": "Automātiski iet uz jaunāko ziņu, atverot sarunu",
|
||||
@@ -334,14 +334,14 @@
|
||||
"com_nav_balance_every": "Katras",
|
||||
"com_nav_balance_hour": "stunda",
|
||||
"com_nav_balance_hours": "stundas",
|
||||
"com_nav_balance_interval": "Intervāls:",
|
||||
"com_nav_balance_interval": "Atjaunošanas biežums:",
|
||||
"com_nav_balance_last_refill": "Pēdējā bilances papildišana:",
|
||||
"com_nav_balance_minute": "minūte",
|
||||
"com_nav_balance_minutes": "minūtes",
|
||||
"com_nav_balance_month": "mēnesis",
|
||||
"com_nav_balance_months": "mēneši",
|
||||
"com_nav_balance_next_refill": "Nākamā bilances papildināšana:",
|
||||
"com_nav_balance_next_refill_info": "Nākamā bilances papildināšana notiks automātiski tikai tad, ja būs izpildīti abi nosacījumi: kopš pēdējās bilances papildināšanas ir pagājis norādītais laika intervāls un uzaicinājuma nosūtīšana izraisītu jūsu atlikuma samazināšanos zem nulles.",
|
||||
"com_nav_balance_next_refill_info": "Nākamā bilances papildināšana notiks automātiski tikai tad, ja būs izpildīti abi nosacījumi: kopš pēdējās bilances papildināšanas ir pagājis norādītais laika atjaunošanas biežums un uzaicinājuma nosūtīšana izraisītu jūsu atlikuma samazināšanos zem nulles.",
|
||||
"com_nav_balance_refill_amount": "Bilances papildināšanas apjoms:",
|
||||
"com_nav_balance_second": "otrais",
|
||||
"com_nav_balance_seconds": "sekundes",
|
||||
@@ -353,10 +353,10 @@
|
||||
"com_nav_chat_commands": "Sarunu komandas",
|
||||
"com_nav_chat_commands_info": "Šīs komandas tiek aktivizētas, ierakstot noteiktas rakstzīmes ziņas sākumā. Katru komandu aktivizē tai norādītais prefikss. Varat tās atspējot, ja bieži izmantojat šīs rakstzīmes ziņojumu sākumā.",
|
||||
"com_nav_chat_direction": "Sarunas virziens",
|
||||
"com_nav_clear_all_chats": "Notīrīt visas sarunas",
|
||||
"com_nav_clear_all_chats": "Dzēst visas saglabātās sarunas",
|
||||
"com_nav_clear_cache_confirm_message": "Vai tiešām vēlaties notīrīt kešatmiņu?",
|
||||
"com_nav_clear_conversation": "Skaidras sarunas",
|
||||
"com_nav_clear_conversation_confirm_message": "Vai tiešām vēlaties notīrīt visas sarunas? Šī darbība ir neatgriezeniska.",
|
||||
"com_nav_clear_conversation_confirm_message": "Vai tiešām vēlaties dzēst visas saglabātās sarunas? Šī darbība ir neatgriezeniska.",
|
||||
"com_nav_close_sidebar": "Aizvērt sānu joslu",
|
||||
"com_nav_commands": "Komandas",
|
||||
"com_nav_confirm_clear": "Apstiprināt dzēšanu",
|
||||
@@ -371,7 +371,7 @@
|
||||
"com_nav_delete_data_info": "Visi jūsu dati tiks dzēsti.",
|
||||
"com_nav_delete_warning": "BRĪDINĀJUMS: Tas neatgriezeniski izdzēsīs jūsu kontu.",
|
||||
"com_nav_enable_cache_tts": "Iespējot kešatmiņu TTS",
|
||||
"com_nav_enable_cloud_browser_voice": "Izmantojiet cloud-based balsis",
|
||||
"com_nav_enable_cloud_browser_voice": "Izmantot mākonī bāzētas balsis",
|
||||
"com_nav_enabled": "Iespējots",
|
||||
"com_nav_engine": "Dzinējs",
|
||||
"com_nav_enter_to_send": "Nospiediet taustiņu Enter, lai nosūtītu ziņas",
|
||||
@@ -392,7 +392,7 @@
|
||||
"com_nav_font_size_xl": "Īpaši liels",
|
||||
"com_nav_font_size_xs": "Īpaši mazs",
|
||||
"com_nav_help_faq": "Palīdzība un bieži uzdotie jautājumi",
|
||||
"com_nav_hide_panel": "Slēpt labās malējās sānu paneli",
|
||||
"com_nav_hide_panel": "Slēpt labo sāna paneli",
|
||||
"com_nav_info_balance": "Bilance parāda, cik daudz tokenu kredītu jums ir atlicis izmantot. Tokenu kredīti tiek pārvērsti naudas vērtībā (piemēram, 1000 kredīti = 0,001 USD).",
|
||||
"com_nav_info_code_artifacts": "Iespējo eksperimentāla koda artefaktu rādīšanu blakus sarunai",
|
||||
"com_nav_info_code_artifacts_agent": "Iespējo koda artefaktu izmantošanu šim aģentam. Pēc noklusējuma tiek pievienotas papildu instrukcijas, kas attiecas uz artefaktu izmantošanu, ja vien nav iespējots \"Pielāgots uzvednes režīms\".",
|
||||
@@ -435,15 +435,17 @@
|
||||
"com_nav_lang_spanish": "Spāņu",
|
||||
"com_nav_lang_swedish": "Zviedru",
|
||||
"com_nav_lang_thai": "ไทย",
|
||||
"com_nav_lang_tibetan": "Tibetiešu",
|
||||
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||
"com_nav_lang_turkish": "Türkçe",
|
||||
"com_nav_lang_ukrainian": "Ukraiņu",
|
||||
"com_nav_lang_uyghur": "Uyƣur tili",
|
||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||
"com_nav_language": "Valoda",
|
||||
"com_nav_latex_parsing": "LaTeX parsēšana ziņās (var ietekmēt veiktspēju)",
|
||||
"com_nav_log_out": "Izrakstīties",
|
||||
"com_nav_long_audio_warning": "Garāku tekstu apstrāde prasīs ilgāku laiku.",
|
||||
"com_nav_maximize_chat_space": "Maksimāli izmantojiet sarunas telpu",
|
||||
"com_nav_maximize_chat_space": "Maksimāli izmantot sarunas telpas izmērus",
|
||||
"com_nav_mcp_configure_server": "Konfigurēt {{0}}",
|
||||
"com_nav_mcp_status_connecting": "{{0}} - Savienojas",
|
||||
"com_nav_mcp_vars_update_error": "Kļūda, atjauninot MCP pielāgotos lietotāja parametrus: {{0}}",
|
||||
@@ -463,7 +465,7 @@
|
||||
"com_nav_profile_picture": "Profila attēls",
|
||||
"com_nav_save_badges_state": "Saglabāt nozīmīšu stāvokli",
|
||||
"com_nav_save_drafts": "Saglabāt melnrakstus lokāli",
|
||||
"com_nav_scroll_button": "Ritiniet līdz beigu pogai",
|
||||
"com_nav_scroll_button": "Pāriet uz pēdējo ierakstu poga",
|
||||
"com_nav_search_placeholder": "Meklēt ziņas",
|
||||
"com_nav_send_message": "Sūtīt ziņu",
|
||||
"com_nav_setting_account": "Konts",
|
||||
@@ -479,10 +481,10 @@
|
||||
"com_nav_show_code": "Vienmēr rādīt kodu, izmantojot koda interpretētāju",
|
||||
"com_nav_show_thinking": "Pēc noklusējuma atvērt domāšanas nolaižamos sarakstus",
|
||||
"com_nav_slash_command": "/-Komanda",
|
||||
"com_nav_slash_command_description": "Pārslēgt komandu \"/\", lai atlasītu uzvedni, izmantojot tastatūru",
|
||||
"com_nav_speech_to_text": "Runas pārvēršana tekstā",
|
||||
"com_nav_slash_command_description": "Ieslēgt komandu \"/\", lai atlasītu uzvedni izmantojot tastatūru",
|
||||
"com_nav_speech_to_text": "Balss pārvēršana tekstā",
|
||||
"com_nav_stop_generating": "Pārtraukt ģenerēšanu",
|
||||
"com_nav_text_to_speech": "Teksts runā",
|
||||
"com_nav_text_to_speech": "Teksta pārvēršana balsī",
|
||||
"com_nav_theme": "Tēma",
|
||||
"com_nav_theme_dark": "Tumšs",
|
||||
"com_nav_theme_light": "Gaišs",
|
||||
@@ -506,7 +508,6 @@
|
||||
"com_sidepanel_hide_panel": "Slēpt paneli",
|
||||
"com_sidepanel_manage_files": "Pārvaldīt failus",
|
||||
"com_sidepanel_mcp_no_servers_with_vars": "Nav MCP serveru ar konfigurējamiem mainīgajiem.",
|
||||
"com_sidepanel_mcp_variables_for": "MCP parametri {{0}}",
|
||||
"com_sidepanel_parameters": "Parametri",
|
||||
"com_sources_image_alt": "Meklēšanas rezultāta attēls",
|
||||
"com_sources_more_sources": "+{{count}} avoti",
|
||||
@@ -553,7 +554,6 @@
|
||||
"com_ui_agent_var": "{{0}} aģents",
|
||||
"com_ui_agent_version": "Versija",
|
||||
"com_ui_agent_version_active": "Aktīvā versija",
|
||||
"com_ui_agent_version_duplicate": "Atrasta dublikāta versija. Šī darbība izveidotu versiju, kas ir identiska citai, jau esošai versijai. {{versionIndex}}.",
|
||||
"com_ui_agent_version_empty": "Nav pieejamu versiju",
|
||||
"com_ui_agent_version_error": "Kļūda, ielādējot versijas",
|
||||
"com_ui_agent_version_history": "Versiju vēsture",
|
||||
@@ -716,13 +716,13 @@
|
||||
"com_ui_delete_tool": "Dzēst rīku",
|
||||
"com_ui_delete_tool_confirm": "Vai tiešām vēlaties dzēst šo rīku?",
|
||||
"com_ui_deleted": "Dzēsts",
|
||||
"com_ui_deleting_file": "Tiek dzēsts fails...",
|
||||
"com_ui_deleting_file": "Dzēšu failu...",
|
||||
"com_ui_descending": "Dilstošs",
|
||||
"com_ui_description": "Apraksts",
|
||||
"com_ui_description_placeholder": "Pēc izvēles: ievadiet aprakstu, kas jāparāda uzvednē",
|
||||
"com_ui_deselect_all": "Noņemt atlasi visam",
|
||||
"com_ui_detailed": "Detalizēta",
|
||||
"com_ui_disabling": "Atspējošana...",
|
||||
"com_ui_disabling": "Atspējo...",
|
||||
"com_ui_download": "Lejupielādēt",
|
||||
"com_ui_download_artifact": "Lejupielādēt artefaktu",
|
||||
"com_ui_download_backup": "Lejupielādēt rezerves kodus",
|
||||
@@ -733,7 +733,7 @@
|
||||
"com_ui_dropdown_variables_info": "Izveidojiet pielāgotas nolaižamās izvēlnes savām uzvednēm:{{variable_name:option1|option2|option3}}`",
|
||||
"com_ui_duplicate": "Dublikāts",
|
||||
"com_ui_duplication_error": "Sarunas dublēšanas laikā radās kļūda.",
|
||||
"com_ui_duplication_processing": "Sarunas dublēšana...",
|
||||
"com_ui_duplication_processing": "Dublēju sarunu...",
|
||||
"com_ui_duplication_success": "Saruna veiksmīgi dublēta",
|
||||
"com_ui_edit": "Rediģēt",
|
||||
"com_ui_edit_editing_image": "Attēla rediģēšana",
|
||||
@@ -798,7 +798,7 @@
|
||||
"com_ui_fork_info_visible": "Šī opcija atzaro tikai redzamās ziņas; citiem vārdiem sakot, tiešo ceļu uz mērķa ziņām bez atzariem.",
|
||||
"com_ui_fork_more_details_about": "Skatiet papildu informāciju un detaļas par \"{{0}}\" atzarojuma variantu",
|
||||
"com_ui_fork_more_info_options": "Skatiet detalizētu visu atzarojuma opciju un to darbības skaidrojumu",
|
||||
"com_ui_fork_processing": "Sarunas atzarošana...",
|
||||
"com_ui_fork_processing": "Atzaroju sarunu...",
|
||||
"com_ui_fork_remember": "Atcerēties",
|
||||
"com_ui_fork_remember_checked": "Jūsu izvēle tiks atcerēta pēc lietošanas. To var jebkurā laikā mainīt iestatījumos.",
|
||||
"com_ui_fork_split_target": "Sāciet atzarošanu šeit",
|
||||
@@ -807,7 +807,7 @@
|
||||
"com_ui_fork_visible": "Tikai redzamās ziņas",
|
||||
"com_ui_generate_backup": "Ģenerēt rezerves kodus",
|
||||
"com_ui_generate_qrcode": "Ģenerēt QR kodu",
|
||||
"com_ui_generating": "Notiek ģenerēšana...",
|
||||
"com_ui_generating": "Ģenerē...",
|
||||
"com_ui_generation_settings": "Ģenerēšanas iestatījumi",
|
||||
"com_ui_getting_started": "Darba sākšana",
|
||||
"com_ui_global_group": "kaut kam šeit ir jānotiek. bija tukšs",
|
||||
@@ -838,7 +838,7 @@
|
||||
"com_ui_instructions": "Instrukcijas",
|
||||
"com_ui_key": "Atslēga",
|
||||
"com_ui_late_night": "Priecīgu vēlu nakti",
|
||||
"com_ui_latest_footer": "Katrs mākslīgais intelekts ikvienam.",
|
||||
"com_ui_latest_footer": "Mākslīgais intelekts ikvienam.",
|
||||
"com_ui_latest_production_version": "Jaunākā produkcijas versija",
|
||||
"com_ui_latest_version": "Jaunākā versija",
|
||||
"com_ui_librechat_code_api_key": "Iegūstiet savu LibreChat koda interpretatora API atslēgu",
|
||||
@@ -851,15 +851,11 @@
|
||||
"com_ui_manage": "Pārvaldīt",
|
||||
"com_ui_max_tags": "Maksimālais atļautais skaits ir {{0}}, izmantojot jaunākās vērtības.",
|
||||
"com_ui_mcp_authenticated_success": "MCP serveris '{{0}}' veiksmīgi autentificēts",
|
||||
"com_ui_mcp_dialog_desc": "Lūdzu, ievadiet nepieciešamo informāciju zemāk.",
|
||||
"com_ui_mcp_enter_var": "Ievadiet vērtību {{0}}",
|
||||
"com_ui_mcp_init_failed": "Neizdevās inicializēt MCP serveri",
|
||||
"com_ui_mcp_initialize": "Inicializēt",
|
||||
"com_ui_mcp_initialized_success": "MCP serveris '{{0}}' veiksmīgi inicializēts",
|
||||
"com_ui_mcp_not_authenticated": "{{0}} nav autentificēts (nepieciešams OAuth).",
|
||||
"com_ui_mcp_not_initialized": "{{0}} nav inicializēts",
|
||||
"com_ui_mcp_oauth_cancelled": "OAuth pieteikšanās atcelta {{0}}",
|
||||
"com_ui_mcp_oauth_no_url": "Nepieciešama OAuth autentifikācija, bet URL nav padots",
|
||||
"com_ui_mcp_oauth_timeout": "OAuth pieteikšanās beidzās priekš {{0}}",
|
||||
"com_ui_mcp_server_not_found": "Serveris nav atrasts.",
|
||||
"com_ui_mcp_servers": "MCP serveri",
|
||||
@@ -918,7 +914,6 @@
|
||||
"com_ui_oauth_error_missing_code": "Trūkst autorizācijas koda. Lūdzu, mēģiniet vēlreiz.",
|
||||
"com_ui_oauth_error_missing_state": "Trūkst stāvokļa parametrs. Lūdzu, mēģiniet vēlreiz.",
|
||||
"com_ui_oauth_error_title": "Autentifikācija neizdevās",
|
||||
"com_ui_oauth_flow_desc": "Pabeidziet OAuth plūsmu jaunajā logā un pēc tam atgriezieties šeit.",
|
||||
"com_ui_oauth_success_description": "Jūsu autentifikācija bija veiksmīga. Šis logs aizvērsies pēc",
|
||||
"com_ui_oauth_success_title": "Autentifikācija veiksmīga",
|
||||
"com_ui_of": "no",
|
||||
@@ -948,13 +943,13 @@
|
||||
"com_ui_provider": "Pakalpojumu sniedzējs",
|
||||
"com_ui_quality": "Kvalitāte",
|
||||
"com_ui_read_aloud": "Lasīt skaļi",
|
||||
"com_ui_redirecting_to_provider": "Pāradresācija uz {{0}}, lūdzu, uzgaidiet...",
|
||||
"com_ui_reference_saved_memories": "Atsauces uz saglabātajām atmiņām",
|
||||
"com_ui_reference_saved_memories_description": "Ļaujiet asistentam atsaukties uz jūsu saglabātajām atmiņām un izmantot tās, atbildot",
|
||||
"com_ui_redirecting_to_provider": "Pārvirzu uz {{0}}, lūdzu, uzgaidiet...",
|
||||
"com_ui_reference_saved_memories": "References uz saglabātajām atmiņām",
|
||||
"com_ui_reference_saved_memories_description": "Ļaut asistentam atsaukties uz jūsu saglabātajām atmiņām un izmantot tās atbildot",
|
||||
"com_ui_refresh_link": "Atsvaidzināt saiti",
|
||||
"com_ui_regenerate": "Atjaunot",
|
||||
"com_ui_regenerate_backup": "Atjaunot rezerves kodus",
|
||||
"com_ui_regenerating": "Atjaunošanās...",
|
||||
"com_ui_regenerating": "Atjaunojas...",
|
||||
"com_ui_region": "Reģions",
|
||||
"com_ui_reinitialize": "Reinicializēt",
|
||||
"com_ui_rename": "Pārdēvēt",
|
||||
@@ -966,7 +961,7 @@
|
||||
"com_ui_reset_zoom": "Atiestatīt tālummaiņu",
|
||||
"com_ui_result": "Rezultāts",
|
||||
"com_ui_revoke": "Atsaukt",
|
||||
"com_ui_revoke_info": "Atsaukt visus lietotāja sniegtos kredenciāļu datus",
|
||||
"com_ui_revoke_info": "Atcelt visus lietotāja sniegtos lietotāja datus",
|
||||
"com_ui_revoke_key_confirm": "Vai tiešām vēlaties atsaukt šo atslēgu?",
|
||||
"com_ui_revoke_key_endpoint": "Atsaukt atslēgu priekš {{0}}",
|
||||
"com_ui_revoke_keys": "Atsaukt atslēgas",
|
||||
@@ -1034,7 +1029,7 @@
|
||||
"com_ui_temporary": "Pagaidu saruna",
|
||||
"com_ui_terms_and_conditions": "Noteikumi un nosacījumi",
|
||||
"com_ui_terms_of_service": "Pakalpojumu sniegšanas noteikumi",
|
||||
"com_ui_thinking": "Domājot...",
|
||||
"com_ui_thinking": "Domā...",
|
||||
"com_ui_thoughts": "Domas",
|
||||
"com_ui_token": "tokens",
|
||||
"com_ui_token_exchange_method": "Tokenu apmaiņas metode",
|
||||
@@ -1073,11 +1068,10 @@
|
||||
"com_ui_use_backup_code": "Izmantojiet rezerves kodu",
|
||||
"com_ui_use_memory": "Izmantot atmiņu",
|
||||
"com_ui_use_micrphone": "Izmantot mikrofonu",
|
||||
"com_ui_use_prompt": "Izmantojiet uzvedni",
|
||||
"com_ui_used": "Lietots",
|
||||
"com_ui_value": "Vērtība",
|
||||
"com_ui_variables": "Mainīgie",
|
||||
"com_ui_variables_info": "Mainīgo veidošanai tekstā izmantojiet dubultās iekavas, piemēram, `{{example variable}}`, lai vēlāk aizpildītu, izmantojot uzvedni.",
|
||||
"com_ui_variables_info": "Mainīgo veidošanai tekstā izmantot dubultās iekavas, piemēram, `{{example variable}}`, lai vēlāk aizpildītu, izmantojot uzvedni.",
|
||||
"com_ui_verify": "Pārbaudīt",
|
||||
"com_ui_version_var": "Versija {{0}}",
|
||||
"com_ui_versions": "Versijas",
|
||||
@@ -1111,4 +1105,4 @@
|
||||
"com_ui_yes": "Jā",
|
||||
"com_ui_zoom": "Tālummaiņa",
|
||||
"com_user_message": "Tu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +351,5 @@
|
||||
"com_ui_terms_and_conditions": "Gebruiksvoorwaarden",
|
||||
"com_ui_unarchive": "Uit archiveren",
|
||||
"com_ui_unarchive_error": "Kan conversatie niet uit archiveren",
|
||||
"com_ui_upload_success": "Bestand succesvol geüpload",
|
||||
"com_ui_use_prompt": "Gebruik prompt"
|
||||
}
|
||||
"com_ui_upload_success": "Bestand succesvol geüpload"
|
||||
}
|
||||
|
||||
@@ -708,7 +708,6 @@
|
||||
"com_ui_upload_success": "Pomyślnie przesłano plik",
|
||||
"com_ui_upload_type": "Wybierz typ przesyłania",
|
||||
"com_ui_use_micrphone": "Użyj mikrofonu",
|
||||
"com_ui_use_prompt": "Użyj podpowiedzi",
|
||||
"com_ui_variables": "Zmienne",
|
||||
"com_ui_variables_info": "Użyj podwójnych nawiasów klamrowych w tekście, aby utworzyć zmienne, np. `{{przykładowa zmienna}}`, które później można wypełnić podczas używania promptu.",
|
||||
"com_ui_version_var": "Wersja {{0}}",
|
||||
@@ -718,4 +717,4 @@
|
||||
"com_ui_yes": "Tak",
|
||||
"com_ui_zoom": "Powiększ",
|
||||
"com_user_message": "Ty"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"com_agents_no_access": "Não tens permissões para editar este agente.",
|
||||
"com_agents_no_agent_id_error": "Nenhum ID de agente encontrado. Certifique-se de que o agente seja criado primeiro.",
|
||||
"com_agents_not_available": "Agente não disponível.",
|
||||
"com_agents_search_info": "Quando ativado, permite seu agente buscar informações atualizadas na web. Requer uma chave de API válida.",
|
||||
"com_agents_search_name": "Pesquisar agentes por nome",
|
||||
"com_agents_update_error": "Houve um erro ao atualizar seu agente.",
|
||||
"com_assistants_action_attempt": "Assistente quer falar com {{0}}",
|
||||
@@ -130,6 +131,7 @@
|
||||
"com_auth_reset_password_if_email_exists": "Se uma conta com esse e-mail existir, um e-mail com instruções para redefinir a senha foi enviado. Certifique-se de verificar sua pasta de spam.",
|
||||
"com_auth_reset_password_link_sent": "E-mail enviado",
|
||||
"com_auth_reset_password_success": "Senha redefinida com sucesso",
|
||||
"com_auth_saml_login": "Continue com SAML",
|
||||
"com_auth_sign_in": "Entrar",
|
||||
"com_auth_sign_up": "Inscrever-se",
|
||||
"com_auth_submit_registration": "Enviar registro",
|
||||
@@ -158,6 +160,7 @@
|
||||
"com_endpoint_anthropic_thinking_budget": "Determina o número máximo de tokens que o Claude pode utilizar para o seu processo de raciocínio interno. Orçamentos maiores podem melhorar a qualidade da resposta, permitindo uma análise mais completa para problemas complexos, embora o Claude possa não usar todo o orçamento alocado, especialmente em intervalos acima de 32K. Essa configuração deve ser menor que \"Máximo de tokens de saída\".",
|
||||
"com_endpoint_anthropic_topk": "Top-k altera como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).",
|
||||
"com_endpoint_anthropic_topp": "Top-p altera como o modelo seleciona tokens para saída. Os tokens são selecionados dos mais prováveis (veja o parâmetro topK) até os menos prováveis até que a soma de suas probabilidades atinja o valor top-p.",
|
||||
"com_endpoint_anthropic_use_web_search": "Habilita a funcionalidade de pesquisa na web usando o recurso integrado da Anthropic. Isso permite que o modelo pesquise informações atualizadas na web e forneça respostas mais precisas e atuais.",
|
||||
"com_endpoint_assistant": "Assistente",
|
||||
"com_endpoint_assistant_model": "Modelo de Assistente",
|
||||
"com_endpoint_assistant_placeholder": "Por favor, selecione um Assistente no Painel Lateral Direito",
|
||||
@@ -203,6 +206,7 @@
|
||||
"com_endpoint_google_custom_name_placeholder": "Defina um nome personalizado para o Google",
|
||||
"com_endpoint_google_maxoutputtokens": "Número máximo de tokens que podem ser gerados na resposta. Especifique um valor mais baixo para respostas mais curtas e um valor mais alto para respostas mais longas. Nota: os modelos podem parar antes de atingir esse máximo.",
|
||||
"com_endpoint_google_temp": "Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.",
|
||||
"com_endpoint_google_thinking": "Habilita ou desabilita o pensamento. Essa opção é suportada apenas por certos modelos (série 2.5). Para modelos antigos, esta opção pode não ter efeito.",
|
||||
"com_endpoint_google_topk": "Top-k altera como o modelo seleciona tokens para saída. Um top-k de 1 significa que o token selecionado é o mais provável entre todos os tokens no vocabulário do modelo (também chamado de decodificação gananciosa), enquanto um top-k de 3 significa que o próximo token é selecionado entre os 3 tokens mais prováveis (usando temperatura).",
|
||||
"com_endpoint_google_topp": "Top-p altera como o modelo seleciona tokens para saída. Os tokens são selecionados dos mais prováveis (veja o parâmetro topK) até os menos prováveis até que a soma de suas probabilidades atinja o valor top-p.",
|
||||
"com_endpoint_instructions_assistants": "Substituir Instruções",
|
||||
@@ -227,6 +231,7 @@
|
||||
"com_endpoint_openai_stop": "Até 4 sequências onde a API parará de gerar mais tokens.",
|
||||
"com_endpoint_openai_temp": "Valores mais altos = mais aleatório, enquanto valores mais baixos = mais focado e determinístico. Recomendamos alterar isso ou Top P, mas não ambos.",
|
||||
"com_endpoint_openai_topp": "Uma alternativa à amostragem com temperatura, chamada amostragem de núcleo, onde o modelo considera os resultados dos tokens com massa de probabilidade top_p. Então, 0.1 significa que apenas os tokens que compreendem os 10% principais da massa de probabilidade são considerados. Recomendamos alterar isso ou a temperatura, mas não ambos.",
|
||||
"com_endpoint_openai_use_responses_api": "Usa a API de Respostas ao invés de Conclusões de Chat, que inclui funcionalidades extendidas da OpenAI. Requerida para o1-pro, o3-pro, e para habilitar resumos de raciocínio.",
|
||||
"com_endpoint_output": "Saída",
|
||||
"com_endpoint_plug_image_detail": "Detalhe da Imagem",
|
||||
"com_endpoint_plug_resend_files": "Reenviar Arquivos",
|
||||
@@ -259,6 +264,7 @@
|
||||
"com_endpoint_reasoning_effort": "Esforço de raciocínio",
|
||||
"com_endpoint_save_as_preset": "Salvar Como Preset",
|
||||
"com_endpoint_search": "Procurar endpoint por nome",
|
||||
"com_endpoint_search_models": "Buscar modelos...",
|
||||
"com_endpoint_set_custom_name": "Defina um nome personalizado, caso você possa encontrar este preset",
|
||||
"com_endpoint_skip_hover": "Habilitar pular a etapa de conclusão, que revisa a resposta final e os passos gerados",
|
||||
"com_endpoint_stop": "Sequências de Parada",
|
||||
@@ -305,6 +311,16 @@
|
||||
"com_nav_auto_transcribe_audio": "Transcrever áudio automaticamente",
|
||||
"com_nav_automatic_playback": "Reprodução Automática da Última Mensagem",
|
||||
"com_nav_balance": "Crédito",
|
||||
"com_nav_balance_day": "dia",
|
||||
"com_nav_balance_days": "dias",
|
||||
"com_nav_balance_hour": "hora",
|
||||
"com_nav_balance_hours": "horas",
|
||||
"com_nav_balance_minute": "minuto",
|
||||
"com_nav_balance_minutes": "minutos",
|
||||
"com_nav_balance_month": "mês",
|
||||
"com_nav_balance_months": "meses",
|
||||
"com_nav_balance_second": "segundo",
|
||||
"com_nav_balance_seconds": "segundos",
|
||||
"com_nav_browser": "Navegador",
|
||||
"com_nav_change_picture": "Mudar foto",
|
||||
"com_nav_chat_commands": "Comandos do chat",
|
||||
@@ -362,9 +378,11 @@
|
||||
"com_nav_info_show_thinking": "Quando ativado, o chat apresentará os menus pendentes de raciocínio abertos por predefinição, permitindo-lhe ver o raciocínio da IA em tempo real. Quando desativado, os menus suspensos de raciocínio permanecerão fechados por predefinição para uma interface mais limpa e simplificada",
|
||||
"com_nav_info_user_name_display": "Quando habilitado, o nome de usuário do remetente será mostrado acima de cada mensagem que você enviar. Quando desabilitado, você verá apenas \"Você\" acima de suas mensagens.",
|
||||
"com_nav_lang_arabic": "العربية",
|
||||
"com_nav_lang_armenian": "Armênio",
|
||||
"com_nav_lang_auto": "Detecção automática",
|
||||
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
|
||||
"com_nav_lang_chinese": "中文",
|
||||
"com_nav_lang_danish": "Dinamarquês",
|
||||
"com_nav_lang_dutch": "Nederlands",
|
||||
"com_nav_lang_english": "English",
|
||||
"com_nav_lang_estonian": "Eesti keel",
|
||||
@@ -445,6 +463,7 @@
|
||||
"com_sidepanel_hide_panel": "Ocultar Painel",
|
||||
"com_sidepanel_manage_files": "Gerenciar Arquivos",
|
||||
"com_sidepanel_parameters": "Parâmetros",
|
||||
"com_sources_tab_images": "Imagens",
|
||||
"com_ui_2fa_account_security": "A autenticação de dois fatores acrescenta uma camada extra de segurança à sua conta",
|
||||
"com_ui_2fa_disable": "Desabilitar 2FA",
|
||||
"com_ui_2fa_disable_error": "Ocorreu um erro ao desativar a autenticação de dois fatores",
|
||||
@@ -456,13 +475,17 @@
|
||||
"com_ui_2fa_setup": "Configurar 2FA",
|
||||
"com_ui_2fa_verified": "Autenticação de dois fatores verificada com sucesso",
|
||||
"com_ui_accept": "Eu aceito",
|
||||
"com_ui_active": "Ativo",
|
||||
"com_ui_add": "Adicionar",
|
||||
"com_ui_add_mcp": "Adicionar MCP",
|
||||
"com_ui_add_mcp_server": "Adicionar Servidor MCP",
|
||||
"com_ui_add_model_preset": "Adicionar um modelo ou predefinição para uma resposta adicional",
|
||||
"com_ui_add_multi_conversation": "Adicionar multi-conversação",
|
||||
"com_ui_admin": "Admin",
|
||||
"com_ui_admin_access_warning": "Desabilitar o acesso de Admin a esse recurso pode causar problemas inesperados na IU que exigem atualização. Se salvo, a única maneira de reverter é por meio da configuração de interface na configuração librechat.yaml que afeta todas as funções.",
|
||||
"com_ui_admin_settings": "Configurações de Admin",
|
||||
"com_ui_advanced": "Avançado",
|
||||
"com_ui_advanced_settings": "Opções Avançadas",
|
||||
"com_ui_agent": "Agente",
|
||||
"com_ui_agent_delete_error": "Houve um erro ao excluir o agente",
|
||||
"com_ui_agent_deleted": "Agente excluído com sucesso",
|
||||
@@ -470,6 +493,12 @@
|
||||
"com_ui_agent_duplicated": "Agente duplicado com sucesso",
|
||||
"com_ui_agent_editing_allowed": "Outros usuários já podem editar este agente",
|
||||
"com_ui_agent_shared_to_all": "algo precisa ir aqui. esta vazio",
|
||||
"com_ui_agent_version": "Versão",
|
||||
"com_ui_agent_version_active": "Versão Ativa",
|
||||
"com_ui_agent_version_empty": "Não há versões disponíveis",
|
||||
"com_ui_agent_version_history": "Histórico de Versões",
|
||||
"com_ui_agent_version_no_date": "Data não disponível",
|
||||
"com_ui_agent_version_unknown_date": "Data desconhecida",
|
||||
"com_ui_agents": "Agentes",
|
||||
"com_ui_agents_allow_create": "Permitir a criação de agentes",
|
||||
"com_ui_agents_allow_share_global": "Permitir compartilhamento de agentes para todos os usuários",
|
||||
@@ -495,12 +524,15 @@
|
||||
"com_ui_attach_error_openai": "Não é possível anexar arquivos de Assistente a outros endpoints",
|
||||
"com_ui_attach_error_size": "Limite de tamanho de arquivo excedido para o endpoint:",
|
||||
"com_ui_attach_error_type": "Tipo de arquivo não suportado para o endpoint:",
|
||||
"com_ui_attach_remove": "Remover arquivo",
|
||||
"com_ui_attach_warn_endpoint": "Arquivos não compatíveis podem ser ignorados sem uma ferramenta compatível",
|
||||
"com_ui_attachment": "Anexo",
|
||||
"com_ui_auth_type": "Tipo de autenticação",
|
||||
"com_ui_auth_url": "URL de autorização",
|
||||
"com_ui_authenticate": "Autenticar",
|
||||
"com_ui_authentication": "Autenticação",
|
||||
"com_ui_authentication_type": "Tipo de Autenticação",
|
||||
"com_ui_available_tools": "Ferramentas Disponíveis",
|
||||
"com_ui_avatar": "Avatar",
|
||||
"com_ui_azure": "Azure",
|
||||
"com_ui_back_to_chat": "Voltar ao Chat",
|
||||
@@ -532,6 +564,8 @@
|
||||
"com_ui_bulk_delete_error": "Falha ao excluir links compartilhados",
|
||||
"com_ui_callback_url": "URL de retorno de chamada",
|
||||
"com_ui_cancel": "Cancelar",
|
||||
"com_ui_cancelled": "Cancelado",
|
||||
"com_ui_category": "Categoria",
|
||||
"com_ui_chat": "Chat",
|
||||
"com_ui_chat_history": "Histórico de Chat",
|
||||
"com_ui_clear": "Limpar",
|
||||
@@ -540,6 +574,7 @@
|
||||
"com_ui_client_secret": "Segredo do cliente",
|
||||
"com_ui_close": "Fechar",
|
||||
"com_ui_close_menu": "Fechar Menu",
|
||||
"com_ui_close_window": "Fechar Janela",
|
||||
"com_ui_code": "Código",
|
||||
"com_ui_collapse_chat": "Recolher bate-papo",
|
||||
"com_ui_command_placeholder": "Opcional: Insira um comando para o prompt ou o nome será usado.",
|
||||
@@ -548,9 +583,12 @@
|
||||
"com_ui_confirm_action": "Confirmar Ação",
|
||||
"com_ui_confirm_admin_use_change": "Alterar esta configuração bloqueará o acesso para administradores, incluindo você. Tem certeza de que deseja prosseguir?",
|
||||
"com_ui_confirm_change": "Confirmar alteração",
|
||||
"com_ui_connecting": "Conectando",
|
||||
"com_ui_context": "Contexto",
|
||||
"com_ui_continue": "Continuar",
|
||||
"com_ui_continue_oauth": "Continuar com OAuth",
|
||||
"com_ui_controls": "Controles",
|
||||
"com_ui_convo_delete_error": "Falha ao excluir conversa",
|
||||
"com_ui_copied": "Copiado!",
|
||||
"com_ui_copied_to_clipboard": "Copiado para a área de transferência",
|
||||
"com_ui_copy_code": "Copiar código",
|
||||
@@ -558,7 +596,9 @@
|
||||
"com_ui_copy_to_clipboard": "Copiar para a área de transferência",
|
||||
"com_ui_create": "Criar",
|
||||
"com_ui_create_link": "Criar link",
|
||||
"com_ui_create_memory": "Criar Memória",
|
||||
"com_ui_create_prompt": "Criar Prompt",
|
||||
"com_ui_creating_image": "Criando a imagem. Pode levar algum tempo",
|
||||
"com_ui_currently_production": "Atualmente em produção",
|
||||
"com_ui_custom": "Personalizado",
|
||||
"com_ui_custom_header_name": "Nome do cabeçalho personalizado",
|
||||
@@ -591,13 +631,22 @@
|
||||
"com_ui_delete_confirm": "Isso excluirá",
|
||||
"com_ui_delete_confirm_prompt_version_var": "Isso excluirá a versão selecionada para \"{{0}}\". Se não houver outras versões, o prompt será excluído.",
|
||||
"com_ui_delete_conversation": "Excluir chat?",
|
||||
"com_ui_delete_mcp": "Remover MCP",
|
||||
"com_ui_delete_mcp_confirm": "Você tem certeza que quer remover este servidor MCP?",
|
||||
"com_ui_delete_mcp_error": "Falha ao remover servidor MCP",
|
||||
"com_ui_delete_mcp_success": "Servidor MCP removido com sucesso",
|
||||
"com_ui_delete_memory": "Remover Memória",
|
||||
"com_ui_delete_prompt": "Excluir Prompt?",
|
||||
"com_ui_delete_shared_link": "Excluir link compartilhado?",
|
||||
"com_ui_delete_success": "Removido com sucesso",
|
||||
"com_ui_delete_tool": "Excluir Ferramenta",
|
||||
"com_ui_delete_tool_confirm": "Tem certeza de que deseja excluir esta ferramenta?",
|
||||
"com_ui_deleted": "Removido",
|
||||
"com_ui_deleting_file": "Removendo arquivo...",
|
||||
"com_ui_descending": "Desc",
|
||||
"com_ui_description": "Descrição",
|
||||
"com_ui_description_placeholder": "Opcional: Insira uma descrição para exibir para o prompt",
|
||||
"com_ui_detailed": "Detalhado",
|
||||
"com_ui_disabling": "Desativando...",
|
||||
"com_ui_download": "Download",
|
||||
"com_ui_download_artifact": "Download artefato",
|
||||
@@ -612,18 +661,35 @@
|
||||
"com_ui_duplication_processing": "Duplicando conversa...",
|
||||
"com_ui_duplication_success": "Conversa duplicada com sucesso",
|
||||
"com_ui_edit": "Editar",
|
||||
"com_ui_edit_mcp_server": "Editar Servidor MCP",
|
||||
"com_ui_edit_memory": "Editar Memória",
|
||||
"com_ui_empty_category": "-",
|
||||
"com_ui_endpoint": "Endpoint",
|
||||
"com_ui_endpoint_menu": "Menu endpoint LLM",
|
||||
"com_ui_enter": "Entrar",
|
||||
"com_ui_enter_api_key": "Insira a chave da API",
|
||||
"com_ui_enter_key": "Inserir chave",
|
||||
"com_ui_enter_openapi_schema": "Insira seu esquema OpenAPI aqui",
|
||||
"com_ui_enter_value": "Inserir valor",
|
||||
"com_ui_error": "Erro",
|
||||
"com_ui_error_connection": "Erro ao conectar ao servidor, tente atualizar a página.",
|
||||
"com_ui_error_save_admin_settings": "Houve um erro ao salvar suas configurações de admin.",
|
||||
"com_ui_error_updating_preferences": "Erro ao atualizar preferências",
|
||||
"com_ui_examples": "Exemplos",
|
||||
"com_ui_expand_chat": "Expandir Chat",
|
||||
"com_ui_export_convo_modal": "Exportar Modal de Conversação",
|
||||
"com_ui_feedback_more": "Mais...",
|
||||
"com_ui_feedback_more_information": "Fornecer feedback adicional",
|
||||
"com_ui_feedback_negative": "Precisa de melhorias",
|
||||
"com_ui_feedback_placeholder": "Por favor, forneça qualquer feedback adicional aqui",
|
||||
"com_ui_feedback_positive": "Amei isso",
|
||||
"com_ui_feedback_tag_accurate_reliable": "Preciso e confiável",
|
||||
"com_ui_feedback_tag_creative_solution": "Solução Criativa",
|
||||
"com_ui_feedback_tag_not_helpful": "Faltou informação útil",
|
||||
"com_ui_feedback_tag_unjustified_refusal": "Recusado com razão",
|
||||
"com_ui_field_required": "Este campo é obrigatório",
|
||||
"com_ui_file_size": "Tamanho do Arquivo",
|
||||
"com_ui_files": "Arquivos",
|
||||
"com_ui_filter_prompts": "Filtrar prompts",
|
||||
"com_ui_filter_prompts_name": "Filtrar prompts por nome",
|
||||
"com_ui_finance": "Financiar",
|
||||
@@ -652,13 +718,23 @@
|
||||
"com_ui_generate_backup": "Gerar códigos de backup",
|
||||
"com_ui_generate_qrcode": "Gerar QR Code",
|
||||
"com_ui_generating": "Gerando...",
|
||||
"com_ui_generation_settings": "Configurações de Geração",
|
||||
"com_ui_global_group": "algo precisa ir aqui. estava vazio",
|
||||
"com_ui_go_back": "Volte",
|
||||
"com_ui_go_to_conversation": "Ir para a conversa",
|
||||
"com_ui_good_afternoon": "Boa tarde",
|
||||
"com_ui_good_evening": "Boa noite",
|
||||
"com_ui_good_morning": "Bom dia",
|
||||
"com_ui_happy_birthday": "É meu 1º aniversário!",
|
||||
"com_ui_hide_image_details": "Esconder Detalhes de Imagem",
|
||||
"com_ui_hide_password": "Esconder senha",
|
||||
"com_ui_hide_qr": "Ocultar QR Code",
|
||||
"com_ui_host": "Host",
|
||||
"com_ui_icon": "Ícone",
|
||||
"com_ui_idea": "Ideias",
|
||||
"com_ui_image_created": "Imagem criada",
|
||||
"com_ui_image_details": "Detalhes da Imagem",
|
||||
"com_ui_image_edited": "Imagem editada",
|
||||
"com_ui_image_gen": "Geração de Imagem",
|
||||
"com_ui_import": "Importar",
|
||||
"com_ui_import_conversation_error": "Houve um erro ao importar suas conversas",
|
||||
@@ -668,6 +744,7 @@
|
||||
"com_ui_include_shadcnui": "Incluir instruções de componentes shadcn/ui",
|
||||
"com_ui_input": "Entrada",
|
||||
"com_ui_instructions": "Instruções",
|
||||
"com_ui_key": "Chave",
|
||||
"com_ui_latest_footer": "Toda IA para Todos.",
|
||||
"com_ui_latest_production_version": "Última versão de produção",
|
||||
"com_ui_latest_version": "Ultima versão",
|
||||
@@ -679,6 +756,28 @@
|
||||
"com_ui_logo": "{{0}} Logo",
|
||||
"com_ui_manage": "Gerenciar",
|
||||
"com_ui_max_tags": "O número máximo permitido é {{0}}, usando os valores mais recentes.",
|
||||
"com_ui_mcp_authenticated_success": "Servidor MCP '{{0}}' autenticado com sucesso",
|
||||
"com_ui_mcp_enter_var": "Insira um valor para {{0}}",
|
||||
"com_ui_mcp_initialize": "Inicializar",
|
||||
"com_ui_mcp_initialized_success": "Servidor MCP '{{0}}' inicializou com sucesso",
|
||||
"com_ui_mcp_server_not_found": "Servidor não encontrado.",
|
||||
"com_ui_mcp_servers": "Servidores MCP",
|
||||
"com_ui_mcp_update_var": "Atualizar {{0}}",
|
||||
"com_ui_mcp_url": "URL do Servidor MCP",
|
||||
"com_ui_medium": "Médio",
|
||||
"com_ui_memories": "Memórias",
|
||||
"com_ui_memories_allow_create": "Permitir criação de Memórias",
|
||||
"com_ui_memories_allow_update": "Permite a atualização de Memórias",
|
||||
"com_ui_memories_allow_use": "Permite a utilização de Memórias",
|
||||
"com_ui_memories_filter": "Filtrar memórias...",
|
||||
"com_ui_memory": "Memória",
|
||||
"com_ui_memory_created": "Memória criada com sucesso",
|
||||
"com_ui_memory_deleted": "Memória removida",
|
||||
"com_ui_memory_deleted_items": "Memórias removidas",
|
||||
"com_ui_memory_storage_full": "Armazenamento de Memória Cheio",
|
||||
"com_ui_memory_updated": "Memória salva atualizada",
|
||||
"com_ui_memory_updated_items": "Memórias Atualizadas",
|
||||
"com_ui_memory_would_exceed": "Impossível salvar - excederia o limite por {{tokens} tokens. Remova memórias existentes para liberar espaço.",
|
||||
"com_ui_mention": "Mencione um endpoint, assistente ou predefinição para alternar rapidamente para ele",
|
||||
"com_ui_min_tags": "Não é possível remover mais valores, um mínimo de {{0}} é necessário.",
|
||||
"com_ui_misc": "Diversos",
|
||||
@@ -817,7 +916,6 @@
|
||||
"com_ui_use_2fa_code": "Use o código 2FA em vez disso",
|
||||
"com_ui_use_backup_code": "Use o código de backup",
|
||||
"com_ui_use_micrphone": "Usar microfone",
|
||||
"com_ui_use_prompt": "Usar prompt",
|
||||
"com_ui_used": "Usado",
|
||||
"com_ui_variables": "Variáveis",
|
||||
"com_ui_variables_info": "Use chaves duplas no seu texto para criar variáveis, por exemplo, `{{exemplo de variável}}`, para preencher posteriormente ao usar o prompt.",
|
||||
@@ -825,8 +923,31 @@
|
||||
"com_ui_version_var": "Versão {{0}}",
|
||||
"com_ui_versions": "Versões",
|
||||
"com_ui_view_source": "Ver chat de origem",
|
||||
"com_ui_web_search": "Busca na web",
|
||||
"com_ui_web_search_cohere_key": "Insira a chave de API Cohere",
|
||||
"com_ui_web_search_firecrawl_url": "URL da API Firecrawl (opcional)",
|
||||
"com_ui_web_search_jina_key": "Insira a chave de API Jina",
|
||||
"com_ui_web_search_processing": "Resultados de processamento",
|
||||
"com_ui_web_search_provider": "Provedor de Buscas",
|
||||
"com_ui_web_search_provider_searxng": "SearXNG",
|
||||
"com_ui_web_search_provider_serper": "Serper API",
|
||||
"com_ui_web_search_provider_serper_key": "Obtenha sua chave de API Serper",
|
||||
"com_ui_web_search_reading": "Resultados da leitura",
|
||||
"com_ui_web_search_reranker": "Reranker",
|
||||
"com_ui_web_search_reranker_cohere": "Cohere",
|
||||
"com_ui_web_search_reranker_cohere_key": "Obtenha sua chave de API Cohere",
|
||||
"com_ui_web_search_reranker_jina": "Jina AI",
|
||||
"com_ui_web_search_reranker_jina_key": "Obtenha sua chave de API Jina",
|
||||
"com_ui_web_search_scraper": "Scraper",
|
||||
"com_ui_web_search_scraper_firecrawl": "Firecrawl API",
|
||||
"com_ui_web_search_scraper_firecrawl_key": "Obtenha sua chave de API Firecrawl",
|
||||
"com_ui_web_search_searxng_api_key": "Insira sua Chave de API SearXNG (opcional)",
|
||||
"com_ui_web_searching": "Procurando na web",
|
||||
"com_ui_web_searching_again": "Procurando na web novamente",
|
||||
"com_ui_weekend_morning": "Boa semana",
|
||||
"com_ui_write": "Escrevendo",
|
||||
"com_ui_x_selected": "{{0}} selecionado",
|
||||
"com_ui_yes": "Sim",
|
||||
"com_ui_zoom": "Zoom",
|
||||
"com_user_message": "Você"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,14 @@
|
||||
"com_agents_file_search_disabled": "O Agente deve ser criado antes carregar ficheiros para Pesquisar.",
|
||||
"com_agents_file_search_info": "Quando ativo, os agentes serão informados dos nomes de ficheiros listados abaixo, permitindo aos mesmos a extração de contexto relevante.",
|
||||
"com_agents_instructions_placeholder": "As instruções do sistema que o agente usa",
|
||||
"com_agents_mcp_description_placeholder": "Em poucas palavras explica o que faz",
|
||||
"com_agents_mcp_icon_size": "Tamanho mínimo é 128 x 128 px",
|
||||
"com_agents_mcp_name_placeholder": "Ferramenta Costumizada",
|
||||
"com_agents_mcps_disabled": "Precisas de criar um agente antes de adicionar MCPs.",
|
||||
"com_agents_missing_provider_model": "Por favor, escolhe um provedor e modelo antes de criar um agente.",
|
||||
"com_agents_name_placeholder": "Opcional: O nome do agente",
|
||||
"com_agents_no_access": "Não tens permissões para editar este agente.",
|
||||
"com_agents_no_agent_id_error": "Nenhum ID de Agente Encontrado. Por favor, garante que tens um agente criado.",
|
||||
"com_agents_not_available": "Agente não disponível.",
|
||||
"com_agents_search_name": "Pesquisar agentes por nome",
|
||||
"com_agents_update_error": "Houve um erro ao atualizar seu agente.",
|
||||
@@ -181,6 +186,7 @@
|
||||
"com_endpoint_default_empty": "padrão: vazio",
|
||||
"com_endpoint_default_with_num": "padrão: {{0}}",
|
||||
"com_endpoint_deprecated": "Deprecado",
|
||||
"com_endpoint_disable_streaming_label": "Desligar Streaming",
|
||||
"com_endpoint_examples": "Presets",
|
||||
"com_endpoint_export": "Exportar",
|
||||
"com_endpoint_export_share": "Exportar/Compartilhar",
|
||||
@@ -258,6 +264,7 @@
|
||||
"com_endpoint_top_k": "Top K",
|
||||
"com_endpoint_top_p": "Top P",
|
||||
"com_endpoint_use_active_assistant": "Usar Assistente Ativo",
|
||||
"com_endpoint_use_responses_api": "Usar Respostas da API",
|
||||
"com_error_expired_user_key": "A chave fornecida para {{0}} expirou em {{1}}. Por favor, forneça uma nova chave e tente novamente.",
|
||||
"com_error_files_dupe": "Ficheiro duplicado detectado",
|
||||
"com_error_files_empty": "Ficheiros vazios não são permitidos.",
|
||||
@@ -276,6 +283,7 @@
|
||||
"com_files_number_selected": "{{0}} de {{1}} arquivo(s) selecionado(s)",
|
||||
"com_generated_files": "Ficheiros gerados:",
|
||||
"com_hide_examples": "Ocultar Exemplos",
|
||||
"com_info_heic_converting": "Converter imagem HEIC para JPEG...",
|
||||
"com_nav_2fa": "Autenticação de dois fatores (2FA)",
|
||||
"com_nav_account_settings": "Configurações da Conta",
|
||||
"com_nav_always_make_prod": "Sempre tornar novas versões produção",
|
||||
@@ -293,16 +301,21 @@
|
||||
"com_nav_auto_transcribe_audio": "Transcrever áudio automaticamente",
|
||||
"com_nav_automatic_playback": "Reprodução Automática da Última Mensagem",
|
||||
"com_nav_balance": "Equilíbrio",
|
||||
"com_nav_balance_auto_refill_disabled": "Carregamento automático está desligado.",
|
||||
"com_nav_balance_auto_refill_error": "Erro nas configurações de carregamento automático.",
|
||||
"com_nav_balance_auto_refill_settings": "Configurações de Carregamento Automático",
|
||||
"com_nav_balance_day": "dia",
|
||||
"com_nav_balance_days": "dias",
|
||||
"com_nav_balance_every": "Todos",
|
||||
"com_nav_balance_hour": "horas",
|
||||
"com_nav_balance_hours": "horas",
|
||||
"com_nav_balance_interval": "Intervalo:",
|
||||
"com_nav_balance_last_refill": "Último Carregamento:",
|
||||
"com_nav_balance_minute": "minuto",
|
||||
"com_nav_balance_minutes": "minutos",
|
||||
"com_nav_balance_month": "mês",
|
||||
"com_nav_balance_months": "meses",
|
||||
"com_nav_balance_next_refill": "Próximo Carregamento:",
|
||||
"com_nav_balance_second": "segundo",
|
||||
"com_nav_balance_seconds": "segundos",
|
||||
"com_nav_balance_week": "semana",
|
||||
@@ -364,9 +377,13 @@
|
||||
"com_nav_info_save_draft": "Quando habilitado, o texto e os anexos que você inserir no formulário de chat serão salvos automaticamente localmente como rascunhos. Esses rascunhos estarão disponíveis mesmo se você recarregar a página ou mudar para uma conversa diferente. Os rascunhos são armazenados localmente no seu dispositivo e são excluídos uma vez que a mensagem é enviada.",
|
||||
"com_nav_info_user_name_display": "Quando habilitado, o nome de usuário do remetente será mostrado acima de cada mensagem que você enviar. Quando desabilitado, você verá apenas \"Você\" acima de suas mensagens.",
|
||||
"com_nav_lang_arabic": "العربية",
|
||||
"com_nav_lang_armenian": "Armênio",
|
||||
"com_nav_lang_auto": "Detecção automática",
|
||||
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
|
||||
"com_nav_lang_catalan": "Catalão",
|
||||
"com_nav_lang_chinese": "中文",
|
||||
"com_nav_lang_czech": "Checo",
|
||||
"com_nav_lang_danish": "Dinamarquês",
|
||||
"com_nav_lang_dutch": "Nederlands",
|
||||
"com_nav_lang_english": "English",
|
||||
"com_nav_lang_estonian": "Eesti keel",
|
||||
@@ -375,10 +392,12 @@
|
||||
"com_nav_lang_georgian": "ქართული",
|
||||
"com_nav_lang_german": "Deutsch",
|
||||
"com_nav_lang_hebrew": "עברית",
|
||||
"com_nav_lang_hungarian": "Húngaro",
|
||||
"com_nav_lang_indonesia": "Indonesia",
|
||||
"com_nav_lang_italian": "Italiano",
|
||||
"com_nav_lang_japanese": "日本語",
|
||||
"com_nav_lang_korean": "한국어",
|
||||
"com_nav_lang_persian": "Persa",
|
||||
"com_nav_lang_polish": "Polski",
|
||||
"com_nav_lang_portuguese": "Português",
|
||||
"com_nav_lang_russian": "Русский",
|
||||
@@ -393,6 +412,8 @@
|
||||
"com_nav_log_out": "Sair",
|
||||
"com_nav_long_audio_warning": "Textos mais longos levarão mais tempo para processar.",
|
||||
"com_nav_maximize_chat_space": "Maximizar espaço de conversa",
|
||||
"com_nav_mcp_configure_server": "Configurar {{0}}",
|
||||
"com_nav_mcp_status_connecting": "{{0}} - A ligar",
|
||||
"com_nav_modular_chat": "Habilitar troca de Endpoints no meio da conversa",
|
||||
"com_nav_my_files": "Meus Arquivos",
|
||||
"com_nav_not_supported": "Não Suportado",
|
||||
@@ -411,9 +432,12 @@
|
||||
"com_nav_search_placeholder": "Buscar mensagens",
|
||||
"com_nav_send_message": "Enviar mensagem",
|
||||
"com_nav_setting_account": "Conta",
|
||||
"com_nav_setting_balance": "Saldo",
|
||||
"com_nav_setting_chat": "Chat",
|
||||
"com_nav_setting_data": "Controles de dados",
|
||||
"com_nav_setting_general": "Geral",
|
||||
"com_nav_setting_mcp": "Configurações MCP",
|
||||
"com_nav_setting_personalization": "Personalização",
|
||||
"com_nav_setting_speech": "Fala",
|
||||
"com_nav_settings": "Configurações",
|
||||
"com_nav_shared_links": "Links compartilhados",
|
||||
@@ -447,6 +471,8 @@
|
||||
"com_sidepanel_hide_panel": "Ocultar Painel",
|
||||
"com_sidepanel_manage_files": "Gerenciar Arquivos",
|
||||
"com_sidepanel_parameters": "Parâmetros",
|
||||
"com_sources_image_alt": "Resultado da pesquisa de imagem",
|
||||
"com_sources_more_sources": "+{{count}} fontes",
|
||||
"com_sources_tab_all": "Todos",
|
||||
"com_sources_tab_images": "Imagens",
|
||||
"com_sources_tab_news": "Notícias",
|
||||
@@ -462,7 +488,10 @@
|
||||
"com_ui_2fa_verified": "Autenticação de dois fatores verificado com sucesso",
|
||||
"com_ui_accept": "Eu aceito",
|
||||
"com_ui_action_button": "Botão de Acção",
|
||||
"com_ui_active": "Ativo",
|
||||
"com_ui_add": "Adicionar",
|
||||
"com_ui_add_mcp": "Adicionar MCP",
|
||||
"com_ui_add_mcp_server": "Adicionar Servidor MCP",
|
||||
"com_ui_add_model_preset": "Adicionar um modelo ou predefinição para uma resposta adicional",
|
||||
"com_ui_add_multi_conversation": "Adicionar conversação múltiplca",
|
||||
"com_ui_adding_details": "A adicionar detalhes",
|
||||
@@ -483,9 +512,12 @@
|
||||
"com_ui_agent_var": "{{0}} agente",
|
||||
"com_ui_agent_version": "Versão",
|
||||
"com_ui_agent_version_active": "Versão ativa",
|
||||
"com_ui_agent_version_empty": "Sem versões disponíveis",
|
||||
"com_ui_agent_version_history": "Histórico de versões",
|
||||
"com_ui_agent_version_no_date": "Data não disponível",
|
||||
"com_ui_agent_version_restore": "Restaurar",
|
||||
"com_ui_agent_version_title": "Versão {{versionNumber}}",
|
||||
"com_ui_agent_version_unknown_date": "Data desconhecida",
|
||||
"com_ui_agents": "Agentes",
|
||||
"com_ui_agents_allow_create": "Permitir a criação de Agentes",
|
||||
"com_ui_agents_allow_share_global": "Permitir a partilha de Agentes com todos os utilizadores",
|
||||
@@ -516,8 +548,10 @@
|
||||
"com_ui_attachment": "Anexo",
|
||||
"com_ui_auth_type": "Tipo de Autenticação",
|
||||
"com_ui_auth_url": "Endereço de Autorização",
|
||||
"com_ui_authenticate": "Autenticar",
|
||||
"com_ui_authentication": "Autenticação",
|
||||
"com_ui_authentication_type": "Tipo de Autenticação",
|
||||
"com_ui_auto": "Automático",
|
||||
"com_ui_avatar": "Avatar",
|
||||
"com_ui_azure": "Azure",
|
||||
"com_ui_back_to_chat": "Voltar ao Chat",
|
||||
@@ -559,6 +593,7 @@
|
||||
"com_ui_client_secret": "Client Secret",
|
||||
"com_ui_close": "Fechar",
|
||||
"com_ui_close_menu": "Fechar Menu",
|
||||
"com_ui_close_window": "Fechar Janela",
|
||||
"com_ui_code": "Código",
|
||||
"com_ui_collapse_chat": "Colapsar Conversa",
|
||||
"com_ui_command_placeholder": "Opcional: Insira um comando para o prompt ou o nome será usado.",
|
||||
@@ -567,8 +602,10 @@
|
||||
"com_ui_confirm_action": "Confirmar Ação",
|
||||
"com_ui_confirm_admin_use_change": "Mudar esta configuração irá bloquear acessos para administradores, você inclusivé. Tem a certeza que pretende avançar?",
|
||||
"com_ui_confirm_change": "Confirmar alteração",
|
||||
"com_ui_connecting": "A ligar",
|
||||
"com_ui_context": "Contexto",
|
||||
"com_ui_continue": "Continuar",
|
||||
"com_ui_continue_oauth": "Continuar com OAuth",
|
||||
"com_ui_controls": "Controles",
|
||||
"com_ui_copied": "Copiado!",
|
||||
"com_ui_copied_to_clipboard": "Copiado para a área de transferência",
|
||||
@@ -577,6 +614,7 @@
|
||||
"com_ui_copy_to_clipboard": "Copiar para a área de transferência",
|
||||
"com_ui_create": "Criar",
|
||||
"com_ui_create_link": "Criar link",
|
||||
"com_ui_create_memory": "Criar memória",
|
||||
"com_ui_create_prompt": "Criar Prompt",
|
||||
"com_ui_currently_production": "Atualmente em produção",
|
||||
"com_ui_custom": "Costumizar",
|
||||
@@ -610,6 +648,7 @@
|
||||
"com_ui_delete_confirm": "Isso excluirá",
|
||||
"com_ui_delete_confirm_prompt_version_var": "Isso excluirá a versão selecionada para \"{{0}}\". Se não houver outras versões, o prompt será excluído.",
|
||||
"com_ui_delete_conversation": "Excluir chat?",
|
||||
"com_ui_delete_mcp": "Apagar MCP",
|
||||
"com_ui_delete_prompt": "Excluir Prompt?",
|
||||
"com_ui_delete_shared_link": "Apagar endereço partilhado?",
|
||||
"com_ui_delete_tool": "Excluir Ferramenta",
|
||||
@@ -856,7 +895,6 @@
|
||||
"com_ui_use_2fa_code": "Usar Código 2FA",
|
||||
"com_ui_use_backup_code": "Usar Código da cópia de segurança",
|
||||
"com_ui_use_micrphone": "Usar microfone",
|
||||
"com_ui_use_prompt": "Usar prompt",
|
||||
"com_ui_used": "Usado",
|
||||
"com_ui_variables": "Variáveis",
|
||||
"com_ui_variables_info": "Use chaves duplas no seu texto para criar variáveis, por exemplo, `{{exemplo de variável}}`, para preencher posteriormente ao usar o prompt.",
|
||||
@@ -879,4 +917,4 @@
|
||||
"com_ui_yes": "Sim",
|
||||
"com_ui_zoom": "Ampliar",
|
||||
"com_user_message": "Você"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user