Compare commits

...

27 Commits

Author SHA1 Message Date
Danny Avila
4175a3ea19 v0.8.0-rc1 (#8846) 2025-08-04 15:25:07 -04:00
github-actions[bot]
02dc71f4b7 🌍 i18n: Update translation.json with latest translations (#8845)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-04 15:24:24 -04:00
github-actions[bot]
a6c99a3267 🌍 i18n: Update translation.json with latest translations (#8828)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-04 14:54:07 -04:00
SollalF
fcefc6eedf feat: Add OpenID Audience Parameter (#8837)
*  feat: Add OpenID audience parameter support in authorization requests

* Updated .env.example to include OPENID_AUDIENCE variable for configuration.
* Enhanced openidStrategy to set the audience parameter in authorization requests if specified, improving OpenID integration.

* Update .env.example

* Update openidStrategy.js

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
2025-08-04 14:49:36 -04:00
Marco Beretta
dfdafdbd09 🖌️ feat: add animation styles for popovers and tooltips (#8831)
* feat: Update client version to 0.2.2 and add animation styles for popovers and tooltips

* refactor: Remove focus outline styles from Dropdown component

* feat: Update client version to 0.2.3 and add Select component export

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2025-08-04 14:44:00 -04:00
Danny Avila
33834cd484 🧹 feat: Automatic File Cleanup for Mistral OCR Uploads (#8827)
* chore: Handle optional token_endpoint in OAuth metadata discovery

* chore: Simplify permission typing logic in checkAccess function

* feat: Implement `deleteMistralFile` function and integrate file cleanup in `uploadMistralOCR`
2025-08-03 17:11:14 -04:00
Danny Avila
7ef2c626e2 🛠️ feat: Add Reset-Meili-Sync Script for MongoDB Flags (#8823) 2025-08-02 18:04:04 -04:00
Danny Avila
bc43423f58 🌍 i18n: Add Tibetan and Ukrainian languages to localization (#8819)
* 🌍 i18n: Add Tibetan and Ukrainian languages to localization

* feat: Update language selector to include Tibetan and Ukrainian options
* feat: Add translation files for Tibetan and Ukrainian languages
* chore: Update English translation.json with new language keys
* docs: Create localization guide for adding new languages

* Update README.md
2025-08-02 12:37:18 -04:00
Danny Avila
863401bcdf 🔧 fix: Assistants API SDK calls to match Updated Arguments (#8818)
* chore: remove comments in agents endpoint error handler

* chore: improve openai sdk typing

* chore: improve typing for azure asst init

* 🔧 fix: Assistants API SDK calls to match Updated Arguments
2025-08-02 12:19:58 -04:00
Danny Avila
33c8b87edd 📦 chore: Bump @modelcontextprotocol/sdk to v1.17.1 (#8809) 2025-08-01 16:10:12 -04:00
github-actions[bot]
077248a8a7 🌍 i18n: Update translation.json with latest translations (#8808)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-01 15:55:42 -04:00
Danny Avila
c6fb4686ef 🌐 ci: Bump Locize i18n Action Version 2025-08-01 15:52:00 -04:00
Dustin Healy
f1c6e4d55e 🧪 ci: Unit Tests for MCP Routes (#8803)
* refactor: remove unreachable if checks from mcp routes

* test: add tests for mcp routes
2025-08-01 14:47:00 -04:00
William Kim
e192c99c7d 🔧 fix: Apply Convo Export filename sanitization at export, not input (#8779)
Co-authored-by: Woosub Kim <woosub.kim@navercorp.com>
2025-07-31 07:28:33 -04:00
wartek69
056172f007 🔒 feat: MCP OAuth Config for Metadata Parameters (#8691)
* fix(mcp): add default metadata for pre-configured oauth

* removed lingering comment

* added configurable options & jest unit tests

* Update handler.test.ts

* Update handler.ts

---------

Co-authored-by: Alex <aleksander.chernyavskiy@seafar.eu>
Co-authored-by: Danny Avila <danacordially@gmail.com>
2025-07-31 07:24:49 -04:00
github-actions[bot]
5eed5009e9 🌍 i18n: Update translation.json with latest translations (#8771)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-30 15:59:27 -04:00
Danny Avila
6fc9abd4ad ✂️ fix: Remove Image Payloads from Memory Processing (#8770) 2025-07-30 15:54:33 -04:00
github-actions[bot]
03a924eaca 🌍 i18n: Update translation.json with latest translations (#8739)
* 🌍 i18n: Update translation.json with latest translations

* chore: Remove unused translation keys for MCP custom variables

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-07-30 15:23:12 -04:00
Danny Avila
25c993d93e 📦 chore: bump @librechat/agents to v2.4.69 (#8769) 2025-07-30 15:18:17 -04:00
Marco Beretta
09659c1040 🔨 style: Improve MCP UI (#8745)
* refactor: Enhance MCP components with improved UI elements and localization updates

* refactor: Clean up MCP components by removing unused imports and improving layout

* refactor: Update server status badge styling for improved UI consistency

* refactor: Move group up a level so 'X' and background highlight occur at same time for cancellation button

* refactor: Remove unused translation keys from the localization file

---------

Co-authored-by: Dustin Healy <dustinhealy1@gmail.com>
2025-07-30 14:56:22 -04:00
Josh Mullin
19a8f5c545 🐦 fix: Prioritize OIDC Username Claims to Prevent First Name Usernames (#8695)
Now prioritizes preferred_username claim, then the nonstandard
username claim, then email.

Removed given_name as a possible username choice to avoid exposing users’ first names as
usernames.

Updated openidStrategy.spec.js to reflect the new claim order.

Fixed mock OpenID server behavior where preferred_username was always
hardcoded, causing test failures.

Adjusted OpenID setup test to align with new username parameter
behavior.
2025-07-30 14:43:42 -04:00
Dustin Healy
1050346915 feat: Add Support for customUserVar Replacement in 'args' Field (#8743) 2025-07-30 14:37:56 -04:00
Marco Beretta
8a1a38f346 🔑 fix: Update Conversation Mutation to use ID from Payload (#8758) 2025-07-30 14:34:30 -04:00
Danny Avila
32081245da 🪵 chore: Remove Unnecessary Comments 2025-07-29 14:59:58 -04:00
Dustin Healy
6fd3b569ac ⚒️ fix: MCP Initialization Flows (#8734)
* fix: add OAuth flow back in to success state

* feat: disable server clicks during initialization to prevent spam

* fix: correct new tab behavior for OAuth between one-click and normal initialization flows

* fix: stop polling on error during oauth (was infinite popping toasts because we didn't clear interval)

* fix: cleanupServerState should be called after successful cancelOauth, not before

* fix: change from completeFlow to failFlow to avoid stale client IDs on OAuth after cancellation

* fix: add logic to differentiate between cancelled and failed flows when checking status for indicators (so error triangle indicator doesn't show up on cancellaiton)
2025-07-29 14:54:07 -04:00
Jakub Hrozek
6671fcb714 🛂 refactor: Use discoverAuthorizationServerMetadata for MCP OAuth (#8723)
* Use discoverAuthorizationServerMetadata instead of discoverMetadata

Uses the discoverAuthorizationServerMetadata function from the upstream
TS SDK. This has the advantage of falling back to OIDC discovery
metadata if the OAuth discovery metadata doesn't exist which is the case
with e.g. keycloak.

* chore: import order

---------

Co-authored-by: Danny Avila <danacordially@gmail.com>
2025-07-29 09:09:52 -04:00
Dustin Healy
c4677ab3fb 🔑 refactor: MCP Settings Rendering Logic for OAuth Servers (#8718)
* feat: add OAuth servers to conditional rendering logic for MCPPanel in SideNav

* feat: add startup flag check to conditional rendering logic

* fix: correct improper handling of failure state in reinitialize endpoint

* fix: change MCP config components to better handle servers without customUserVars

- removes the subtle reinitialize button from config components of servers without customUserVars or OAuth
- adds a placeholder message for components where servers have no customUserVars configured

* style: swap CustomUserVarsSection and ServerInitializationSection positions

* style: fix coloring for light mode and align more with existing design patterns

* chore: remove extraneous comments

* chore: reorder imports and `isEnabled` from api package

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2025-07-29 09:08:46 -04:00
104 changed files with 3329 additions and 469 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.69",
"@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",

View File

@@ -512,6 +512,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 +573,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) {

View File

@@ -727,4 +727,231 @@ describe('AgentClient - titleConvo', () => {
});
});
});
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();
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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');
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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,
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {
@@ -184,7 +184,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,

View File

@@ -287,14 +287,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') {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
);
}

View File

@@ -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}`,
}),
);

View File

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

View File

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

View File

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

View File

@@ -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,24 @@ 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
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-5 w-5 text-text-tertiary" />
</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 +68,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 +112,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 +138,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>

View File

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

View File

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

View File

@@ -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>
);
}

View File

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

View File

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

View File

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

View File

@@ -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);
},
},
);

View File

@@ -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;
@@ -171,6 +171,7 @@ export function useMCPServerManager() {
message: localize('com_ui_mcp_oauth_timeout', { 0: serverName }),
status: 'error',
});
clearInterval(pollInterval);
cleanupServerState(serverName);
return;
}
@@ -180,10 +181,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 +207,7 @@ export function useMCPServerManager() {
);
const initializeServer = useCallback(
async (serverName: string) => {
async (serverName: string, autoOpenOAuth: boolean = true) => {
updateServerState(serverName, { isInitializing: true });
try {
@@ -216,7 +222,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 +242,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 +273,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 +327,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 +345,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 +369,7 @@ export function useMCPServerManager() {
}
}
},
[mcpValues, setMCPValues, connectionStatus, initializeServer],
[mcpValues, setMCPValues, connectionStatus, initializeServer, isInitializing],
);
const handleConfigSave = useCallback(

View File

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

View 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

View File

@@ -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": "أنت"
}
}

View File

@@ -0,0 +1 @@
{}

View File

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

View File

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

View File

@@ -908,7 +908,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 +940,4 @@
"com_ui_yes": "Ja",
"com_ui_zoom": "Zoom",
"com_user_message": "Du"
}
}

View File

@@ -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",
@@ -840,7 +839,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 +1046,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 +1083,4 @@
"com_ui_yes": "Ja",
"com_ui_zoom": "Zoom",
"com_user_message": "Du"
}
}

View File

@@ -435,8 +435,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",
@@ -506,7 +508,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",
@@ -849,14 +850,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.",
@@ -916,7 +913,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",

View File

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

View File

@@ -930,7 +930,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 +962,4 @@
"com_ui_yes": "Jah",
"com_ui_zoom": "Suumi",
"com_user_message": "Sina"
}
}

View File

@@ -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": "شما"
}
}

View File

@@ -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ä"
}
}

View File

@@ -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",
@@ -837,7 +836,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 +1019,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 +1038,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 +1074,4 @@
"com_ui_yes": "Oui",
"com_ui_zoom": "Zoom",
"com_user_message": "Vous"
}
}

View File

@@ -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}}} מקורות",
@@ -832,7 +831,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 +1037,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 +1066,4 @@
"com_ui_yes": "כן",
"com_ui_zoom": "זום",
"com_user_message": "אתה"
}
}

View File

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

View File

@@ -198,4 +198,4 @@
"com_ui_write": "Գրում է",
"com_ui_yes": "Այո",
"com_user_message": "Դու"
}
}

View File

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

View File

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

View File

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

View File

@@ -501,7 +501,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}} ソース",
@@ -839,7 +838,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 サーバー",
@@ -1047,7 +1045,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 +1082,4 @@
"com_ui_yes": "はい",
"com_ui_zoom": "ズーム",
"com_user_message": "あなた"
}
}

View File

@@ -66,4 +66,4 @@
"com_nav_lang_turkish": "თურქული",
"com_nav_lang_vietnamese": "ვიეტნამური",
"com_nav_language": "ენა"
}
}

View File

@@ -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 서버 추가",
@@ -527,6 +575,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 +593,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 +640,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 +667,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 +705,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 +759,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 +779,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 +812,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 +841,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 +864,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 +901,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 +934,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 +944,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 +967,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 +984,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 +1002,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 +1028,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 +1061,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 +1076,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 +1088,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 +1098,4 @@
"com_ui_yes": "네",
"com_ui_zoom": "확대/축소",
"com_user_message": "당신"
}
}

View File

@@ -435,8 +435,10 @@
"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",
@@ -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",
@@ -838,7 +839,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 +852,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 +915,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",
@@ -1073,7 +1069,6 @@
"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",
@@ -1111,4 +1106,4 @@
"com_ui_yes": "Jā",
"com_ui_zoom": "Tālummaiņa",
"com_user_message": "Tu"
}
}

View File

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

View File

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

View File

@@ -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ê"
}
}

View File

@@ -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ê"
}
}

View File

@@ -849,7 +849,6 @@
"com_ui_use_2fa_code": "Использовать код 2FA вместо этого",
"com_ui_use_backup_code": "Использовать резервный код вместо этого",
"com_ui_use_micrphone": "Использовать микрофон",
"com_ui_use_prompt": "Использовать промпт",
"com_ui_used": "Использован",
"com_ui_variables": "Переменные",
"com_ui_variables_info": "Используйте двойные фигурные скобки в тексте для создания переменных, например `{{пример переменной}}`, чтобы заполнить их позже при использовании промта.",
@@ -863,4 +862,4 @@
"com_ui_yes": "Да",
"com_ui_zoom": "Масштаб",
"com_user_message": "Вы"
}
}

View File

@@ -420,6 +420,5 @@
"com_ui_terms_and_conditions": "Villkor för användning",
"com_ui_unarchive": "Avarkivera",
"com_ui_unarchive_error": "Kunde inte avarkivera chatt",
"com_ui_upload_success": "Uppladdningen av filen lyckades",
"com_ui_use_prompt": "Använd prompt"
}
"com_ui_upload_success": "Uppladdningen av filen lyckades"
}

View File

@@ -789,7 +789,6 @@
"com_ui_use_2fa_code": "ใช้รหัส 2FA แทน",
"com_ui_use_backup_code": "ใช้รหัสสำรองแทน",
"com_ui_use_micrphone": "ใช้ไมโครโฟน",
"com_ui_use_prompt": "ใช้พรอมต์",
"com_ui_used": "ใช้แล้ว",
"com_ui_variables": "ตัวแปร",
"com_ui_variables_info": "ใช้วงเล็บคู่ในข้อความของคุณเพื่อสร้างตัวแปร เช่น {{example variable}} เพื่อเติมภายหลังเมื่อใช้พรอมต์",
@@ -801,4 +800,4 @@
"com_ui_yes": "ใช่",
"com_ui_zoom": "ขยาย",
"com_user_message": "คุณ"
}
}

View File

@@ -716,7 +716,6 @@
"com_ui_upload_success": "Dosya başarıyla yüklendi",
"com_ui_upload_type": "Yükleme Türünü Seç",
"com_ui_use_micrphone": "Mikrofon kullan",
"com_ui_use_prompt": "İstemi kullan",
"com_ui_variables": "Değişkenler",
"com_ui_variables_info": "İstemi kullanırken daha sonra doldurmak üzere metninizde çift süslü parantez kullanın, örn. `{{example variable}}`.",
"com_ui_version_var": "Sürüm {{0}}",
@@ -725,4 +724,4 @@
"com_ui_yes": "Evet",
"com_ui_zoom": "Yakınlaştır",
"com_user_message": "Sen"
}
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -375,7 +375,6 @@
"com_ui_unarchive_error": "Không thể bỏ lưu trữ cuộc trò chuyện",
"com_ui_upload": "Tải lên",
"com_ui_upload_success": "Tải tệp thành công",
"com_ui_use_prompt": "Sử dụng gợi ý",
"com_ui_versions": "Phiên bản",
"com_ui_web_searching_again": "Tìm kiếm lại trên web",
"com_ui_write": "Ghi",
@@ -383,4 +382,4 @@
"com_ui_yes": "Đồng ý",
"com_ui_zoom": "Phóng",
"com_warning_resubmit_unsupported": "Điểm cuối này không hỗ trợ việc gửi lại tin nhắn AI."
}
}

View File

@@ -13,7 +13,7 @@
"com_agents_enable_file_search": "启用文件搜索",
"com_agents_file_context": "文件上下文OCR",
"com_agents_file_context_disabled": "必须先创建智能体,才能上传文件用于文件上下文。",
"com_agents_file_context_info": "作为”上下文“上传的文件会通过 OCR 处理以提取文本,然后将其添加到代理的指令中。这非常适合文档、带有文本的图或 PDF 文件等需要文件完整文本内容的场景。",
"com_agents_file_context_info": "作为 ”上下文“ 上传的文件会通过 OCR 处理以提取文本,然后将其添加到智能体的指令中。这非常适合文档、带有文本的图或 PDF 文件等需要文件完整文本内容的场景。",
"com_agents_file_search_disabled": "必须先创建智能体,才能上传文件用于文件搜索。",
"com_agents_file_search_info": "启用后,系统会告知代理以下列出的具体文件名,使其能够从这些文件中检索相关内容。",
"com_agents_instructions_placeholder": "智能体使用的系统指令",
@@ -143,6 +143,7 @@
"com_auth_username_min_length": "用户名至少 2 个字符",
"com_auth_verify_your_identity": "验证您的身份",
"com_auth_welcome_back": "欢迎",
"com_citation_more_details": "有关 {{label}} 的更多详情",
"com_citation_source": "来源",
"com_click_to_download": "(点击此处下载)",
"com_download_expired": "下载已过期",
@@ -208,6 +209,7 @@
"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-32768 个词元。Gemini 2.5 Flash 支持 0-24576 个词元。Gemini 2.5 Flash Lite 支持 512-24576 个词元。\\n\\n留空或设置为 “-1” 以让模型自动决定何时以及思考多少。默认情况下Gemini 2.5 Flash Lite 不进行思考。",
"com_endpoint_google_topk": "top-k 会改变模型选择输出词元的方式。top-k 为 1 意味着所选词是模型词汇中概率最大的(也称为贪心解码),而 top-k 为 3 意味着下一个词是从 3 个概率最大的词中选出的(使用随机性)。",
"com_endpoint_google_topp": "top-p核采样会改变模型选择输出词的方式。从概率最大的 K参见 topK 参数)向最小的 K 选择,直到它们的概率之和等于 top-p 值。",
"com_endpoint_google_use_search_grounding": "使用 Google 的基础搜索特性,通过实时网络搜索结果优化响应。这使得模型能够访问当前信息,提供更准确、更及时的答案。",
@@ -393,7 +395,7 @@
"com_nav_hide_panel": "隐藏最右侧面板",
"com_nav_info_balance": "余额显示您剩余的词元额度。词元额度对应一定的货币价值例如1000 额度 = 0.001 美元)。",
"com_nav_info_code_artifacts": "启用在对话旁显示实验性代码工件",
"com_nav_info_code_artifacts_agent": "使该代理能够使用代码件。默认情况下,除非启用“自定义提示模式”,否则会添加与附件使用相关的额外说明。",
"com_nav_info_code_artifacts_agent": "使该智能体能够使用代码件。默认情况下,除非启用“自定义提示模式”,否则会添加与 Artifacts 使用相关的额外说明。",
"com_nav_info_custom_prompt_mode": "启用后,默认的 Artifacts 系统提示词将不会包含在内。在此模式下,必须手动提供所有生成工件的指令。",
"com_nav_info_enter_to_send": "启用后,按下 `ENTER` 将发送您的消息。禁用后,按下 `ENTER` 将添加新行,您需要按下 `CTRL + ENTER` / `⌘ + ENTER` 来发送消息。",
"com_nav_info_fork_change_default": "`仅可见消息` 仅包含到所选消息的直接路径,`包含相关分支` 添加路径上的分支,`包含所有目标` 包括所有连接的消息和分支。",
@@ -433,8 +435,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": "语言",
@@ -504,7 +508,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}} 个来源",
@@ -530,6 +533,7 @@
"com_ui_add_mcp_server": "添加 MCP 服务器",
"com_ui_add_model_preset": "添加一个模型或预设以获得额外的回复",
"com_ui_add_multi_conversation": "添加多个对话",
"com_ui_adding_details": "添加细节",
"com_ui_admin": "管理",
"com_ui_admin_access_warning": "禁用管理员对此特性的访问可能会导致界面出现异常,需要刷新页面。如果保存此设置,唯一的恢复方式是通过 librechat.yaml 配置文件中的界面设置进行修改,这将影响所有角色。",
"com_ui_admin_settings": "管理员设置",
@@ -846,15 +850,11 @@
"com_ui_manage": "管理",
"com_ui_max_tags": "最多允许 {{0}} 个,用最新值。",
"com_ui_mcp_authenticated_success": "MCP 服务器 “{{0}}” 认证成功",
"com_ui_mcp_dialog_desc": "请在下方输入必要的信息。",
"com_ui_mcp_enter_var": "输入值:{{0}}",
"com_ui_mcp_init_failed": "初始化 MCP 服务器失败",
"com_ui_mcp_initialize": "初始化",
"com_ui_mcp_initialized_success": "MCP 服务器 “{{0}}” 初始化成功",
"com_ui_mcp_not_authenticated": "{{0}} 未认证(需要 OAuth",
"com_ui_mcp_not_initialized": "{{0}} 未初始化",
"com_ui_mcp_oauth_cancelled": "{{0}} OAuth 登录已取消",
"com_ui_mcp_oauth_no_url": "需要 OAuth 认证,但未提供 URL",
"com_ui_mcp_oauth_timeout": "{{0}} OAuth 登录超时",
"com_ui_mcp_server_not_found": "未找到服务器。",
"com_ui_mcp_servers": "MCP 服务器",
@@ -913,7 +913,6 @@
"com_ui_oauth_error_missing_code": "缺少身份验证代码。请重试。",
"com_ui_oauth_error_missing_state": "缺少状态参数。请重试。",
"com_ui_oauth_error_title": "认证失败",
"com_ui_oauth_flow_desc": "在新窗口中完成 OAuth 流程,然后返回此处。",
"com_ui_oauth_success_description": "您的身份验证成功。此窗口将在以下时间后关闭:",
"com_ui_oauth_success_title": "认证成功",
"com_ui_of": "/",
@@ -1040,6 +1039,7 @@
"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": "未知",
@@ -1067,7 +1067,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": "变量",
@@ -1078,7 +1077,6 @@
"com_ui_view_memory": "查看记忆",
"com_ui_view_source": "查看来源对话",
"com_ui_web_search": "网络搜索",
"com_ui_web_search_api_subtitle": "搜索网络以获取最新信息",
"com_ui_web_search_cohere_key": "输入 Cohere API Key",
"com_ui_web_search_firecrawl_url": "Firecrawl API URL可选",
"com_ui_web_search_jina_key": "输入 Jina API Key",
@@ -1106,4 +1104,4 @@
"com_ui_yes": "是的",
"com_ui_zoom": "缩放",
"com_user_message": "您"
}
}

View File

@@ -9,9 +9,13 @@
"com_agents_create_error": "建立您的代理時發生錯誤。",
"com_agents_description_placeholder": "選填:在此描述您的代理程式",
"com_agents_enable_file_search": "啟用檔案搜尋",
"com_agents_file_context": "文件內容 (OCR)",
"com_agents_file_search_disabled": "必須先建立代理才能上傳檔案進行檔案搜尋。",
"com_agents_file_search_info": "啟用後,代理將會被告知以下列出的確切檔案名稱,使其能夠從這些檔案中擷取相關內容。",
"com_agents_instructions_placeholder": "代理程式使用的系統指令",
"com_agents_mcp_description_placeholder": "簡要解釋它的作用",
"com_agents_mcp_icon_size": "最小尺寸 128 x 128 px",
"com_agents_mcp_name_placeholder": "自定義工具",
"com_agents_missing_provider_model": "請在建立代理前選擇供應商和模型。",
"com_agents_name_placeholder": "選填:代理人的名稱",
"com_agents_no_access": "您沒有權限編輯此助理",
@@ -59,6 +63,7 @@
"com_assistants_update_error": "更新您的助理時發生錯誤。",
"com_assistants_update_success": "更新成功",
"com_auth_already_have_account": "已經有帳號了?",
"com_auth_apple_login": "Apple登入",
"com_auth_back_to_login": "返回登入",
"com_auth_click": "點選",
"com_auth_click_here": "點選這裡",
@@ -121,6 +126,7 @@
"com_auth_username_max_length": "使用者名稱長度必須少於 20 個字元",
"com_auth_username_min_length": "使用者名稱長度必須至少有 2 個字元",
"com_auth_welcome_back": "歡迎回來",
"com_citation_source": "來源",
"com_click_to_download": "(點選此處下載)",
"com_download_expired": "下載已過期",
"com_download_expires": "(點擊此處下載 - {{0}} 後過期)",
@@ -134,6 +140,7 @@
"com_endpoint_anthropic_temp": "範圍從 0 到 1。對於分析/多選題,使用接近 0 的溫度,對於創意和生成式任務,使用接近 1 的溫度。我們建議修改這個或 Top P但不建議兩者都修改。",
"com_endpoint_anthropic_topk": "Top-k 改變模型選擇輸出 token 的方式。Top-k 為 1 表示所選 token 在模型詞彙表中所有 token 中最可能(也稱為貪婪解碼),而 Top-k 為 3 表示下一個 token 從最可能的 3 個 token 中選擇(使用溫度)。",
"com_endpoint_anthropic_topp": "Top-p 改變模型選擇輸出 token 的方式。從最可能的 K見 topK 參數)開始選擇 token直到它們的機率之和達到 top-p 值。",
"com_endpoint_anthropic_use_web_search": "啟用 Anthropic 內建的網路搜尋功能,使模型能夠在網路上搜尋最新資訊,從而提供更準確、即時的回應。",
"com_endpoint_assistant": "助理",
"com_endpoint_assistant_model": "AI 模型",
"com_endpoint_assistant_placeholder": "請從右側面板選擇一位助理",
@@ -178,6 +185,7 @@
"com_endpoint_google_temp": "較高的值表示更隨機,而較低的值表示更集中和確定。我們建議修改這個或 Top P但不建議兩者都修改。",
"com_endpoint_google_topk": "Top-k 調整模型如何選取輸出的 token。當 Top-k 設為 1 時,模型會選取在其詞彙庫中機率最高的 token 進行輸出(這也被稱為貪婪解碼)。相對地,當 Top-k 設為 3 時,模型會從機率最高的三個 token 中選取下一個輸出 token這會涉及到所謂的「溫度」調整",
"com_endpoint_google_topp": "Top-p 調整模型在輸出 token 時的選擇機制。從最可能的 K見 topK 參數)開始選擇 token直到它們的機率之和達到 top-p 值。",
"com_endpoint_google_use_search_grounding": "使用 Google 的基礎搜尋功能,以即時的網路搜尋結果增強回應。這使模型能夠取得最新資訊,並提供更準確、最新的答案。",
"com_endpoint_instructions_assistants": "覆寫提示指令",
"com_endpoint_instructions_assistants_placeholder": "覆寫助理的提示指令。這對於在每次執行時修改行為很有用。",
"com_endpoint_max_output_tokens": "最大輸出 token 數",
@@ -199,6 +207,7 @@
"com_endpoint_openai_stop": "最多 4 個序列API 將在生成更多 token 時停止。",
"com_endpoint_openai_temp": "較高的值表示更隨機,而較低的值表示更集中和確定。我們建議修改這個或 Top P但不建議兩者都修改。",
"com_endpoint_openai_topp": "與溫度取樣的替代方法,稱為核心取樣,其中模型考慮 top_p 機率質量的 token 結果。所以 0.1 表示只考慮佔 top 10% 機率質量的 token。我們建議修改這個或溫度但不建議兩者都修改。",
"com_endpoint_openai_use_web_search": "啟用 OpenAI 內建的網路搜尋功能,使模型能夠在網路上搜尋最新資訊,從而提供更準確、即時的回應。",
"com_endpoint_output": "輸出",
"com_endpoint_plug_image_detail": "影像詳細資訊",
"com_endpoint_plug_resend_files": "重新傳送檔案",
@@ -245,7 +254,7 @@
"com_error_files_upload": "上傳檔案時發生錯誤",
"com_error_files_upload_canceled": "檔案上傳請求已取消。注意:檔案上傳可能仍在處理中,需要手動刪除。",
"com_error_files_validation": "驗證檔案時發生錯誤。",
"com_error_input_length": "最新訊息的字元數過長,已超過字元限制({{0}}。請縮短您的訊息內容、在對話參數中調整最大上下文大小,或是建立分支對話以繼續。",
"com_error_input_length": "最新訊息的 Token 數量過多,已超出 Token 限制,或是您設定的 Token 限制參數有誤,影響了上下文視窗。更多資訊:{{0}}。請縮短您的訊息、調整對話參數中最大上下文大小,或分叉 (fork) 此對話以繼續。",
"com_error_invalid_user_key": "提供的金鑰無效。請提供有效的金鑰並重試。",
"com_error_moderation": "您所提交的內容似乎被我們的內容審查系統標記為不符合社群準則。我們無法就此特定主題繼續進行討論。如果您有任何其他問題或想要探討的主題,請編輯您的訊息或開啟新的對話。",
"com_error_no_base_url": "找不到基礎 URL。請提供一個基礎 URL 後再試一次。",
@@ -612,7 +621,6 @@
"com_ui_logo": "{{0}} 標誌",
"com_ui_manage": "管理",
"com_ui_max_tags": "允許的最大數量為 {{0}},已使用最新值。",
"com_ui_mcp_dialog_desc": "請在下方輸入必要資訊。",
"com_ui_mcp_enter_var": "請輸入 {{0}} 的值",
"com_ui_mcp_server_not_found": "找不到伺服器。",
"com_ui_mcp_url": "MCP 伺服器",
@@ -623,12 +631,18 @@
"com_ui_memories_allow_update": "允許更新記憶",
"com_ui_memories_allow_use": "允許使用記憶",
"com_ui_memories_filter": "篩選記憶...",
"com_ui_memory": "記憶",
"com_ui_memory_already_exceeded": "記憶體儲存已滿——已超過 {{tokens}} 個 token。在新增記憶前請先刪除現有記憶。",
"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}} 個 token。請刪除現有記憶以釋放空間。",
"com_ui_mention": "提及端點、助理或預設設定以快速切換",
"com_ui_min_tags": "無法再移除更多值,至少需要 {{0}} 個。",
"com_ui_model": "模型",
@@ -737,12 +751,30 @@
"com_ui_usage": "使用率",
"com_ui_use_memory": "使用記憶",
"com_ui_use_micrphone": "使用麥克風",
"com_ui_use_prompt": "使用提示",
"com_ui_variables": "變數",
"com_ui_variables_info": "在文字中使用雙大括號來建立變數,例如 `{{example variable}}`,以便在使用提示時填入。",
"com_ui_version_var": "版本 {{0}}",
"com_ui_versions": "版本",
"com_ui_web_search": "網路搜尋",
"com_ui_web_search_cohere_key": "輸入 Cohere API Key",
"com_ui_web_search_firecrawl_url": "Firecrawl API URL可選",
"com_ui_web_search_jina_key": "輸入 Jina API Key",
"com_ui_web_search_processing": "正在處理結果",
"com_ui_web_search_provider_searxng": "SearXNG",
"com_ui_web_search_provider_serper": "Serper API",
"com_ui_web_search_provider_serper_key": "取得您的 Serper API key",
"com_ui_web_search_reading": "正在讀取結果",
"com_ui_web_search_reranker_cohere": "Cohere",
"com_ui_web_search_reranker_cohere_key": "取得您的 Cohere API key",
"com_ui_web_search_reranker_jina": "Jina AI",
"com_ui_web_search_reranker_jina_key": "取得您的 Jina API key",
"com_ui_web_search_scraper_firecrawl": "Firecrawl API",
"com_ui_web_search_scraper_firecrawl_key": "取得您的 Firecrawl API key",
"com_ui_web_search_searxng_api_key": "輸入 SearXNG API Key (可選)",
"com_ui_web_search_searxng_instance_url": "SearXNG Instance URL",
"com_ui_web_searching": "正在搜尋網路",
"com_ui_web_searching_again": "正在重新搜尋網路",
"com_ui_yes": "是",
"com_ui_zoom": "縮放",
"com_user_message": "您"
}
}

View File

@@ -0,0 +1,92 @@
const path = require('path');
const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose'));
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const { askQuestion, silentExit } = require('./helpers');
const connect = require('./connect');
(async () => {
await connect();
console.purple('---------------------------------------');
console.purple('Reset MeiliSearch Synchronization Flags');
console.purple('---------------------------------------');
console.yellow('\nThis script will reset the MeiliSearch indexing flags in MongoDB.');
console.yellow('Use this when MeiliSearch data has been deleted or corrupted,');
console.yellow('and you need to trigger a full re-synchronization.\n');
const confirm = await askQuestion(
'Are you sure you want to reset all MeiliSearch sync flags? (y/N): ',
);
if (confirm.toLowerCase() !== 'y') {
console.orange('Operation cancelled.');
silentExit(0);
}
try {
// Reset _meiliIndex flags for messages
console.cyan('\nResetting message sync flags...');
const messageResult = await mongoose.connection.db
.collection('messages')
.updateMany({ _meiliIndex: true }, { $set: { _meiliIndex: false } });
console.green(`✓ Reset ${messageResult.modifiedCount} message sync flags`);
// Reset _meiliIndex flags for conversations
console.cyan('\nResetting conversation sync flags...');
const conversationResult = await mongoose.connection.db
.collection('conversations')
.updateMany({ _meiliIndex: true }, { $set: { _meiliIndex: false } });
console.green(`✓ Reset ${conversationResult.modifiedCount} conversation sync flags`);
// Get current counts
const totalMessages = await mongoose.connection.db.collection('messages').countDocuments();
const totalConversations = await mongoose.connection.db
.collection('conversations')
.countDocuments();
console.purple('\n---------------------------------------');
console.green('MeiliSearch sync flags have been reset successfully!');
console.cyan(`\nTotal messages to sync: ${totalMessages}`);
console.cyan(`Total conversations to sync: ${totalConversations}`);
console.yellow('\nThe next time LibreChat starts or performs a sync check,');
console.yellow('all data will be re-indexed into MeiliSearch.');
console.purple('---------------------------------------\n');
// Ask if user wants to see advanced options
const showAdvanced = await askQuestion('Show advanced options? (y/N): ');
if (showAdvanced.toLowerCase() === 'y') {
console.cyan('\nAdvanced Options:');
console.yellow('1. To trigger immediate sync, restart LibreChat');
console.yellow('2. To disable sync, set MEILI_NO_SYNC=true in .env');
console.yellow(
'3. To adjust sync batch size, set MEILI_SYNC_BATCH_SIZE in .env (default: 100)',
);
console.yellow('4. To adjust sync delay, set MEILI_SYNC_DELAY_MS in .env (default: 100ms)');
console.yellow(
'5. To change sync threshold, set MEILI_SYNC_THRESHOLD in .env (default: 1000)\n',
);
}
silentExit(0);
} catch (error) {
console.red('\nError resetting MeiliSearch sync flags:');
console.error(error);
silentExit(1);
}
})();
process.on('uncaughtException', (err) => {
if (!err.message.includes('fetch failed')) {
console.error('There was an uncaught error:');
console.error(err);
}
if (err.message.includes('fetch failed')) {
return;
} else {
process.exit(1);
}
});

View File

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

View File

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

59
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "LibreChat",
"version": "v0.7.9",
"version": "v0.8.0-rc1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "LibreChat",
"version": "v0.7.9",
"version": "v0.8.0-rc1",
"license": "ISC",
"workspaces": [
"api",
@@ -47,7 +47,7 @@
},
"api": {
"name": "@librechat/backend",
"version": "v0.7.9",
"version": "v0.8.0-rc1",
"license": "ISC",
"dependencies": {
"@anthropic-ai/sdk": "^0.52.0",
@@ -65,10 +65,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.69",
"@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",
@@ -2735,7 +2735,7 @@
},
"client": {
"name": "@librechat/frontend",
"version": "v0.7.9",
"version": "v0.8.0-rc1",
"license": "ISC",
"dependencies": {
"@ariakit/react": "^0.4.15",
@@ -21276,13 +21276,13 @@
}
},
"node_modules/@langchain/langgraph": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.3.11.tgz",
"integrity": "sha512-Lh8oga4ismQyw1NGZKoHPdeGke1g5HMF7V0nBlc5R7GnV8tfC6pdsXjiEH6sYsHsRDInfy8uQeob/BwEmMSSbQ==",
"version": "0.3.12",
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.3.12.tgz",
"integrity": "sha512-4jKvfmxxgQyKnCvXdFbcKt6MdfaJoQ2WWqBR16o2E6D2RxqHvnLMMClZh4FSd6WYw39z5LGWvzRapFbRMqxu1A==",
"license": "MIT",
"dependencies": {
"@langchain/langgraph-checkpoint": "~0.0.18",
"@langchain/langgraph-sdk": "~0.0.100",
"@langchain/langgraph-sdk": "~0.0.102",
"uuid": "^10.0.0",
"zod": "^3.25.32"
},
@@ -21328,9 +21328,9 @@
}
},
"node_modules/@langchain/langgraph-sdk": {
"version": "0.0.100",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.100.tgz",
"integrity": "sha512-mQuj0KgjD31Me+/W658OtdlOACOjgipWp/hF80OY4w4LqWCNIQWJBWMZ3f1/E8jpog/XBCROR37auFc7Fj+4Dw==",
"version": "0.0.104",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.104.tgz",
"integrity": "sha512-wUO6GMy65Y7DsWtjTJ3dA59enrZy2wN4o48AMYN7dF7u/PMXXYyBjBCKSzgVWqO6uWH2yNpyGDrcMwKuk5kQLA==",
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.15",
@@ -21573,9 +21573,9 @@
}
},
"node_modules/@librechat/agents": {
"version": "2.4.68",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.68.tgz",
"integrity": "sha512-05UhnUJJ6/I8KVkhJ9NrQcm3UKhA/cXG8yT2VU+QQRJoDf7qnt47DRBP87ZEWRGMLh2civq1OWQPW2BHf2eL4A==",
"version": "2.4.69",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.4.69.tgz",
"integrity": "sha512-Yt0rttqOaZQeZPIB68I8RdnU6SHeh0OJV5yEg8mx9EHTA7SnV/lOlDhn424aXdpMvYZYuxAt/Fev3jTC7qKiTg==",
"license": "MIT",
"dependencies": {
"@langchain/anthropic": "^0.3.24",
@@ -22148,9 +22148,9 @@
}
},
"node_modules/@librechat/agents/node_modules/openai": {
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.10.1.tgz",
"integrity": "sha512-fq6xVfv1/gpLbsj8fArEt3b6B9jBxdhAK+VJ+bDvbUvNd+KTLlA3bnDeYZaBsGH9LUhJ1M1yXfp9sEyBLMx6eA==",
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-5.11.0.tgz",
"integrity": "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
@@ -22442,9 +22442,9 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.0.tgz",
"integrity": "sha512-qFfbWFA7r1Sd8D697L7GkTd36yqDuTkvz0KfOGkgXR8EUhQn3/EDNIR/qUdQNMT8IjmasBvHWuXeisxtXTQT2g==",
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.1.tgz",
"integrity": "sha512-CPle1OQehbWqd25La9Ack5B07StKIxh4+Bf19qnpZKJC1oI22Y0czZHbifjw1UoczIfKBwBDAp/dFxvHG13B5A==",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.6",
@@ -28936,10 +28936,11 @@
}
},
"node_modules/@testing-library/react": {
"version": "14.2.1",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.2.1.tgz",
"integrity": "sha512-sGdjws32ai5TLerhvzThYFbpnF9XtL65Cjf+gB0Dhr29BGqK+mAeN7SURSdu+eqgET4ANcWoC7FQpkaiGvBr+A==",
"version": "14.3.1",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz",
"integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^9.0.0",
@@ -51413,9 +51414,9 @@
},
"peerDependencies": {
"@langchain/core": "^0.3.62",
"@librechat/agents": "^2.4.68",
"@librechat/agents": "^2.4.69",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.17.0",
"@modelcontextprotocol/sdk": "^1.17.1",
"axios": "^1.8.2",
"diff": "^7.0.0",
"eventsource": "^3.0.2",
@@ -51506,7 +51507,7 @@
},
"packages/client": {
"name": "@librechat/client",
"version": "0.2.0",
"version": "0.2.3",
"devDependencies": {
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.2",
@@ -51804,7 +51805,7 @@
},
"packages/data-provider": {
"name": "librechat-data-provider",
"version": "0.7.902",
"version": "0.7.903",
"license": "ISC",
"dependencies": {
"axios": "^1.8.2",

View File

@@ -1,6 +1,6 @@
{
"name": "LibreChat",
"version": "v0.7.9",
"version": "v0.8.0-rc1",
"description": "",
"workspaces": [
"api",
@@ -32,6 +32,7 @@
"reset-password": "node config/reset-password.js",
"ban-user": "node config/ban-user.js",
"delete-user": "node config/delete-user.js",
"reset-meili-sync": "node config/reset-meili-sync.js",
"update-banner": "node config/update-banner.js",
"delete-banner": "node config/delete-banner.js",
"backend": "cross-env NODE_ENV=production node api/server/index.js",

View File

@@ -70,9 +70,9 @@
},
"peerDependencies": {
"@langchain/core": "^0.3.62",
"@librechat/agents": "^2.4.68",
"@librechat/agents": "^2.4.69",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.17.0",
"@modelcontextprotocol/sdk": "^1.17.1",
"axios": "^1.8.2",
"diff": "^7.0.0",
"eventsource": "^3.0.2",

View File

@@ -50,8 +50,9 @@ import type { MistralFileUploadResponse, MistralSignedUrlResponse, OCRResult } f
import { logger as mockLogger } from '@librechat/data-schemas';
import {
uploadDocumentToMistral,
uploadMistralOCR,
uploadAzureMistralOCR,
deleteMistralFile,
uploadMistralOCR,
getSignedUrl,
performOCR,
} from './crud';
@@ -216,6 +217,56 @@ describe('MistralOCR Service', () => {
});
});
describe('deleteMistralFile', () => {
it('should delete a file from Mistral API', async () => {
mockAxios.delete!.mockResolvedValueOnce({ data: {} });
await deleteMistralFile({
fileId: 'file-123',
apiKey: 'test-api-key',
baseURL: 'https://api.mistral.ai/v1',
});
expect(mockAxios.delete).toHaveBeenCalledWith('https://api.mistral.ai/v1/files/file-123', {
headers: {
Authorization: 'Bearer test-api-key',
},
});
});
it('should use default baseURL when not provided', async () => {
mockAxios.delete!.mockResolvedValueOnce({ data: {} });
await deleteMistralFile({
fileId: 'file-456',
apiKey: 'test-api-key',
});
expect(mockAxios.delete).toHaveBeenCalledWith('https://api.mistral.ai/v1/files/file-456', {
headers: {
Authorization: 'Bearer test-api-key',
},
});
});
it('should not throw when deletion fails', async () => {
mockAxios.delete!.mockRejectedValueOnce(new Error('Delete failed'));
// Should not throw
await expect(
deleteMistralFile({
fileId: 'file-789',
apiKey: 'test-api-key',
}),
).resolves.not.toThrow();
expect(mockLogger.error).toHaveBeenCalledWith(
'Error deleting Mistral file file-789:',
expect.any(Error),
);
});
});
describe('performOCR', () => {
it('should perform OCR using Mistral API (document_url)', async () => {
const mockResponse: { data: OCRResult } = {
@@ -1345,6 +1396,340 @@ describe('MistralOCR Service', () => {
expect(authHeader).toBe('Bearer hardcoded-api-key-12345');
});
});
describe('File cleanup', () => {
beforeEach(() => {
const mockReadStream: MockReadStream = {
on: jest.fn().mockImplementation(function (
this: MockReadStream,
event: string,
handler: () => void,
) {
if (event === 'end') {
handler();
}
return this;
}),
pipe: jest.fn().mockImplementation(function (this: MockReadStream) {
return this;
}),
pause: jest.fn(),
resume: jest.fn(),
emit: jest.fn(),
once: jest.fn(),
destroy: jest.fn(),
path: '/tmp/upload/file.pdf',
fd: 1,
flags: 'r',
mode: 0o666,
autoClose: true,
bytesRead: 0,
closed: false,
pending: false,
};
(jest.mocked(fs).createReadStream as jest.Mock).mockReturnValue(mockReadStream);
// Clear all mocks before each test
mockAxios.delete!.mockClear();
});
it('should delete the uploaded file after successful OCR processing', async () => {
mockLoadAuthValues.mockResolvedValue({
OCR_API_KEY: 'test-api-key',
OCR_BASEURL: 'https://api.mistral.ai/v1',
});
// Mock file upload response
mockAxios.post!.mockResolvedValueOnce({
data: {
id: 'file-cleanup-123',
object: 'file',
bytes: 1024,
created_at: Date.now(),
filename: 'document.pdf',
purpose: 'ocr',
} as MistralFileUploadResponse,
});
// Mock signed URL response
mockAxios.get!.mockResolvedValueOnce({
data: {
url: 'https://signed-url.com',
expires_at: Date.now() + 86400000,
} as MistralSignedUrlResponse,
});
// Mock OCR response
mockAxios.post!.mockResolvedValueOnce({
data: {
model: 'mistral-ocr-latest',
pages: [
{
index: 0,
markdown: 'OCR content',
images: [],
dimensions: { dpi: 300, height: 1100, width: 850 },
},
],
document_annotation: '',
usage_info: {
pages_processed: 1,
doc_size_bytes: 1024,
},
},
});
// Mock delete file response
mockAxios.delete!.mockResolvedValueOnce({ data: {} });
const req = {
user: { id: 'user123' },
app: {
locals: {
ocr: {
apiKey: '${OCR_API_KEY}',
baseURL: '${OCR_BASEURL}',
mistralModel: 'mistral-ocr-latest',
},
},
},
} as unknown as ExpressRequest;
const file = {
path: '/tmp/upload/file.pdf',
originalname: 'document.pdf',
mimetype: 'application/pdf',
} as Express.Multer.File;
await uploadMistralOCR({
req,
file,
loadAuthValues: mockLoadAuthValues,
});
// Verify delete was called with correct parameters
expect(mockAxios.delete).toHaveBeenCalledWith(
'https://api.mistral.ai/v1/files/file-cleanup-123',
{
headers: {
Authorization: 'Bearer test-api-key',
},
},
);
expect(mockAxios.delete).toHaveBeenCalledTimes(1);
});
it('should delete the uploaded file even when OCR processing fails', async () => {
mockLoadAuthValues.mockResolvedValue({
OCR_API_KEY: 'test-api-key',
OCR_BASEURL: 'https://api.mistral.ai/v1',
});
// Mock file upload response
mockAxios.post!.mockResolvedValueOnce({
data: {
id: 'file-cleanup-456',
object: 'file',
bytes: 1024,
created_at: Date.now(),
filename: 'document.pdf',
purpose: 'ocr',
} as MistralFileUploadResponse,
});
// Mock signed URL response
mockAxios.get!.mockResolvedValueOnce({
data: {
url: 'https://signed-url.com',
expires_at: Date.now() + 86400000,
} as MistralSignedUrlResponse,
});
// Mock OCR to fail
mockAxios.post!.mockRejectedValueOnce(new Error('OCR processing failed'));
// Mock delete file response
mockAxios.delete!.mockResolvedValueOnce({ data: {} });
const req = {
user: { id: 'user123' },
app: {
locals: {
ocr: {
apiKey: '${OCR_API_KEY}',
baseURL: '${OCR_BASEURL}',
mistralModel: 'mistral-ocr-latest',
},
},
},
} as unknown as ExpressRequest;
const file = {
path: '/tmp/upload/file.pdf',
originalname: 'document.pdf',
mimetype: 'application/pdf',
} as Express.Multer.File;
await expect(
uploadMistralOCR({
req,
file,
loadAuthValues: mockLoadAuthValues,
}),
).rejects.toThrow('Error uploading document to Mistral OCR API');
// Verify delete was still called despite the error
expect(mockAxios.delete).toHaveBeenCalledWith(
'https://api.mistral.ai/v1/files/file-cleanup-456',
{
headers: {
Authorization: 'Bearer test-api-key',
},
},
);
expect(mockAxios.delete).toHaveBeenCalledTimes(1);
});
it('should handle deletion errors gracefully without throwing', async () => {
mockLoadAuthValues.mockResolvedValue({
OCR_API_KEY: 'test-api-key',
OCR_BASEURL: 'https://api.mistral.ai/v1',
});
// Mock file upload response
mockAxios.post!.mockResolvedValueOnce({
data: {
id: 'file-cleanup-789',
object: 'file',
bytes: 1024,
created_at: Date.now(),
filename: 'document.pdf',
purpose: 'ocr',
} as MistralFileUploadResponse,
});
// Mock signed URL response
mockAxios.get!.mockResolvedValueOnce({
data: {
url: 'https://signed-url.com',
expires_at: Date.now() + 86400000,
} as MistralSignedUrlResponse,
});
// Mock OCR response
mockAxios.post!.mockResolvedValueOnce({
data: {
model: 'mistral-ocr-latest',
pages: [
{
index: 0,
markdown: 'OCR content',
images: [],
dimensions: { dpi: 300, height: 1100, width: 850 },
},
],
document_annotation: '',
usage_info: {
pages_processed: 1,
doc_size_bytes: 1024,
},
},
});
// Mock delete to fail
mockAxios.delete!.mockRejectedValueOnce(new Error('Delete failed'));
const req = {
user: { id: 'user123' },
app: {
locals: {
ocr: {
apiKey: '${OCR_API_KEY}',
baseURL: '${OCR_BASEURL}',
mistralModel: 'mistral-ocr-latest',
},
},
},
} as unknown as ExpressRequest;
const file = {
path: '/tmp/upload/file.pdf',
originalname: 'document.pdf',
mimetype: 'application/pdf',
} as Express.Multer.File;
// Should not throw even if delete fails
const result = await uploadMistralOCR({
req,
file,
loadAuthValues: mockLoadAuthValues,
});
expect(result).toEqual({
filename: 'document.pdf',
bytes: expect.any(Number),
filepath: 'mistral_ocr',
text: 'OCR content\n\n',
images: [],
});
// Verify delete was attempted
expect(mockAxios.delete).toHaveBeenCalledWith(
'https://api.mistral.ai/v1/files/file-cleanup-789',
{
headers: {
Authorization: 'Bearer test-api-key',
},
},
);
// Verify error was logged
expect(mockLogger.error).toHaveBeenCalledWith(
'Error deleting Mistral file file-cleanup-789:',
expect.any(Error),
);
});
it('should not attempt cleanup if file upload fails', async () => {
mockLoadAuthValues.mockResolvedValue({
OCR_API_KEY: 'test-api-key',
OCR_BASEURL: 'https://api.mistral.ai/v1',
});
// Mock file upload to fail
mockAxios.post!.mockRejectedValueOnce(new Error('Upload failed'));
const req = {
user: { id: 'user123' },
app: {
locals: {
ocr: {
apiKey: '${OCR_API_KEY}',
baseURL: '${OCR_BASEURL}',
mistralModel: 'mistral-ocr-latest',
},
},
},
} as unknown as ExpressRequest;
const file = {
path: '/tmp/upload/file.pdf',
originalname: 'document.pdf',
mimetype: 'application/pdf',
} as Express.Multer.File;
await expect(
uploadMistralOCR({
req,
file,
loadAuthValues: mockLoadAuthValues,
}),
).rejects.toThrow('Error uploading document to Mistral OCR API');
// Verify delete was NOT called since upload failed
expect(mockAxios.delete).not.toHaveBeenCalled();
});
});
});
describe('uploadAzureMistralOCR', () => {

View File

@@ -172,6 +172,35 @@ export async function performOCR({
});
}
/**
* Deletes a file from Mistral API
* @param params Delete parameters
* @param params.fileId The file ID to delete
* @param params.apiKey Mistral API key
* @param params.baseURL Mistral API base URL
* @returns Promise that resolves when the file is deleted
*/
export async function deleteMistralFile({
fileId,
apiKey,
baseURL = DEFAULT_MISTRAL_BASE_URL,
}: {
fileId: string;
apiKey: string;
baseURL?: string;
}): Promise<void> {
try {
const result = await axios.delete(`${baseURL}/files/${fileId}`, {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
logger.debug(`Mistral file ${fileId} deleted successfully:`, result.data);
} catch (error) {
logger.error(`Error deleting Mistral file ${fileId}:`, error);
}
}
/**
* Determines if a value needs to be loaded from environment
*/
@@ -335,8 +364,14 @@ function createOCRError(error: unknown, baseMessage: string): Error {
* along with the `filename` and `bytes` properties.
*/
export const uploadMistralOCR = async (context: OCRContext): Promise<MistralOCRUploadResult> => {
let mistralFileId: string | undefined;
let apiKey: string | undefined;
let baseURL: string | undefined;
try {
const { apiKey, baseURL } = await loadAuthConfig(context);
const authConfig = await loadAuthConfig(context);
apiKey = authConfig.apiKey;
baseURL = authConfig.baseURL;
const model = getModelConfig(context.req.app.locals?.ocr);
const mistralFile = await uploadDocumentToMistral({
@@ -346,6 +381,8 @@ export const uploadMistralOCR = async (context: OCRContext): Promise<MistralOCRU
baseURL,
});
mistralFileId = mistralFile.id;
const signedUrlResponse = await getSignedUrl({
apiKey,
baseURL,
@@ -354,11 +391,11 @@ export const uploadMistralOCR = async (context: OCRContext): Promise<MistralOCRU
const documentType = getDocumentType(context.file);
const ocrResult = await performOCR({
apiKey,
baseURL,
model,
url: signedUrlResponse.url,
documentType,
baseURL,
apiKey,
model,
});
if (!ocrResult || !ocrResult.pages || ocrResult.pages.length === 0) {
@@ -368,6 +405,10 @@ export const uploadMistralOCR = async (context: OCRContext): Promise<MistralOCRU
}
const { text, images } = processOCRResult(ocrResult);
if (mistralFileId && apiKey && baseURL) {
await deleteMistralFile({ fileId: mistralFileId, apiKey, baseURL });
}
return {
filename: context.file.originalname,
bytes: text.length * 4,
@@ -376,6 +417,9 @@ export const uploadMistralOCR = async (context: OCRContext): Promise<MistralOCRU
images,
};
} catch (error) {
if (mistralFileId && apiKey && baseURL) {
await deleteMistralFile({ fileId: mistralFileId, apiKey, baseURL });
}
throw createOCRError(error, 'Error uploading document to Mistral OCR API:');
}
};

View File

@@ -655,6 +655,47 @@ describe('Environment Variable Extraction (MCP)', () => {
);
});
it('should process customUserVars in args field', () => {
const user = createTestUser({
id: 'user-123',
email: 'test@example.com',
});
const customUserVars = {
MY_API_KEY: 'user-provided-api-key-12345',
PROFILE_NAME: 'production-profile',
};
const obj: MCPOptions = {
command: 'npx',
args: [
'-y',
'@smithery/cli@latest',
'run',
'@upstash/context7-mcp',
'--key',
'{{MY_API_KEY}}',
'--profile',
'{{PROFILE_NAME}}',
'--user',
'{{LIBRECHAT_USER_EMAIL}}',
],
};
const result = processMCPEnv(obj, user, customUserVars);
expect('args' in result && result.args).toEqual([
'-y',
'@smithery/cli@latest',
'run',
'@upstash/context7-mcp',
'--key',
'user-provided-api-key-12345',
'--profile',
'production-profile',
'--user',
'test@example.com',
]);
});
it('should prioritize customUserVars over user fields and system env vars if placeholders are the same (though not recommended)', () => {
// This tests the order of operations: customUserVars -> userFields -> systemEnv
// BUt it's generally not recommended to have overlapping placeholder names.

View File

@@ -0,0 +1,190 @@
import { MCPOAuthHandler } from './handler';
import type { MCPOptions } from 'librechat-data-provider';
jest.mock('@librechat/data-schemas', () => ({
logger: {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}));
jest.mock('@modelcontextprotocol/sdk/client/auth.js', () => ({
startAuthorization: jest.fn(),
}));
import { startAuthorization } from '@modelcontextprotocol/sdk/client/auth.js';
const mockStartAuthorization = startAuthorization as jest.MockedFunction<typeof startAuthorization>;
describe('MCPOAuthHandler - Configurable OAuth Metadata', () => {
const mockServerName = 'test-server';
const mockServerUrl = 'https://example.com/mcp';
const mockUserId = 'user-123';
beforeEach(() => {
jest.clearAllMocks();
process.env.DOMAIN_SERVER = 'http://localhost:3080';
// Mock startAuthorization to return a successful response
mockStartAuthorization.mockResolvedValue({
authorizationUrl: new URL('https://auth.example.com/oauth/authorize?client_id=test'),
codeVerifier: 'test-code-verifier',
});
});
afterEach(() => {
delete process.env.DOMAIN_SERVER;
});
describe('Pre-configured OAuth Metadata Fields', () => {
const baseConfig: MCPOptions['oauth'] = {
authorization_url: 'https://auth.example.com/oauth/authorize',
token_url: 'https://auth.example.com/oauth/token',
client_id: 'test-client-id',
client_secret: 'test-client-secret',
};
it('should use default values when OAuth metadata fields are not configured', async () => {
await MCPOAuthHandler.initiateOAuthFlow(
mockServerName,
mockServerUrl,
mockUserId,
baseConfig,
);
expect(mockStartAuthorization).toHaveBeenCalledWith(
mockServerUrl,
expect.objectContaining({
metadata: expect.objectContaining({
grant_types_supported: ['authorization_code', 'refresh_token'],
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'],
response_types_supported: ['code'],
code_challenge_methods_supported: ['S256', 'plain'],
}),
}),
);
});
it('should use custom grant_types_supported when provided', async () => {
const config = {
...baseConfig,
grant_types_supported: ['authorization_code'],
};
await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config);
expect(mockStartAuthorization).toHaveBeenCalledWith(
mockServerUrl,
expect.objectContaining({
metadata: expect.objectContaining({
grant_types_supported: ['authorization_code'],
}),
}),
);
});
it('should use custom token_endpoint_auth_methods_supported when provided', async () => {
const config = {
...baseConfig,
token_endpoint_auth_methods_supported: ['client_secret_post'],
};
await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config);
expect(mockStartAuthorization).toHaveBeenCalledWith(
mockServerUrl,
expect.objectContaining({
metadata: expect.objectContaining({
token_endpoint_auth_methods_supported: ['client_secret_post'],
}),
}),
);
});
it('should use custom response_types_supported when provided', async () => {
const config = {
...baseConfig,
response_types_supported: ['code', 'token'],
};
await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config);
expect(mockStartAuthorization).toHaveBeenCalledWith(
mockServerUrl,
expect.objectContaining({
metadata: expect.objectContaining({
response_types_supported: ['code', 'token'],
}),
}),
);
});
it('should use custom code_challenge_methods_supported when provided', async () => {
const config = {
...baseConfig,
code_challenge_methods_supported: ['S256'],
};
await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config);
expect(mockStartAuthorization).toHaveBeenCalledWith(
mockServerUrl,
expect.objectContaining({
metadata: expect.objectContaining({
code_challenge_methods_supported: ['S256'],
}),
}),
);
});
it('should use all custom OAuth metadata fields when provided together', async () => {
const config = {
...baseConfig,
grant_types_supported: ['authorization_code', 'client_credentials'],
token_endpoint_auth_methods_supported: ['none'],
response_types_supported: ['code', 'token', 'id_token'],
code_challenge_methods_supported: ['S256'],
};
await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config);
expect(mockStartAuthorization).toHaveBeenCalledWith(
mockServerUrl,
expect.objectContaining({
metadata: expect.objectContaining({
grant_types_supported: ['authorization_code', 'client_credentials'],
token_endpoint_auth_methods_supported: ['none'],
response_types_supported: ['code', 'token', 'id_token'],
code_challenge_methods_supported: ['S256'],
}),
}),
);
});
it('should handle empty arrays as valid custom values', async () => {
const config = {
...baseConfig,
grant_types_supported: [],
token_endpoint_auth_methods_supported: [],
response_types_supported: [],
code_challenge_methods_supported: [],
};
await MCPOAuthHandler.initiateOAuthFlow(mockServerName, mockServerUrl, mockUserId, config);
expect(mockStartAuthorization).toHaveBeenCalledWith(
mockServerUrl,
expect.objectContaining({
metadata: expect.objectContaining({
grant_types_supported: [],
token_endpoint_auth_methods_supported: [],
response_types_supported: [],
code_challenge_methods_supported: [],
}),
}),
);
});
});
});

View File

@@ -1,10 +1,10 @@
import { randomBytes } from 'crypto';
import { logger } from '@librechat/data-schemas';
import {
discoverOAuthMetadata,
registerClient,
startAuthorization,
exchangeAuthorization,
discoverAuthorizationServerMetadata,
discoverOAuthProtectedResourceMetadata,
} from '@modelcontextprotocol/sdk/client/auth.js';
import { OAuthMetadataSchema } from '@modelcontextprotocol/sdk/shared/auth.js';
@@ -61,7 +61,7 @@ export class MCPOAuthHandler {
// Discover OAuth metadata
logger.debug(`[MCPOAuth] Discovering OAuth metadata from ${authServerUrl}`);
const rawMetadata = await discoverOAuthMetadata(authServerUrl);
const rawMetadata = await discoverAuthorizationServerMetadata(authServerUrl);
if (!rawMetadata) {
logger.error(`[MCPOAuth] Failed to discover OAuth metadata from ${authServerUrl}`);
@@ -181,9 +181,22 @@ export class MCPOAuthHandler {
authorization_endpoint: config.authorization_url,
token_endpoint: config.token_url,
issuer: serverUrl,
scopes_supported: config.scope?.split(' '),
scopes_supported: config.scope?.split(' ') ?? [],
grant_types_supported: config?.grant_types_supported ?? [
'authorization_code',
'refresh_token',
],
token_endpoint_auth_methods_supported: config?.token_endpoint_auth_methods_supported ?? [
'client_secret_basic',
'client_secret_post',
],
response_types_supported: config?.response_types_supported ?? ['code'],
code_challenge_methods_supported: config?.code_challenge_methods_supported ?? [
'S256',
'plain',
],
};
logger.debug(`[MCPOAuth] metadata for "${serverName}": ${JSON.stringify(metadata)}`);
const clientInfo: OAuthClientInformation = {
client_id: config.client_id,
client_secret: config.client_secret,
@@ -466,7 +479,10 @@ export class MCPOAuthHandler {
throw new Error('No token URL available for refresh');
} else {
/** Auto-discover OAuth configuration for refresh */
const { metadata: oauthMetadata } = await this.discoverMetadata(metadata.serverUrl);
const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl);
if (!oauthMetadata) {
throw new Error('Failed to discover OAuth metadata for token refresh');
}
if (!oauthMetadata.token_endpoint) {
throw new Error('No token endpoint found in OAuth metadata');
}
@@ -584,9 +600,9 @@ export class MCPOAuthHandler {
}
/** Auto-discover OAuth configuration for refresh */
const { metadata: oauthMetadata } = await this.discoverMetadata(metadata.serverUrl);
const oauthMetadata = await discoverAuthorizationServerMetadata(metadata.serverUrl);
if (!oauthMetadata.token_endpoint) {
if (!oauthMetadata?.token_endpoint) {
throw new Error('No token endpoint found in OAuth metadata');
}

View File

@@ -63,13 +63,10 @@ export const checkAccess = async ({
}
const role = await getRoleByName(user.role);
if (role && role.permissions && role.permissions[permissionType]) {
const permissionValue = role?.permissions?.[permissionType as keyof typeof role.permissions];
if (role && role.permissions && permissionValue) {
const hasAnyPermission = permissions.every((permission) => {
if (
role.permissions?.[permissionType as keyof typeof role.permissions]?.[
permission as keyof (typeof role.permissions)[typeof permissionType]
]
) {
if (permissionValue[permission as keyof typeof permissionValue]) {
return true;
}

View File

@@ -124,6 +124,14 @@ export function processMCPEnv(
newObj.env = processedEnv;
}
if ('args' in newObj && newObj.args) {
const processedArgs: string[] = [];
for (const originalValue of newObj.args) {
processedArgs.push(processSingleValue({ originalValue, customUserVars, user }));
}
newObj.args = processedArgs;
}
// Process headers if they exist (for WebSocket, SSE, StreamableHTTP types)
// Note: `env` and `headers` are on different branches of the MCPOptions union type.
if ('headers' in newObj && newObj.headers) {

View File

@@ -1,6 +1,6 @@
{
"name": "@librechat/client",
"version": "0.2.0",
"version": "0.2.3",
"description": "React components for LibreChat",
"main": "dist/index.js",
"module": "dist/index.es.js",

View File

@@ -0,0 +1,27 @@
.animate-popover {
transform-origin: top;
opacity: 0;
transition:
opacity 150ms cubic-bezier(0.4, 0, 0.2, 1),
transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
transform: scale(0.95) translateY(-0.5rem);
}
.animate-popover[data-enter] {
opacity: 1;
transform: scale(1) translateY(0);
}
.animate-popover-left {
transform-origin: left;
opacity: 0;
transition:
opacity 150ms cubic-bezier(0.4, 0, 0.2, 1),
transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
transform: scale(0.95) translateX(-0.5rem);
}
.animate-popover-left[data-enter] {
opacity: 1;
transform: scale(1) translateX(0);
}

View File

@@ -4,6 +4,7 @@ import { Search, ChevronDown } from 'lucide-react';
import { useMemo, useState, useRef, memo, useEffect } from 'react';
import { SelectRenderer } from '@ariakit/react-core/select/select-renderer';
import type { OptionWithIcon } from '~/common';
import './AnimatePopover.css';
import { cn } from '~/utils';
interface ControlComboboxProps {

View File

@@ -0,0 +1,78 @@
.popover-ui {
display: flex;
max-height: min(var(--popover-available-height, 1700px), 1700px);
flex-direction: column;
overflow: auto;
overscroll-behavior: contain;
border-radius: 1rem;
border-width: 1px;
border-style: solid;
border-color: var(--border-light);
background-color: var(--surface-primary);
padding: 0.5rem;
color: var(--text-primary);
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
transform-origin: top;
opacity: 0;
transition-property: opacity, scale, translate;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
scale: 0.95;
translate: 0 -0.5rem;
margin-top: 4px;
margin-right: -2px;
}
.popover-animate {
opacity: 0;
transform: scale(0.95) translateY(-0.5rem);
transition:
opacity 150ms cubic-bezier(0.4, 0, 0.2, 1),
transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
.popover-animate[data-enter] {
opacity: 1;
transform: scale(1) translateY(0);
}
.popover-ui:focus-visible,
.popover-ui[data-focus-visible] {
outline: var(--bg-surface-hover);
outline-offset: -1px;
}
.popover-ui:where(.dark, .dark *) {
background-color: var(--surface-secondary);
color: var(--text-secondary);
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.25),
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.select-item {
display: flex;
cursor: pointer;
scroll-margin: 0.5rem;
align-items: center;
gap: 0.5rem;
border-radius: 0.5rem;
padding: 0.5rem;
outline: none !important;
}
.select-item[aria-disabled='true'] {
opacity: 0.5;
}
.select-item[data-active-item] {
background-color: var(--surface-hover);
color: var(--text-primary);
}
.popover-ui[data-enter] {
opacity: 1;
scale: 1;
translate: 0;
}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import * as Select from '@ariakit/react/select';
import type { Option } from '~/common';
import { cn } from '~/utils/';
import './Dropdown.css';
interface DropdownProps {
value?: string;

View File

@@ -2,6 +2,7 @@ import React from 'react';
import * as Ariakit from '@ariakit/react';
import type * as t from '~/common';
import { cn } from '~/utils';
import './Dropdown.css';
interface DropdownProps {
keyPrefix?: string;

View File

@@ -8,6 +8,7 @@ import {
SelectPopover,
SelectProvider,
} from '@ariakit/react';
import './AnimatePopover.css';
import { cn } from '~/utils';
interface MultiSelectProps<T extends string> {

View File

@@ -121,7 +121,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
className={cn('text-sm text-text-secondary', className)}
{...props}
/>
));

View File

@@ -1,7 +1,6 @@
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons';
import { cn } from '~/utils';
// @ts-ignore - Radix UI type conflicts with React types

View File

@@ -0,0 +1,20 @@
.tooltip {
z-index: 50;
cursor: pointer;
border-radius: 0.275rem;
background-color: var(--surface-primary);
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
font-size: 1rem;
line-height: 1.5rem;
color: black;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 0.25);
}
.tooltip:where(.dark, .dark *) {
background-color: var(--surface-primary);
color: white;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.35);
}

View File

@@ -2,6 +2,7 @@ import * as Ariakit from '@ariakit/react';
import { AnimatePresence, motion } from 'framer-motion';
import { forwardRef, useMemo } from 'react';
import { cn } from '~/utils';
import './Tooltip.css';
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
description: string;

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