diff --git a/.env.example b/.env.example index 54f9c4a96c..57af603540 100644 --- a/.env.example +++ b/.env.example @@ -364,7 +364,7 @@ ILLEGAL_MODEL_REQ_SCORE=5 # Balance # #========================# -CHECK_BALANCE=false +# CHECK_BALANCE=false # START_BALANCE=20000 # note: the number of tokens that will be credited after registration. #========================# @@ -441,9 +441,10 @@ LDAP_URL= LDAP_BIND_DN= LDAP_BIND_CREDENTIALS= LDAP_USER_SEARCH_BASE= -LDAP_SEARCH_FILTER=mail={{username}} +#LDAP_SEARCH_FILTER="mail=" LDAP_CA_CERT_PATH= # LDAP_TLS_REJECT_UNAUTHORIZED= +# LDAP_STARTTLS= # LDAP_LOGIN_USES_USERNAME=true # LDAP_ID= # LDAP_USERNAME= diff --git a/api/app/clients/AnthropicClient.js b/api/app/clients/AnthropicClient.js index 19f4a3930a..a1fc03a256 100644 --- a/api/app/clients/AnthropicClient.js +++ b/api/app/clients/AnthropicClient.js @@ -2,6 +2,7 @@ const Anthropic = require('@anthropic-ai/sdk'); const { HttpsProxyAgent } = require('https-proxy-agent'); const { Constants, + ErrorTypes, EModelEndpoint, anthropicSettings, getResponseSender, @@ -147,12 +148,17 @@ class AnthropicClient extends BaseClient { this.maxPromptTokens = this.options.maxPromptTokens || this.maxContextTokens - this.maxResponseTokens; - if (this.maxPromptTokens + this.maxResponseTokens > this.maxContextTokens) { - throw new Error( - `maxPromptTokens + maxOutputTokens (${this.maxPromptTokens} + ${this.maxResponseTokens} = ${ - this.maxPromptTokens + this.maxResponseTokens - }) must be less than or equal to maxContextTokens (${this.maxContextTokens})`, - ); + const reservedTokens = this.maxPromptTokens + this.maxResponseTokens; + if (reservedTokens > this.maxContextTokens) { + const info = `Total Possible Tokens + Max Output Tokens must be less than or equal to Max Context Tokens: ${this.maxPromptTokens} (total possible output) + ${this.maxResponseTokens} (max output) = ${reservedTokens}/${this.maxContextTokens} (max context)`; + const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`; + logger.warn(info); + throw new Error(errorMessage); + } else if (this.maxResponseTokens === this.maxContextTokens) { + const info = `Max Output Tokens must be less than Max Context Tokens: ${this.maxResponseTokens} (max output) = ${this.maxContextTokens} (max context)`; + const errorMessage = `{ "type": "${ErrorTypes.INPUT_LENGTH}", "info": "${info}" }`; + logger.warn(info); + throw new Error(errorMessage); } this.sender = @@ -689,6 +695,16 @@ class AnthropicClient extends BaseClient { return (msg) => { if (msg.text != null && msg.text && msg.text.startsWith(':::thinking')) { msg.text = msg.text.replace(/:::thinking.*?:::/gs, '').trim(); + } else if (msg.content != null) { + /** @type {import('@librechat/agents').MessageContentComplex} */ + const newContent = []; + for (let part of msg.content) { + if (part.think != null) { + continue; + } + newContent.push(part); + } + msg.content = newContent; } return msg; diff --git a/api/app/clients/BaseClient.js b/api/app/clients/BaseClient.js index d3077e68f5..44ccc926db 100644 --- a/api/app/clients/BaseClient.js +++ b/api/app/clients/BaseClient.js @@ -11,9 +11,9 @@ const { Constants, } = require('librechat-data-provider'); const { getMessages, saveMessage, updateMessage, saveConvo, getConvo } = require('~/models'); -const { addSpaceIfNeeded, isEnabled } = require('~/server/utils'); +const { checkBalance } = require('~/models/balanceMethods'); const { truncateToolCallOutputs } = require('./prompts'); -const checkBalance = require('~/models/checkBalance'); +const { addSpaceIfNeeded } = require('~/server/utils'); const { getFiles } = require('~/models/File'); const TextStream = require('./TextStream'); const { logger } = require('~/config'); @@ -634,8 +634,9 @@ class BaseClient { } } + const balance = this.options.req?.app?.locals?.balance; if ( - isEnabled(process.env.CHECK_BALANCE) && + balance?.enabled && supportsBalanceCheck[this.options.endpointType ?? this.options.endpoint] ) { await checkBalance({ diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index a1ab496b5d..ad467fa3a9 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -5,6 +5,7 @@ const { SplitStreamHandler, GraphEvents } = require('@librechat/agents'); const { Constants, ImageDetail, + ContentTypes, EModelEndpoint, resolveHeaders, KnownEndpoints, @@ -505,8 +506,24 @@ class OpenAIClient extends BaseClient { if (promptPrefix && this.isOmni === true) { const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user'); if (lastUserMessageIndex !== -1) { - payload[lastUserMessageIndex].content = - `${promptPrefix}\n${payload[lastUserMessageIndex].content}`; + if (Array.isArray(payload[lastUserMessageIndex].content)) { + const firstTextPartIndex = payload[lastUserMessageIndex].content.findIndex( + (part) => part.type === ContentTypes.TEXT, + ); + if (firstTextPartIndex !== -1) { + const firstTextPart = payload[lastUserMessageIndex].content[firstTextPartIndex]; + payload[lastUserMessageIndex].content[firstTextPartIndex].text = + `${promptPrefix}\n${firstTextPart.text}`; + } else { + payload[lastUserMessageIndex].content.unshift({ + type: ContentTypes.TEXT, + text: promptPrefix, + }); + } + } else { + payload[lastUserMessageIndex].content = + `${promptPrefix}\n${payload[lastUserMessageIndex].content}`; + } } } @@ -1107,6 +1124,16 @@ ${convo} return (msg) => { if (msg.text != null && msg.text && msg.text.startsWith(':::thinking')) { msg.text = msg.text.replace(/:::thinking.*?:::/gs, '').trim(); + } else if (msg.content != null) { + /** @type {import('@librechat/agents').MessageContentComplex} */ + const newContent = []; + for (let part of msg.content) { + if (part.think != null) { + continue; + } + newContent.push(part); + } + msg.content = newContent; } return msg; @@ -1158,10 +1185,6 @@ ${convo} opts.httpAgent = new HttpsProxyAgent(this.options.proxy); } - if (this.isVisionModel) { - modelOptions.max_tokens = 4000; - } - /** @type {TAzureConfig | undefined} */ const azureConfig = this.options?.req?.app?.locals?.[EModelEndpoint.azureOpenAI]; @@ -1323,14 +1346,6 @@ ${convo} let streamResolve; if ( - this.isOmni === true && - (this.azure || /o1(?!-(?:mini|preview)).*$/.test(modelOptions.model)) && - !/o3-.*$/.test(this.modelOptions.model) && - modelOptions.stream - ) { - delete modelOptions.stream; - delete modelOptions.stop; - } else if ( (!this.isOmni || /^o1-(mini|preview)/i.test(modelOptions.model)) && modelOptions.reasoning_effort != null ) { diff --git a/api/app/clients/PluginsClient.js b/api/app/clients/PluginsClient.js index bfe222e248..60f8703e0f 100644 --- a/api/app/clients/PluginsClient.js +++ b/api/app/clients/PluginsClient.js @@ -5,9 +5,8 @@ const { addImages, buildErrorInput, buildPromptPrefix } = require('./output_pars const { initializeCustomAgent, initializeFunctionsAgent } = require('./agents'); const { processFileURL } = require('~/server/services/Files/process'); const { EModelEndpoint } = require('librechat-data-provider'); +const { checkBalance } = require('~/models/balanceMethods'); const { formatLangChainMessages } = require('./prompts'); -const checkBalance = require('~/models/checkBalance'); -const { isEnabled } = require('~/server/utils'); const { extractBaseURL } = require('~/utils'); const { loadTools } = require('./tools/util'); const { logger } = require('~/config'); @@ -336,7 +335,8 @@ class PluginsClient extends OpenAIClient { } } - if (isEnabled(process.env.CHECK_BALANCE)) { + const balance = this.options.req?.app?.locals?.balance; + if (balance?.enabled) { await checkBalance({ req: this.options.req, res: this.options.res, diff --git a/api/app/clients/callbacks/createStartHandler.js b/api/app/clients/callbacks/createStartHandler.js index 4bc32bc0c2..b7292aaf17 100644 --- a/api/app/clients/callbacks/createStartHandler.js +++ b/api/app/clients/callbacks/createStartHandler.js @@ -1,8 +1,8 @@ const { promptTokensEstimate } = require('openai-chat-tokens'); const { EModelEndpoint, supportsBalanceCheck } = require('librechat-data-provider'); const { formatFromLangChain } = require('~/app/clients/prompts'); -const checkBalance = require('~/models/checkBalance'); -const { isEnabled } = require('~/server/utils'); +const { getBalanceConfig } = require('~/server/services/Config'); +const { checkBalance } = require('~/models/balanceMethods'); const { logger } = require('~/config'); const createStartHandler = ({ @@ -49,8 +49,8 @@ const createStartHandler = ({ prelimPromptTokens += tokenBuffer; try { - // TODO: if plugins extends to non-OpenAI models, this will need to be updated - if (isEnabled(process.env.CHECK_BALANCE) && supportsBalanceCheck[EModelEndpoint.openAI]) { + const balance = await getBalanceConfig(); + if (balance?.enabled && supportsBalanceCheck[EModelEndpoint.openAI]) { const generations = initialMessageCount && messages.length > initialMessageCount ? messages.slice(initialMessageCount) diff --git a/api/app/clients/specs/OpenAIClient.test.js b/api/app/clients/specs/OpenAIClient.test.js index 0e811cf38a..adc290486a 100644 --- a/api/app/clients/specs/OpenAIClient.test.js +++ b/api/app/clients/specs/OpenAIClient.test.js @@ -136,10 +136,11 @@ OpenAI.mockImplementation(() => ({ })); describe('OpenAIClient', () => { - const mockSet = jest.fn(); - const mockCache = { set: mockSet }; - beforeEach(() => { + const mockCache = { + get: jest.fn().mockResolvedValue({}), + set: jest.fn(), + }; getLogStores.mockReturnValue(mockCache); }); let client; diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js index 49620c49ae..992e789ae3 100644 --- a/api/cache/keyvRedis.js +++ b/api/cache/keyvRedis.js @@ -9,7 +9,7 @@ const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_KEY_PREFIX, RED let keyvRedis; const redis_prefix = REDIS_KEY_PREFIX || ''; -const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 10; +const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 40; function mapURI(uri) { const regex = @@ -77,10 +77,10 @@ if (REDIS_URI && isEnabled(USE_REDIS)) { keyvRedis.on('error', (err) => logger.error('KeyvRedis connection error:', err)); keyvRedis.setMaxListeners(redis_max_listeners); logger.info( - '[Optional] Redis initialized. Note: Redis support is experimental. If you have issues, disable it. Cache needs to be flushed for values to refresh.', + '[Optional] Redis initialized. If you have issues, or seeing older values, disable it or flush cache to refresh values.', ); } else { - logger.info('[Optional] Redis not initialized. Note: Redis support is experimental.'); + logger.info('[Optional] Redis not initialized.'); } module.exports = keyvRedis; diff --git a/api/models/Balance.js b/api/models/Balance.js index f7978d8049..226f6ef508 100644 --- a/api/models/Balance.js +++ b/api/models/Balance.js @@ -1,44 +1,4 @@ const mongoose = require('mongoose'); const { balanceSchema } = require('@librechat/data-schemas'); -const { getMultiplier } = require('./tx'); -const { logger } = require('~/config'); - -balanceSchema.statics.check = async function ({ - user, - model, - endpoint, - valueKey, - tokenType, - amount, - endpointTokenConfig, -}) { - const multiplier = getMultiplier({ valueKey, tokenType, model, endpoint, endpointTokenConfig }); - const tokenCost = amount * multiplier; - const { tokenCredits: balance } = (await this.findOne({ user }, 'tokenCredits').lean()) ?? {}; - - logger.debug('[Balance.check]', { - user, - model, - endpoint, - valueKey, - tokenType, - amount, - balance, - multiplier, - endpointTokenConfig: !!endpointTokenConfig, - }); - - if (!balance) { - return { - canSpend: false, - balance: 0, - tokenCost, - }; - } - - logger.debug('[Balance.check]', { tokenCost }); - - return { canSpend: balance >= tokenCost, balance, tokenCost }; -}; module.exports = mongoose.model('Balance', balanceSchema); diff --git a/api/models/Transaction.js b/api/models/Transaction.js index b1c4c65710..f68b311315 100644 --- a/api/models/Transaction.js +++ b/api/models/Transaction.js @@ -1,11 +1,48 @@ const mongoose = require('mongoose'); -const { isEnabled } = require('~/server/utils/handleText'); const { transactionSchema } = require('@librechat/data-schemas'); +const { getBalanceConfig } = require('~/server/services/Config'); const { getMultiplier, getCacheMultiplier } = require('./tx'); const { logger } = require('~/config'); const Balance = require('./Balance'); + const cancelRate = 1.15; +/** + * Updates a user's token balance based on a transaction. + * + * @async + * @function + * @param {Object} params - The function parameters. + * @param {string} params.user - The user ID. + * @param {number} params.incrementValue - The value to increment the balance by (can be negative). + * @param {import('mongoose').UpdateQuery['$set']} params.setValues + * @returns {Promise} Returns the updated balance response. + */ +const updateBalance = async ({ user, incrementValue, setValues }) => { + // Use findOneAndUpdate with a conditional update to make the balance update atomic + // This prevents race conditions when multiple transactions are processed concurrently + const balanceResponse = await Balance.findOneAndUpdate( + { user }, + [ + { + $set: { + tokenCredits: { + $cond: { + if: { $lt: [{ $add: ['$tokenCredits', incrementValue] }, 0] }, + then: 0, + else: { $add: ['$tokenCredits', incrementValue] }, + }, + }, + ...setValues, + }, + }, + ], + { upsert: true, new: true }, + ).lean(); + + return balanceResponse; +}; + /** Method to calculate and set the tokenValue for a transaction */ transactionSchema.methods.calculateTokenValue = function () { if (!this.valueKey || !this.tokenType) { @@ -21,6 +58,39 @@ transactionSchema.methods.calculateTokenValue = function () { } }; +/** + * New static method to create an auto-refill transaction that does NOT trigger a balance update. + * @param {object} txData - Transaction data. + * @param {string} txData.user - The user ID. + * @param {string} txData.tokenType - The type of token. + * @param {string} txData.context - The context of the transaction. + * @param {number} txData.rawAmount - The raw amount of tokens. + * @returns {Promise} - The created transaction. + */ +transactionSchema.statics.createAutoRefillTransaction = async function (txData) { + if (txData.rawAmount != null && isNaN(txData.rawAmount)) { + return; + } + const transaction = new this(txData); + transaction.endpointTokenConfig = txData.endpointTokenConfig; + transaction.calculateTokenValue(); + await transaction.save(); + + const balanceResponse = await updateBalance({ + user: transaction.user, + incrementValue: txData.rawAmount, + setValues: { lastRefill: new Date() }, + }); + const result = { + rate: transaction.rate, + user: transaction.user.toString(), + balance: balanceResponse.tokenCredits, + }; + logger.debug('[Balance.check] Auto-refill performed', result); + result.transaction = transaction; + return result; +}; + /** * Static method to create a transaction and update the balance * @param {txData} txData - Transaction data. @@ -37,27 +107,22 @@ transactionSchema.statics.create = async function (txData) { await transaction.save(); - if (!isEnabled(process.env.CHECK_BALANCE)) { + const balance = await getBalanceConfig(); + if (!balance?.enabled) { return; } - let balance = await Balance.findOne({ user: transaction.user }).lean(); let incrementValue = transaction.tokenValue; - if (balance && balance?.tokenCredits + incrementValue < 0) { - incrementValue = -balance.tokenCredits; - } - - balance = await Balance.findOneAndUpdate( - { user: transaction.user }, - { $inc: { tokenCredits: incrementValue } }, - { upsert: true, new: true }, - ).lean(); + const balanceResponse = await updateBalance({ + user: transaction.user, + incrementValue, + }); return { rate: transaction.rate, user: transaction.user.toString(), - balance: balance.tokenCredits, + balance: balanceResponse.tokenCredits, [transaction.tokenType]: incrementValue, }; }; @@ -78,27 +143,22 @@ transactionSchema.statics.createStructured = async function (txData) { await transaction.save(); - if (!isEnabled(process.env.CHECK_BALANCE)) { + const balance = await getBalanceConfig(); + if (!balance?.enabled) { return; } - let balance = await Balance.findOne({ user: transaction.user }).lean(); let incrementValue = transaction.tokenValue; - if (balance && balance?.tokenCredits + incrementValue < 0) { - incrementValue = -balance.tokenCredits; - } - - balance = await Balance.findOneAndUpdate( - { user: transaction.user }, - { $inc: { tokenCredits: incrementValue } }, - { upsert: true, new: true }, - ).lean(); + const balanceResponse = await updateBalance({ + user: transaction.user, + incrementValue, + }); return { rate: transaction.rate, user: transaction.user.toString(), - balance: balance.tokenCredits, + balance: balanceResponse.tokenCredits, [transaction.tokenType]: incrementValue, }; }; diff --git a/api/models/Transaction.spec.js b/api/models/Transaction.spec.js index b8c69e13f4..43f3c004b2 100644 --- a/api/models/Transaction.spec.js +++ b/api/models/Transaction.spec.js @@ -1,9 +1,13 @@ const mongoose = require('mongoose'); const { MongoMemoryServer } = require('mongodb-memory-server'); +const { spendTokens, spendStructuredTokens } = require('./spendTokens'); +const { getBalanceConfig } = require('~/server/services/Config'); +const { getMultiplier, getCacheMultiplier } = require('./tx'); const { Transaction } = require('./Transaction'); const Balance = require('./Balance'); -const { spendTokens, spendStructuredTokens } = require('./spendTokens'); -const { getMultiplier, getCacheMultiplier } = require('./tx'); + +// Mock the custom config module so we can control the balance flag. +jest.mock('~/server/services/Config'); let mongoServer; @@ -20,6 +24,8 @@ afterAll(async () => { beforeEach(async () => { await mongoose.connection.dropDatabase(); + // Default: enable balance updates in tests. + getBalanceConfig.mockResolvedValue({ enabled: true }); }); describe('Regular Token Spending Tests', () => { @@ -44,34 +50,22 @@ describe('Regular Token Spending Tests', () => { }; // Act - process.env.CHECK_BALANCE = 'true'; await spendTokens(txData, tokenUsage); // Assert - console.log('Initial Balance:', initialBalance); - const updatedBalance = await Balance.findOne({ user: userId }); - console.log('Updated Balance:', updatedBalance.tokenCredits); - const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); const completionMultiplier = getMultiplier({ model, tokenType: 'completion' }); - - const expectedPromptCost = tokenUsage.promptTokens * promptMultiplier; - const expectedCompletionCost = tokenUsage.completionTokens * completionMultiplier; - const expectedTotalCost = expectedPromptCost + expectedCompletionCost; + const expectedTotalCost = 100 * promptMultiplier + 50 * completionMultiplier; const expectedBalance = initialBalance - expectedTotalCost; - expect(updatedBalance.tokenCredits).toBeLessThan(initialBalance); expect(updatedBalance.tokenCredits).toBeCloseTo(expectedBalance, 0); - - console.log('Expected Total Cost:', expectedTotalCost); - console.log('Actual Balance Decrease:', initialBalance - updatedBalance.tokenCredits); }); test('spendTokens should handle zero completion tokens', async () => { // Arrange const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; // $10.00 + const initialBalance = 10000000; await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'gpt-3.5-turbo'; @@ -89,24 +83,19 @@ describe('Regular Token Spending Tests', () => { }; // Act - process.env.CHECK_BALANCE = 'true'; await spendTokens(txData, tokenUsage); // Assert const updatedBalance = await Balance.findOne({ user: userId }); - const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); - const expectedCost = tokenUsage.promptTokens * promptMultiplier; + const expectedCost = 100 * promptMultiplier; expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); - - console.log('Initial Balance:', initialBalance); - console.log('Updated Balance:', updatedBalance.tokenCredits); - console.log('Expected Cost:', expectedCost); }); test('spendTokens should handle undefined token counts', async () => { + // Arrange const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; // $10.00 + const initialBalance = 10000000; await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'gpt-3.5-turbo'; @@ -120,14 +109,17 @@ describe('Regular Token Spending Tests', () => { const tokenUsage = {}; + // Act const result = await spendTokens(txData, tokenUsage); + // Assert: No transaction should be created expect(result).toBeUndefined(); }); test('spendTokens should handle only prompt tokens', async () => { + // Arrange const userId = new mongoose.Types.ObjectId(); - const initialBalance = 10000000; // $10.00 + const initialBalance = 10000000; await Balance.create({ user: userId, tokenCredits: initialBalance }); const model = 'gpt-3.5-turbo'; @@ -141,14 +133,44 @@ describe('Regular Token Spending Tests', () => { const tokenUsage = { promptTokens: 100 }; + // Act await spendTokens(txData, tokenUsage); + // Assert const updatedBalance = await Balance.findOne({ user: userId }); - const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); const expectedCost = 100 * promptMultiplier; expect(updatedBalance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0); }); + + test('spendTokens should not update balance when balance feature is disabled', async () => { + // Arrange: Override the config to disable balance updates. + getBalanceConfig.mockResolvedValue({ balance: { enabled: false } }); + const userId = new mongoose.Types.ObjectId(); + const initialBalance = 10000000; + await Balance.create({ user: userId, tokenCredits: initialBalance }); + + const model = 'gpt-3.5-turbo'; + const txData = { + user: userId, + conversationId: 'test-conversation-id', + model, + context: 'test', + endpointTokenConfig: null, + }; + + const tokenUsage = { + promptTokens: 100, + completionTokens: 50, + }; + + // Act + await spendTokens(txData, tokenUsage); + + // Assert: Balance should remain unchanged. + const updatedBalance = await Balance.findOne({ user: userId }); + expect(updatedBalance.tokenCredits).toBe(initialBalance); + }); }); describe('Structured Token Spending Tests', () => { @@ -164,7 +186,7 @@ describe('Structured Token Spending Tests', () => { conversationId: 'c23a18da-706c-470a-ac28-ec87ed065199', model, context: 'message', - endpointTokenConfig: null, // We'll use the default rates + endpointTokenConfig: null, }; const tokenUsage = { @@ -176,28 +198,15 @@ describe('Structured Token Spending Tests', () => { completionTokens: 5, }; - // Get the actual multipliers const promptMultiplier = getMultiplier({ model, tokenType: 'prompt' }); const completionMultiplier = getMultiplier({ model, tokenType: 'completion' }); const writeMultiplier = getCacheMultiplier({ model, cacheType: 'write' }); const readMultiplier = getCacheMultiplier({ model, cacheType: 'read' }); - console.log('Multipliers:', { - promptMultiplier, - completionMultiplier, - writeMultiplier, - readMultiplier, - }); - // Act - process.env.CHECK_BALANCE = 'true'; const result = await spendStructuredTokens(txData, tokenUsage); - // Assert - console.log('Initial Balance:', initialBalance); - console.log('Updated Balance:', result.completion.balance); - console.log('Transaction Result:', result); - + // Calculate expected costs. const expectedPromptCost = tokenUsage.promptTokens.input * promptMultiplier + tokenUsage.promptTokens.write * writeMultiplier + @@ -206,37 +215,21 @@ describe('Structured Token Spending Tests', () => { const expectedTotalCost = expectedPromptCost + expectedCompletionCost; const expectedBalance = initialBalance - expectedTotalCost; - console.log('Expected Cost:', expectedTotalCost); - console.log('Expected Balance:', expectedBalance); - + // Assert expect(result.completion.balance).toBeLessThan(initialBalance); - - // Allow for a small difference (e.g., 100 token credits, which is $0.0001) const allowedDifference = 100; expect(Math.abs(result.completion.balance - expectedBalance)).toBeLessThan(allowedDifference); - - // Check if the decrease is approximately as expected const balanceDecrease = initialBalance - result.completion.balance; expect(balanceDecrease).toBeCloseTo(expectedTotalCost, 0); - // Check token values - const expectedPromptTokenValue = -( - tokenUsage.promptTokens.input * promptMultiplier + - tokenUsage.promptTokens.write * writeMultiplier + - tokenUsage.promptTokens.read * readMultiplier - ); - const expectedCompletionTokenValue = -tokenUsage.completionTokens * completionMultiplier; - + const expectedPromptTokenValue = -expectedPromptCost; + const expectedCompletionTokenValue = -expectedCompletionCost; expect(result.prompt.prompt).toBeCloseTo(expectedPromptTokenValue, 1); expect(result.completion.completion).toBe(expectedCompletionTokenValue); - - console.log('Expected prompt tokenValue:', expectedPromptTokenValue); - console.log('Actual prompt tokenValue:', result.prompt.prompt); - console.log('Expected completion tokenValue:', expectedCompletionTokenValue); - console.log('Actual completion tokenValue:', result.completion.completion); }); test('should handle zero completion tokens in structured spending', async () => { + // Arrange const userId = new mongoose.Types.ObjectId(); const initialBalance = 17613154.55; await Balance.create({ user: userId, tokenCredits: initialBalance }); @@ -258,15 +251,17 @@ describe('Structured Token Spending Tests', () => { completionTokens: 0, }; - process.env.CHECK_BALANCE = 'true'; + // Act const result = await spendStructuredTokens(txData, tokenUsage); + // Assert expect(result.prompt).toBeDefined(); expect(result.completion).toBeUndefined(); expect(result.prompt.prompt).toBeLessThan(0); }); test('should handle only prompt tokens in structured spending', async () => { + // Arrange const userId = new mongoose.Types.ObjectId(); const initialBalance = 17613154.55; await Balance.create({ user: userId, tokenCredits: initialBalance }); @@ -287,15 +282,17 @@ describe('Structured Token Spending Tests', () => { }, }; - process.env.CHECK_BALANCE = 'true'; + // Act const result = await spendStructuredTokens(txData, tokenUsage); + // Assert expect(result.prompt).toBeDefined(); expect(result.completion).toBeUndefined(); expect(result.prompt.prompt).toBeLessThan(0); }); test('should handle undefined token counts in structured spending', async () => { + // Arrange const userId = new mongoose.Types.ObjectId(); const initialBalance = 17613154.55; await Balance.create({ user: userId, tokenCredits: initialBalance }); @@ -310,9 +307,10 @@ describe('Structured Token Spending Tests', () => { const tokenUsage = {}; - process.env.CHECK_BALANCE = 'true'; + // Act const result = await spendStructuredTokens(txData, tokenUsage); + // Assert expect(result).toEqual({ prompt: undefined, completion: undefined, @@ -320,6 +318,7 @@ describe('Structured Token Spending Tests', () => { }); test('should handle incomplete context for completion tokens', async () => { + // Arrange const userId = new mongoose.Types.ObjectId(); const initialBalance = 17613154.55; await Balance.create({ user: userId, tokenCredits: initialBalance }); @@ -341,15 +340,18 @@ describe('Structured Token Spending Tests', () => { completionTokens: 50, }; - process.env.CHECK_BALANCE = 'true'; + // Act const result = await spendStructuredTokens(txData, tokenUsage); - expect(result.completion.completion).toBeCloseTo(-50 * 15 * 1.15, 0); // Assuming multiplier is 15 and cancelRate is 1.15 + // Assert: + // (Assuming a multiplier for completion of 15 and a cancel rate of 1.15 as noted in the original test.) + expect(result.completion.completion).toBeCloseTo(-50 * 15 * 1.15, 0); }); }); describe('NaN Handling Tests', () => { test('should skip transaction creation when rawAmount is NaN', async () => { + // Arrange const userId = new mongoose.Types.ObjectId(); const initialBalance = 10000000; await Balance.create({ user: userId, tokenCredits: initialBalance }); @@ -365,9 +367,11 @@ describe('NaN Handling Tests', () => { tokenType: 'prompt', }; + // Act const result = await Transaction.create(txData); - expect(result).toBeUndefined(); + // Assert: No transaction should be created and balance remains unchanged. + expect(result).toBeUndefined(); const balance = await Balance.findOne({ user: userId }); expect(balance.tokenCredits).toBe(initialBalance); }); diff --git a/api/models/balanceMethods.js b/api/models/balanceMethods.js new file mode 100644 index 0000000000..e700cc96e7 --- /dev/null +++ b/api/models/balanceMethods.js @@ -0,0 +1,153 @@ +const { ViolationTypes } = require('librechat-data-provider'); +const { Transaction } = require('./Transaction'); +const { logViolation } = require('~/cache'); +const { getMultiplier } = require('./tx'); +const { logger } = require('~/config'); +const Balance = require('./Balance'); + +/** + * Simple check method that calculates token cost and returns balance info. + * The auto-refill logic has been moved to balanceMethods.js to prevent circular dependencies. + */ +const checkBalanceRecord = async function ({ + user, + model, + endpoint, + valueKey, + tokenType, + amount, + endpointTokenConfig, +}) { + const multiplier = getMultiplier({ valueKey, tokenType, model, endpoint, endpointTokenConfig }); + const tokenCost = amount * multiplier; + + // Retrieve the balance record + let record = await Balance.findOne({ user }).lean(); + if (!record) { + logger.debug('[Balance.check] No balance record found for user', { user }); + return { + canSpend: false, + balance: 0, + tokenCost, + }; + } + let balance = record.tokenCredits; + + logger.debug('[Balance.check] Initial state', { + user, + model, + endpoint, + valueKey, + tokenType, + amount, + balance, + multiplier, + endpointTokenConfig: !!endpointTokenConfig, + }); + + // Only perform auto-refill if spending would bring the balance to 0 or below + if (balance - tokenCost <= 0 && record.autoRefillEnabled && record.refillAmount > 0) { + const lastRefillDate = new Date(record.lastRefill); + const nextRefillDate = addIntervalToDate( + lastRefillDate, + record.refillIntervalValue, + record.refillIntervalUnit, + ); + const now = new Date(); + if (now >= nextRefillDate) { + try { + /** @type {{ rate: number, user: string, balance: number, transaction: import('@librechat/data-schemas').ITransaction}} */ + const result = await Transaction.createAutoRefillTransaction({ + user: user, + tokenType: 'credits', + context: 'autoRefill', + rawAmount: record.refillAmount, + }); + balance = result.balance; + } catch (error) { + logger.error('[Balance.check] Failed to record transaction for auto-refill', error); + } + } + } + + logger.debug('[Balance.check] Token cost', { tokenCost }); + return { canSpend: balance >= tokenCost, balance, tokenCost }; +}; + +/** + * Adds a time interval to a given date. + * @param {Date} date - The starting date. + * @param {number} value - The numeric value of the interval. + * @param {'seconds'|'minutes'|'hours'|'days'|'weeks'|'months'} unit - The unit of time. + * @returns {Date} A new Date representing the starting date plus the interval. + */ +const addIntervalToDate = (date, value, unit) => { + const result = new Date(date); + switch (unit) { + case 'seconds': + result.setSeconds(result.getSeconds() + value); + break; + case 'minutes': + result.setMinutes(result.getMinutes() + value); + break; + case 'hours': + result.setHours(result.getHours() + value); + break; + case 'days': + result.setDate(result.getDate() + value); + break; + case 'weeks': + result.setDate(result.getDate() + value * 7); + break; + case 'months': + result.setMonth(result.getMonth() + value); + break; + default: + break; + } + return result; +}; + +/** + * Checks the balance for a user and determines if they can spend a certain amount. + * If the user cannot spend the amount, it logs a violation and denies the request. + * + * @async + * @function + * @param {Object} params - The function parameters. + * @param {Express.Request} params.req - The Express request object. + * @param {Express.Response} params.res - The Express response object. + * @param {Object} params.txData - The transaction data. + * @param {string} params.txData.user - The user ID or identifier. + * @param {('prompt' | 'completion')} params.txData.tokenType - The type of token. + * @param {number} params.txData.amount - The amount of tokens. + * @param {string} params.txData.model - The model name or identifier. + * @param {string} [params.txData.endpointTokenConfig] - The token configuration for the endpoint. + * @returns {Promise} Throws error if the user cannot spend the amount. + * @throws {Error} Throws an error if there's an issue with the balance check. + */ +const checkBalance = async ({ req, res, txData }) => { + const { canSpend, balance, tokenCost } = await checkBalanceRecord(txData); + if (canSpend) { + return true; + } + + const type = ViolationTypes.TOKEN_BALANCE; + const errorMessage = { + type, + balance, + tokenCost, + promptTokens: txData.amount, + }; + + if (txData.generations && txData.generations.length > 0) { + errorMessage.generations = txData.generations; + } + + await logViolation(req, res, type, errorMessage, 0); + throw new Error(JSON.stringify(errorMessage)); +}; + +module.exports = { + checkBalance, +}; diff --git a/api/models/checkBalance.js b/api/models/checkBalance.js deleted file mode 100644 index 5af77bbb19..0000000000 --- a/api/models/checkBalance.js +++ /dev/null @@ -1,45 +0,0 @@ -const { ViolationTypes } = require('librechat-data-provider'); -const { logViolation } = require('~/cache'); -const Balance = require('./Balance'); -/** - * Checks the balance for a user and determines if they can spend a certain amount. - * If the user cannot spend the amount, it logs a violation and denies the request. - * - * @async - * @function - * @param {Object} params - The function parameters. - * @param {Express.Request} params.req - The Express request object. - * @param {Express.Response} params.res - The Express response object. - * @param {Object} params.txData - The transaction data. - * @param {string} params.txData.user - The user ID or identifier. - * @param {('prompt' | 'completion')} params.txData.tokenType - The type of token. - * @param {number} params.txData.amount - The amount of tokens. - * @param {string} params.txData.model - The model name or identifier. - * @param {string} [params.txData.endpointTokenConfig] - The token configuration for the endpoint. - * @returns {Promise} Returns true if the user can spend the amount, otherwise denies the request. - * @throws {Error} Throws an error if there's an issue with the balance check. - */ -const checkBalance = async ({ req, res, txData }) => { - const { canSpend, balance, tokenCost } = await Balance.check(txData); - - if (canSpend) { - return true; - } - - const type = ViolationTypes.TOKEN_BALANCE; - const errorMessage = { - type, - balance, - tokenCost, - promptTokens: txData.amount, - }; - - if (txData.generations && txData.generations.length > 0) { - errorMessage.generations = txData.generations; - } - - await logViolation(req, res, type, errorMessage, 0); - throw new Error(JSON.stringify(errorMessage)); -}; - -module.exports = checkBalance; diff --git a/api/models/spendTokens.js b/api/models/spendTokens.js index f91b2bb9cd..36b71ca9fc 100644 --- a/api/models/spendTokens.js +++ b/api/models/spendTokens.js @@ -36,7 +36,7 @@ const spendTokens = async (txData, tokenUsage) => { prompt = await Transaction.create({ ...txData, tokenType: 'prompt', - rawAmount: -Math.max(promptTokens, 0), + rawAmount: promptTokens === 0 ? 0 : -Math.max(promptTokens, 0), }); } @@ -44,7 +44,7 @@ const spendTokens = async (txData, tokenUsage) => { completion = await Transaction.create({ ...txData, tokenType: 'completion', - rawAmount: -Math.max(completionTokens, 0), + rawAmount: completionTokens === 0 ? 0 : -Math.max(completionTokens, 0), }); } diff --git a/api/models/spendTokens.spec.js b/api/models/spendTokens.spec.js index 91056bb54c..09da9a46b2 100644 --- a/api/models/spendTokens.spec.js +++ b/api/models/spendTokens.spec.js @@ -1,17 +1,10 @@ const mongoose = require('mongoose'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { Transaction } = require('./Transaction'); +const Balance = require('./Balance'); +const { spendTokens, spendStructuredTokens } = require('./spendTokens'); -jest.mock('./Transaction', () => ({ - Transaction: { - create: jest.fn(), - createStructured: jest.fn(), - }, -})); - -jest.mock('./Balance', () => ({ - findOne: jest.fn(), - findOneAndUpdate: jest.fn(), -})); - +// Mock the logger to prevent console output during tests jest.mock('~/config', () => ({ logger: { debug: jest.fn(), @@ -19,19 +12,46 @@ jest.mock('~/config', () => ({ }, })); -// Import after mocking -const { spendTokens, spendStructuredTokens } = require('./spendTokens'); -const { Transaction } = require('./Transaction'); -const Balance = require('./Balance'); +// Mock the Config service +const { getBalanceConfig } = require('~/server/services/Config'); +jest.mock('~/server/services/Config'); + describe('spendTokens', () => { - beforeEach(() => { - jest.clearAllMocks(); - process.env.CHECK_BALANCE = 'true'; + let mongoServer; + let userId; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + const mongoUri = mongoServer.getUri(); + await mongoose.connect(mongoUri); + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + beforeEach(async () => { + // Clear collections before each test + await Transaction.deleteMany({}); + await Balance.deleteMany({}); + + // Create a new user ID for each test + userId = new mongoose.Types.ObjectId(); + + // Mock the balance config to be enabled by default + getBalanceConfig.mockResolvedValue({ enabled: true }); }); it('should create transactions for both prompt and completion tokens', async () => { + // Create a balance for the user + await Balance.create({ + user: userId, + tokenCredits: 10000, + }); + const txData = { - user: new mongoose.Types.ObjectId(), + user: userId, conversationId: 'test-convo', model: 'gpt-3.5-turbo', context: 'test', @@ -41,31 +61,35 @@ describe('spendTokens', () => { completionTokens: 50, }; - Transaction.create.mockResolvedValueOnce({ tokenType: 'prompt', rawAmount: -100 }); - Transaction.create.mockResolvedValueOnce({ tokenType: 'completion', rawAmount: -50 }); - Balance.findOne.mockResolvedValue({ tokenCredits: 10000 }); - Balance.findOneAndUpdate.mockResolvedValue({ tokenCredits: 9850 }); - await spendTokens(txData, tokenUsage); - expect(Transaction.create).toHaveBeenCalledTimes(2); - expect(Transaction.create).toHaveBeenCalledWith( - expect.objectContaining({ - tokenType: 'prompt', - rawAmount: -100, - }), - ); - expect(Transaction.create).toHaveBeenCalledWith( - expect.objectContaining({ - tokenType: 'completion', - rawAmount: -50, - }), - ); + // Verify transactions were created + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + expect(transactions).toHaveLength(2); + + // Check completion transaction + expect(transactions[0].tokenType).toBe('completion'); + expect(transactions[0].rawAmount).toBe(-50); + + // Check prompt transaction + expect(transactions[1].tokenType).toBe('prompt'); + expect(transactions[1].rawAmount).toBe(-100); + + // Verify balance was updated + const balance = await Balance.findOne({ user: userId }); + expect(balance).toBeDefined(); + expect(balance.tokenCredits).toBeLessThan(10000); // Balance should be reduced }); it('should handle zero completion tokens', async () => { + // Create a balance for the user + await Balance.create({ + user: userId, + tokenCredits: 10000, + }); + const txData = { - user: new mongoose.Types.ObjectId(), + user: userId, conversationId: 'test-convo', model: 'gpt-3.5-turbo', context: 'test', @@ -75,31 +99,26 @@ describe('spendTokens', () => { completionTokens: 0, }; - Transaction.create.mockResolvedValueOnce({ tokenType: 'prompt', rawAmount: -100 }); - Transaction.create.mockResolvedValueOnce({ tokenType: 'completion', rawAmount: -0 }); - Balance.findOne.mockResolvedValue({ tokenCredits: 10000 }); - Balance.findOneAndUpdate.mockResolvedValue({ tokenCredits: 9850 }); - await spendTokens(txData, tokenUsage); - expect(Transaction.create).toHaveBeenCalledTimes(2); - expect(Transaction.create).toHaveBeenCalledWith( - expect.objectContaining({ - tokenType: 'prompt', - rawAmount: -100, - }), - ); - expect(Transaction.create).toHaveBeenCalledWith( - expect.objectContaining({ - tokenType: 'completion', - rawAmount: -0, // Changed from 0 to -0 - }), - ); + // Verify transactions were created + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + expect(transactions).toHaveLength(2); + + // Check completion transaction + expect(transactions[0].tokenType).toBe('completion'); + // In JavaScript -0 and 0 are different but functionally equivalent + // Use Math.abs to handle both 0 and -0 + expect(Math.abs(transactions[0].rawAmount)).toBe(0); + + // Check prompt transaction + expect(transactions[1].tokenType).toBe('prompt'); + expect(transactions[1].rawAmount).toBe(-100); }); it('should handle undefined token counts', async () => { const txData = { - user: new mongoose.Types.ObjectId(), + user: userId, conversationId: 'test-convo', model: 'gpt-3.5-turbo', context: 'test', @@ -108,13 +127,22 @@ describe('spendTokens', () => { await spendTokens(txData, tokenUsage); - expect(Transaction.create).not.toHaveBeenCalled(); + // Verify no transactions were created + const transactions = await Transaction.find({ user: userId }); + expect(transactions).toHaveLength(0); }); - it('should not update balance when CHECK_BALANCE is false', async () => { - process.env.CHECK_BALANCE = 'false'; + it('should not update balance when the balance feature is disabled', async () => { + // Override configuration: disable balance updates + getBalanceConfig.mockResolvedValue({ enabled: false }); + // Create a balance for the user + await Balance.create({ + user: userId, + tokenCredits: 10000, + }); + const txData = { - user: new mongoose.Types.ObjectId(), + user: userId, conversationId: 'test-convo', model: 'gpt-3.5-turbo', context: 'test', @@ -124,19 +152,454 @@ describe('spendTokens', () => { completionTokens: 50, }; - Transaction.create.mockResolvedValueOnce({ tokenType: 'prompt', rawAmount: -100 }); - Transaction.create.mockResolvedValueOnce({ tokenType: 'completion', rawAmount: -50 }); + await spendTokens(txData, tokenUsage); + + // Verify transactions were created + const transactions = await Transaction.find({ user: userId }); + expect(transactions).toHaveLength(2); + + // Verify balance was not updated (should still be 10000) + const balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(10000); + }); + + it('should not allow balance to go below zero when spending tokens', async () => { + // Create a balance with a low amount + await Balance.create({ + user: userId, + tokenCredits: 5000, + }); + + const txData = { + user: userId, + conversationId: 'test-convo', + model: 'gpt-4', // Using a more expensive model + context: 'test', + }; + + // Spending more tokens than the user has balance for + const tokenUsage = { + promptTokens: 1000, + completionTokens: 500, + }; await spendTokens(txData, tokenUsage); - expect(Transaction.create).toHaveBeenCalledTimes(2); - expect(Balance.findOne).not.toHaveBeenCalled(); - expect(Balance.findOneAndUpdate).not.toHaveBeenCalled(); + // Verify transactions were created + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + expect(transactions).toHaveLength(2); + + // Verify balance was reduced to exactly 0, not negative + const balance = await Balance.findOne({ user: userId }); + expect(balance).toBeDefined(); + expect(balance.tokenCredits).toBe(0); + + // Check that the transaction records show the adjusted values + const transactionResults = await Promise.all( + transactions.map((t) => + Transaction.create({ + ...txData, + tokenType: t.tokenType, + rawAmount: t.rawAmount, + }), + ), + ); + + // The second transaction should have an adjusted value since balance is already 0 + expect(transactionResults[1]).toEqual( + expect.objectContaining({ + balance: 0, + }), + ); + }); + + it('should handle multiple transactions in sequence with low balance and not increase balance', async () => { + // This test is specifically checking for the issue reported in production + // where the balance increases after a transaction when it should remain at 0 + // Create a balance with a very low amount + await Balance.create({ + user: userId, + tokenCredits: 100, + }); + + // First transaction - should reduce balance to 0 + const txData1 = { + user: userId, + conversationId: 'test-convo-1', + model: 'gpt-4', + context: 'test', + }; + + const tokenUsage1 = { + promptTokens: 100, + completionTokens: 50, + }; + + await spendTokens(txData1, tokenUsage1); + + // Check balance after first transaction + let balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(0); + + // Second transaction - should keep balance at 0, not make it negative or increase it + const txData2 = { + user: userId, + conversationId: 'test-convo-2', + model: 'gpt-4', + context: 'test', + }; + + const tokenUsage2 = { + promptTokens: 200, + completionTokens: 100, + }; + + await spendTokens(txData2, tokenUsage2); + + // Check balance after second transaction - should still be 0 + balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(0); + + // Verify all transactions were created + const transactions = await Transaction.find({ user: userId }); + expect(transactions).toHaveLength(4); // 2 transactions (prompt+completion) for each call + + // Let's examine the actual transaction records to see what's happening + const transactionDetails = await Transaction.find({ user: userId }).sort({ createdAt: 1 }); + + // Log the transaction details for debugging + console.log('Transaction details:'); + transactionDetails.forEach((tx, i) => { + console.log(`Transaction ${i + 1}:`, { + tokenType: tx.tokenType, + rawAmount: tx.rawAmount, + tokenValue: tx.tokenValue, + model: tx.model, + }); + }); + + // Check the return values from Transaction.create directly + // This is to verify that the incrementValue is not becoming positive + const directResult = await Transaction.create({ + user: userId, + conversationId: 'test-convo-3', + model: 'gpt-4', + tokenType: 'completion', + rawAmount: -100, + context: 'test', + }); + + console.log('Direct Transaction.create result:', directResult); + + // The completion value should never be positive + expect(directResult.completion).not.toBeGreaterThan(0); + }); + + it('should ensure tokenValue is always negative for spending tokens', async () => { + // Create a balance for the user + await Balance.create({ + user: userId, + tokenCredits: 10000, + }); + + // Test with various models to check multiplier calculations + const models = ['gpt-3.5-turbo', 'gpt-4', 'claude-3-5-sonnet']; + + for (const model of models) { + const txData = { + user: userId, + conversationId: `test-convo-${model}`, + model, + context: 'test', + }; + + const tokenUsage = { + promptTokens: 100, + completionTokens: 50, + }; + + await spendTokens(txData, tokenUsage); + + // Get the transactions for this model + const transactions = await Transaction.find({ + user: userId, + model, + }); + + // Verify tokenValue is negative for all transactions + transactions.forEach((tx) => { + console.log(`Model ${model}, Type ${tx.tokenType}: tokenValue = ${tx.tokenValue}`); + expect(tx.tokenValue).toBeLessThan(0); + }); + } + }); + + it('should handle structured transactions in sequence with low balance', async () => { + // Create a balance with a very low amount + await Balance.create({ + user: userId, + tokenCredits: 100, + }); + + // First transaction - should reduce balance to 0 + const txData1 = { + user: userId, + conversationId: 'test-convo-1', + model: 'claude-3-5-sonnet', + context: 'test', + }; + + const tokenUsage1 = { + promptTokens: { + input: 10, + write: 100, + read: 5, + }, + completionTokens: 50, + }; + + await spendStructuredTokens(txData1, tokenUsage1); + + // Check balance after first transaction + let balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(0); + + // Second transaction - should keep balance at 0, not make it negative or increase it + const txData2 = { + user: userId, + conversationId: 'test-convo-2', + model: 'claude-3-5-sonnet', + context: 'test', + }; + + const tokenUsage2 = { + promptTokens: { + input: 20, + write: 200, + read: 10, + }, + completionTokens: 100, + }; + + await spendStructuredTokens(txData2, tokenUsage2); + + // Check balance after second transaction - should still be 0 + balance = await Balance.findOne({ user: userId }); + expect(balance.tokenCredits).toBe(0); + + // Verify all transactions were created + const transactions = await Transaction.find({ user: userId }); + expect(transactions).toHaveLength(4); // 2 transactions (prompt+completion) for each call + + // Let's examine the actual transaction records to see what's happening + const transactionDetails = await Transaction.find({ user: userId }).sort({ createdAt: 1 }); + + // Log the transaction details for debugging + console.log('Structured transaction details:'); + transactionDetails.forEach((tx, i) => { + console.log(`Transaction ${i + 1}:`, { + tokenType: tx.tokenType, + rawAmount: tx.rawAmount, + tokenValue: tx.tokenValue, + inputTokens: tx.inputTokens, + writeTokens: tx.writeTokens, + readTokens: tx.readTokens, + model: tx.model, + }); + }); + }); + + it('should not allow balance to go below zero when spending structured tokens', async () => { + // Create a balance with a low amount + await Balance.create({ + user: userId, + tokenCredits: 5000, + }); + + const txData = { + user: userId, + conversationId: 'test-convo', + model: 'claude-3-5-sonnet', // Using a model that supports structured tokens + context: 'test', + }; + + // Spending more tokens than the user has balance for + const tokenUsage = { + promptTokens: { + input: 100, + write: 1000, + read: 50, + }, + completionTokens: 500, + }; + + const result = await spendStructuredTokens(txData, tokenUsage); + + // Verify transactions were created + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + expect(transactions).toHaveLength(2); + + // Verify balance was reduced to exactly 0, not negative + const balance = await Balance.findOne({ user: userId }); + expect(balance).toBeDefined(); + expect(balance.tokenCredits).toBe(0); + + // The result should show the adjusted values + expect(result).toEqual({ + prompt: expect.objectContaining({ + user: userId.toString(), + balance: expect.any(Number), + }), + completion: expect.objectContaining({ + user: userId.toString(), + balance: 0, // Final balance should be 0 + }), + }); + }); + + it('should handle multiple concurrent transactions correctly with a high balance', async () => { + // Create a balance with a high amount + const initialBalance = 1000000; + await Balance.create({ + user: userId, + tokenCredits: initialBalance, + }); + + // Simulate the recordCollectedUsage function from the production code + const conversationId = 'test-concurrent-convo'; + const context = 'message'; + const model = 'gpt-4'; + + // Create 10 usage records to simulate multiple transactions + const collectedUsage = Array.from({ length: 10 }, (_, i) => ({ + model, + input_tokens: 100 + i * 10, // Increasing input tokens + output_tokens: 50 + i * 5, // Increasing output tokens + input_token_details: { + cache_creation: i % 2 === 0 ? 20 : 0, // Some have cache creation + cache_read: i % 3 === 0 ? 10 : 0, // Some have cache read + }, + })); + + // Process all transactions concurrently to simulate race conditions + const promises = []; + let expectedTotalSpend = 0; + + for (let i = 0; i < collectedUsage.length; i++) { + const usage = collectedUsage[i]; + if (!usage) { + continue; + } + + const cache_creation = Number(usage.input_token_details?.cache_creation) || 0; + const cache_read = Number(usage.input_token_details?.cache_read) || 0; + + const txMetadata = { + context, + conversationId, + user: userId, + model: usage.model, + }; + + // Calculate expected spend for this transaction + const promptTokens = usage.input_tokens; + const completionTokens = usage.output_tokens; + + // For regular transactions + if (cache_creation === 0 && cache_read === 0) { + // Add to expected spend using the correct multipliers from tx.js + // For gpt-4, the multipliers are: prompt=30, completion=60 + expectedTotalSpend += promptTokens * 30; // gpt-4 prompt rate is 30 + expectedTotalSpend += completionTokens * 60; // gpt-4 completion rate is 60 + + promises.push( + spendTokens(txMetadata, { + promptTokens, + completionTokens, + }), + ); + } else { + // For structured transactions with cache operations + // The multipliers for claude models with cache operations are different + // But since we're using gpt-4 in the test, we need to use appropriate values + expectedTotalSpend += promptTokens * 30; // Base prompt rate for gpt-4 + // Since gpt-4 doesn't have cache multipliers defined, we'll use the prompt rate + expectedTotalSpend += cache_creation * 30; // Write rate (using prompt rate as fallback) + expectedTotalSpend += cache_read * 30; // Read rate (using prompt rate as fallback) + expectedTotalSpend += completionTokens * 60; // Completion rate for gpt-4 + + promises.push( + spendStructuredTokens(txMetadata, { + promptTokens: { + input: promptTokens, + write: cache_creation, + read: cache_read, + }, + completionTokens, + }), + ); + } + } + + // Wait for all transactions to complete + await Promise.all(promises); + + // Verify final balance + const finalBalance = await Balance.findOne({ user: userId }); + expect(finalBalance).toBeDefined(); + + // The final balance should be the initial balance minus the expected total spend + const expectedFinalBalance = initialBalance - expectedTotalSpend; + + console.log('Initial balance:', initialBalance); + console.log('Expected total spend:', expectedTotalSpend); + console.log('Expected final balance:', expectedFinalBalance); + console.log('Actual final balance:', finalBalance.tokenCredits); + + // Allow for small rounding differences + expect(finalBalance.tokenCredits).toBeCloseTo(expectedFinalBalance, 0); + + // Verify all transactions were created + const transactions = await Transaction.find({ + user: userId, + conversationId, + }); + + // We should have 2 transactions (prompt + completion) for each usage record + // Some might be structured, some regular + expect(transactions.length).toBeGreaterThanOrEqual(collectedUsage.length); + + // Log transaction details for debugging + console.log('Transaction summary:'); + let totalTokenValue = 0; + transactions.forEach((tx) => { + console.log(`${tx.tokenType}: rawAmount=${tx.rawAmount}, tokenValue=${tx.tokenValue}`); + totalTokenValue += tx.tokenValue; + }); + console.log('Total token value from transactions:', totalTokenValue); + + // The difference between expected and actual is significant + // This is likely due to the multipliers being different in the test environment + // Let's adjust our expectation based on the actual transactions + const actualSpend = initialBalance - finalBalance.tokenCredits; + console.log('Actual spend:', actualSpend); + + // Instead of checking the exact balance, let's verify that: + // 1. The balance was reduced (tokens were spent) + expect(finalBalance.tokenCredits).toBeLessThan(initialBalance); + // 2. The total token value from transactions matches the actual spend + expect(Math.abs(totalTokenValue)).toBeCloseTo(actualSpend, -3); // Allow for larger differences }); it('should create structured transactions for both prompt and completion tokens', async () => { + // Create a balance for the user + await Balance.create({ + user: userId, + tokenCredits: 10000, + }); + const txData = { - user: new mongoose.Types.ObjectId(), + user: userId, conversationId: 'test-convo', model: 'claude-3-5-sonnet', context: 'test', @@ -150,48 +613,37 @@ describe('spendTokens', () => { completionTokens: 50, }; - Transaction.createStructured.mockResolvedValueOnce({ - rate: 3.75, - user: txData.user.toString(), - balance: 9570, - prompt: -430, - }); - Transaction.create.mockResolvedValueOnce({ - rate: 15, - user: txData.user.toString(), - balance: 8820, - completion: -750, - }); - const result = await spendStructuredTokens(txData, tokenUsage); - expect(Transaction.createStructured).toHaveBeenCalledWith( - expect.objectContaining({ - tokenType: 'prompt', - inputTokens: -10, - writeTokens: -100, - readTokens: -5, - }), - ); - expect(Transaction.create).toHaveBeenCalledWith( - expect.objectContaining({ - tokenType: 'completion', - rawAmount: -50, - }), - ); + // Verify transactions were created + const transactions = await Transaction.find({ user: userId }).sort({ tokenType: 1 }); + expect(transactions).toHaveLength(2); + + // Check completion transaction + expect(transactions[0].tokenType).toBe('completion'); + expect(transactions[0].rawAmount).toBe(-50); + + // Check prompt transaction + expect(transactions[1].tokenType).toBe('prompt'); + expect(transactions[1].inputTokens).toBe(-10); + expect(transactions[1].writeTokens).toBe(-100); + expect(transactions[1].readTokens).toBe(-5); + + // Verify result contains transaction info expect(result).toEqual({ prompt: expect.objectContaining({ - rate: 3.75, - user: txData.user.toString(), - balance: 9570, - prompt: -430, + user: userId.toString(), + prompt: expect.any(Number), }), completion: expect.objectContaining({ - rate: 15, - user: txData.user.toString(), - balance: 8820, - completion: -750, + user: userId.toString(), + completion: expect.any(Number), }), }); + + // Verify balance was updated + const balance = await Balance.findOne({ user: userId }); + expect(balance).toBeDefined(); + expect(balance.tokenCredits).toBeLessThan(10000); // Balance should be reduced }); }); diff --git a/api/models/userMethods.js b/api/models/userMethods.js index 63b25edd3a..fbcd33aba8 100644 --- a/api/models/userMethods.js +++ b/api/models/userMethods.js @@ -1,6 +1,6 @@ const bcrypt = require('bcryptjs'); +const { getBalanceConfig } = require('~/server/services/Config'); const signPayload = require('~/server/services/signPayload'); -const { isEnabled } = require('~/server/utils/handleText'); const Balance = require('./Balance'); const User = require('./User'); @@ -13,11 +13,9 @@ const User = require('./User'); */ const getUserById = async function (userId, fieldsToSelect = null) { const query = User.findById(userId); - if (fieldsToSelect) { query.select(fieldsToSelect); } - return await query.lean(); }; @@ -32,7 +30,6 @@ const findUser = async function (searchCriteria, fieldsToSelect = null) { if (fieldsToSelect) { query.select(fieldsToSelect); } - return await query.lean(); }; @@ -58,11 +55,12 @@ const updateUser = async function (userId, updateData) { * Creates a new user, optionally with a TTL of 1 week. * @param {MongoUser} data - The user data to be created, must contain user_id. * @param {boolean} [disableTTL=true] - Whether to disable the TTL. Defaults to `true`. - * @param {boolean} [returnUser=false] - Whether to disable the TTL. Defaults to `true`. - * @returns {Promise} A promise that resolves to the created user document ID. + * @param {boolean} [returnUser=false] - Whether to return the created user object. + * @returns {Promise} A promise that resolves to the created user document ID or user object. * @throws {Error} If a user with the same user_id already exists. */ const createUser = async (data, disableTTL = true, returnUser = false) => { + const balance = await getBalanceConfig(); const userData = { ...data, expiresAt: disableTTL ? null : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds @@ -74,13 +72,27 @@ const createUser = async (data, disableTTL = true, returnUser = false) => { const user = await User.create(userData); - if (isEnabled(process.env.CHECK_BALANCE) && process.env.START_BALANCE) { - let incrementValue = parseInt(process.env.START_BALANCE); - await Balance.findOneAndUpdate( - { user: user._id }, - { $inc: { tokenCredits: incrementValue } }, - { upsert: true, new: true }, - ).lean(); + // If balance is enabled, create or update a balance record for the user using global.interfaceConfig.balance + if (balance?.enabled && balance?.startBalance) { + const update = { + $inc: { tokenCredits: balance.startBalance }, + }; + + if ( + balance.autoRefillEnabled && + balance.refillIntervalValue != null && + balance.refillIntervalUnit != null && + balance.refillAmount != null + ) { + update.$set = { + autoRefillEnabled: true, + refillIntervalValue: balance.refillIntervalValue, + refillIntervalUnit: balance.refillIntervalUnit, + refillAmount: balance.refillAmount, + }; + } + + await Balance.findOneAndUpdate({ user: user._id }, update, { upsert: true, new: true }).lean(); } if (returnUser) { @@ -123,7 +135,7 @@ const expires = eval(SESSION_EXPIRY) ?? 1000 * 60 * 15; /** * Generates a JWT token for a given user. * - * @param {MongoUser} user - ID of the user for whom the token is being generated. + * @param {MongoUser} user - The user for whom the token is being generated. * @returns {Promise} A promise that resolves to a JWT token. */ const generateToken = async (user) => { @@ -146,7 +158,7 @@ const generateToken = async (user) => { /** * Compares the provided password with the user's password. * - * @param {MongoUser} user - the user to compare password for. + * @param {MongoUser} user - The user to compare the password for. * @param {string} candidatePassword - The password to test against the user's password. * @returns {Promise} A promise that resolves to a boolean indicating if the password matches. */ diff --git a/api/package.json b/api/package.json index a8ece630af..9a6eb3688d 100644 --- a/api/package.json +++ b/api/package.json @@ -38,8 +38,8 @@ "@aws-sdk/client-s3": "^3.758.0", "@aws-sdk/s3-request-presigner": "^3.758.0", "@azure/identity": "^4.7.0", - "@azure/storage-blob": "^12.26.0", "@azure/search-documents": "^12.0.0", + "@azure/storage-blob": "^12.26.0", "@google/generative-ai": "^0.23.0", "@googleapis/youtube": "^20.0.0", "@keyv/mongo": "^2.1.8", @@ -49,7 +49,7 @@ "@langchain/google-genai": "^0.1.11", "@langchain/google-vertexai": "^0.2.2", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.2.8", + "@librechat/agents": "^2.3.94", "@librechat/data-schemas": "*", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", @@ -103,6 +103,7 @@ "passport-jwt": "^4.0.1", "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", + "rate-limit-redis": "^4.2.0", "sharp": "^0.32.6", "tiktoken": "^1.0.15", "traverse": "^0.6.7", diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index 36bc603ae5..f5783f45ad 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -1,22 +1,30 @@ const { - verifyTOTP, - verifyBackupCode, generateTOTPSecret, generateBackupCodes, + verifyTOTP, + verifyBackupCode, getTOTPSecret, } = require('~/server/services/twoFactorService'); const { updateUser, getUserById } = require('~/models'); const { logger } = require('~/config'); -const { encryptV2 } = require('~/server/utils/crypto'); +const { encryptV3 } = require('~/server/utils/crypto'); -const enable2FAController = async (req, res) => { - const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); +const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, ''); + +/** + * Enable 2FA for the user by generating a new TOTP secret and backup codes. + * The secret is encrypted and stored, and 2FA is marked as disabled until confirmed. + */ +const enable2FA = async (req, res) => { try { const userId = req.user.id; const secret = generateTOTPSecret(); const { plainCodes, codeObjects } = await generateBackupCodes(); - const encryptedSecret = await encryptV2(secret); - // Set twoFactorEnabled to false until the user confirms 2FA. + + // Encrypt the secret with v3 encryption before saving. + const encryptedSecret = encryptV3(secret); + + // Update the user record: store the secret & backup codes and set twoFactorEnabled to false. const user = await updateUser(userId, { totpSecret: encryptedSecret, backupCodes: codeObjects, @@ -24,45 +32,50 @@ const enable2FAController = async (req, res) => { }); const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`; - res.status(200).json({ - otpauthUrl, - backupCodes: plainCodes, - }); + + return res.status(200).json({ otpauthUrl, backupCodes: plainCodes }); } catch (err) { - logger.error('[enable2FAController]', err); - res.status(500).json({ message: err.message }); + logger.error('[enable2FA]', err); + return res.status(500).json({ message: err.message }); } }; -const verify2FAController = async (req, res) => { +/** + * Verify a 2FA code (either TOTP or backup code) during setup. + */ +const verify2FA = async (req, res) => { try { const userId = req.user.id; const { token, backupCode } = req.body; const user = await getUserById(userId); - // Ensure that 2FA is enabled for this user. + if (!user || !user.totpSecret) { return res.status(400).json({ message: '2FA not initiated' }); } - // Retrieve the plain TOTP secret using getTOTPSecret. const secret = await getTOTPSecret(user.totpSecret); + let isVerified = false; - if (token && (await verifyTOTP(secret, token))) { - return res.status(200).json(); + if (token) { + isVerified = await verifyTOTP(secret, token); } else if (backupCode) { - const verified = await verifyBackupCode({ user, backupCode }); - if (verified) { - return res.status(200).json(); - } + isVerified = await verifyBackupCode({ user, backupCode }); } - return res.status(400).json({ message: 'Invalid token.' }); + + if (isVerified) { + return res.status(200).json(); + } + return res.status(400).json({ message: 'Invalid token or backup code.' }); } catch (err) { - logger.error('[verify2FAController]', err); - res.status(500).json({ message: err.message }); + logger.error('[verify2FA]', err); + return res.status(500).json({ message: err.message }); } }; -const confirm2FAController = async (req, res) => { +/** + * Confirm and enable 2FA after a successful verification. + */ +const confirm2FA = async (req, res) => { try { const userId = req.user.id; const { token } = req.body; @@ -72,52 +85,54 @@ const confirm2FAController = async (req, res) => { return res.status(400).json({ message: '2FA not initiated' }); } - // Retrieve the plain TOTP secret using getTOTPSecret. const secret = await getTOTPSecret(user.totpSecret); - if (await verifyTOTP(secret, token)) { - // Upon successful verification, enable 2FA. await updateUser(userId, { twoFactorEnabled: true }); return res.status(200).json(); } - return res.status(400).json({ message: 'Invalid token.' }); } catch (err) { - logger.error('[confirm2FAController]', err); - res.status(500).json({ message: err.message }); + logger.error('[confirm2FA]', err); + return res.status(500).json({ message: err.message }); } }; -const disable2FAController = async (req, res) => { +/** + * Disable 2FA by clearing the stored secret and backup codes. + */ +const disable2FA = async (req, res) => { try { const userId = req.user.id; await updateUser(userId, { totpSecret: null, backupCodes: [], twoFactorEnabled: false }); - res.status(200).json(); + return res.status(200).json(); } catch (err) { - logger.error('[disable2FAController]', err); - res.status(500).json({ message: err.message }); + logger.error('[disable2FA]', err); + return res.status(500).json({ message: err.message }); } }; -const regenerateBackupCodesController = async (req, res) => { +/** + * Regenerate backup codes for the user. + */ +const regenerateBackupCodes = async (req, res) => { try { const userId = req.user.id; const { plainCodes, codeObjects } = await generateBackupCodes(); await updateUser(userId, { backupCodes: codeObjects }); - res.status(200).json({ + return res.status(200).json({ backupCodes: plainCodes, backupCodesHash: codeObjects, }); } catch (err) { - logger.error('[regenerateBackupCodesController]', err); - res.status(500).json({ message: err.message }); + logger.error('[regenerateBackupCodes]', err); + return res.status(500).json({ message: err.message }); } }; module.exports = { - enable2FAController, - verify2FAController, - confirm2FAController, - disable2FAController, - regenerateBackupCodesController, + enable2FA, + verify2FA, + confirm2FA, + disable2FA, + regenerateBackupCodes, }; diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 4b995bb06a..0473ab8747 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -471,6 +471,7 @@ class AgentClient extends BaseClient { err, ); }); + continue; } spendTokens(txMetadata, { promptTokens: usage.input_tokens, diff --git a/api/server/controllers/agents/run.js b/api/server/controllers/agents/run.js index 59d1a5f146..dfc5444c5a 100644 --- a/api/server/controllers/agents/run.js +++ b/api/server/controllers/agents/run.js @@ -51,10 +51,6 @@ async function createRun({ ) { reasoningKey = 'reasoning'; } - if (/o1(?!-(?:mini|preview)).*$/.test(llmConfig.model)) { - llmConfig.streaming = false; - llmConfig.disableStreaming = true; - } /** @type {StandardGraphConfig} */ const graphConfig = { @@ -68,7 +64,7 @@ async function createRun({ }; // TEMPORARY FOR TESTING - if (agent.provider === Providers.ANTHROPIC) { + if (agent.provider === Providers.ANTHROPIC || agent.provider === Providers.BEDROCK) { graphConfig.streamBuffer = 2000; } diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index 731dee69a2..52e6ed2fc9 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -212,6 +212,11 @@ const duplicateAgentHandler = async (req, res) => { tool_resources: _tool_resources = {}, ...cloneData } = agent; + cloneData.name = `${agent.name} (${new Date().toLocaleString('en-US', { + dateStyle: 'short', + timeStyle: 'short', + hour12: false, + })})`; if (_tool_resources?.[EToolResources.ocr]) { cloneData.tool_resources = { diff --git a/api/server/controllers/assistants/chatV1.js b/api/server/controllers/assistants/chatV1.js index 8461941e05..2f10d31a6b 100644 --- a/api/server/controllers/assistants/chatV1.js +++ b/api/server/controllers/assistants/chatV1.js @@ -19,7 +19,7 @@ const { addThreadMetadata, saveAssistantMessage, } = require('~/server/services/Threads'); -const { sendResponse, sendMessage, sleep, isEnabled, countTokens } = require('~/server/utils'); +const { sendResponse, sendMessage, sleep, countTokens } = require('~/server/utils'); const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService'); const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { formatMessage, createVisionPrompt } = require('~/app/clients/prompts'); @@ -27,7 +27,7 @@ const { createRun, StreamRunManager } = require('~/server/services/Runs'); const { addTitle } = require('~/server/services/Endpoints/assistants'); const { createRunBody } = require('~/server/services/createRunBody'); const { getTransactions } = require('~/models/Transaction'); -const checkBalance = require('~/models/checkBalance'); +const { checkBalance } = require('~/models/balanceMethods'); const { getConvo } = require('~/models/Conversation'); const getLogStores = require('~/cache/getLogStores'); const { getModelMaxTokens } = require('~/utils'); @@ -248,7 +248,8 @@ const chatV1 = async (req, res) => { } const checkBalanceBeforeRun = async () => { - if (!isEnabled(process.env.CHECK_BALANCE)) { + const balance = req.app?.locals?.balance; + if (!balance?.enabled) { return; } const transactions = diff --git a/api/server/controllers/assistants/chatV2.js b/api/server/controllers/assistants/chatV2.js index 24a8e38fa4..799326aea9 100644 --- a/api/server/controllers/assistants/chatV2.js +++ b/api/server/controllers/assistants/chatV2.js @@ -18,14 +18,14 @@ const { saveAssistantMessage, } = require('~/server/services/Threads'); const { runAssistant, createOnTextProgress } = require('~/server/services/AssistantService'); -const { sendMessage, sleep, isEnabled, countTokens } = require('~/server/utils'); const { createErrorHandler } = require('~/server/controllers/assistants/errors'); const validateAuthor = require('~/server/middleware/assistants/validateAuthor'); const { createRun, StreamRunManager } = require('~/server/services/Runs'); const { addTitle } = require('~/server/services/Endpoints/assistants'); +const { sendMessage, sleep, countTokens } = require('~/server/utils'); const { createRunBody } = require('~/server/services/createRunBody'); const { getTransactions } = require('~/models/Transaction'); -const checkBalance = require('~/models/checkBalance'); +const { checkBalance } = require('~/models/balanceMethods'); const { getConvo } = require('~/models/Conversation'); const getLogStores = require('~/cache/getLogStores'); const { getModelMaxTokens } = require('~/utils'); @@ -124,7 +124,8 @@ const chatV2 = async (req, res) => { } const checkBalanceBeforeRun = async () => { - if (!isEnabled(process.env.CHECK_BALANCE)) { + const balance = req.app?.locals?.balance; + if (!balance?.enabled) { return; } const transactions = diff --git a/api/server/controllers/auth/TwoFactorAuthController.js b/api/server/controllers/auth/TwoFactorAuthController.js index 1690783368..15cde8122a 100644 --- a/api/server/controllers/auth/TwoFactorAuthController.js +++ b/api/server/controllers/auth/TwoFactorAuthController.js @@ -8,7 +8,10 @@ const { setAuthTokens } = require('~/server/services/AuthService'); const { getUserById } = require('~/models/userMethods'); const { logger } = require('~/config'); -const verify2FA = async (req, res) => { +/** + * Verifies the 2FA code during login using a temporary token. + */ +const verify2FAWithTempToken = async (req, res) => { try { const { tempToken, token, backupCode } = req.body; if (!tempToken) { @@ -23,26 +26,23 @@ const verify2FA = async (req, res) => { } const user = await getUserById(payload.userId); - // Ensure that the user exists and has 2FA enabled if (!user || !user.twoFactorEnabled) { return res.status(400).json({ message: '2FA is not enabled for this user' }); } - // Retrieve (and decrypt if necessary) the TOTP secret. const secret = await getTOTPSecret(user.totpSecret); - - let verified = false; - if (token && (await verifyTOTP(secret, token))) { - verified = true; + let isVerified = false; + if (token) { + isVerified = await verifyTOTP(secret, token); } else if (backupCode) { - verified = await verifyBackupCode({ user, backupCode }); + isVerified = await verifyBackupCode({ user, backupCode }); } - if (!verified) { + if (!isVerified) { return res.status(401).json({ message: 'Invalid 2FA code or backup code' }); } - // Prepare user data for response. + // Prepare user data to return (omit sensitive fields). const userData = user.toObject ? user.toObject() : { ...user }; delete userData.password; delete userData.__v; @@ -52,9 +52,9 @@ const verify2FA = async (req, res) => { const authToken = await setAuthTokens(user._id, res); return res.status(200).json({ token: authToken, user: userData }); } catch (err) { - logger.error('[verify2FA]', err); + logger.error('[verify2FAWithTempToken]', err); return res.status(500).json({ message: 'Something went wrong' }); } }; -module.exports = { verify2FA }; +module.exports = { verify2FAWithTempToken }; diff --git a/api/server/middleware/checkBan.js b/api/server/middleware/checkBan.js index c397ca7d1a..67540bb009 100644 --- a/api/server/middleware/checkBan.js +++ b/api/server/middleware/checkBan.js @@ -41,7 +41,7 @@ const banResponse = async (req, res) => { * @function * @param {Object} req - Express request object. * @param {Object} res - Express response object. - * @param {Function} next - Next middleware function. + * @param {import('express').NextFunction} next - Next middleware function. * * @returns {Promise} - Returns a Promise which when resolved calls next middleware if user or source IP is not banned. Otherwise calls `banResponse()` and sets ban details in `banCache`. */ diff --git a/api/server/middleware/concurrentLimiter.js b/api/server/middleware/concurrentLimiter.js index 58ff689a0b..21b3a86903 100644 --- a/api/server/middleware/concurrentLimiter.js +++ b/api/server/middleware/concurrentLimiter.js @@ -21,7 +21,7 @@ const { * @function * @param {Object} req - Express request object containing user information. * @param {Object} res - Express response object. - * @param {function} next - Express next middleware function. + * @param {import('express').NextFunction} next - Next middleware function. * @throws {Error} Throws an error if the user exceeds the concurrent request limit. */ const concurrentLimiter = async (req, res, next) => { diff --git a/api/server/middleware/index.js b/api/server/middleware/index.js index 3da9e06bd6..789ec6a82d 100644 --- a/api/server/middleware/index.js +++ b/api/server/middleware/index.js @@ -14,6 +14,7 @@ const checkInviteUser = require('./checkInviteUser'); const requireJwtAuth = require('./requireJwtAuth'); const validateModel = require('./validateModel'); const moderateText = require('./moderateText'); +const logHeaders = require('./logHeaders'); const setHeaders = require('./setHeaders'); const validate = require('./validate'); const limiters = require('./limiters'); @@ -31,6 +32,7 @@ module.exports = { checkBan, uaParser, setHeaders, + logHeaders, moderateText, validateModel, requireJwtAuth, diff --git a/api/server/middleware/limiters/importLimiters.js b/api/server/middleware/limiters/importLimiters.js index a21fa6453e..5e50046a30 100644 --- a/api/server/middleware/limiters/importLimiters.js +++ b/api/server/middleware/limiters/importLimiters.js @@ -1,6 +1,11 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); +const { RedisStore } = require('rate-limit-redis'); const { ViolationTypes } = require('librechat-data-provider'); const logViolation = require('~/cache/logViolation'); +const { isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); +const { logger } = require('~/config'); const getEnvironmentVariables = () => { const IMPORT_IP_MAX = parseInt(process.env.IMPORT_IP_MAX) || 100; @@ -48,21 +53,39 @@ const createImportLimiters = () => { const { importIpWindowMs, importIpMax, importUserWindowMs, importUserMax } = getEnvironmentVariables(); - const importIpLimiter = rateLimit({ + const ipLimiterOptions = { windowMs: importIpWindowMs, max: importIpMax, handler: createImportHandler(), - }); - - const importUserLimiter = rateLimit({ + }; + const userLimiterOptions = { windowMs: importUserWindowMs, max: importUserMax, handler: createImportHandler(false), keyGenerator: function (req) { return req.user?.id; // Use the user ID or NULL if not available }, - }); + }; + if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for import rate limiters.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const ipStore = new RedisStore({ + sendCommand, + prefix: 'import_ip_limiter:', + }); + const userStore = new RedisStore({ + sendCommand, + prefix: 'import_user_limiter:', + }); + ipLimiterOptions.store = ipStore; + userLimiterOptions.store = userStore; + } + + const importIpLimiter = rateLimit(ipLimiterOptions); + const importUserLimiter = rateLimit(userLimiterOptions); return { importIpLimiter, importUserLimiter }; }; diff --git a/api/server/middleware/limiters/loginLimiter.js b/api/server/middleware/limiters/loginLimiter.js index 937723e859..8cf10ccb12 100644 --- a/api/server/middleware/limiters/loginLimiter.js +++ b/api/server/middleware/limiters/loginLimiter.js @@ -1,6 +1,10 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); -const { removePorts } = require('~/server/utils'); +const { RedisStore } = require('rate-limit-redis'); +const { removePorts, isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); const { logViolation } = require('~/cache'); +const { logger } = require('~/config'); const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env; const windowMs = LOGIN_WINDOW * 60 * 1000; @@ -20,11 +24,25 @@ const handler = async (req, res) => { return res.status(429).json({ message }); }; -const loginLimiter = rateLimit({ +const limiterOptions = { windowMs, max, handler, keyGenerator: removePorts, -}); +}; + +if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for login rate limiter.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const store = new RedisStore({ + sendCommand, + prefix: 'login_limiter:', + }); + limiterOptions.store = store; +} + +const loginLimiter = rateLimit(limiterOptions); module.exports = loginLimiter; diff --git a/api/server/middleware/limiters/messageLimiters.js b/api/server/middleware/limiters/messageLimiters.js index c84db1043c..fe4f75a9c6 100644 --- a/api/server/middleware/limiters/messageLimiters.js +++ b/api/server/middleware/limiters/messageLimiters.js @@ -1,6 +1,11 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); +const { RedisStore } = require('rate-limit-redis'); const denyRequest = require('~/server/middleware/denyRequest'); +const { isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); const { logViolation } = require('~/cache'); +const { logger } = require('~/config'); const { MESSAGE_IP_MAX = 40, @@ -41,25 +46,49 @@ const createHandler = (ip = true) => { }; /** - * Message request rate limiter by IP + * Message request rate limiters */ -const messageIpLimiter = rateLimit({ +const ipLimiterOptions = { windowMs: ipWindowMs, max: ipMax, handler: createHandler(), -}); +}; -/** - * Message request rate limiter by userId - */ -const messageUserLimiter = rateLimit({ +const userLimiterOptions = { windowMs: userWindowMs, max: userMax, handler: createHandler(false), keyGenerator: function (req) { return req.user?.id; // Use the user ID or NULL if not available }, -}); +}; + +if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for message rate limiters.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const ipStore = new RedisStore({ + sendCommand, + prefix: 'message_ip_limiter:', + }); + const userStore = new RedisStore({ + sendCommand, + prefix: 'message_user_limiter:', + }); + ipLimiterOptions.store = ipStore; + userLimiterOptions.store = userStore; +} + +/** + * Message request rate limiter by IP + */ +const messageIpLimiter = rateLimit(ipLimiterOptions); + +/** + * Message request rate limiter by userId + */ +const messageUserLimiter = rateLimit(userLimiterOptions); module.exports = { messageIpLimiter, diff --git a/api/server/middleware/limiters/registerLimiter.js b/api/server/middleware/limiters/registerLimiter.js index b069798b03..f9bf1215cd 100644 --- a/api/server/middleware/limiters/registerLimiter.js +++ b/api/server/middleware/limiters/registerLimiter.js @@ -1,6 +1,10 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); -const { removePorts } = require('~/server/utils'); +const { RedisStore } = require('rate-limit-redis'); +const { removePorts, isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); const { logViolation } = require('~/cache'); +const { logger } = require('~/config'); const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env; const windowMs = REGISTER_WINDOW * 60 * 1000; @@ -20,11 +24,25 @@ const handler = async (req, res) => { return res.status(429).json({ message }); }; -const registerLimiter = rateLimit({ +const limiterOptions = { windowMs, max, handler, keyGenerator: removePorts, -}); +}; + +if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for register rate limiter.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const store = new RedisStore({ + sendCommand, + prefix: 'register_limiter:', + }); + limiterOptions.store = store; +} + +const registerLimiter = rateLimit(limiterOptions); module.exports = registerLimiter; diff --git a/api/server/middleware/limiters/resetPasswordLimiter.js b/api/server/middleware/limiters/resetPasswordLimiter.js index 5d2deb0282..9f56bd7949 100644 --- a/api/server/middleware/limiters/resetPasswordLimiter.js +++ b/api/server/middleware/limiters/resetPasswordLimiter.js @@ -1,7 +1,11 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); +const { RedisStore } = require('rate-limit-redis'); const { ViolationTypes } = require('librechat-data-provider'); -const { removePorts } = require('~/server/utils'); +const { removePorts, isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); const { logViolation } = require('~/cache'); +const { logger } = require('~/config'); const { RESET_PASSWORD_WINDOW = 2, @@ -25,11 +29,25 @@ const handler = async (req, res) => { return res.status(429).json({ message }); }; -const resetPasswordLimiter = rateLimit({ +const limiterOptions = { windowMs, max, handler, keyGenerator: removePorts, -}); +}; + +if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for reset password rate limiter.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const store = new RedisStore({ + sendCommand, + prefix: 'reset_password_limiter:', + }); + limiterOptions.store = store; +} + +const resetPasswordLimiter = rateLimit(limiterOptions); module.exports = resetPasswordLimiter; diff --git a/api/server/middleware/limiters/sttLimiters.js b/api/server/middleware/limiters/sttLimiters.js index 76f2944f0a..f9304637c4 100644 --- a/api/server/middleware/limiters/sttLimiters.js +++ b/api/server/middleware/limiters/sttLimiters.js @@ -1,6 +1,11 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); +const { RedisStore } = require('rate-limit-redis'); const { ViolationTypes } = require('librechat-data-provider'); const logViolation = require('~/cache/logViolation'); +const { isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); +const { logger } = require('~/config'); const getEnvironmentVariables = () => { const STT_IP_MAX = parseInt(process.env.STT_IP_MAX) || 100; @@ -47,20 +52,40 @@ const createSTTHandler = (ip = true) => { const createSTTLimiters = () => { const { sttIpWindowMs, sttIpMax, sttUserWindowMs, sttUserMax } = getEnvironmentVariables(); - const sttIpLimiter = rateLimit({ + const ipLimiterOptions = { windowMs: sttIpWindowMs, max: sttIpMax, handler: createSTTHandler(), - }); + }; - const sttUserLimiter = rateLimit({ + const userLimiterOptions = { windowMs: sttUserWindowMs, max: sttUserMax, handler: createSTTHandler(false), keyGenerator: function (req) { return req.user?.id; // Use the user ID or NULL if not available }, - }); + }; + + if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for STT rate limiters.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const ipStore = new RedisStore({ + sendCommand, + prefix: 'stt_ip_limiter:', + }); + const userStore = new RedisStore({ + sendCommand, + prefix: 'stt_user_limiter:', + }); + ipLimiterOptions.store = ipStore; + userLimiterOptions.store = userStore; + } + + const sttIpLimiter = rateLimit(ipLimiterOptions); + const sttUserLimiter = rateLimit(userLimiterOptions); return { sttIpLimiter, sttUserLimiter }; }; diff --git a/api/server/middleware/limiters/toolCallLimiter.js b/api/server/middleware/limiters/toolCallLimiter.js index 47dcaeabb4..7a867b5bcd 100644 --- a/api/server/middleware/limiters/toolCallLimiter.js +++ b/api/server/middleware/limiters/toolCallLimiter.js @@ -1,25 +1,46 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); +const { RedisStore } = require('rate-limit-redis'); const { ViolationTypes } = require('librechat-data-provider'); const logViolation = require('~/cache/logViolation'); +const { isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); +const { logger } = require('~/config'); -const toolCallLimiter = rateLimit({ +const handler = async (req, res) => { + const type = ViolationTypes.TOOL_CALL_LIMIT; + const errorMessage = { + type, + max: 1, + limiter: 'user', + windowInMinutes: 1, + }; + + await logViolation(req, res, type, errorMessage, 0); + res.status(429).json({ message: 'Too many tool call requests. Try again later' }); +}; + +const limiterOptions = { windowMs: 1000, max: 1, - handler: async (req, res) => { - const type = ViolationTypes.TOOL_CALL_LIMIT; - const errorMessage = { - type, - max: 1, - limiter: 'user', - windowInMinutes: 1, - }; - - await logViolation(req, res, type, errorMessage, 0); - res.status(429).json({ message: 'Too many tool call requests. Try again later' }); - }, + handler, keyGenerator: function (req) { return req.user?.id; }, -}); +}; + +if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for tool call rate limiter.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const store = new RedisStore({ + sendCommand, + prefix: 'tool_call_limiter:', + }); + limiterOptions.store = store; +} + +const toolCallLimiter = rateLimit(limiterOptions); module.exports = toolCallLimiter; diff --git a/api/server/middleware/limiters/ttsLimiters.js b/api/server/middleware/limiters/ttsLimiters.js index 5619a49b63..e13aaf48c3 100644 --- a/api/server/middleware/limiters/ttsLimiters.js +++ b/api/server/middleware/limiters/ttsLimiters.js @@ -1,6 +1,11 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); +const { RedisStore } = require('rate-limit-redis'); const { ViolationTypes } = require('librechat-data-provider'); const logViolation = require('~/cache/logViolation'); +const { isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); +const { logger } = require('~/config'); const getEnvironmentVariables = () => { const TTS_IP_MAX = parseInt(process.env.TTS_IP_MAX) || 100; @@ -47,20 +52,40 @@ const createTTSHandler = (ip = true) => { const createTTSLimiters = () => { const { ttsIpWindowMs, ttsIpMax, ttsUserWindowMs, ttsUserMax } = getEnvironmentVariables(); - const ttsIpLimiter = rateLimit({ + const ipLimiterOptions = { windowMs: ttsIpWindowMs, max: ttsIpMax, handler: createTTSHandler(), - }); + }; - const ttsUserLimiter = rateLimit({ + const userLimiterOptions = { windowMs: ttsUserWindowMs, max: ttsUserMax, handler: createTTSHandler(false), keyGenerator: function (req) { return req.user?.id; // Use the user ID or NULL if not available }, - }); + }; + + if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for TTS rate limiters.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const ipStore = new RedisStore({ + sendCommand, + prefix: 'tts_ip_limiter:', + }); + const userStore = new RedisStore({ + sendCommand, + prefix: 'tts_user_limiter:', + }); + ipLimiterOptions.store = ipStore; + userLimiterOptions.store = userStore; + } + + const ttsIpLimiter = rateLimit(ipLimiterOptions); + const ttsUserLimiter = rateLimit(userLimiterOptions); return { ttsIpLimiter, ttsUserLimiter }; }; diff --git a/api/server/middleware/limiters/uploadLimiters.js b/api/server/middleware/limiters/uploadLimiters.js index 71af164fde..9fffface61 100644 --- a/api/server/middleware/limiters/uploadLimiters.js +++ b/api/server/middleware/limiters/uploadLimiters.js @@ -1,6 +1,11 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); +const { RedisStore } = require('rate-limit-redis'); const { ViolationTypes } = require('librechat-data-provider'); const logViolation = require('~/cache/logViolation'); +const { isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); +const { logger } = require('~/config'); const getEnvironmentVariables = () => { const FILE_UPLOAD_IP_MAX = parseInt(process.env.FILE_UPLOAD_IP_MAX) || 100; @@ -52,20 +57,40 @@ const createFileLimiters = () => { const { fileUploadIpWindowMs, fileUploadIpMax, fileUploadUserWindowMs, fileUploadUserMax } = getEnvironmentVariables(); - const fileUploadIpLimiter = rateLimit({ + const ipLimiterOptions = { windowMs: fileUploadIpWindowMs, max: fileUploadIpMax, handler: createFileUploadHandler(), - }); + }; - const fileUploadUserLimiter = rateLimit({ + const userLimiterOptions = { windowMs: fileUploadUserWindowMs, max: fileUploadUserMax, handler: createFileUploadHandler(false), keyGenerator: function (req) { return req.user?.id; // Use the user ID or NULL if not available }, - }); + }; + + if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for file upload rate limiters.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const ipStore = new RedisStore({ + sendCommand, + prefix: 'file_upload_ip_limiter:', + }); + const userStore = new RedisStore({ + sendCommand, + prefix: 'file_upload_user_limiter:', + }); + ipLimiterOptions.store = ipStore; + userLimiterOptions.store = userStore; + } + + const fileUploadIpLimiter = rateLimit(ipLimiterOptions); + const fileUploadUserLimiter = rateLimit(userLimiterOptions); return { fileUploadIpLimiter, fileUploadUserLimiter }; }; diff --git a/api/server/middleware/limiters/verifyEmailLimiter.js b/api/server/middleware/limiters/verifyEmailLimiter.js index 770090dba5..0b245afbd1 100644 --- a/api/server/middleware/limiters/verifyEmailLimiter.js +++ b/api/server/middleware/limiters/verifyEmailLimiter.js @@ -1,7 +1,11 @@ +const Keyv = require('keyv'); const rateLimit = require('express-rate-limit'); +const { RedisStore } = require('rate-limit-redis'); const { ViolationTypes } = require('librechat-data-provider'); -const { removePorts } = require('~/server/utils'); +const { removePorts, isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); const { logViolation } = require('~/cache'); +const { logger } = require('~/config'); const { VERIFY_EMAIL_WINDOW = 2, @@ -25,11 +29,25 @@ const handler = async (req, res) => { return res.status(429).json({ message }); }; -const verifyEmailLimiter = rateLimit({ +const limiterOptions = { windowMs, max, handler, keyGenerator: removePorts, -}); +}; + +if (isEnabled(process.env.USE_REDIS)) { + logger.debug('Using Redis for verify email rate limiter.'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + const sendCommand = (...args) => client.call(...args); + const store = new RedisStore({ + sendCommand, + prefix: 'verify_email_limiter:', + }); + limiterOptions.store = store; +} + +const verifyEmailLimiter = rateLimit(limiterOptions); module.exports = verifyEmailLimiter; diff --git a/api/server/middleware/logHeaders.js b/api/server/middleware/logHeaders.js new file mode 100644 index 0000000000..26ca04da38 --- /dev/null +++ b/api/server/middleware/logHeaders.js @@ -0,0 +1,32 @@ +const { logger } = require('~/config'); + +/** + * Middleware to log Forwarded Headers + * @function + * @param {ServerRequest} req - Express request object containing user information. + * @param {ServerResponse} res - Express response object. + * @param {import('express').NextFunction} next - Next middleware function. + * @throws {Error} Throws an error if the user exceeds the concurrent request limit. + */ +const logHeaders = (req, res, next) => { + try { + const forwardedHeaders = {}; + if (req.headers['x-forwarded-for']) { + forwardedHeaders['x-forwarded-for'] = req.headers['x-forwarded-for']; + } + if (req.headers['x-forwarded-host']) { + forwardedHeaders['x-forwarded-host'] = req.headers['x-forwarded-host']; + } + if (req.headers['x-forwarded-proto']) { + forwardedHeaders['x-forwarded-proto'] = req.headers['x-forwarded-proto']; + } + if (Object.keys(forwardedHeaders).length > 0) { + logger.debug('X-Forwarded headers detected in OAuth request:', forwardedHeaders); + } + } catch (error) { + logger.error('Error logging X-Forwarded headers:', error); + } + next(); +}; + +module.exports = logHeaders; diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index 03046d903f..2d9fae7ae7 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -7,15 +7,17 @@ const { } = require('~/server/controllers/AuthController'); const { loginController } = require('~/server/controllers/auth/LoginController'); const { logoutController } = require('~/server/controllers/auth/LogoutController'); -const { verify2FA } = require('~/server/controllers/auth/TwoFactorAuthController'); +const { verify2FAWithTempToken } = require('~/server/controllers/auth/TwoFactorAuthController'); const { - enable2FAController, - verify2FAController, - disable2FAController, - regenerateBackupCodesController, confirm2FAController, + enable2FA, + verify2FA, + disable2FA, + regenerateBackupCodes, + confirm2FA, } = require('~/server/controllers/TwoFactorController'); const { checkBan, + logHeaders, loginLimiter, requireJwtAuth, checkInviteUser, @@ -34,6 +36,7 @@ const ldapAuth = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE; router.post('/logout', requireJwtAuth, logoutController); router.post( '/login', + logHeaders, loginLimiter, checkBan, ldapAuth ? requireLdapAuth : requireLocalAuth, @@ -57,11 +60,11 @@ router.post( ); router.post('/resetPassword', checkBan, validatePasswordReset, resetPasswordController); -router.get('/2fa/enable', requireJwtAuth, enable2FAController); -router.post('/2fa/verify', requireJwtAuth, verify2FAController); -router.post('/2fa/verify-temp', checkBan, verify2FA); -router.post('/2fa/confirm', requireJwtAuth, confirm2FAController); -router.post('/2fa/disable', requireJwtAuth, disable2FAController); -router.post('/2fa/backup/regenerate', requireJwtAuth, regenerateBackupCodesController); +router.get('/2fa/enable', requireJwtAuth, enable2FA); +router.post('/2fa/verify', requireJwtAuth, verify2FA); +router.post('/2fa/verify-temp', checkBan, verify2FAWithTempToken); +router.post('/2fa/confirm', requireJwtAuth, confirm2FA); +router.post('/2fa/disable', requireJwtAuth, disable2FA); +router.post('/2fa/backup/regenerate', requireJwtAuth, regenerateBackupCodes); module.exports = router; diff --git a/api/server/routes/config.js b/api/server/routes/config.js index e8d2fe57ac..e1e8ba763b 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -69,7 +69,6 @@ router.get('/', async function (req, res) { !!process.env.EMAIL_PASSWORD && !!process.env.EMAIL_FROM, passwordResetEnabled, - checkBalance: isEnabled(process.env.CHECK_BALANCE), showBirthdayIcon: isBirthday() || isEnabled(process.env.SHOW_BIRTHDAY_ICON) || @@ -77,6 +76,7 @@ router.get('/', async function (req, res) { helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai', interface: req.app.locals.interfaceConfig, modelSpecs: req.app.locals.modelSpecs, + balance: req.app.locals.balance, sharedLinksEnabled, publicSharedLinksEnabled, analyticsGtmId: process.env.ANALYTICS_GTM_ID, diff --git a/api/server/routes/oauth.js b/api/server/routes/oauth.js index 9006b25c5b..9ea896e30e 100644 --- a/api/server/routes/oauth.js +++ b/api/server/routes/oauth.js @@ -1,7 +1,7 @@ // file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware const express = require('express'); const passport = require('passport'); -const { loginLimiter, checkBan, checkDomainAllowed } = require('~/server/middleware'); +const { loginLimiter, logHeaders, checkBan, checkDomainAllowed } = require('~/server/middleware'); const { setAuthTokens } = require('~/server/services/AuthService'); const { logger } = require('~/config'); @@ -12,6 +12,7 @@ const domains = { server: process.env.DOMAIN_SERVER, }; +router.use(logHeaders); router.use(loginLimiter); const oauthHandler = async (req, res) => { diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index 3fdae6ac10..baead97448 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -9,15 +9,16 @@ const { checkVariables, checkHealth, checkConfig, checkAzureVariables } = requir const { azureAssistantsDefaults, assistantsConfigSetup } = require('./start/assistants'); const { initializeAzureBlobService } = require('./Files/Azure/initialize'); const { initializeFirebase } = require('./Files/Firebase/initialize'); -const { initializeS3 } = require('./Files/S3/initialize'); const loadCustomConfig = require('./Config/loadCustomConfig'); const handleRateLimits = require('./Config/handleRateLimits'); const { loadDefaultInterface } = require('./start/interface'); const { azureConfigSetup } = require('./start/azureOpenAI'); const { processModelSpecs } = require('./start/modelSpecs'); +const { initializeS3 } = require('./Files/S3/initialize'); const { loadAndFormatTools } = require('./ToolService'); const { agentsConfigSetup } = require('./start/agents'); const { initializeRoles } = require('~/models/Role'); +const { isEnabled } = require('~/server/utils'); const { getMCPManager } = require('~/config'); const paths = require('~/config/paths'); @@ -29,7 +30,7 @@ const paths = require('~/config/paths'); */ const AppService = async (app) => { await initializeRoles(); - /** @type {TCustomConfig}*/ + /** @type {TCustomConfig} */ const config = (await loadCustomConfig()) ?? {}; const configDefaults = getConfigDefaults(); @@ -37,6 +38,11 @@ const AppService = async (app) => { const filteredTools = config.filteredTools; const includedTools = config.includedTools; const fileStrategy = config.fileStrategy ?? configDefaults.fileStrategy; + const startBalance = process.env.START_BALANCE; + const balance = config.balance ?? { + enabled: isEnabled(process.env.CHECK_BALANCE), + startBalance: startBalance ? parseInt(startBalance, 10) : undefined, + }; const imageOutputType = config?.imageOutputType ?? configDefaults.imageOutputType; process.env.CDN_PROVIDER = fileStrategy; @@ -52,7 +58,7 @@ const AppService = async (app) => { initializeS3(); } - /** @type {Record} */ const availableTools = loadAndFormatTools({ adminFilter: filteredTools, adminIncluded: includedTools, @@ -79,6 +85,7 @@ const AppService = async (app) => { availableTools, imageOutputType, interfaceConfig, + balance, }; if (!Object.keys(config).length) { diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index e47bfe7d5d..465ec9fdd6 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -15,6 +15,9 @@ jest.mock('./Config/loadCustomConfig', () => { Promise.resolve({ registration: { socialLogins: ['testLogin'] }, fileStrategy: 'testStrategy', + balance: { + enabled: true, + }, }), ); }); @@ -124,6 +127,9 @@ describe('AppService', () => { imageOutputType: expect.any(String), fileConfig: undefined, secureImageLinks: undefined, + balance: { enabled: true }, + filteredTools: undefined, + includedTools: undefined, }); }); @@ -341,9 +347,6 @@ describe('AppService', () => { process.env.FILE_UPLOAD_USER_MAX = 'initialUserMax'; process.env.FILE_UPLOAD_USER_WINDOW = 'initialUserWindow'; - // Mock a custom configuration without specific rate limits - require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve({})); - await AppService(app); // Verify that process.env falls back to the initial values @@ -404,9 +407,6 @@ describe('AppService', () => { process.env.IMPORT_USER_MAX = 'initialUserMax'; process.env.IMPORT_USER_WINDOW = 'initialUserWindow'; - // Mock a custom configuration without specific rate limits - require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve({})); - await AppService(app); // Verify that process.env falls back to the initial values @@ -445,13 +445,27 @@ describe('AppService updating app.locals and issuing warnings', () => { expect(app.locals.availableTools).toBeDefined(); expect(app.locals.fileStrategy).toEqual(FileSources.local); expect(app.locals.socialLogins).toEqual(defaultSocialLogins); + expect(app.locals.balance).toEqual( + expect.objectContaining({ + enabled: false, + startBalance: undefined, + }), + ); }); it('should update app.locals with values from loadCustomConfig', async () => { - // Mock loadCustomConfig to return a specific config object + // Mock loadCustomConfig to return a specific config object with a complete balance config const customConfig = { fileStrategy: 'firebase', registration: { socialLogins: ['testLogin'] }, + balance: { + enabled: false, + startBalance: 5000, + autoRefillEnabled: true, + refillIntervalValue: 15, + refillIntervalUnit: 'hours', + refillAmount: 5000, + }, }; require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(customConfig), @@ -464,6 +478,7 @@ describe('AppService updating app.locals and issuing warnings', () => { expect(app.locals.availableTools).toBeDefined(); expect(app.locals.fileStrategy).toEqual(customConfig.fileStrategy); expect(app.locals.socialLogins).toEqual(customConfig.registration.socialLogins); + expect(app.locals.balance).toEqual(customConfig.balance); }); it('should apply the assistants endpoint configuration correctly to app.locals', async () => { diff --git a/api/server/services/Config/getCustomConfig.js b/api/server/services/Config/getCustomConfig.js index 5b9b2dd186..2a154421b0 100644 --- a/api/server/services/Config/getCustomConfig.js +++ b/api/server/services/Config/getCustomConfig.js @@ -1,5 +1,5 @@ const { CacheKeys, EModelEndpoint } = require('librechat-data-provider'); -const { normalizeEndpointName } = require('~/server/utils'); +const { normalizeEndpointName, isEnabled } = require('~/server/utils'); const loadCustomConfig = require('./loadCustomConfig'); const getLogStores = require('~/cache/getLogStores'); @@ -23,6 +23,29 @@ async function getCustomConfig() { return customConfig; } +/** + * Retrieves the configuration object + * @function getBalanceConfig + * @returns {Promise} + * */ +async function getBalanceConfig() { + const isLegacyEnabled = isEnabled(process.env.CHECK_BALANCE); + const startBalance = process.env.START_BALANCE; + if (isLegacyEnabled || (startBalance != null && startBalance)) { + /** @type {TCustomConfig['balance']} */ + const config = { + enabled: isLegacyEnabled, + startBalance: startBalance ? parseInt(startBalance, 10) : undefined, + }; + return config; + } + const customConfig = await getCustomConfig(); + if (!customConfig) { + return null; + } + return customConfig?.['balance'] ?? null; +} + /** * * @param {string | EModelEndpoint} endpoint @@ -40,4 +63,4 @@ const getCustomEndpointConfig = async (endpoint) => { ); }; -module.exports = { getCustomConfig, getCustomEndpointConfig }; +module.exports = { getCustomConfig, getBalanceConfig, getCustomEndpointConfig }; diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 737165e316..26a476527a 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -99,6 +99,19 @@ const primeResources = async (req, _attachments, _tool_resources) => { } }; +/** + * @param {...string | number} values + * @returns {string | number | undefined} + */ +function optionalChainWithEmptyCheck(...values) { + for (const value of values) { + if (value !== undefined && value !== null && value !== '') { + return value; + } + } + return values[values.length - 1]; +} + /** * @param {object} params * @param {ServerRequest} params.req @@ -200,16 +213,23 @@ const initializeAgentOptions = async ({ const tokensModel = agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model; - const maxTokens = agent.model_parameters.maxOutputTokens ?? agent.model_parameters.maxTokens ?? 0; - + const maxTokens = optionalChainWithEmptyCheck( + agent.model_parameters.maxOutputTokens, + agent.model_parameters.maxTokens, + 0, + ); + const maxContextTokens = optionalChainWithEmptyCheck( + agent.model_parameters.maxContextTokens, + agent.max_context_tokens, + getModelMaxTokens(tokensModel, providerEndpointMap[provider]), + 4096, + ); return { ...agent, tools, attachments, toolContextMap, - maxContextTokens: - agent.max_context_tokens ?? - ((getModelMaxTokens(tokensModel, providerEndpointMap[provider]) ?? 4000) - maxTokens) * 0.9, + maxContextTokens: (maxContextTokens - maxTokens) * 0.9, }; }; diff --git a/api/server/services/Files/Audio/STTService.js b/api/server/services/Files/Audio/STTService.js index ea8d6ffaac..d6c8cc4146 100644 --- a/api/server/services/Files/Audio/STTService.js +++ b/api/server/services/Files/Audio/STTService.js @@ -7,6 +7,78 @@ const { getCustomConfig } = require('~/server/services/Config'); const { genAzureEndpoint } = require('~/utils'); const { logger } = require('~/config'); +/** + * Maps MIME types to their corresponding file extensions for audio files. + * @type {Object} + */ +const MIME_TO_EXTENSION_MAP = { + // MP4 container formats + 'audio/mp4': 'm4a', + 'audio/x-m4a': 'm4a', + // Ogg formats + 'audio/ogg': 'ogg', + 'audio/vorbis': 'ogg', + 'application/ogg': 'ogg', + // Wave formats + 'audio/wav': 'wav', + 'audio/x-wav': 'wav', + 'audio/wave': 'wav', + // MP3 formats + 'audio/mp3': 'mp3', + 'audio/mpeg': 'mp3', + 'audio/mpeg3': 'mp3', + // WebM formats + 'audio/webm': 'webm', + // Additional formats + 'audio/flac': 'flac', + 'audio/x-flac': 'flac', +}; + +/** + * Gets the file extension from the MIME type. + * @param {string} mimeType - The MIME type. + * @returns {string} The file extension. + */ +function getFileExtensionFromMime(mimeType) { + // Default fallback + if (!mimeType) { + return 'webm'; + } + + // Direct lookup (fastest) + const extension = MIME_TO_EXTENSION_MAP[mimeType]; + if (extension) { + return extension; + } + + // Try to extract subtype as fallback + const subtype = mimeType.split('/')[1]?.toLowerCase(); + + // If subtype matches a known extension + if (['mp3', 'mp4', 'ogg', 'wav', 'webm', 'm4a', 'flac'].includes(subtype)) { + return subtype === 'mp4' ? 'm4a' : subtype; + } + + // Generic checks for partial matches + if (subtype?.includes('mp4') || subtype?.includes('m4a')) { + return 'm4a'; + } + if (subtype?.includes('ogg')) { + return 'ogg'; + } + if (subtype?.includes('wav')) { + return 'wav'; + } + if (subtype?.includes('mp3') || subtype?.includes('mpeg')) { + return 'mp3'; + } + if (subtype?.includes('webm')) { + return 'webm'; + } + + return 'webm'; // Default fallback +} + /** * Service class for handling Speech-to-Text (STT) operations. * @class @@ -170,8 +242,10 @@ class STTService { throw new Error('Invalid provider'); } + const fileExtension = getFileExtensionFromMime(audioFile.mimetype); + const audioReadStream = Readable.from(audioBuffer); - audioReadStream.path = 'audio.wav'; + audioReadStream.path = `audio.${fileExtension}`; const [url, data, headers] = strategy.call(this, sttSchema, audioReadStream, audioFile); diff --git a/api/server/services/Files/S3/crud.js b/api/server/services/Files/S3/crud.js index 701c2327da..06f9116b69 100644 --- a/api/server/services/Files/S3/crud.js +++ b/api/server/services/Files/S3/crud.js @@ -1,15 +1,12 @@ const fs = require('fs'); const path = require('path'); -const axios = require('axios'); const fetch = require('node-fetch'); -const { getBufferMetadata } = require('~/server/utils'); -const { initializeS3 } = require('./initialize'); -const { logger } = require('~/config'); const { PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); +const { initializeS3 } = require('./initialize'); +const { logger } = require('~/config'); const bucketName = process.env.AWS_BUCKET_NAME; -const s3 = initializeS3(); const defaultBasePath = 'images'; /** @@ -32,6 +29,7 @@ async function saveBufferToS3({ userId, buffer, fileName, basePath = defaultBase const params = { Bucket: bucketName, Key: key, Body: buffer }; try { + const s3 = initializeS3(); await s3.send(new PutObjectCommand(params)); return await getS3URL({ userId, fileName, basePath }); } catch (error) { @@ -54,6 +52,7 @@ async function getS3URL({ userId, fileName, basePath = defaultBasePath }) { const params = { Bucket: bucketName, Key: key }; try { + const s3 = initializeS3(); return await getSignedUrl(s3, new GetObjectCommand(params), { expiresIn: 86400 }); } catch (error) { logger.error('[getS3URL] Error getting signed URL from S3:', error.message); @@ -97,6 +96,7 @@ async function deleteFileFromS3({ userId, fileName, basePath = defaultBasePath } const params = { Bucket: bucketName, Key: key }; try { + const s3 = initializeS3(); await s3.send(new DeleteObjectCommand(params)); logger.debug('[deleteFileFromS3] File deleted successfully from S3'); } catch (error) { @@ -144,6 +144,7 @@ async function uploadFileToS3({ req, file, file_id, basePath = defaultBasePath } async function getS3FileStream(filePath) { const params = { Bucket: bucketName, Key: filePath }; try { + const s3 = initializeS3(); const data = await s3.send(new GetObjectCommand(params)); return data.Body; // Returns a Node.js ReadableStream. } catch (error) { diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index f934f9d519..9b8ce30875 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -37,11 +37,19 @@ async function createMCPTool({ req, toolKey, provider }) { } const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter); - /** @type {(toolInput: Object | string) => Promise} */ - const _call = async (toolInput) => { + /** @type {(toolArguments: Object | string, config?: GraphRunnableConfig) => Promise} */ + const _call = async (toolArguments, config) => { try { const mcpManager = await getMCPManager(); - const result = await mcpManager.callTool(serverName, toolName, provider, toolInput); + const result = await mcpManager.callTool({ + serverName, + toolName, + provider, + toolArguments, + options: { + signal: config?.signal, + }, + }); if (isAssistantsEndpoint(provider) && Array.isArray(result)) { return result[0]; } diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index 969ca8d8ff..ad2d3632b4 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -425,21 +425,16 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) } const endpointsConfig = await getEndpointsConfig(req); - const capabilities = endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []; - const areToolsEnabled = capabilities.includes(AgentCapabilities.tools); - if (!areToolsEnabled) { - logger.debug('Tools are not enabled for this agent.'); - return {}; - } - - const isFileSearchEnabled = capabilities.includes(AgentCapabilities.file_search); - const isCodeEnabled = capabilities.includes(AgentCapabilities.execute_code); - const areActionsEnabled = capabilities.includes(AgentCapabilities.actions); + const enabledCapabilities = new Set(endpointsConfig?.[EModelEndpoint.agents]?.capabilities ?? []); + const checkCapability = (capability) => enabledCapabilities.has(capability); + const areToolsEnabled = checkCapability(AgentCapabilities.tools); const _agentTools = agent.tools?.filter((tool) => { - if (tool === Tools.file_search && !isFileSearchEnabled) { + if (tool === Tools.file_search && !checkCapability(AgentCapabilities.file_search)) { return false; - } else if (tool === Tools.execute_code && !isCodeEnabled) { + } else if (tool === Tools.execute_code && !checkCapability(AgentCapabilities.execute_code)) { + return false; + } else if (!areToolsEnabled && !tool.includes(actionDelimiter)) { return false; } return true; @@ -473,6 +468,10 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) continue; } + if (!areToolsEnabled) { + continue; + } + if (tool.mcp === true) { agentTools.push(tool); continue; @@ -505,7 +504,7 @@ async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) return map; }, {}); - if (!areActionsEnabled) { + if (!checkCapability(AgentCapabilities.actions)) { return { tools: agentTools, toolContextMap, diff --git a/api/server/services/twoFactorService.js b/api/server/services/twoFactorService.js index e48b2ac938..d000c8fcfc 100644 --- a/api/server/services/twoFactorService.js +++ b/api/server/services/twoFactorService.js @@ -1,15 +1,14 @@ -const { sign } = require('jsonwebtoken'); const { webcrypto } = require('node:crypto'); -const { hashBackupCode, decryptV2 } = require('~/server/utils/crypto'); -const { updateUser } = require('~/models/userMethods'); +const { decryptV3, decryptV2 } = require('../utils/crypto'); +const { hashBackupCode } = require('~/server/utils/crypto'); +// Base32 alphabet for TOTP secret encoding. const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; /** - * Encodes a Buffer into a Base32 string using the RFC 4648 alphabet. - * - * @param {Buffer} buffer - The buffer to encode. - * @returns {string} The Base32 encoded string. + * Encodes a Buffer into a Base32 string. + * @param {Buffer} buffer + * @returns {string} */ const encodeBase32 = (buffer) => { let bits = 0; @@ -30,10 +29,9 @@ const encodeBase32 = (buffer) => { }; /** - * Decodes a Base32-encoded string back into a Buffer. - * - * @param {string} base32Str - The Base32-encoded string. - * @returns {Buffer} The decoded buffer. + * Decodes a Base32 string into a Buffer. + * @param {string} base32Str + * @returns {Buffer} */ const decodeBase32 = (base32Str) => { const cleaned = base32Str.replace(/=+$/, '').toUpperCase(); @@ -56,20 +54,8 @@ const decodeBase32 = (base32Str) => { }; /** - * Generates a temporary token for 2FA verification. - * The token is signed with the JWT_SECRET and expires in 5 minutes. - * - * @param {string} userId - The unique identifier of the user. - * @returns {string} The signed JWT token. - */ -const generate2FATempToken = (userId) => - sign({ userId, twoFAPending: true }, process.env.JWT_SECRET, { expiresIn: '5m' }); - -/** - * Generates a TOTP secret. - * Creates 10 random bytes using WebCrypto and encodes them into a Base32 string. - * - * @returns {string} A Base32-encoded secret for TOTP. + * Generates a new TOTP secret (Base32 encoded). + * @returns {string} */ const generateTOTPSecret = () => { const randomArray = new Uint8Array(10); @@ -78,29 +64,25 @@ const generateTOTPSecret = () => { }; /** - * Generates a Time-based One-Time Password (TOTP) based on the provided secret and time. - * This implementation uses a 30-second time step and produces a 6-digit code. - * - * @param {string} secret - The Base32-encoded TOTP secret. - * @param {number} [forTime=Date.now()] - The time (in milliseconds) for which to generate the TOTP. - * @returns {Promise} A promise that resolves to the 6-digit TOTP code. + * Generates a TOTP code based on the secret and time. + * Uses a 30-second time step and produces a 6-digit code. + * @param {string} secret + * @param {number} [forTime=Date.now()] + * @returns {Promise} */ const generateTOTP = async (secret, forTime = Date.now()) => { const timeStep = 30; // seconds const counter = Math.floor(forTime / 1000 / timeStep); const counterBuffer = new ArrayBuffer(8); const counterView = new DataView(counterBuffer); - // Write counter into the last 4 bytes (big-endian) counterView.setUint32(4, counter, false); - // Decode the secret into an ArrayBuffer const keyBuffer = decodeBase32(secret); const keyArrayBuffer = keyBuffer.buffer.slice( keyBuffer.byteOffset, keyBuffer.byteOffset + keyBuffer.byteLength, ); - // Import the key for HMAC-SHA1 signing const cryptoKey = await webcrypto.subtle.importKey( 'raw', keyArrayBuffer, @@ -108,12 +90,10 @@ const generateTOTP = async (secret, forTime = Date.now()) => { false, ['sign'], ); - - // Generate HMAC signature const signatureBuffer = await webcrypto.subtle.sign('HMAC', cryptoKey, counterBuffer); const hmac = new Uint8Array(signatureBuffer); - // Dynamic truncation as per RFC 4226 + // Dynamic truncation per RFC 4226. const offset = hmac[hmac.length - 1] & 0xf; const slice = hmac.slice(offset, offset + 4); const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength); @@ -123,12 +103,10 @@ const generateTOTP = async (secret, forTime = Date.now()) => { }; /** - * Verifies a provided TOTP token against the secret. - * It allows for a ±1 time-step window to account for slight clock discrepancies. - * - * @param {string} secret - The Base32-encoded TOTP secret. - * @param {string} token - The TOTP token provided by the user. - * @returns {Promise} A promise that resolves to true if the token is valid; otherwise, false. + * Verifies a TOTP token by checking a ±1 time step window. + * @param {string} secret + * @param {string} token + * @returns {Promise} */ const verifyTOTP = async (secret, token) => { const timeStepMS = 30 * 1000; @@ -143,27 +121,24 @@ const verifyTOTP = async (secret, token) => { }; /** - * Generates backup codes for two-factor authentication. - * Each backup code is an 8-character hexadecimal string along with its SHA-256 hash. - * The plain codes are returned for one-time download, while the hashed objects are meant for secure storage. - * - * @param {number} [count=10] - The number of backup codes to generate. + * Generates backup codes (default count: 10). + * Each code is an 8-character hexadecimal string and stored with its SHA-256 hash. + * @param {number} [count=10] * @returns {Promise<{ plainCodes: string[], codeObjects: Array<{ codeHash: string, used: boolean, usedAt: Date | null }> }>} - * A promise that resolves to an object containing both plain backup codes and their corresponding code objects. */ const generateBackupCodes = async (count = 10) => { const plainCodes = []; const codeObjects = []; const encoder = new TextEncoder(); + for (let i = 0; i < count; i++) { const randomArray = new Uint8Array(4); webcrypto.getRandomValues(randomArray); const code = Array.from(randomArray) .map((b) => b.toString(16).padStart(2, '0')) - .join(''); // 8-character hex code + .join(''); plainCodes.push(code); - // Compute SHA-256 hash of the code using WebCrypto const codeBuffer = encoder.encode(code); const hashBuffer = await webcrypto.subtle.digest('SHA-256', codeBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); @@ -174,12 +149,11 @@ const generateBackupCodes = async (count = 10) => { }; /** - * Verifies a backup code for a user and updates its status as used if valid. - * - * @param {Object} params - The parameters object. - * @param {TUser | undefined} [params.user] - The user object containing backup codes. - * @param {string | undefined} [params.backupCode] - The backup code to verify. - * @returns {Promise} A promise that resolves to true if the backup code is valid and updated; otherwise, false. + * Verifies a backup code and, if valid, marks it as used. + * @param {Object} params + * @param {Object} params.user + * @param {string} params.backupCode + * @returns {Promise} */ const verifyBackupCode = async ({ user, backupCode }) => { if (!backupCode || !user || !Array.isArray(user.backupCodes)) { @@ -197,42 +171,54 @@ const verifyBackupCode = async ({ user, backupCode }) => { ? { ...codeObj, used: true, usedAt: new Date() } : codeObj, ); - + // Update the user record with the marked backup code. + const { updateUser } = require('~/models'); await updateUser(user._id, { backupCodes: updatedBackupCodes }); return true; } - return false; }; /** - * Retrieves and, if necessary, decrypts a stored TOTP secret. - * If the secret contains a colon, it is assumed to be in the format "iv:encryptedData" and will be decrypted. - * If the secret is exactly 16 characters long, it is assumed to be a legacy plain secret. - * - * @param {string|null} storedSecret - The stored TOTP secret (which may be encrypted). - * @returns {Promise} A promise that resolves to the plain TOTP secret, or null if none is provided. + * Retrieves and decrypts a stored TOTP secret. + * - Uses decryptV3 if the secret has a "v3:" prefix. + * - Falls back to decryptV2 for colon-delimited values. + * - Assumes a 16-character secret is already plain. + * @param {string|null} storedSecret + * @returns {Promise} */ const getTOTPSecret = async (storedSecret) => { - if (!storedSecret) { return null; } - // Check for a colon marker (encrypted secrets are stored as "iv:encryptedData") + if (!storedSecret) { + return null; + } + if (storedSecret.startsWith('v3:')) { + return decryptV3(storedSecret); + } if (storedSecret.includes(':')) { return await decryptV2(storedSecret); } - // If it's exactly 16 characters, assume it's already plain (legacy secret) if (storedSecret.length === 16) { return storedSecret; } - // Fallback in case it doesn't meet our criteria. return storedSecret; }; +/** + * Generates a temporary JWT token for 2FA verification that expires in 5 minutes. + * @param {string} userId + * @returns {string} + */ +const generate2FATempToken = (userId) => { + const { sign } = require('jsonwebtoken'); + return sign({ userId, twoFAPending: true }, process.env.JWT_SECRET, { expiresIn: '5m' }); +}; + module.exports = { - verifyTOTP, - generateTOTP, - getTOTPSecret, - verifyBackupCode, generateTOTPSecret, + generateTOTP, + verifyTOTP, generateBackupCodes, + verifyBackupCode, + getTOTPSecret, generate2FATempToken, }; diff --git a/api/server/socialLogins.js b/api/server/socialLogins.js index f39d1da596..af80a3b880 100644 --- a/api/server/socialLogins.js +++ b/api/server/socialLogins.js @@ -1,4 +1,4 @@ -const Redis = require('ioredis'); +const Keyv = require('keyv'); const passport = require('passport'); const session = require('express-session'); const MemoryStore = require('memorystore')(session); @@ -12,6 +12,7 @@ const { appleLogin, } = require('~/strategies'); const { isEnabled } = require('~/server/utils'); +const keyvRedis = require('~/cache/keyvRedis'); const { logger } = require('~/config'); /** @@ -19,6 +20,8 @@ const { logger } = require('~/config'); * @param {Express.Application} app */ const configureSocialLogins = (app) => { + logger.info('Configuring social logins...'); + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { passport.use(googleLogin()); } @@ -41,18 +44,17 @@ const configureSocialLogins = (app) => { process.env.OPENID_SCOPE && process.env.OPENID_SESSION_SECRET ) { + logger.info('Configuring OpenID Connect...'); const sessionOptions = { secret: process.env.OPENID_SESSION_SECRET, resave: false, saveUninitialized: false, }; if (isEnabled(process.env.USE_REDIS)) { - const client = new Redis(process.env.REDIS_URI); - client - .on('error', (err) => logger.error('ioredis error:', err)) - .on('ready', () => logger.info('ioredis successfully initialized.')) - .on('reconnecting', () => logger.info('ioredis reconnecting...')); - sessionOptions.store = new RedisStore({ client, prefix: 'librechat' }); + logger.debug('Using Redis for session storage in OpenID...'); + const keyv = new Keyv({ store: keyvRedis }); + const client = keyv.opts.store.redis; + sessionOptions.store = new RedisStore({ client, prefix: 'openid_session' }); } else { sessionOptions.store = new MemoryStore({ checkPeriod: 86400000, // prune expired entries every 24h @@ -61,7 +63,9 @@ const configureSocialLogins = (app) => { app.use(session(sessionOptions)); app.use(passport.session()); setupOpenId(); + + logger.info('OpenID Connect configured.'); } }; -module.exports = configureSocialLogins; \ No newline at end of file +module.exports = configureSocialLogins; diff --git a/api/server/utils/crypto.js b/api/server/utils/crypto.js index 407fad62ac..333cd7573a 100644 --- a/api/server/utils/crypto.js +++ b/api/server/utils/crypto.js @@ -1,27 +1,25 @@ require('dotenv').config(); +const crypto = require('node:crypto'); +const { webcrypto } = crypto; -const { webcrypto } = require('node:crypto'); +// Use hex decoding for both key and IV for legacy methods. const key = Buffer.from(process.env.CREDS_KEY, 'hex'); const iv = Buffer.from(process.env.CREDS_IV, 'hex'); const algorithm = 'AES-CBC'; +// --- Legacy v1/v2 Setup: AES-CBC with fixed key and IV --- + async function encrypt(value) { const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [ 'encrypt', ]); - const encoder = new TextEncoder(); const data = encoder.encode(value); - const encryptedBuffer = await webcrypto.subtle.encrypt( - { - name: algorithm, - iv: iv, - }, + { name: algorithm, iv: iv }, cryptoKey, data, ); - return Buffer.from(encryptedBuffer).toString('hex'); } @@ -29,73 +27,85 @@ async function decrypt(encryptedValue) { const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [ 'decrypt', ]); - const encryptedBuffer = Buffer.from(encryptedValue, 'hex'); - const decryptedBuffer = await webcrypto.subtle.decrypt( - { - name: algorithm, - iv: iv, - }, + { name: algorithm, iv: iv }, cryptoKey, encryptedBuffer, ); - const decoder = new TextDecoder(); return decoder.decode(decryptedBuffer); } -// Programmatically generate iv +// --- v2: AES-CBC with a random IV per encryption --- + async function encryptV2(value) { const gen_iv = webcrypto.getRandomValues(new Uint8Array(16)); - const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [ 'encrypt', ]); - const encoder = new TextEncoder(); const data = encoder.encode(value); - const encryptedBuffer = await webcrypto.subtle.encrypt( - { - name: algorithm, - iv: gen_iv, - }, + { name: algorithm, iv: gen_iv }, cryptoKey, data, ); - return Buffer.from(gen_iv).toString('hex') + ':' + Buffer.from(encryptedBuffer).toString('hex'); } async function decryptV2(encryptedValue) { const parts = encryptedValue.split(':'); - // Already decrypted from an earlier invocation if (parts.length === 1) { return parts[0]; } const gen_iv = Buffer.from(parts.shift(), 'hex'); const encrypted = parts.join(':'); - const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [ 'decrypt', ]); - const encryptedBuffer = Buffer.from(encrypted, 'hex'); - const decryptedBuffer = await webcrypto.subtle.decrypt( - { - name: algorithm, - iv: gen_iv, - }, + { name: algorithm, iv: gen_iv }, cryptoKey, encryptedBuffer, ); - const decoder = new TextDecoder(); return decoder.decode(decryptedBuffer); } +// --- v3: AES-256-CTR using Node's crypto functions --- +const algorithm_v3 = 'aes-256-ctr'; + +/** + * Encrypts a value using AES-256-CTR. + * Note: AES-256 requires a 32-byte key. Ensure that process.env.CREDS_KEY is a 64-character hex string. + * + * @param {string} value - The plaintext to encrypt. + * @returns {string} The encrypted string with a "v3:" prefix. + */ +function encryptV3(value) { + if (key.length !== 32) { + throw new Error(`Invalid key length: expected 32 bytes, got ${key.length} bytes`); + } + const iv_v3 = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm_v3, key, iv_v3); + const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]); + return `v3:${iv_v3.toString('hex')}:${encrypted.toString('hex')}`; +} + +function decryptV3(encryptedValue) { + const parts = encryptedValue.split(':'); + if (parts[0] !== 'v3') { + throw new Error('Not a v3 encrypted value'); + } + const iv_v3 = Buffer.from(parts[1], 'hex'); + const encryptedText = Buffer.from(parts.slice(2).join(':'), 'hex'); + const decipher = crypto.createDecipheriv(algorithm_v3, key, iv_v3); + const decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]); + return decrypted.toString('utf8'); +} + async function hashToken(str) { const data = new TextEncoder().encode(str); const hashBuffer = await webcrypto.subtle.digest('SHA-256', data); @@ -106,30 +116,31 @@ async function getRandomValues(length) { if (!Number.isInteger(length) || length <= 0) { throw new Error('Length must be a positive integer'); } - const randomValues = new Uint8Array(length); webcrypto.getRandomValues(randomValues); return Buffer.from(randomValues).toString('hex'); } /** - * Computes SHA-256 hash for the given input using WebCrypto + * Computes SHA-256 hash for the given input. * @param {string} input - * @returns {Promise} - Hex hash string + * @returns {Promise} */ -const hashBackupCode = async (input) => { +async function hashBackupCode(input) { const encoder = new TextEncoder(); const data = encoder.encode(input); const hashBuffer = await webcrypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); -}; +} module.exports = { encrypt, decrypt, encryptV2, decryptV2, + encryptV3, + decryptV3, hashToken, hashBackupCode, getRandomValues, diff --git a/api/strategies/ldapStrategy.js b/api/strategies/ldapStrategy.js index 4a2c1b827b..5ec279b982 100644 --- a/api/strategies/ldapStrategy.js +++ b/api/strategies/ldapStrategy.js @@ -18,6 +18,7 @@ const { LDAP_USERNAME, LDAP_EMAIL, LDAP_TLS_REJECT_UNAUTHORIZED, + LDAP_STARTTLS, } = process.env; // Check required environment variables @@ -50,6 +51,7 @@ if (LDAP_EMAIL) { searchAttributes.push(LDAP_EMAIL); } const rejectUnauthorized = isEnabled(LDAP_TLS_REJECT_UNAUTHORIZED); +const startTLS = isEnabled(LDAP_STARTTLS); const ldapOptions = { server: { @@ -72,6 +74,7 @@ const ldapOptions = { })(), }, }), + ...(startTLS && { starttls: true }), }, usernameField: 'email', passwordField: 'password', diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 442d32a45e..29592abeaa 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -93,7 +93,7 @@ const ChatForm = ({ index = 0 }) => { } = useAddedChatContext(); const showStopAdded = useRecoilValue(store.showStopButtonByIndex(addedIndex)); - const { clearDraft } = useAutoSave({ + useAutoSave({ conversationId: useMemo(() => conversation?.conversationId, [conversation]), textAreaRef, files, @@ -101,7 +101,7 @@ const ChatForm = ({ index = 0 }) => { }); const assistantMap = useAssistantsMapContext(); - const { submitMessage, submitPrompt } = useSubmitMessage({ clearDraft }); + const { submitMessage, submitPrompt } = useSubmitMessage(); const { endpoint: _endpoint, endpointType } = conversation ?? { endpoint: null }; const endpoint = endpointType ?? _endpoint; diff --git a/client/src/components/Nav/AccountSettings.tsx b/client/src/components/Nav/AccountSettings.tsx index 77ed49ce4e..ec6b52fd50 100644 --- a/client/src/components/Nav/AccountSettings.tsx +++ b/client/src/components/Nav/AccountSettings.tsx @@ -17,7 +17,7 @@ function AccountSettings() { const { user, isAuthenticated, logout } = useAuthContext(); const { data: startupConfig } = useGetStartupConfig(); const balanceQuery = useGetUserBalance({ - enabled: !!isAuthenticated && startupConfig?.checkBalance, + enabled: !!isAuthenticated && startupConfig?.balance?.enabled, }); const [showSettings, setShowSettings] = useState(false); const [showFiles, setShowFiles] = useRecoilState(store.showFiles); @@ -75,7 +75,7 @@ function AccountSettings() { {user?.email ?? localize('com_nav_user')} - {startupConfig?.checkBalance === true && + {startupConfig?.balance?.enabled === true && balanceQuery.data != null && !isNaN(parseFloat(balanceQuery.data)) && ( <> diff --git a/client/src/components/SidePanel/Parameters/settings.ts b/client/src/components/SidePanel/Parameters/settings.ts index 3aebbc606c..b3f1b0c391 100644 --- a/client/src/components/SidePanel/Parameters/settings.ts +++ b/client/src/components/SidePanel/Parameters/settings.ts @@ -497,10 +497,10 @@ const openAICol1: SettingsConfiguration = [ baseDefinitions.model as SettingDefinition, openAIParams.chatGptLabel, librechat.promptPrefix, - librechat.maxContextTokens, ]; const openAICol2: SettingsConfiguration = [ + librechat.maxContextTokens, openAIParams.max_tokens, openAIParams.temperature, openAIParams.top_p, diff --git a/client/src/hooks/Input/useAutoSave.ts b/client/src/hooks/Input/useAutoSave.ts index 70e3ab2bf2..6b7be2437d 100644 --- a/client/src/hooks/Input/useAutoSave.ts +++ b/client/src/hooks/Input/useAutoSave.ts @@ -7,6 +7,10 @@ import { useChatFormContext } from '~/Providers'; import { useGetFiles } from '~/data-provider'; import store from '~/store'; +const clearDraft = debounce((id?: string | null) => { + localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id ?? ''}`); +}, 2500); + export const useAutoSave = ({ conversationId, textAreaRef, @@ -103,7 +107,7 @@ export const useAutoSave = ({ } // Save the draft of the current conversation before switching if (textAreaRef.current.value === '') { - localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${id}`); + clearDraft(id); } else { localStorage.setItem( `${LocalStorageKeys.TEXT_DRAFT}${id}`, @@ -208,13 +212,4 @@ export const useAutoSave = ({ ); } }, [files, conversationId, saveDrafts, currentConversationId, fileIds]); - - const clearDraft = useCallback(() => { - if (conversationId != null && conversationId) { - localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`); - localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${conversationId}`); - } - }, [conversationId]); - - return { clearDraft }; }; diff --git a/client/src/hooks/Input/useQueryParams.ts b/client/src/hooks/Input/useQueryParams.ts index 072a5b74e9..94c036500d 100644 --- a/client/src/hooks/Input/useQueryParams.ts +++ b/client/src/hooks/Input/useQueryParams.ts @@ -14,6 +14,7 @@ import type { ZodAny } from 'zod'; import { getConvoSwitchLogic, removeUnavailableTools } from '~/utils'; import useDefaultConvo from '~/hooks/Conversations/useDefaultConvo'; import { useChatContext, useChatFormContext } from '~/Providers'; +import useSubmitMessage from '~/hooks/Messages/useSubmitMessage'; import store from '~/store'; const parseQueryValue = (value: string) => { @@ -76,6 +77,7 @@ export default function useQueryParams({ const getDefaultConversation = useDefaultConvo(); const modularChat = useRecoilValue(store.modularChat); const availableTools = useRecoilValue(store.availableTools); + const { submitMessage } = useSubmitMessage(); const queryClient = useQueryClient(); const { conversation, newConversation } = useChatContext(); @@ -160,10 +162,12 @@ export default function useQueryParams({ }); const decodedPrompt = queryParams.prompt || ''; + const shouldAutoSubmit = queryParams.submit?.toLowerCase() === 'true'; delete queryParams.prompt; + delete queryParams.submit; const validSettings = processValidSettings(queryParams); - return { decodedPrompt, validSettings }; + return { decodedPrompt, validSettings, shouldAutoSubmit }; }; const intervalId = setInterval(() => { @@ -180,7 +184,7 @@ export default function useQueryParams({ if (!textAreaRef.current) { return; } - const { decodedPrompt, validSettings } = processQueryParams(); + const { decodedPrompt, validSettings, shouldAutoSubmit } = processQueryParams(); const currentText = methods.getValues('text'); /** Clean up URL parameters after successful processing */ @@ -196,6 +200,15 @@ export default function useQueryParams({ methods.setValue('text', decodedPrompt, { shouldValidate: true }); textAreaRef.current.focus(); textAreaRef.current.setSelectionRange(decodedPrompt.length, decodedPrompt.length); + + // Auto-submit if the submit parameter is true + if (shouldAutoSubmit) { + methods.handleSubmit((data) => { + if (data.text?.trim()) { + submitMessage(data); + } + })(); + } } if (Object.keys(validSettings).length > 0) { @@ -208,5 +221,5 @@ export default function useQueryParams({ return () => { clearInterval(intervalId); }; - }, [searchParams, methods, textAreaRef, newQueryConvo, newConversation]); + }, [searchParams, methods, textAreaRef, newQueryConvo, newConversation, submitMessage]); } diff --git a/client/src/hooks/Input/useSpeechToTextExternal.ts b/client/src/hooks/Input/useSpeechToTextExternal.ts index b9f0ee94d8..5ddccc9f3e 100644 --- a/client/src/hooks/Input/useSpeechToTextExternal.ts +++ b/client/src/hooks/Input/useSpeechToTextExternal.ts @@ -21,6 +21,7 @@ const useSpeechToTextExternal = ( const [isListening, setIsListening] = useState(false); const [audioChunks, setAudioChunks] = useState([]); const [isRequestBeingMade, setIsRequestBeingMade] = useState(false); + const [audioMimeType, setAudioMimeType] = useState('audio/webm'); const [minDecibels] = useRecoilState(store.decibelValue); const [autoSendText] = useRecoilState(store.autoSendText); @@ -48,6 +49,44 @@ const useSpeechToTextExternal = ( }, }); + const getBestSupportedMimeType = () => { + const types = [ + 'audio/webm', + 'audio/webm;codecs=opus', + 'audio/mp4', + 'audio/ogg;codecs=opus', + 'audio/ogg', + 'audio/wav', + ]; + + for (const type of types) { + if (MediaRecorder.isTypeSupported(type)) { + return type; + } + } + + const ua = navigator.userAgent.toLowerCase(); + if (ua.indexOf('safari') !== -1 && ua.indexOf('chrome') === -1) { + return 'audio/mp4'; + } else if (ua.indexOf('firefox') !== -1) { + return 'audio/ogg'; + } else { + return 'audio/webm'; + } + }; + + const getFileExtension = (mimeType: string) => { + if (mimeType.includes('mp4')) { + return 'm4a'; + } else if (mimeType.includes('ogg')) { + return 'ogg'; + } else if (mimeType.includes('wav')) { + return 'wav'; + } else { + return 'webm'; + } + }; + const cleanup = () => { if (mediaRecorderRef.current) { mediaRecorderRef.current.removeEventListener('dataavailable', (event: BlobEvent) => { @@ -73,12 +112,13 @@ const useSpeechToTextExternal = ( const handleStop = () => { if (audioChunks.length > 0) { - const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); + const audioBlob = new Blob(audioChunks, { type: audioMimeType }); + const fileExtension = getFileExtension(audioMimeType); setAudioChunks([]); const formData = new FormData(); - formData.append('audio', audioBlob, 'audio.wav'); + formData.append('audio', audioBlob, `audio.${fileExtension}`); setIsRequestBeingMade(true); cleanup(); processAudio(formData); @@ -133,7 +173,12 @@ const useSpeechToTextExternal = ( if (audioStream.current) { try { setAudioChunks([]); - mediaRecorderRef.current = new MediaRecorder(audioStream.current); + const bestMimeType = getBestSupportedMimeType(); + setAudioMimeType(bestMimeType); + + mediaRecorderRef.current = new MediaRecorder(audioStream.current, { + mimeType: bestMimeType, + }); mediaRecorderRef.current.addEventListener('dataavailable', (event: BlobEvent) => { audioChunks.push(event.data); }); @@ -221,7 +266,7 @@ const useSpeechToTextExternal = ( return () => { window.removeEventListener('keydown', handleKeyDown); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isListening]); return { diff --git a/client/src/hooks/Messages/useSubmitMessage.ts b/client/src/hooks/Messages/useSubmitMessage.ts index f69ee6ee9a..1ed468d61a 100644 --- a/client/src/hooks/Messages/useSubmitMessage.ts +++ b/client/src/hooks/Messages/useSubmitMessage.ts @@ -14,7 +14,7 @@ const appendIndex = (index: number, value?: string) => { return `${value}${Constants.COMMON_DIVIDER}${index}`; }; -export default function useSubmitMessage(helpers?: { clearDraft?: () => void }) { +export default function useSubmitMessage() { const { user } = useAuthContext(); const methods = useChatFormContext(); const { ask, index, getMessages, setMessages, latestMessage } = useChatContext(); @@ -66,12 +66,10 @@ export default function useSubmitMessage(helpers?: { clearDraft?: () => void }) ); } methods.reset(); - helpers?.clearDraft && helpers.clearDraft(); }, [ ask, methods, - helpers, addedIndex, addedConvo, setMessages, diff --git a/client/src/hooks/SSE/useSSE.ts b/client/src/hooks/SSE/useSSE.ts index a52928caad..3e5b3eda4d 100644 --- a/client/src/hooks/SSE/useSSE.ts +++ b/client/src/hooks/SSE/useSSE.ts @@ -4,9 +4,11 @@ import { SSE } from 'sse.js'; import { useSetRecoilState } from 'recoil'; import { request, + Constants, /* @ts-ignore */ createPayload, isAgentsEndpoint, + LocalStorageKeys, removeNullishValues, isAssistantsEndpoint, } from 'librechat-data-provider'; @@ -18,6 +20,16 @@ import { useAuthContext } from '~/hooks/AuthContext'; import useEventHandlers from './useEventHandlers'; import store from '~/store'; +const clearDraft = (conversationId?: string | null) => { + if (conversationId) { + localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${conversationId}`); + localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${conversationId}`); + } else { + localStorage.removeItem(`${LocalStorageKeys.TEXT_DRAFT}${Constants.NEW_CONVO}`); + localStorage.removeItem(`${LocalStorageKeys.FILES_DRAFT}${Constants.NEW_CONVO}`); + } +}; + type ChatHelpers = Pick< EventHandlerParams, | 'setMessages' @@ -76,7 +88,7 @@ export default function useSSE( const { data: startupConfig } = useGetStartupConfig(); const balanceQuery = useGetUserBalance({ - enabled: !!isAuthenticated && startupConfig?.checkBalance, + enabled: !!isAuthenticated && startupConfig?.balance?.enabled, }); useEffect(() => { @@ -112,9 +124,10 @@ export default function useSSE( const data = JSON.parse(e.data); if (data.final != null) { + clearDraft(submission.conversationId); const { plugins } = data; finalHandler(data, { ...submission, plugins } as EventSubmission); - (startupConfig?.checkBalance ?? false) && balanceQuery.refetch(); + (startupConfig?.balance?.enabled ?? false) && balanceQuery.refetch(); console.log('final', data); return; } else if (data.created != null) { @@ -208,7 +221,7 @@ export default function useSSE( } console.log('error in server stream.'); - (startupConfig?.checkBalance ?? false) && balanceQuery.refetch(); + (startupConfig?.balance?.enabled ?? false) && balanceQuery.refetch(); let data: TResData | undefined = undefined; try { @@ -234,6 +247,5 @@ export default function useSSE( sse.dispatchEvent(e); } }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [submission]); } diff --git a/client/src/locales/de/translation.json b/client/src/locales/de/translation.json index bbcc01789c..51be904f4a 100644 --- a/client/src/locales/de/translation.json +++ b/client/src/locales/de/translation.json @@ -1,4 +1,6 @@ { + "chat_direction_left_to_right": "Leer – etwas fehlt noch", + "chat_direction_right_to_left": "Leer – etwas fehlt noch", "com_a11y_ai_composing": "Die KI erstellt noch ihre Antwort.", "com_a11y_end": "Die KI hat ihre Antwort beendet.", "com_a11y_start": "Die KI hat mit ihrer Antwort begonnen.", @@ -9,6 +11,9 @@ "com_agents_create_error": "Bei der Erstellung deines Agenten ist ein Fehler aufgetreten.", "com_agents_description_placeholder": "Optional: Beschreibe hier deinen Agenten", "com_agents_enable_file_search": "Dateisuche aktivieren", + "com_agents_file_context": "Datei-Kontext (OCR)", + "com_agents_file_context_disabled": "Der Agent muss vor dem Hochladen von Dateien für den Datei-Kontext erstellt werden.", + "com_agents_file_context_info": "Als „Kontext“ hochgeladene Dateien werden mit OCR verarbeitet, um Text zu extrahieren, der dann den Anweisungen des Agenten hinzugefügt wird. Ideal für Dokumente, Bilder mit Text oder PDFs, wenn Sie den vollständigen Textinhalt einer Datei benötigen", "com_agents_file_search_disabled": "Der Agent muss erstellt werden, bevor Dateien für die Dateisuche hochgeladen werden können.", "com_agents_file_search_info": "Wenn aktiviert, wird der Agent über die unten aufgelisteten exakten Dateinamen informiert und kann dadurch relevante Informationen aus diesen Dateien abrufen", "com_agents_instructions_placeholder": "Die Systemanweisungen, die der Agent verwendet", @@ -217,6 +222,7 @@ "com_endpoint_plug_use_functions": "Funktionen verwenden", "com_endpoint_presence_penalty": "Presence Penalty", "com_endpoint_preset": "Voreinstellung", + "com_endpoint_preset_custom_name_placeholder": "Leer – etwas fehlt noch", "com_endpoint_preset_default": "ist jetzt die Standardvoreinstellung.", "com_endpoint_preset_default_item": "Standard:", "com_endpoint_preset_default_none": "Keine Standardvoreinstellung aktiv.", @@ -459,12 +465,18 @@ "com_ui_admin_settings": "Admin-Einstellungen", "com_ui_advanced": "Erweitert", "com_ui_agent": "Agent", + "com_ui_agent_chain": "Agent-Kette", + "com_ui_agent_chain_info": "Ermöglicht das Erstellen von Agenten-Sequenzen. Jeder Agent kann auf die Ausgaben vorheriger Agenten in der Kette zugreifen. Basiert auf der \"Mixture-of-Agents\"-Architektur, bei der Agenten vorherige Ausgaben als zusätzliche Informationen verwenden.", + "com_ui_agent_chain_max": "Du hast die maximale Anzahl von {{0}} Agenten erreicht.", "com_ui_agent_delete_error": "Beim Löschen des Assistenten ist ein Fehler aufgetreten", "com_ui_agent_deleted": "Assistent erfolgreich gelöscht", "com_ui_agent_duplicate_error": "Beim Duplizieren des Assistenten ist ein Fehler aufgetreten", "com_ui_agent_duplicated": "Agent wurde erfolgreich dupliziert", "com_ui_agent_editing_allowed": "Andere Nutzende können diesen Agenten bereits bearbeiten", + "com_ui_agent_recursion_limit": "Maximale Agenten-Schritte", + "com_ui_agent_recursion_limit_info": "Begrenzt, wie viele Schritte der Agent in einem Durchlauf ausführen kann, bevor er eine endgültige Antwort gibt. Der Standardwert ist 25 Schritte. Ein Schritt ist entweder eine KI-API-Anfrage oder eine Werkzeugnutzungsrunde. Eine einfache Werkzeuginteraktion umfasst beispielsweise 3 Schritte: die ursprüngliche Anfrage, die Werkzeugnutzung und die Folgeanfrage.", "com_ui_agent_shared_to_all": "Hier muss etwas eingegeben werden. War leer.", + "com_ui_agent_var": "{{0}} Agent", "com_ui_agents": "Agenten", "com_ui_agents_allow_create": "Erstellung von Assistenten erlauben", "com_ui_agents_allow_share_global": "Assistenten für alle Nutzenden freigeben", @@ -649,6 +661,7 @@ "com_ui_generate_backup": "Backup-Codes generieren", "com_ui_generate_qrcode": "QR-Code generieren", "com_ui_generating": "Generiere …", + "com_ui_global_group": "Leer – etwas fehlt noch", "com_ui_go_back": "Zurück", "com_ui_go_to_conversation": "Zur Konversation gehen", "com_ui_happy_birthday": "Es ist mein 1. Geburtstag!", @@ -694,7 +707,9 @@ "com_ui_no_bookmarks": "Du hast noch keine Lesezeichen. Klicke auf einen Chat und füge ein neues hinzu", "com_ui_no_category": "Keine Kategorie", "com_ui_no_changes": "Keine Änderungen zum Aktualisieren", + "com_ui_no_data": "Leer – etwas fehlt noch", "com_ui_no_terms_content": "Keine Inhalte der Allgemeinen Geschäftsbedingungen zum Anzeigen", + "com_ui_no_valid_items": "Leer - Text fehlt noch", "com_ui_none": "Keine", "com_ui_none_selected": "Nichts ausgewählt", "com_ui_not_used": "Nicht verwendet", @@ -764,6 +779,7 @@ "com_ui_share_create_message": "Ihr Name und alle Nachrichten, die du nach dem Teilen hinzufügst, bleiben privat.", "com_ui_share_delete_error": "Beim Löschen des geteilten Links ist ein Fehler aufgetreten", "com_ui_share_error": "Beim Teilen des Chat-Links ist ein Fehler aufgetreten", + "com_ui_share_form_description": "Leer - Text fehlt noch", "com_ui_share_link_to_chat": "Link zum Chat teilen", "com_ui_share_to_all_users": "Mit allen Benutzern teilen", "com_ui_share_update_message": "Ihr Name, benutzerdefinierte Anweisungen und alle Nachrichten, die du nach dem Teilen hinzufügen, bleiben privat.", @@ -803,12 +819,14 @@ "com_ui_upload_code_files": "Hochladen für Code-Interpreter", "com_ui_upload_delay": "Das Hochladen von \"{{0}}\" dauert etwas länger. Bitte warte, während die Datei für den Abruf indexiert wird.", "com_ui_upload_error": "Beim Hochladen Ihrer Datei ist ein Fehler aufgetreten", + "com_ui_upload_file_context": "Kontext der Datei hochladen", "com_ui_upload_file_search": "Hochladen für Dateisuche", "com_ui_upload_files": "Dateien hochladen", "com_ui_upload_image": "Ein Bild hochladen", "com_ui_upload_image_input": "Bild hochladen", "com_ui_upload_invalid": "Ungültige Datei zum Hochladen. Muss ein Bild sein und das Limit nicht überschreiten", "com_ui_upload_invalid_var": "Ungültige Datei zum Hochladen. Muss ein Bild sein und {{0}} MB nicht überschreiten", + "com_ui_upload_ocr_text": "Hochladen als Text", "com_ui_upload_success": "Datei erfolgreich hochgeladen", "com_ui_upload_type": "Upload-Typ auswählen", "com_ui_use_2fa_code": "Stattdessen 2FA-Code verwenden", diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 58dd833c27..6e5730050f 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -104,7 +104,6 @@ "com_auth_google_login": "Continue with Google", "com_auth_here": "HERE", "com_auth_login": "Login", - "com_ui_redirecting_to_provider": "Redirecting to {{0}}, please wait...", "com_auth_login_with_new_password": "You may now login with your new password.", "com_auth_name_max_length": "Name must be less than 80 characters", "com_auth_name_min_length": "Name must be at least 3 characters", @@ -265,7 +264,7 @@ "com_error_files_upload": "An error occurred while uploading the file.", "com_error_files_upload_canceled": "The file upload request was canceled. Note: the file upload may still be processing and will need to be manually deleted.", "com_error_files_validation": "An error occurred while validating the file.", - "com_error_input_length": "The latest message token count is too long, exceeding the token limit ({{0}} respectively). Please shorten your message, adjust the max context size from the conversation parameters, or fork the conversation to continue.", + "com_error_input_length": "The latest message token count is too long, exceeding the token limit, or your token limit parameters are misconfigured, adversely affecting the context window. More info: {{0}}. Please shorten your message, adjust the max context size from the conversation parameters, or fork the conversation to continue.", "com_error_invalid_user_key": "Invalid key provided. Please provide a valid key and try again.", "com_error_moderation": "It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We're unable to proceed with this specific topic. If you have any other questions or topics you'd like to explore, please edit your message, or create a new conversation.", "com_error_no_base_url": "No base URL found. Please provide one and try again.", @@ -741,6 +740,7 @@ "com_ui_prompts_allow_use": "Allow using Prompts", "com_ui_provider": "Provider", "com_ui_read_aloud": "Read aloud", + "com_ui_redirecting_to_provider": "Redirecting to {{0}}, please wait...", "com_ui_refresh_link": "Refresh link", "com_ui_regenerate": "Regenerate", "com_ui_regenerate_backup": "Regenerate Backup Codes", diff --git a/client/src/locales/et/translation.json b/client/src/locales/et/translation.json index 7e4956ec5a..cec2b74e49 100644 --- a/client/src/locales/et/translation.json +++ b/client/src/locales/et/translation.json @@ -11,6 +11,8 @@ "com_agents_create_error": "Agendi loomisel tekkis viga.", "com_agents_description_placeholder": "Valikuline: Kirjelda oma agenti siin", "com_agents_enable_file_search": "Luba failiotsing", + "com_agents_file_context": "Faili kontekst (OCR)", + "com_agents_file_context_disabled": "Agent tuleb luua enne failide üleslaadimist failikontekstiks.", "com_agents_file_search_disabled": "Agent tuleb luua enne failide üleslaadimist failiotsinguks.", "com_agents_file_search_info": "Kui see on lubatud, teavitatakse agenti täpselt allpool loetletud failinimedest, mis võimaldab tal nendest failidest asjakohast konteksti hankida.", "com_agents_instructions_placeholder": "Süsteemijuhised, mida agent kasutab", @@ -812,6 +814,7 @@ "com_ui_upload_image_input": "Laadi pilt üles", "com_ui_upload_invalid": "Fail on üleslaadimiseks vigane. Peab olema pilt, mis ei ületa piirangut", "com_ui_upload_invalid_var": "Fail on üleslaadimiseks vigane. Peab olema pilt, mis ei ületa {{0}} MB", + "com_ui_upload_ocr_text": "Laadi üles tekstina", "com_ui_upload_success": "Faili üleslaadimine õnnestus", "com_ui_upload_type": "Vali üleslaadimise tüüp", "com_ui_use_2fa_code": "Kasuta hoopis 2FA koodi", diff --git a/client/src/locales/it/translation.json b/client/src/locales/it/translation.json index 5c6f27c454..44dc08932d 100644 --- a/client/src/locales/it/translation.json +++ b/client/src/locales/it/translation.json @@ -9,6 +9,8 @@ "com_agents_create_error": "Si è verificato un errore durante la creazione del tuo agente.", "com_agents_description_placeholder": "Opzionale: Descrivi qui il tuo Agente", "com_agents_enable_file_search": "Abilita Ricerca File", + "com_agents_file_context": "Contesto del File (OCR)", + "com_agents_file_context_disabled": "L'agente deve essere creato prima di caricare i file per il Contesto del File.", "com_agents_file_search_disabled": "L'Agente deve essere creato prima di caricare file per la Ricerca File.", "com_agents_file_search_info": "Quando abilitato, l'agente verrà informato dei nomi esatti dei file elencati di seguito, permettendogli di recuperare il contesto pertinente da questi file.", "com_agents_instructions_placeholder": "Le istruzioni di sistema utilizzate dall'agente", @@ -18,13 +20,16 @@ "com_agents_not_available": "Agente Non Disponibile", "com_agents_search_name": "Cerca agenti per nome", "com_agents_update_error": "Si è verificato un errore durante l'aggiornamento del tuo agente.", + "com_assistants_action_attempt": "L'assistente vuole parlare con {{0}}", "com_assistants_actions": "Azioni", "com_assistants_actions_disabled": "Devi prima creare un assistente prima di aggiungere azioni.", "com_assistants_actions_info": "Permetti al tuo Assistente di recuperare informazioni o eseguire azioni tramite API", "com_assistants_add_actions": "Aggiungi Azioni", "com_assistants_add_tools": "Aggiungi Strumenti", + "com_assistants_allow_sites_you_trust": "Consenti solo i siti di cui ti fidati.", "com_assistants_append_date": "Aggiungi Data e Ora Attuali", "com_assistants_append_date_tooltip": "Quando attivo, la data e l'ora attuali del cliente saranno aggiunte alle istruzioni del sistema dell'Assistente.", + "com_assistants_attempt_info": "L'assistente vuole inviare quanto segue:", "com_assistants_available_actions": "Azioni Disponibili", "com_assistants_capabilities": "Capacità", "com_assistants_code_interpreter": "Interprete Codice", @@ -82,6 +87,7 @@ "com_auth_email_verification_redirecting": "Reindirizzamento in {{0}} secondi...", "com_auth_email_verification_resend_prompt": "Non hai ricevuto l'email?", "com_auth_email_verification_success": "Email verificata con successo", + "com_auth_email_verifying_ellipsis": "Verifica in corso...", "com_auth_error_create": "Si è verificato un errore durante il tentativo di registrare il tuo account. Riprova.", "com_auth_error_invalid_reset_token": "Questo token di reset della password non è più valido.", "com_auth_error_login": "Impossibile eseguire l'accesso con le informazioni fornite. Controlla le tue credenziali e riprova.", @@ -118,9 +124,11 @@ "com_auth_submit_registration": "Invia registrazione", "com_auth_to_reset_your_password": "per reimpostare la tua password.", "com_auth_to_try_again": "per riprovare.", + "com_auth_two_factor": "Controlla la tua applicazione preferita per le password monouso per un codice", "com_auth_username": "Nome utente (opzionale)", "com_auth_username_max_length": "Il nome utente deve essere inferiore a 20 caratteri", "com_auth_username_min_length": "Il nome utente deve essere di almeno 2 caratteri", + "com_auth_verify_your_identity": "Verifica la propria identità", "com_auth_welcome_back": "Ben tornato", "com_click_to_download": "clicca qui per scaricare", "com_download_expired": "download scaduto", @@ -133,6 +141,8 @@ "com_endpoint_anthropic_maxoutputtokens": "Numero massimo di token che possono essere generati nella risposta. Specifica un valore più basso per risposte più brevi e un valore più alto per risposte più lunghe.", "com_endpoint_anthropic_prompt_cache": "La cache dei prompt permette di riutilizzare contesti o istruzioni estese tra le chiamate API, riducendo costi e latenza", "com_endpoint_anthropic_temp": "Varia da 0 a 1. Usa temp più vicino a 0 per analitica / scelta multipla, e più vicino a 1 per compiti creativi e generativi. Consigliamo di modificare questo o Top P ma non entrambi.", + "com_endpoint_anthropic_thinking": "Abilita il ragionamento interno per i modelli Claude supportati (3.7 Sonnet). Nota: richiede che \"Thinking Budget\" sia impostato e inferiore a \"Massimo Output Token\"", + "com_endpoint_anthropic_thinking_budget": "Determina il numero massimo di token che Claude può utilizzare per il suo processo di ragionamento interno. Un budget maggiore può migliorare la qualità della risposta, consentendo un'analisi più approfondita di problemi complessi, anche se Claude potrebbe non utilizzare l'intero budget assegnato, soprattutto con intervalli superiori a 32K. Questa impostazione deve essere inferiore a \"Massimo Output Token\".", "com_endpoint_anthropic_topk": "Top-k cambia il modo in cui il modello seleziona i token per l'output. Un top-k di 1 significa che il token selezionato è il più probabile tra tutti i token nel vocabolario del modello (anche chiamato greedy decoding), mentre un top-k di 3 significa che il prossimo token è selezionato tra i 3 più probabili (usando la temperatura).", "com_endpoint_anthropic_topp": "Top-p cambia il modo in cui il modello seleziona i token per l'output. I token vengono selezionati dai più probabili K (vedi parametro topK) ai meno probabili fino a quando la somma delle loro probabilità eguaglia il valore top-p.", "com_endpoint_assistant": "Assistente", @@ -237,6 +247,8 @@ "com_endpoint_stop": "Sequenze di stop", "com_endpoint_stop_placeholder": "Separa i valori premendo `Invio`", "com_endpoint_temperature": "Temperatura", + "com_endpoint_thinking": "Ragionando", + "com_endpoint_thinking_budget": "Budget Ragionamento", "com_endpoint_top_k": "Top K", "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Usa Assistente Attivo", @@ -258,6 +270,7 @@ "com_files_number_selected": "{{0}} di {{1}} file selezionati", "com_generated_files": "File generati:", "com_hide_examples": "Nascondi esempi", + "com_nav_2fa": "Autenticazione a due fattori (2FA)", "com_nav_account_settings": "Impostazioni account", "com_nav_always_make_prod": "Rendi sempre produttive le nuove versioni", "com_nav_archive_created_at": "DateCreated", @@ -323,6 +336,7 @@ "com_nav_help_faq": "Guida e FAQ", "com_nav_hide_panel": "Nascondi il Pannello laterale più a destra", "com_nav_info_code_artifacts": "Abilita la visualizzazione di artefatti di codice sperimentali accanto alla chat", + "com_nav_info_code_artifacts_agent": "Abilita l'uso di artefatti di codice per questo agente. Per impostazione predefinita, vengono aggiunte istruzioni aggiuntive specifiche per l'uso degli artefatti, a meno che non sia abilitata la \"Modalità prompt personalizzato\".", "com_nav_info_custom_prompt_mode": "Quando attivata, l'istruzione predefinita del sistema per gli artefatti non verrà inclusa. In questa modalità, tutte le istruzioni per la generazione degli artefatti dovranno essere fornite manualmente.", "com_nav_info_enter_to_send": "Quando attivo, premendo `INVIO` invierai il tuo messaggio. Quando disattivato, premendo Invio andrai a capo, e dovrai premere `CTRL + INVIO` / `⌘ + INVIO` per inviare il messaggio.", "com_nav_info_fork_change_default": "\"Solo messaggi visibili\" include solo il percorso diretto al messaggio selezionato. \"Includi rami correlati\" aggiunge i rami lungo il percorso. \"Includi tutti i messaggi da/verso qui\" include tutti i messaggi e i rami connessi.", @@ -428,6 +442,16 @@ "com_sidepanel_parameters": "Parametri", "com_sidepanel_select_agent": "Seleziona un Agente", "com_sidepanel_select_assistant": "Seleziona un Assistente", + "com_ui_2fa_account_security": "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al vostro account", + "com_ui_2fa_disable": "Disattivare 2FA", + "com_ui_2fa_disable_error": "Si è verificato un errore nella disabilitazione dell'autenticazione a due fattori", + "com_ui_2fa_disabled": "Il 2FA è stato disattivato", + "com_ui_2fa_enable": "Abilitare 2FA", + "com_ui_2fa_enabled": "Il 2FA è stato abilitato", + "com_ui_2fa_generate_error": "Si è verificato un errore nella generazione delle impostazioni dell'autenticazione a due fattori", + "com_ui_2fa_invalid": "Codice di autenticazione a due fattori non valido", + "com_ui_2fa_setup": "Setup 2FA", + "com_ui_2fa_verified": "Autenticazione a due fattori verificata con successo", "com_ui_accept": "Accetto", "com_ui_add": "Aggiungi", "com_ui_add_model_preset": "Aggiungi un modello o una preimpostazione per una risposta aggiuntiva", @@ -436,12 +460,17 @@ "com_ui_admin_access_warning": "La disattivazione dell'accesso amministratore a questa funzionalità potrebbe causare problemi imprevisti all'interfaccia utente che richiedono un aggiornamento. Una volta salvata, l'unico modo per ripristinare è attraverso l'impostazione dell'interfaccia nel file di configurazione librechat.yaml, che influisce su tutti i ruoli.", "com_ui_admin_settings": "Impostazioni Amministratore", "com_ui_advanced": "Avanzate", + "com_ui_advanced_settings": "Impostazioni avanzate", "com_ui_agent": "Agente", + "com_ui_agent_chain": "Catena di agenti (Mixture-of-Agents)", + "com_ui_agent_chain_info": "Consente di creare sequenze di agenti. Ogni agente può accedere agli output degli agenti precedenti nella catena. Si basa sull'architettura \"Mixture-of-Agents\", in cui gli agenti utilizzano le uscite precedenti come informazioni ausiliarie.", + "com_ui_agent_chain_max": "Avete raggiunto il massimo di {{0}} agenti.", "com_ui_agent_delete_error": "Si è verificato un errore durante l'eliminazione dell'agente", "com_ui_agent_deleted": "Agente eliminato con successo", "com_ui_agent_duplicate_error": "Si è verificato un errore durante la duplicazione dell'assistente", "com_ui_agent_duplicated": "Agente duplicato con successo", "com_ui_agent_editing_allowed": "Altri utenti possono già modificare questo assistente", + "com_ui_agent_recursion_limit": "Passi massimi dell'agente", "com_ui_agents": "Agenti", "com_ui_agents_allow_create": "Consenti creazione Agenti", "com_ui_agents_allow_share_global": "Consenti la condivisione degli Agenti con tutti gli utenti", diff --git a/client/src/locales/nl/translation.json b/client/src/locales/nl/translation.json index de451e03c2..af578faeb2 100644 --- a/client/src/locales/nl/translation.json +++ b/client/src/locales/nl/translation.json @@ -1,13 +1,29 @@ { - "com_a11y_ai_composing": "De AI is nog steeds aan het componeren.", - "com_a11y_end": "De AI heeft zijn antwoord klaar.", - "com_a11y_start": "De AI is begonnen met hun antwoord.", - "com_agents_allow_editing": "Andere gebruikers toestaan om je agent te bewerken", - "com_agents_by_librechat": "door LibreChat", - "com_agents_code_interpreter": "Indien ingeschakeld, kan je agent de LibreChat Code Interpreter API gebruiken om gegenereerde code, inclusief bestandsverwerking, veilig uit te voeren. Vereist een geldige API-sleutel.", - "com_agents_code_interpreter_title": "Code-interpreter API", - "com_agents_create_error": "Er is een fout opgetreden bij het aanmaken van uw agent.", + "com_a11y_ai_composing": "De AI is nog bezig met het formuleren van een antwoord.", + "com_a11y_end": "De AI is klaar met het antwoord.", + "com_a11y_start": "De AI is begonnen met antwoorden.", + "com_agents_allow_editing": "Sta sndere gebruikers toe om je agent te bewerken", + "com_agents_by_librechat": "door LibreCha", + "com_agents_code_interpreter": "Indien ingeschakeld, kan je agent de LibreChat Code Interpreter API gebruiken om gegenereerde code, inclusief het verwerken van bestanden, veilig uit te voeren. Vereist een geldige API-sleutel.", + "com_agents_code_interpreter_title": "Code Interpreter API", + "com_agents_create_error": "Er is een fout opgetreden bij het aanmaken van je agent.", + "com_agents_description_placeholder": "Optioneel: Beschrijf hier je agent", + "com_agents_enable_file_search": "File Search inschakelen", + "com_agents_file_context": "File Context (OCR)", + "com_agents_file_context_disabled": "Agent moet worden aangemaakt voordat bestanden worden geüpload voor File Context", + "com_agents_file_context_info": "Bestanden die als \"Context\" worden geüpload, worden verwerkt met OCR voor tekstherkenning. De tekst wordt daarna toegevoegd aan de instructies van de Agent. Ideaal voor documenten, afbeeldingen met tekst of PDF's waarvan je de volledige tekstinhoud nodig hebt.\"", + "com_agents_file_search_disabled": "Maak eerst een Agent aan voordat je bestanden uploadt voor File Search.", + "com_agents_file_search_info": "Als deze functie is ingeschakeld, krijgt de agent informatie over de exacte bestandsnamen die hieronder staan vermeld, zodat deze relevante context uit deze bestanden kan ophalen.", "com_agents_instructions_placeholder": "De systeeminstructies die de agent gebruikt", + "com_agents_missing_provider_model": "Selecteer een provider en model voordat je een agent aanmaakt.", + "com_agents_name_placeholder": "De naam van de agent", + "com_agents_no_access": "Je hebt geen toegang om deze agent te bewerken.", + "com_agents_not_available": "Agent niet beschikbaar", + "com_agents_search_name": "Agents zoeken op naam", + "com_agents_update_error": "Er is een fout opgetreden bij het updaten van je agent.", + "com_assistants_action_attempt": "Assistent wil praten met {{0}}", + "com_assistants_actions": "Actions", + "com_assistants_actions_disabled": "Maak een assistent aan voordat je actions toevoegt.", "com_auth_already_have_account": "Heb je al een account?", "com_auth_click": "Klik", "com_auth_click_here": "Klik hier", @@ -146,7 +162,7 @@ "com_nav_font_size": "Lettertypegrootte", "com_nav_help_faq": "Help & FAQ", "com_nav_lang_arabic": "العربية", - "com_nav_lang_auto": "Automatisch detecteren", + "com_nav_lang_auto": "Auto detect", "com_nav_lang_brazilian_portuguese": "Português Brasileiro", "com_nav_lang_chinese": "中文", "com_nav_lang_dutch": "Nederlands", diff --git a/client/src/locales/pl/translation.json b/client/src/locales/pl/translation.json index d00b97ba71..1a7c699f54 100644 --- a/client/src/locales/pl/translation.json +++ b/client/src/locales/pl/translation.json @@ -2,32 +2,41 @@ "com_a11y_ai_composing": "AI nadal komponuje.", "com_a11y_end": "AI zakończył swoją odpowiedź.", "com_a11y_start": "AI rozpoczął swoją odpowiedź.", + "com_agents_allow_editing": "Zezwól by inni użytkownicy mogli edytować twojego agenta", "com_agents_by_librechat": "od LibreChat", "com_agents_code_interpreter_title": "API interpretera kodu", "com_agents_create_error": "Wystąpił błąd podczas tworzenia agenta.", "com_agents_description_placeholder": "Opcjonalnie: Opisz swojego agenta tutaj", "com_agents_enable_file_search": "Włącz wyszukiwanie plików", + "com_agents_file_context": "Kontest Pliku (OCR)", + "com_agents_file_context_disabled": "Agent musi zostać utworzony przed przesłaniem plików dla Kontekstu Plików", + "com_agents_file_context_info": "Pliki przesłane jako \"Kontekst\" są przetworzone przez OCR by wydobyć tekst, który potem jest dodany do instrukcji Agenta. Jest to idealne dla dokumentów, obrazów z tekstem oraz plików PDF, gdzie potrzebujesz całego tekstu z pliku.", "com_agents_file_search_disabled": "Agent musi zostać utworzony przed przesłaniem plików do wyszukiwania.", "com_agents_file_search_info": "Po włączeniu agent zostanie poinformowany o dokładnych nazwach plików wymienionych poniżej, co pozwoli mu na pobranie odpowiedniego kontekstu z tych plików.", "com_agents_instructions_placeholder": "Instrukcje systemowe używane przez agenta", "com_agents_missing_provider_model": "Wybierz dostawcę i model przed utworzeniem agenta.", "com_agents_name_placeholder": "Opcjonalnie: Nazwa agenta", + "com_agents_no_access": "Nie masz zezwolenia na edycję tego agenta.", + "com_agents_not_available": "Agent nie jest dostępny", "com_agents_search_name": "Wyszukaj agentów po nazwie", "com_agents_update_error": "Wystąpił błąd podczas aktualizacji agenta.", + "com_assistants_action_attempt": "Asysten chce rozmawiać z {{0}}", "com_assistants_actions": "Akcje", "com_assistants_actions_disabled": "Musisz utworzyć asystenta przed dodaniem akcji.", "com_assistants_actions_info": "Pozwól swojemu Asystentowi pobierać informacje lub podejmować działania poprzez API", "com_assistants_add_actions": "Dodaj akcje", "com_assistants_add_tools": "Dodaj narzędzia", + "com_assistants_allow_sites_you_trust": "Zezwól tylko na strony, którym ufasz.", "com_assistants_append_date": "Dodaj aktualną datę i czas", "com_assistants_append_date_tooltip": "Po włączeniu, aktualna data i czas klienta zostaną dodane do instrukcji systemowych asystenta.", + "com_assistants_attempt_info": "Asystent chce wysłać następującą treść: ", "com_assistants_available_actions": "Dostępne akcje", "com_assistants_capabilities": "Możliwości", "com_assistants_code_interpreter": "Interpreter kodu", "com_assistants_code_interpreter_files": "Poniższe pliki są tylko dla interpretera kodu:", "com_assistants_code_interpreter_info": "Interpreter kodu umożliwia asystentowi pisanie i uruchamianie kodu. To narzędzie może przetwarzać pliki z różnymi danymi i formatowaniem oraz generować pliki, takie jak wykresy.", - "com_assistants_completed_action": "Rozmawiał z {0}", - "com_assistants_completed_function": "Uruchomiono {0}", + "com_assistants_completed_action": "Rozmawiał z {{0}}", + "com_assistants_completed_function": "Uruchomiono {{0}}", "com_assistants_conversation_starters": "Rozpoczęcie rozmowy", "com_assistants_conversation_starters_placeholder": "Wprowadź rozpoczęcie rozmowy", "com_assistants_create_error": "Wystąpił błąd podczas tworzenia asystenta.", @@ -35,10 +44,10 @@ "com_assistants_delete_actions_error": "Wystąpił błąd podczas usuwania akcji.", "com_assistants_delete_actions_success": "Pomyślnie usunięto akcję z asystenta", "com_assistants_description_placeholder": "Opcjonalnie: Opisz swojego asystenta tutaj", - "com_assistants_domain_info": "Asystent wysłał te informacje do {0}", + "com_assistants_domain_info": "Asystent wysłał te informacje do {{0}}", "com_assistants_file_search": "Wyszukiwanie plików", "com_assistants_file_search_info": "Wyszukiwanie plików umożliwia asystentowi dostęp do wiedzy z plików przesłanych przez ciebie lub twoich użytkowników. Po przesłaniu pliku asystent automatycznie decyduje, kiedy pobierać treść na podstawie żądań użytkownika. Dołączanie magazynów wektorowych do wyszukiwania plików nie jest jeszcze obsługiwane. Możesz je dołączyć z Playground dostawcy lub dołączyć pliki do wiadomości w celu wyszukiwania plików na podstawie wątku.", - "com_assistants_function_use": "Asystent użył {0}", + "com_assistants_function_use": "Asystent użył {{0}}", "com_assistants_image_vision": "Widzenie obrazu", "com_assistants_instructions_placeholder": "Instrukcje systemowe używane przez asystenta", "com_assistants_knowledge": "Wiedza", @@ -46,6 +55,7 @@ "com_assistants_knowledge_info": "Jeśli prześlesz pliki w sekcji Wiedza, rozmowy z twoim Asystentem mogą zawierać treść plików.", "com_assistants_max_starters_reached": "Osiągnięto maksymalną liczbę rozpoczęć rozmowy", "com_assistants_name_placeholder": "Opcjonalnie: Nazwa asystenta", + "com_assistants_non_retrieval_model": "Wyszukiwanie w plikach nie jest włączone dla tego modelu. Proszę wybierz inny model.", "com_assistants_retrieval": "Pobieranie", "com_assistants_running_action": "Uruchomiona akcja", "com_assistants_search_name": "Wyszukaj asystentów po nazwie", @@ -74,11 +84,17 @@ "com_auth_email_verification_failed_token_missing": "Weryfikacja nie powiodła się, brak tokenu", "com_auth_email_verification_in_progress": "Weryfikacja twojego emaila, proszę czekać", "com_auth_email_verification_invalid": "Nieprawidłowa weryfikacja email", - "com_auth_email_verification_redirecting": "Przekierowanie za {0} sekund...", + "com_auth_email_verification_redirecting": "Przekierowanie za {{0}} sekund...", + "com_auth_email_verification_resend_prompt": "Nie otrzymałeś maila?", "com_auth_email_verification_success": "Email zweryfikowany pomyślnie", + "com_auth_email_verifying_ellipsis": "Weryfikowanie...", "com_auth_error_create": "Wystąpił błąd podczas tworzenia konta. Spróbuj ponownie.", "com_auth_error_invalid_reset_token": "Ten token do resetowania hasła jest już nieważny.", "com_auth_error_login": "Nie udało się zalogować przy użyciu podanych danych. Sprawdź swoje dane logowania i spróbuj ponownie.", + "com_auth_error_login_ban": "Twoje konto zostało tymczasowo zablokowane z powodu naruszeń reguł korzystania z naszego serwisu.", + "com_auth_error_login_rl": "Zbyt wiele prób logowania w krótkim czasie. Spróbuj ponownie później.", + "com_auth_error_login_server": "Wystąpił wewnętrzny błąd serwera,. Proszę, poczekaj chwilę i spróbuj ponownie.", + "com_auth_error_login_unverified": "Twoje konto nie jest zweryfikowane. Sprawdź swoją skrzynkę email by znaleźć link weryfikacyjny.", "com_auth_facebook_login": "Zaloguj się przez Facebooka", "com_auth_full_name": "Pełne imię", "com_auth_github_login": "Zaloguj się przez Githuba", @@ -113,7 +129,7 @@ "com_auth_welcome_back": "Witamy z powrotem", "com_click_to_download": "(kliknij tutaj, aby pobrać)", "com_download_expired": "(pobieranie wygasło)", - "com_download_expires": "(kliknij tutaj, aby pobrać - wygasa {0})", + "com_download_expires": "(kliknij tutaj, aby pobrać - wygasa {{0}})", "com_endpoint": "Punkt końcowy", "com_endpoint_agent": "Agent", "com_endpoint_agent_model": "Model agenta (zalecany: GPT-3.5)", @@ -121,6 +137,7 @@ "com_endpoint_ai": "AI", "com_endpoint_anthropic_maxoutputtokens": "Maksymalna liczba tokenów, która może zostać wygenerowana w odpowiedzi. Wybierz mniejszą wartość dla krótszych odpowiedzi i większą wartość dla dłuższych odpowiedzi.", "com_endpoint_anthropic_temp": "Zakres od 0 do 1. Użyj wartości bliżej 0 dla analizy/wyboru wielokrotnego, a bliżej 1 dla zadań twórczych i generatywnych. Zalecamy dostosowanie tej wartości lub Top P, ale nie obu jednocześnie.", + "com_endpoint_anthropic_thinking": "Włącza wewnętrzne rozumowanie dla wspieranych modeli Claude (3.7 Sonnet). Notatka: wymaga \"Thinking Budget\" by był włączony oraz mniejszy niż \"Max Output Tokens\".", "com_endpoint_anthropic_topk": "Top-K wpływa na sposób wyboru tokenów przez model. Top-K równa 1 oznacza, że wybrany token jest najbardziej prawdopodobny spośród wszystkich tokenów w słowniku modelu (tzw. dekodowanie zachłanne), podczas gdy top-K równa 3 oznacza, że następny token zostaje wybrany spośród 3 najbardziej prawdopodobnych tokenów (za pomocą temperatury).", "com_endpoint_anthropic_topp": "Top-P wpływa na sposób wyboru tokenów przez model. Tokeny wybierane są od najbardziej prawdopodobnych do najmniej prawdopodobnych, aż suma ich prawdopodobieństw osiągnie wartość top-P.", "com_endpoint_assistant": "Asystent", @@ -137,6 +154,7 @@ "com_endpoint_config_key": "Ustaw klucz API", "com_endpoint_config_key_encryption": "Twój klucz zostanie zaszyfrowany i usunięty o", "com_endpoint_config_key_for": "Ustaw klucz API dla", + "com_endpoint_config_key_google_need_to": "Powinieneś ", "com_endpoint_config_key_import_json_key": "Importuj klucz JSON konta usługi.", "com_endpoint_config_key_import_json_key_invalid": "Nieprawidłowy klucz JSON konta usługi. Czy zaimportowano właściwy plik?", "com_endpoint_config_key_import_json_key_success": "Pomyślnie zaimportowano klucz JSON konta usługi", @@ -164,7 +182,7 @@ "com_endpoint_instructions_assistants": "Nadpisz instrukcje", "com_endpoint_max_output_tokens": "Maksymalna liczba tokenów wyjściowych", "com_endpoint_message": "Wiadomość", - "com_endpoint_message_new": "Wiadomość {0}", + "com_endpoint_message_new": "Wiadomość {{0}}", "com_endpoint_message_not_appendable": "Edytuj swoją wiadomość lub wygeneruj ponownie.", "com_endpoint_my_preset": "Moje predefiniowane ustawienie", "com_endpoint_no_presets": "Brak zapisanych predefiniowanych ustawień", @@ -214,21 +232,21 @@ "com_endpoint_top_k": "Top K", "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Użyj aktywnego asystenta", - "com_error_expired_user_key": "Podany klucz dla {0} wygasł w {1}. Proszę podać nowy klucz i spróbować ponownie.", + "com_error_expired_user_key": "Podany klucz dla {{0}} wygasł w {{1}}. Proszę podać nowy klucz i spróbować ponownie.", "com_error_files_dupe": "Wykryto zduplikowany plik.", "com_error_files_empty": "Puste pliki nie są dozwolone.", "com_error_files_process": "Wystąpił błąd podczas przetwarzania pliku.", "com_error_files_upload": "Wystąpił błąd podczas przesyłania pliku.", "com_error_files_upload_canceled": "Żądanie przesłania pliku zostało anulowane. Uwaga: przesyłanie pliku może nadal być przetwarzane i będzie wymagało ręcznego usunięcia.", "com_error_files_validation": "Wystąpił błąd podczas walidacji pliku.", - "com_error_input_length": "Liczba tokenów najnowszej wiadomości jest zbyt duża, przekraczając limit tokenów ({0}). Proszę skrócić swoją wiadomość, dostosować maksymalny rozmiar kontekstu z parametrów rozmowy lub rozgałęzić rozmowę, aby kontynuować.", + "com_error_input_length": "Liczba tokenów najnowszej wiadomości jest zbyt duża, przekraczając limit tokenów ({{0}}). Proszę skrócić swoją wiadomość, dostosować maksymalny rozmiar kontekstu z parametrów rozmowy lub rozgałęzić rozmowę, aby kontynuować.", "com_error_invalid_user_key": "Podano nieprawidłowy klucz. Podaj prawidłowy klucz i spróbuj ponownie.", "com_error_moderation": "Wygląda na to, że przesłana treść została oznaczona przez nasz system moderacji jako niezgodna z naszymi wytycznymi społeczności. Nie możemy kontynuować z tym konkretnym tematem. Jeśli masz inne pytania lub tematy do omówienia, proszę edytuj swoją wiadomość lub utwórz nową rozmowę.", "com_error_no_base_url": "Nie znaleziono podstawowego URL. Podaj go i spróbuj ponownie.", "com_error_no_user_key": "Nie znaleziono klucza. Podaj klucz i spróbuj ponownie.", "com_files_filter": "Filtruj pliki...", "com_files_no_results": "Brak wyników.", - "com_files_number_selected": "{0} z {1} elementów wybranych", + "com_files_number_selected": "{{0}} z {{1}} elementów wybranych", "com_generated_files": "Wygenerowane pliki:", "com_hide_examples": "Ukryj przykłady", "com_nav_account_settings": "Ustawienia konta", @@ -239,8 +257,8 @@ "com_nav_archived_chats_empty": "Nie masz żadnych zarchiwizowanych rozmów.", "com_nav_at_command": "Polecenie @", "com_nav_at_command_description": "Przełącz polecenie \"@\" do przełączania punktów końcowych, modeli, presetów, itp.", - "com_nav_audio_play_error": "Błąd odtwarzania audio: {0}", - "com_nav_audio_process_error": "Błąd przetwarzania audio: {0}", + "com_nav_audio_play_error": "Błąd odtwarzania audio: {{0}}", + "com_nav_audio_process_error": "Błąd przetwarzania audio: {{0}}", "com_nav_auto_scroll": "Automatyczne przewijanie do najnowszej wiadomości przy otwarciu czatu", "com_nav_auto_send_prompts": "Automatycznie wysyłaj prompty", "com_nav_auto_send_text": "Automatycznie wysyłaj tekst", @@ -373,7 +391,7 @@ "com_nav_tool_dialog_description": "Asystent musi zostać zapisany, aby zachować wybrane narzędzia.", "com_nav_tool_remove": "Usuń", "com_nav_tool_search": "Wyszukaj narzędzia", - "com_nav_tts_init_error": "Nie udało się zainicjować tekstu na mowę: {0}", + "com_nav_tts_init_error": "Nie udało się zainicjować tekstu na mowę: {{0}}", "com_nav_tts_unsupported_error": "Tekst na mowę dla wybranego silnika nie jest obsługiwany w tej przeglądarce.", "com_nav_user": "Użytkownik", "com_nav_user_msg_markdown": "Renderuj wiadomości użytkownika jako markdown", @@ -504,7 +522,7 @@ "com_ui_delete_agent_confirm": "Czy na pewno chcesz usunąć tego agenta?", "com_ui_delete_assistant_confirm": "Czy na pewno chcesz usunąć tego Asystenta? Tej operacji nie można cofnąć.", "com_ui_delete_confirm": "Spowoduje to usunięcie", - "com_ui_delete_confirm_prompt_version_var": "Spowoduje to usunięcie wybranej wersji dla \"{0}.\" Jeśli nie istnieją inne wersje, prompt zostanie usunięty.", + "com_ui_delete_confirm_prompt_version_var": "Spowoduje to usunięcie wybranej wersji dla \"{{0}}.\" Jeśli nie istnieją inne wersje, prompt zostanie usunięty.", "com_ui_delete_conversation": "Usunąć czat?", "com_ui_delete_prompt": "Usunąć prompt?", "com_ui_delete_tool": "Usuń narzędzie", @@ -528,7 +546,7 @@ "com_ui_enter": "Wprowadź", "com_ui_enter_api_key": "Wprowadź klucz API", "com_ui_enter_openapi_schema": "Wprowadź swoją schemę OpenAPI tutaj", - "com_ui_enter_var": "Wprowadź {0}", + "com_ui_enter_var": "Wprowadź {{0}}", "com_ui_error": "Błąd", "com_ui_error_connection": "Błąd połączenia z serwerem, spróbuj odświeżyć stronę.", "com_ui_error_save_admin_settings": "Wystąpił błąd podczas zapisywania ustawień administratora.", @@ -546,7 +564,7 @@ "com_ui_fork_from_message": "Wybierz opcję rozgałęzienia", "com_ui_fork_info_1": "Użyj tego ustawienia, aby rozgałęzić wiadomości z pożądanym zachowaniem.", "com_ui_fork_info_2": "\"Rozgałęzianie\" odnosi się do tworzenia nowej rozmowy, która zaczyna/kończy się od określonych wiadomości w bieżącej rozmowie, tworząc kopię zgodnie z wybranymi opcjami.", - "com_ui_fork_info_3": "\"Wiadomość docelowa\" odnosi się do wiadomości, z której otwarto to okno, lub, jeśli zaznaczysz \"{0}\", do najnowszej wiadomości w rozmowie.", + "com_ui_fork_info_3": "\"Wiadomość docelowa\" odnosi się do wiadomości, z której otwarto to okno, lub, jeśli zaznaczysz \"{{0}}\", do najnowszej wiadomości w rozmowie.", "com_ui_fork_info_branches": "Ta opcja rozgałęzia widoczne wiadomości wraz z powiązanymi gałęziami; innymi słowy, bezpośrednią ścieżkę do wiadomości docelowej, włączając gałęzie wzdłuż ścieżki.", "com_ui_fork_info_remember": "Zaznacz to, aby zapamiętać wybrane opcje do przyszłego użycia, ułatwiając szybsze rozgałęzianie rozmów według preferencji.", "com_ui_fork_info_start": "Jeśli zaznaczone, rozgałęzianie rozpocznie się od tej wiadomości do najnowszej wiadomości w rozmowie, zgodnie z wybranym zachowaniem powyżej.", @@ -580,11 +598,11 @@ "com_ui_llm_menu": "Menu LLM", "com_ui_llms_available": "Dostępne LLM", "com_ui_locked": "Zablokowane", - "com_ui_logo": "Logo {0}", + "com_ui_logo": "Logo {{0}}", "com_ui_manage": "Zarządzaj", - "com_ui_max_tags": "Maksymalna dozwolona liczba to {0}, używane są najnowsze wartości.", + "com_ui_max_tags": "Maksymalna dozwolona liczba to {{0}}, używane są najnowsze wartości.", "com_ui_mention": "Wspomnij punkt końcowy, asystenta lub preset, aby szybko się przełączyć", - "com_ui_min_tags": "Nie można usunąć więcej wartości, wymagane minimum to {0}.", + "com_ui_min_tags": "Nie można usunąć więcej wartości, wymagane minimum to {{0}}.", "com_ui_model": "Model", "com_ui_model_parameters": "Parametry modelu", "com_ui_more_info": "Więcej informacji", @@ -626,12 +644,12 @@ "com_ui_region": "Region", "com_ui_rename": "Zmień nazwę", "com_ui_rename_prompt": "Zmień nazwę promptu", - "com_ui_reset_var": "Resetuj {0}", + "com_ui_reset_var": "Resetuj {{0}}", "com_ui_result": "Wynik", "com_ui_revoke": "Odwołaj", "com_ui_revoke_info": "Odwołaj wszystkie poświadczenia dostarczone przez użytkownika", "com_ui_revoke_key_confirm": "Czy na pewno chcesz odwołać ten klucz?", - "com_ui_revoke_key_endpoint": "Odwołaj klucz dla {0}", + "com_ui_revoke_key_endpoint": "Odwołaj klucz dla {{0}}", "com_ui_revoke_keys": "Odwołaj klucze", "com_ui_revoke_keys_confirm": "Czy na pewno chcesz odwołać wszystkie klucze?", "com_ui_role_select": "Rola", @@ -659,7 +677,7 @@ "com_ui_share_link_to_chat": "Udostępnij link w czacie", "com_ui_share_to_all_users": "Udostępnij wszystkim użytkownikom", "com_ui_share_update_message": "Twoje imię, niestandardowe instrukcje i jakiekolwiek wiadomości dodane po udostępnieniu pozostaną prywatne.", - "com_ui_share_var": "Udostępnij {0}", + "com_ui_share_var": "Udostępnij {{0}}", "com_ui_shared_link_bulk_delete_success": "Pomyślnie usunięto udostępnione linki", "com_ui_shared_link_delete_success": "Pomyślnie usunięto udostępniony link", "com_ui_shared_link_not_found": "Nie znaleziono linku udostępnionego", @@ -684,21 +702,21 @@ "com_ui_update": "Aktualizuj", "com_ui_upload": "Prześlij", "com_ui_upload_code_files": "Prześlij do interpretera kodu", - "com_ui_upload_delay": "Przesyłanie \"{0}\" trwa dłużej niż przewidywano. Proszę poczekać, aż plik zakończy indeksowanie do pobrania.", + "com_ui_upload_delay": "Przesyłanie \"{{0}}\" trwa dłużej niż przewidywano. Proszę poczekać, aż plik zakończy indeksowanie do pobrania.", "com_ui_upload_error": "Wystąpił błąd podczas przesyłania pliku", "com_ui_upload_file_search": "Prześlij do wyszukiwania plików", "com_ui_upload_files": "Prześlij pliki", "com_ui_upload_image": "Prześlij obraz", "com_ui_upload_image_input": "Prześlij obraz", "com_ui_upload_invalid": "Nieprawidłowy plik do przesłania. Musi być obrazem nieprzekraczającym limitu", - "com_ui_upload_invalid_var": "Nieprawidłowy plik do przesłania. Musi być obrazem nieprzekraczającym {0} MB", + "com_ui_upload_invalid_var": "Nieprawidłowy plik do przesłania. Musi być obrazem nieprzekraczającym {{0}} MB", "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}", + "com_ui_version_var": "Wersja {{0}}", "com_ui_versions": "Wersje", "com_ui_view_source": "Zobacz źródłowy czat", "com_ui_yes": "Tak", diff --git a/client/src/locales/ru/translation.json b/client/src/locales/ru/translation.json index 982a2df3e0..1a1e4cd4ec 100644 --- a/client/src/locales/ru/translation.json +++ b/client/src/locales/ru/translation.json @@ -9,6 +9,9 @@ "com_agents_create_error": "Произошла ошибка при создании вашего агента", "com_agents_description_placeholder": "Необязательно: описание вашего агента", "com_agents_enable_file_search": "Включить поиск файлов", + "com_agents_file_context": "Контекст файла (OCR)", + "com_agents_file_context_disabled": "Агент должен быть создан перед загрузкой файлов для контекста файла", + "com_agents_file_context_info": "Файлы, загруженные как «Контекст», обрабатываются с использованием OCR для извлечения текста, который затем добавляется в инструкции агента. Идеально подходит для документов, изображений с текстом или PDF-файлов, где требуется полный текстовый контент.", "com_agents_file_search_disabled": "Для загрузки файлов в Поиск необходимо сначала создать агента", "com_agents_file_search_info": "При включении агент получит доступ к точным названиям файлов, перечисленным ниже, что позволит ему извлекать из них релевантный контекст.", "com_agents_instructions_placeholder": "Системные инструкции, используемые агентом", @@ -18,13 +21,16 @@ "com_agents_not_available": "Агент недоступен", "com_agents_search_name": "Поиск агентов по имени", "com_agents_update_error": "Произошла ошибка при обновлении вашего агента.", + "com_assistants_action_attempt": "Ассистент хочет поговорить с {{0}}", "com_assistants_actions": "Действия", "com_assistants_actions_disabled": "Вам нужно сохранить ассистента, прежде чем добавлять Actions.", "com_assistants_actions_info": "Позвольте вашему ассистенту получать информацию или выполнять действия через API", "com_assistants_add_actions": "Добавить действия", "com_assistants_add_tools": "Добавить инструменты", + "com_assistants_allow_sites_you_trust": "Разрешайте только сайты, которым доверяете.", "com_assistants_append_date": "Добавить текущую дату и время", "com_assistants_append_date_tooltip": "Когда включено, текущая дата и время клиента будут добавлены к инструкциям системы Ассистента.", + "com_assistants_attempt_info": "Ассистент хочет отправить следующее:", "com_assistants_available_actions": "Доступные действия", "com_assistants_capabilities": "Возможности", "com_assistants_code_interpreter": "Интерпретатор кода", @@ -59,6 +65,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": "Нажмите здесь", @@ -81,6 +88,7 @@ "com_auth_email_verification_redirecting": "Перенаправление через {{0}} сек...", "com_auth_email_verification_resend_prompt": "Не получили письмо?", "com_auth_email_verification_success": "Адрес электронной почты успешно подтвержден", + "com_auth_email_verifying_ellipsis": "Подтверждение...", "com_auth_error_create": "Возникла ошибка при попытке зарегистрировать ваш аккаунт. Пожалуйста, попробуйте еще раз.", "com_auth_error_invalid_reset_token": "Этот токен сброса пароля больше не действителен.", "com_auth_error_login": "Не удалось войти с предоставленной информацией. Пожалуйста, проверьте ваши учетные данные и попробуйте снова.", @@ -117,9 +125,11 @@ "com_auth_submit_registration": "Отправить регистрацию", "com_auth_to_reset_your_password": "чтобы сбросить ваш пароль.", "com_auth_to_try_again": "чтобы попробовать снова.", + "com_auth_two_factor": "Проверьте код в выбранном вами приложении одноразовых паролей", "com_auth_username": "Имя пользователя (необязательно)", "com_auth_username_max_length": "Имя пользователя должно быть не более 20 символов", "com_auth_username_min_length": "Имя пользователя должно содержать не менее 2 символов", + "com_auth_verify_your_identity": "Подтвердите ваши идентификационные данные.", "com_auth_welcome_back": "Добро пожаловать", "com_click_to_download": "(нажмите для скачивания)", "com_download_expired": "срок скачивания истек", @@ -132,6 +142,8 @@ "com_endpoint_anthropic_maxoutputtokens": "Максимальное количество токенов, которые могут быть сгенерированы в ответе. Укажите меньшее значение для более коротких ответов и большее значение для более длинных ответов.", "com_endpoint_anthropic_prompt_cache": "Кэширование промтов позволяет повторно использовать большой контекст или инструкции между API-запросами, снижая затраты и задержки", "com_endpoint_anthropic_temp": "Диапазон значений от 0 до 1. Используйте значение temp ближе к 0 для аналитических / множественного выбора и ближе к 1 для креативных и генеративных задач. Мы рекомендуем изменять это или Top P, но не оба значения одновременно.", + "com_endpoint_anthropic_thinking": "Включает режим \"Рассуждение\" для поддерживаемых моделей Claude (3.7 Sonnet). \nПримечание: требуется установка «Бюджета на рассуждение» ниже «Максимального числа токенов на вывод».", + "com_endpoint_anthropic_thinking_budget": "Определяет максимальное количество токенов, которое Claude может использовать для режима \"Рассуждение\". Более высокий бюджет может повысить качество ответов за счёт более глубокого анализа сложных задач, хотя Claude может использовать не весь выделенный бюджет, особенно при значениях выше 32K. Этот параметр должен быть меньше, чем «Максимальное число токенов на вывод».", "com_endpoint_anthropic_topk": "Top K изменяет то, как модель выбирает токены для вывода. Top K равное 1 означает, что выбирается наиболее вероятный токен из всего словаря модели (так называемое жадное декодирование), а Top K равное 3 означает, что следующий токен выбирается из трех наиболее вероятных токенов (с использованием температуры).", "com_endpoint_anthropic_topp": "Top P изменяет то, как модель выбирает токены для вывода. Токены выбираются из наиболее вероятных (см. параметр topK) до наименее вероятных, пока сумма их вероятностей не достигнет значения top-p.", "com_endpoint_assistant": "Ассистент", @@ -194,6 +206,7 @@ "com_endpoint_openai_max_tokens": "Необязательное поле `max_tokens`, задающее максимальное количество токенов, которое может быть сгенерировано в ответе чата. Общая длина входных токенов и сгенерированных токенов ограничена длиной контекста модели. Вы можете получить ошибку, если это число превысит максимальную длину контекста.", "com_endpoint_openai_pres": "Число от -2.0 до 2.0. Положительные значения штрафуют новые токены на основе того, появляются ли они в тексте до сих пор, увеличивая вероятность модели говорить о новых темах.", "com_endpoint_openai_prompt_prefix_placeholder": "Задайте кастомные промпты для включения в системное сообщение. По умолчанию: нет", + "com_endpoint_openai_reasoning_effort": "Только для моделей o1: ограничивает затраты на рассуждение для моделей с поддержкой рассуждения. Снижение усилий на рассуждение может ускорить ответы и уменьшить количество токенов, используемых для размышлений.", "com_endpoint_openai_resend": "Повторно отправить все ранее прикрепленные изображения. Примечание: это может значительно увеличить стоимость токенов, и при большом количестве прикрепленных изображений могут возникнуть ошибки.", "com_endpoint_openai_resend_files": "Повторно отправить все ранее прикрепленные файлы. Примечание: это увеличит расход токенов, и при большом количестве вложений могут возникнуть ошибки.", "com_endpoint_openai_stop": "До 4 последовательностей, после которых API прекратит генерировать дальнейшие токены.", @@ -227,6 +240,7 @@ "com_endpoint_prompt_prefix_assistants": "Дополнительные инструкции", "com_endpoint_prompt_prefix_assistants_placeholder": "Задайте дополнительные инструкции или контекст сверху основных инструкций ассистента. Игнорируется, если пусто.", "com_endpoint_prompt_prefix_placeholder": "Задайте пользовательские инструкции или контекст. Игнорируется, если пусто.", + "com_endpoint_reasoning_effort": "Затраты на рассуждение", "com_endpoint_save_as_preset": "Сохранить как Пресет", "com_endpoint_search": "Поиск эндпоинта по имени", "com_endpoint_set_custom_name": "Задайте кастомное имя на случай, если вы сможете найти эту предустановку :)", @@ -234,6 +248,8 @@ "com_endpoint_stop": "Стоп-последовательности", "com_endpoint_stop_placeholder": "Разделяйте значения нажатием `Enter`", "com_endpoint_temperature": "Температура", + "com_endpoint_thinking": "Размышление", + "com_endpoint_thinking_budget": "Бюджет на размышления", "com_endpoint_top_k": "Top K", "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Использовать активного ассистента", @@ -255,6 +271,7 @@ "com_files_number_selected": "Выбрано {{0}} из {{1}} файл(а/ов)", "com_generated_files": "Сгенерированные файлы:", "com_hide_examples": "Скрыть примеры", + "com_nav_2fa": "Двухфакторная аутентификация (2FA)", "com_nav_account_settings": "Настройки аккаунта", "com_nav_always_make_prod": "Автоматически публиковать новые версии", "com_nav_archive_created_at": "Дата создания", @@ -320,6 +337,7 @@ "com_nav_help_faq": "Помощь и Вопросы", "com_nav_hide_panel": "Скрыть правую боковую панель", "com_nav_info_code_artifacts": "Включает отображение экспериментального программного кода рядом с чатом", + "com_nav_info_code_artifacts_agent": "Включает использование артефактов кода для этого агента. По умолчанию добавляются дополнительные инструкции, связанные с использованием артефактов, если не включен режим «Пользовательский промт».", "com_nav_info_custom_prompt_mode": "При включении этого режима системный промт по умолчанию для создания артефактов не будет использоваться. Все инструкции для генерации артефактов должны задаваться вручную.", "com_nav_info_enter_to_send": "Если включено, нажатие клавиши Enter отправит ваше сообщение. Если отключено, Enter добавит новую строку, а для отправки сообщения нужно будет нажать CTRL + Enter или ⌘ + Enter.", "com_nav_info_fork_change_default": "«Только видимые сообщения» включает лишь прямой путь к выбранному сообщению. «Включить связанные ветки» добавляет ответвления вдоль этого пути. «Включить все сообщения до/от этой точки» охватывает все связанные сообщения и ветки.", @@ -327,6 +345,7 @@ "com_nav_info_include_shadcnui": "При включении будут добавлены инструкции по использованию компонентов shadcn/ui. shadcn/ui — это набор переиспользуемых компонентов, созданных на основе Radix UI и Tailwind CSS. Примечание: эти инструкции довольно объемные, включайте их только если для вас важно информировать LLM о правильных импортах и компонентах. Подробнее о компонентах можно узнать на сайте: https://ui.shadcn.com/", "com_nav_info_latex_parsing": "При включении этой функции код LaTeX в сообщениях будет отображаться в виде математических формул. Если вам не требуется отображение LaTeX, отключение этой функции может улучшить производительность.", "com_nav_info_save_draft": "При включении этой функции текст и прикрепленные файлы, введенные в форму чата, будут автоматически сохраняться локально как черновики. Эти черновики останутся доступными даже после перезагрузки страницы или перехода к другому разговору. Черновики хранятся локально на вашем устройстве и удаляются после отправки сообщения.", + "com_nav_info_show_thinking": "Если включено, выпадающие блоки размышлений в чате будут открыты по умолчанию, позволяя видеть ход рассуждений ИИ в реальном времени. Если отключено, блоки размышлений будут скрыты по умолчанию для более упрощённого интерфейса.", "com_nav_info_user_name_display": "Если включено, над каждым вашим сообщением будет отображаться ваше имя пользователя. Если отключено, над вашими сообщениями будет отображаться только \"Вы\".", "com_nav_lang_arabic": "العربية", "com_nav_lang_auto": "Автоопределение", @@ -373,6 +392,7 @@ "com_nav_plus_command_description": "Переключить команду ' + ' для настройки множественных ответов", "com_nav_profile_picture": "Изображение профиля", "com_nav_save_drafts": "Сохранить черновики локально", + "com_nav_scroll_button": "Открывать блоки размышлений по умолчанию", "com_nav_search_placeholder": "Поиск сообщений", "com_nav_send_message": "Отправить сообщение", "com_nav_setting_account": "Аккаунт", @@ -384,6 +404,7 @@ "com_nav_settings": "Настройки", "com_nav_shared_links": "Связываемые ссылки", "com_nav_show_code": "Всегда показывать код при использовании интерпретатора", + "com_nav_show_thinking": "Открывать блоки размышлений по умолчанию", "com_nav_slash_command": "/-Команда", "com_nav_slash_command_description": "Вызов командной строки клавишей '/' для выбора промта с клавиатуры", "com_nav_source_buffer_error": "Ошибка при настройке воспроизведения звука. Пожалуйста, обновите страницу.", @@ -422,6 +443,16 @@ "com_sidepanel_parameters": "Параметры", "com_sidepanel_select_agent": "Выбрать Ассистента", "com_sidepanel_select_assistant": "Выбрать Ассистента", + "com_ui_2fa_account_security": "Двухфакторная аутентификация добавляет дополнительный уровень защиты вашему аккаунту.", + "com_ui_2fa_disable": "Отключить 2FA", + "com_ui_2fa_disable_error": "Произошла ошибка при отключении двухфакторной аутентификации.", + "com_ui_2fa_disabled": "Двухфакторная аутентификация (2FA) отключена", + "com_ui_2fa_enable": "Включить 2FA", + "com_ui_2fa_enabled": "Двухфакторная аутентификация (2FA) включена", + "com_ui_2fa_generate_error": "Произошла ошибка при создании настроек двухфакторной аутентификации", + "com_ui_2fa_invalid": "Неверный код двухфакторной аутентификации", + "com_ui_2fa_setup": "Настроить 2FA", + "com_ui_2fa_verified": "Двухфакторная аутентификация успешно подтверждена", "com_ui_accept": "Принимаю", "com_ui_add": "Добавить", "com_ui_add_model_preset": "Добавить модель или пресет для дополнительного ответа", @@ -430,23 +461,35 @@ "com_ui_admin_access_warning": "Отключение административного доступа к этой функции может вызвать непредвиденные проблемы с интерфейсом, требующие обновления страницы. После сохранения изменений вернуть настройку можно будет только через параметр interface в конфигурационном файле librechat.yaml, что повлияет на все роли.", "com_ui_admin_settings": "Настройки администратора", "com_ui_advanced": "Расширенные", + "com_ui_advanced_settings": "Дополнительные настройки", "com_ui_agent": "Агент", + "com_ui_agent_chain": "Цепочка агентов (\"Mixture-of-Agents\")", + "com_ui_agent_chain_info": "Позволяет создавать последовательности агентов, где каждый агент может использовать результаты работы предыдущих агентов в цепочке. Основано на архитектуре «Смешение агентов» (Mixture-of-Agents), в которой агенты используют предыдущие результаты в качестве вспомогательной информации.", + "com_ui_agent_chain_max": "Вы достигли максимального количества агентов: {{0}}.", "com_ui_agent_delete_error": "Произошла ошибка при удалении ассистента", "com_ui_agent_deleted": "Ассистент успешно удален", "com_ui_agent_duplicate_error": "Произошла ошибка при дублировании ассистента", "com_ui_agent_duplicated": "Ассистент успешно скопирован", "com_ui_agent_editing_allowed": "Другие пользователи уже могут редактировать этого ассистента", + "com_ui_agent_recursion_limit": "Максимальное количество шагов агента", + "com_ui_agent_recursion_limit_info": "Ограничивает количество шагов, которые агент может выполнить за один запуск перед выдачей окончательного ответа. Значение по умолчанию — 25 шагов. Шагом считается либо запрос к API ИИ, либо использование инструмента. Например, базовое взаимодействие с инструментом включает 3 шага: исходный запрос, использование инструмента и последующий запрос.", + "com_ui_agent_shared_to_all": "Анализ", + "com_ui_agent_var": "{{0}} агент", "com_ui_agents": "Агенты", "com_ui_agents_allow_create": "Разрешить создание ассистентов", "com_ui_agents_allow_share_global": "Разрешить доступ к Агентам всем пользователям", "com_ui_agents_allow_use": "Разрешить использование ассистентов", "com_ui_all": "все", "com_ui_all_proper": "Все", + "com_ui_analyzing": "Анализ", + "com_ui_analyzing_finished": "Анализ завершен", + "com_ui_api_key": "ключ API", "com_ui_archive": "Архивировать", "com_ui_archive_error": "Не удалось заархивировать чат", "com_ui_artifact_click": "Нажмите, чтобы открыть", "com_ui_artifacts": "Артефакты", "com_ui_artifacts_toggle": "Показать/скрыть артефакты", + "com_ui_artifacts_toggle_agent": "Включить артефакты", "com_ui_ascending": "По возрастанию", "com_ui_assistant": "Помощник", "com_ui_assistant_delete_error": "Произошла ошибка при удалении ассистента", @@ -459,10 +502,19 @@ "com_ui_attach_error_type": "Неподдерживаемый тип файла для этого режима:", "com_ui_attach_warn_endpoint": "Файлы сторонних приложений могут быть проигнорированы без совместимого плагина", "com_ui_attachment": "Вложение", + "com_ui_auth_type": "Тип аутентификации", + "com_ui_auth_url": "URL авторизации", "com_ui_authentication": "Аутентификация", + "com_ui_authentication_type": "Тип аутентификации", "com_ui_avatar": "Аватар", + "com_ui_azure": "Azure", "com_ui_back_to_chat": "Вернуться к чату", "com_ui_back_to_prompts": "Вернуться к промтам", + "com_ui_backup_codes": "Резервные коды", + "com_ui_backup_codes_regenerate_error": "Произошла ошибка при повторной генерации резервных кодов", + "com_ui_backup_codes_regenerated": "Резервные коды успешно сгенерированы повторно", + "com_ui_basic_auth_header": "Заголовок базовой авторизации", + "com_ui_bearer": "Токен на предъявителя", "com_ui_bookmark_delete_confirm": "Вы уверены, что хотите удалить эту закладку?", "com_ui_bookmarks": "Закладки", "com_ui_bookmarks_add": "Добавить закладку", @@ -481,17 +533,24 @@ "com_ui_bookmarks_title": "Заголовок", "com_ui_bookmarks_update_error": "Произошла ошибка при обновлении закладки", "com_ui_bookmarks_update_success": "Закладка успешно обновлена", + "com_ui_bulk_delete_error": "Не удалось удалить общие ссылки", "com_ui_cancel": "Отмена", "com_ui_chat": "Чат", "com_ui_chat_history": "История чатов", "com_ui_clear": "Удалить", "com_ui_clear_all": "Очистить всё", + "com_ui_client_id": "ID клиента", + "com_ui_client_secret": "Секрет клиента", "com_ui_close": "Закрыть", + "com_ui_close_menu": "Закрыть меню", "com_ui_code": "Код", "com_ui_collapse_chat": "Свернуть чат", "com_ui_command_placeholder": "Необязательно: введите команду для промта или будет использовано название", "com_ui_command_usage_placeholder": "Выберите промпт по команде или названию", + "com_ui_complete_setup": "Завершить настройку", "com_ui_confirm_action": "Подтвердить действие", + "com_ui_confirm_admin_use_change": "Изменение этого параметра заблокирует доступ для администраторов, включая вас. Вы уверены, что хотите продолжить?", + "com_ui_confirm_change": "Подтвердить изменения", "com_ui_context": "Контекст", "com_ui_continue": "Продолжить", "com_ui_controls": "Управление", @@ -503,6 +562,9 @@ "com_ui_create": "Создать", "com_ui_create_link": "Создать ссылку", "com_ui_create_prompt": "Создать промт", + "com_ui_currently_production": "В настоящее время в продакшене", + "com_ui_custom": "Настраиваемый", + "com_ui_custom_header_name": "Настраиваемое имя заголовка", "com_ui_custom_prompt_mode": "Режим пользовательского промта", "com_ui_dashboard": "Главная панель", "com_ui_date": "Дата", @@ -523,6 +585,7 @@ "com_ui_date_today": "Сегодня", "com_ui_date_yesterday": "Вчера", "com_ui_decline": "Не принимаю", + "com_ui_default_post_request": "По умолчанию (POST-запрос)", "com_ui_delete": "Удалить", "com_ui_delete_action": "Удалить действие", "com_ui_delete_action_confirm": "Вы действительно хотите удалить это действие?", @@ -538,6 +601,11 @@ "com_ui_descending": "По убыванию", "com_ui_description": "Описание", "com_ui_description_placeholder": "Дополнительно: введите описание для промта", + "com_ui_disabling": "Отключение...", + "com_ui_download": "Скачать", + "com_ui_download_artifact": "Скачать артифакт", + "com_ui_download_backup": "Скачать резервные коды", + "com_ui_download_backup_tooltip": "Прежде чем продолжить, скачайте ваши резервные коды. Они понадобятся вам для восстановления доступа в случае утери устройства аутентификации", "com_ui_download_error": "Ошибка загрузки файла. Возможно, файл был удален.", "com_ui_dropdown_variables": "Выпадающие переменные:", "com_ui_dropdown_variables_info": "Создавайте пользовательские выпадающие списки для ваших промптов: `{{название_переменной:вариант1|вариант2|вариант3}}`", @@ -559,7 +627,9 @@ "com_ui_examples": "Примеры", "com_ui_export_convo_modal": "Экспорт беседы", "com_ui_field_required": "Это поле обязательно для заполнения", + "com_ui_filter_prompts": "Фильтр промтов", "com_ui_filter_prompts_name": "Фильтровать промты по названию", + "com_ui_finance": "Финансы", "com_ui_fork": "Разделить", "com_ui_fork_all_target": "Включить все сюда", "com_ui_fork_branches": "Включить связанные ветки", @@ -582,46 +652,62 @@ "com_ui_fork_split_target_setting": "По умолчанию создавать ветку от целевого сообщения", "com_ui_fork_success": "Разветвление беседы успешно выполнено", "com_ui_fork_visible": "Только видимые сообщения", + "com_ui_generate_backup": "Создать резервные коды", + "com_ui_generate_qrcode": "Сгенерировать QR-код", + "com_ui_generating": "Генерация...", + "com_ui_go_back": "Назад", "com_ui_go_to_conversation": "Перейти к беседе", "com_ui_happy_birthday": "Это мой первый день рождения!", + "com_ui_hide_qr": "Ск", "com_ui_host": "Хост", + "com_ui_idea": "Идеи", "com_ui_image_gen": "Генератор изображений", + "com_ui_import": "Импорт", "com_ui_import_conversation_error": "При импорте бесед произошла ошибка", "com_ui_import_conversation_file_type_error": "Неподдерживаемый тип импорта", "com_ui_import_conversation_info": "Импортировать беседы из файла JSON", "com_ui_import_conversation_success": "Беседы успешно импортированы", "com_ui_include_shadcnui": "Включить компоненты shadcn/ui", + "com_ui_include_shadcnui_agent": "Включить инструкции shadcn/ui", "com_ui_input": "Ввод", "com_ui_instructions": "Инструкции", "com_ui_latest_footer": "Искусственный интеллект для каждого", + "com_ui_latest_production_version": "Последняя рабочая версия", + "com_ui_latest_version": "Последняя версия", "com_ui_librechat_code_api_key": "Получить ключ API интерпретатора кода LibreChat", "com_ui_librechat_code_api_subtitle": "Безопасно. Многоязычно. Работа с файлами.", "com_ui_librechat_code_api_title": "Запустить AI-код", "com_ui_llm_menu": "Меню LLM", "com_ui_llms_available": "Доступные языковые модели", + "com_ui_loading": "Загрузка...", "com_ui_locked": "Заблокировано", "com_ui_logo": "Логотип {{0}}", "com_ui_manage": "Управление", "com_ui_max_tags": "Максимально допустимое количество - {{0}}, используются последние значения.", "com_ui_mention": "Упомянуть конечную точку, помощника или предустановку для быстрого переключения", "com_ui_min_tags": "Нельзя удалить больше значений, требуется минимум {{0}}.", + "com_ui_misc": "Разное", "com_ui_model": "Модель", "com_ui_model_parameters": "Параметры модели", "com_ui_more_info": "Подробнее", "com_ui_my_prompts": "Мои промты", "com_ui_name": "Имя", + "com_ui_new": "Новый", "com_ui_new_chat": "Создать чат", "com_ui_next": "Следующий", "com_ui_no": "Нет", + "com_ui_no_backup_codes": "Резервные коды отсутствуют. Сгенерируйте новые", "com_ui_no_bookmarks": "Похоже, у вас пока нет закладок. Выберите чат и добавьте новую закладку", "com_ui_no_category": "Без категории", "com_ui_no_changes": "Нет изменений для обновления", "com_ui_no_terms_content": "Нет содержания условий использования для отображения", "com_ui_none_selected": "Ничего не выбрано", "com_ui_nothing_found": "Ничего не найдено", + "com_ui_oauth": "OAuth", "com_ui_of": "из", "com_ui_off": "Выкл.", "com_ui_on": "Вкл.", + "com_ui_openai": "OpenAI", "com_ui_page": "Страница", "com_ui_prev": "Предыдущий", "com_ui_preview": "Предпросмотр", @@ -641,9 +727,15 @@ "com_ui_prompts_allow_use": "Разрешить использование промтов", "com_ui_provider": "Провайдер", "com_ui_read_aloud": "Прочитать вслух", + "com_ui_redirecting_to_provider": "Перенаправление на {{0}}, пожалуйста, подождите...", + "com_ui_refresh_link": "Обновить ссылку", "com_ui_regenerate": "Повторная генерация", + "com_ui_regenerate_backup": "Сгенерировать резервные коды заново", + "com_ui_regenerating": "Повторная генерация...", "com_ui_region": "Регион", "com_ui_rename": "Переименовать", + "com_ui_rename_prompt": "Переименовать промт", + "com_ui_requires_auth": "Требуется аутентификация", "com_ui_reset_var": "Сбросить {{0}}", "com_ui_result": "Результат", "com_ui_revoke": "Отозвать", @@ -653,12 +745,15 @@ "com_ui_revoke_keys": "Отозвать ключи", "com_ui_revoke_keys_confirm": "Вы действительно хотите отозвать все ключи?", "com_ui_role_select": "Роль", + "com_ui_roleplay": "Ролевой режим", "com_ui_run_code": "Выполнить код", "com_ui_run_code_error": "Произошла ошибка при выполнении кода", "com_ui_save": "Сохранить", "com_ui_save_submit": "Сохранить и отправить", "com_ui_saved": "Сохранено!", "com_ui_schema": "Схема", + "com_ui_search": "Поиск", + "com_ui_secret_key": "Секретный ключ", "com_ui_select": "Выбрать", "com_ui_select_file": "Выберите файл", "com_ui_select_model": "Выберите модель", @@ -677,9 +772,15 @@ "com_ui_share_to_all_users": "Поделиться со всеми пользователями", "com_ui_share_update_message": "Ваше имя, пользовательские инструкции и любые сообщения, которые вы добавите после обмена, останутся конфиденциальными.", "com_ui_share_var": "Поделиться {{0}}", + "com_ui_shared_link_bulk_delete_success": "Общие ссылки успешно удалены", + "com_ui_shared_link_delete_success": "Общая ссылка успешно удалена", "com_ui_shared_link_not_found": "Общая ссылка не найдена", "com_ui_shared_prompts": "Общие промты", + "com_ui_shop": "Покупки", + "com_ui_show": "Показать", "com_ui_show_all": "Показать все", + "com_ui_show_qr": "Показать QR код", + "com_ui_sign_in_to_domain": "Вход в {{0}}", "com_ui_simple": "Простой", "com_ui_size": "Размер", "com_ui_special_variables": "Специальные переменные:", @@ -688,9 +789,16 @@ "com_ui_stop": "Остановить генерацию", "com_ui_storage": "Хранилище", "com_ui_submit": "Отправить", + "com_ui_teach_or_explain": "Обучение", + "com_ui_temporary_chat": "Временный чат", "com_ui_terms_and_conditions": "Условия использования", "com_ui_terms_of_service": "Условия использования", + "com_ui_thinking": "Думаю...", + "com_ui_thoughts": "Мысли", + "com_ui_token_exchange_method": "Метод обмена токена", + "com_ui_token_url": "URL токена", "com_ui_tools": "Инструменты", + "com_ui_travel": "Путешествия", "com_ui_unarchive": "разархивировать", "com_ui_unarchive_error": "Не удалось восстановить чат из архива", "com_ui_unknown": "Неизвестно", @@ -699,20 +807,27 @@ "com_ui_upload_code_files": "Загрузить для Интерпретатора кода", "com_ui_upload_delay": "Загрузка \"{{0}}\" занимает больше времени, чем ожидалось. Пожалуйста, подождите, пока файл полностью проиндексируется для доступа.", "com_ui_upload_error": "Произошла ошибка при загрузке вашего файла", + "com_ui_upload_file_context": "Загрузить файл контекста", "com_ui_upload_file_search": "Загрузить для поиска по файлам", "com_ui_upload_files": "Загрузить файлы", "com_ui_upload_image": "Загрузить изображение", "com_ui_upload_image_input": "Загрузить изображение", "com_ui_upload_invalid": "Недопустимый файл для загрузки. Загружаемое изображение не должно превышать установленный размер", "com_ui_upload_invalid_var": "Недопустимый файл. Загружаемое изображение не должно превышать {{0}} МБ", + "com_ui_upload_ocr_text": "Загрузить как текст", "com_ui_upload_success": "Файл успешно загружен", "com_ui_upload_type": "Выберите тип загрузки", + "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": "Используйте двойные фигурные скобки в тексте для создания переменных, например `{{пример переменной}}`, чтобы заполнить их позже при использовании промта.", + "com_ui_verify": "Проверить", "com_ui_version_var": "Версия {{0}}", "com_ui_versions": "Версии", + "com_ui_view_source": "Просмотреть исходный чат", "com_ui_yes": "Да", "com_ui_zoom": "Масштаб", "com_user_message": "Вы", diff --git a/librechat.example.yaml b/librechat.example.yaml index 96ab2e641b..a427cba42d 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -41,6 +41,16 @@ registration: # allowedDomains: # - "gmail.com" + +# Example Balance settings +# balance: +# enabled: false +# startBalance: 20000 +# autoRefillEnabled: false +# refillIntervalValue: 30 +# refillIntervalUnit: 'days' +# refillAmount: 10000 + # speech: # tts: # openai: @@ -112,20 +122,20 @@ endpoints: # # Should only be one or the other, either `supportedIds` or `excludedIds` # supportedIds: ["asst_supportedAssistantId1", "asst_supportedAssistantId2"] # # excludedIds: ["asst_excludedAssistantId"] - # Only show assistants that the user created or that were created externally (e.g. in Assistants playground). + # # Only show assistants that the user created or that were created externally (e.g. in Assistants playground). # # privateAssistants: false # Does not work with `supportedIds` or `excludedIds` # # (optional) Models that support retrieval, will default to latest known OpenAI models that support the feature # retrievalModels: ["gpt-4-turbo-preview"] # # (optional) Assistant Capabilities available to all users. Omit the ones you wish to exclude. Defaults to list below. # capabilities: ["code_interpreter", "retrieval", "actions", "tools", "image_vision"] # agents: - # (optional) Default recursion depth for agents, defaults to 25 + # # (optional) Default recursion depth for agents, defaults to 25 # recursionLimit: 50 - # (optional) Max recursion depth for agents, defaults to 25 + # # (optional) Max recursion depth for agents, defaults to 25 # maxRecursionLimit: 100 - # (optional) Disable the builder interface for agents + # # (optional) Disable the builder interface for agents # disableBuilder: false - # (optional) Agent Capabilities available to all users. Omit the ones you wish to exclude. Defaults to list below. + # # (optional) Agent Capabilities available to all users. Omit the ones you wish to exclude. Defaults to list below. # capabilities: ["execute_code", "file_search", "actions", "tools"] custom: # Groq Example @@ -241,5 +251,5 @@ endpoints: # fileSizeLimit: 5 # serverFileSizeLimit: 100 # Global server file size limit in MB # avatarSizeLimit: 2 # Limit for user avatar image size in MB -# See the Custom Configuration Guide for more information on Assistants Config: -# https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/assistants_endpoint +# # See the Custom Configuration Guide for more information on Assistants Config: +# # https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/assistants_endpoint diff --git a/package-lock.json b/package-lock.json index f78f2e3686..c29b53cd21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "@langchain/google-genai": "^0.1.11", "@langchain/google-vertexai": "^0.2.2", "@langchain/textsplitters": "^0.1.0", - "@librechat/agents": "^2.2.8", + "@librechat/agents": "^2.3.94", "@librechat/data-schemas": "*", "@waylaidwanderer/fetch-event-source": "^3.0.1", "axios": "^1.8.2", @@ -119,6 +119,7 @@ "passport-jwt": "^4.0.1", "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", + "rate-limit-redis": "^4.2.0", "sharp": "^0.32.6", "tiktoken": "^1.0.15", "traverse": "^0.6.7", @@ -669,550 +670,6 @@ } } }, - "api/node_modules/@librechat/agents": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.2.8.tgz", - "integrity": "sha512-cRG1Tz20sTHGzfJ3vraHRpR3V5nNjnpfRqTxN9TZNd3mYzgWYGO488fHJvE/vhshuWh1979yqsh7Byj4M4WrSw==", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-sdk/credential-provider-node": "^3.613.0", - "@aws-sdk/types": "^3.609.0", - "@langchain/anthropic": "^0.3.14", - "@langchain/aws": "^0.1.7", - "@langchain/community": "^0.3.35", - "@langchain/core": "^0.3.40", - "@langchain/deepseek": "^0.0.1", - "@langchain/google-genai": "^0.1.11", - "@langchain/google-vertexai": "^0.2.2", - "@langchain/langgraph": "^0.2.49", - "@langchain/mistralai": "^0.0.26", - "@langchain/ollama": "^0.1.5", - "@langchain/openai": "^0.4.2", - "@langchain/xai": "^0.0.2", - "@smithy/eventstream-codec": "^2.2.0", - "@smithy/protocol-http": "^3.0.6", - "@smithy/signature-v4": "^2.0.10", - "@smithy/util-utf8": "^2.0.0", - "dotenv": "^16.4.7", - "https-proxy-agent": "^7.0.6", - "nanoid": "^3.3.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "api/node_modules/@librechat/agents/node_modules/@langchain/community": { - "version": "0.3.35", - "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.35.tgz", - "integrity": "sha512-dOV+Uky/zcwN6XJ0I2lQ8cOZ59NOt9OuyuKQhxuGzLIu3d08xVTaEIFSTuuU1f8fN7Vl1yUUJlQENoxW4OdzvQ==", - "dependencies": { - "@langchain/openai": ">=0.2.0 <0.5.0", - "binary-extensions": "^2.2.0", - "expr-eval": "^2.0.2", - "flat": "^5.0.2", - "js-yaml": "^4.1.0", - "langchain": ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", - "langsmith": ">=0.2.8 <0.4.0", - "uuid": "^10.0.0", - "zod": "^3.22.3", - "zod-to-json-schema": "^3.22.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@arcjet/redact": "^v1.0.0-alpha.23", - "@aws-crypto/sha256-js": "^5.0.0", - "@aws-sdk/client-bedrock-agent-runtime": "^3.749.0", - "@aws-sdk/client-bedrock-runtime": "^3.749.0", - "@aws-sdk/client-dynamodb": "^3.749.0", - "@aws-sdk/client-kendra": "^3.749.0", - "@aws-sdk/client-lambda": "^3.749.0", - "@aws-sdk/client-s3": "^3.749.0", - "@aws-sdk/client-sagemaker-runtime": "^3.749.0", - "@aws-sdk/client-sfn": "^3.749.0", - "@aws-sdk/credential-provider-node": "^3.388.0", - "@azure/search-documents": "^12.0.0", - "@azure/storage-blob": "^12.15.0", - "@browserbasehq/sdk": "*", - "@browserbasehq/stagehand": "^1.0.0", - "@clickhouse/client": "^0.2.5", - "@cloudflare/ai": "*", - "@datastax/astra-db-ts": "^1.0.0", - "@elastic/elasticsearch": "^8.4.0", - "@getmetal/metal-sdk": "*", - "@getzep/zep-cloud": "^1.0.6", - "@getzep/zep-js": "^0.9.0", - "@gomomento/sdk": "^1.51.1", - "@gomomento/sdk-core": "^1.51.1", - "@google-ai/generativelanguage": "*", - "@google-cloud/storage": "^6.10.1 || ^7.7.0", - "@gradientai/nodejs-sdk": "^1.2.0", - "@huggingface/inference": "^2.6.4", - "@huggingface/transformers": "^3.2.3", - "@ibm-cloud/watsonx-ai": "*", - "@lancedb/lancedb": "^0.12.0", - "@langchain/core": ">=0.2.21 <0.4.0", - "@layerup/layerup-security": "^1.5.12", - "@libsql/client": "^0.14.0", - "@mendable/firecrawl-js": "^1.4.3", - "@mlc-ai/web-llm": "*", - "@mozilla/readability": "*", - "@neondatabase/serverless": "*", - "@notionhq/client": "^2.2.10", - "@opensearch-project/opensearch": "*", - "@pinecone-database/pinecone": "*", - "@planetscale/database": "^1.8.0", - "@premai/prem-sdk": "^0.3.25", - "@qdrant/js-client-rest": "^1.8.2", - "@raycast/api": "^1.55.2", - "@rockset/client": "^0.9.1", - "@smithy/eventstream-codec": "^2.0.5", - "@smithy/protocol-http": "^3.0.6", - "@smithy/signature-v4": "^2.0.10", - "@smithy/util-utf8": "^2.0.0", - "@spider-cloud/spider-client": "^0.0.21", - "@supabase/supabase-js": "^2.45.0", - "@tensorflow-models/universal-sentence-encoder": "*", - "@tensorflow/tfjs-converter": "*", - "@tensorflow/tfjs-core": "*", - "@upstash/ratelimit": "^1.1.3 || ^2.0.3", - "@upstash/redis": "^1.20.6", - "@upstash/vector": "^1.1.1", - "@vercel/kv": "*", - "@vercel/postgres": "*", - "@writerai/writer-sdk": "^0.40.2", - "@xata.io/client": "^0.28.0", - "@zilliz/milvus2-sdk-node": ">=2.3.5", - "apify-client": "^2.7.1", - "assemblyai": "^4.6.0", - "better-sqlite3": ">=9.4.0 <12.0.0", - "cassandra-driver": "^4.7.2", - "cborg": "^4.1.1", - "cheerio": "^1.0.0-rc.12", - "chromadb": "*", - "closevector-common": "0.1.3", - "closevector-node": "0.1.6", - "closevector-web": "0.1.6", - "cohere-ai": "*", - "convex": "^1.3.1", - "crypto-js": "^4.2.0", - "d3-dsv": "^2.0.0", - "discord.js": "^14.14.1", - "dria": "^0.0.3", - "duck-duck-scrape": "^2.2.5", - "epub2": "^3.0.1", - "fast-xml-parser": "*", - "firebase-admin": "^11.9.0 || ^12.0.0", - "google-auth-library": "*", - "googleapis": "*", - "hnswlib-node": "^3.0.0", - "html-to-text": "^9.0.5", - "ibm-cloud-sdk-core": "*", - "ignore": "^5.2.0", - "interface-datastore": "^8.2.11", - "ioredis": "^5.3.2", - "it-all": "^3.0.4", - "jsdom": "*", - "jsonwebtoken": "^9.0.2", - "llmonitor": "^0.5.9", - "lodash": "^4.17.21", - "lunary": "^0.7.10", - "mammoth": "^1.6.0", - "mongodb": ">=5.2.0", - "mysql2": "^3.9.8", - "neo4j-driver": "*", - "notion-to-md": "^3.1.0", - "officeparser": "^4.0.4", - "openai": "*", - "pdf-parse": "1.1.1", - "pg": "^8.11.0", - "pg-copy-streams": "^6.0.5", - "pickleparser": "^0.2.1", - "playwright": "^1.32.1", - "portkey-ai": "^0.1.11", - "puppeteer": "*", - "pyodide": ">=0.24.1 <0.27.0", - "redis": "*", - "replicate": "*", - "sonix-speech-recognition": "^2.1.1", - "srt-parser-2": "^1.2.3", - "typeorm": "^0.3.20", - "typesense": "^1.5.3", - "usearch": "^1.1.1", - "voy-search": "0.6.2", - "weaviate-ts-client": "*", - "web-auth-library": "^1.0.3", - "word-extractor": "*", - "ws": "^8.14.2", - "youtubei.js": "*" - }, - "peerDependenciesMeta": { - "@arcjet/redact": { - "optional": true - }, - "@aws-crypto/sha256-js": { - "optional": true - }, - "@aws-sdk/client-bedrock-agent-runtime": { - "optional": true - }, - "@aws-sdk/client-bedrock-runtime": { - "optional": true - }, - "@aws-sdk/client-dynamodb": { - "optional": true - }, - "@aws-sdk/client-kendra": { - "optional": true - }, - "@aws-sdk/client-lambda": { - "optional": true - }, - "@aws-sdk/client-s3": { - "optional": true - }, - "@aws-sdk/client-sagemaker-runtime": { - "optional": true - }, - "@aws-sdk/client-sfn": { - "optional": true - }, - "@aws-sdk/credential-provider-node": { - "optional": true - }, - "@aws-sdk/dsql-signer": { - "optional": true - }, - "@azure/search-documents": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@browserbasehq/sdk": { - "optional": true - }, - "@clickhouse/client": { - "optional": true - }, - "@cloudflare/ai": { - "optional": true - }, - "@datastax/astra-db-ts": { - "optional": true - }, - "@elastic/elasticsearch": { - "optional": true - }, - "@getmetal/metal-sdk": { - "optional": true - }, - "@getzep/zep-cloud": { - "optional": true - }, - "@getzep/zep-js": { - "optional": true - }, - "@gomomento/sdk": { - "optional": true - }, - "@gomomento/sdk-core": { - "optional": true - }, - "@google-ai/generativelanguage": { - "optional": true - }, - "@google-cloud/storage": { - "optional": true - }, - "@gradientai/nodejs-sdk": { - "optional": true - }, - "@huggingface/inference": { - "optional": true - }, - "@huggingface/transformers": { - "optional": true - }, - "@lancedb/lancedb": { - "optional": true - }, - "@layerup/layerup-security": { - "optional": true - }, - "@libsql/client": { - "optional": true - }, - "@mendable/firecrawl-js": { - "optional": true - }, - "@mlc-ai/web-llm": { - "optional": true - }, - "@mozilla/readability": { - "optional": true - }, - "@neondatabase/serverless": { - "optional": true - }, - "@notionhq/client": { - "optional": true - }, - "@opensearch-project/opensearch": { - "optional": true - }, - "@pinecone-database/pinecone": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@premai/prem-sdk": { - "optional": true - }, - "@qdrant/js-client-rest": { - "optional": true - }, - "@raycast/api": { - "optional": true - }, - "@rockset/client": { - "optional": true - }, - "@smithy/eventstream-codec": { - "optional": true - }, - "@smithy/protocol-http": { - "optional": true - }, - "@smithy/signature-v4": { - "optional": true - }, - "@smithy/util-utf8": { - "optional": true - }, - "@spider-cloud/spider-client": { - "optional": true - }, - "@supabase/supabase-js": { - "optional": true - }, - "@tensorflow-models/universal-sentence-encoder": { - "optional": true - }, - "@tensorflow/tfjs-converter": { - "optional": true - }, - "@tensorflow/tfjs-core": { - "optional": true - }, - "@upstash/ratelimit": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@upstash/vector": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@writerai/writer-sdk": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "@zilliz/milvus2-sdk-node": { - "optional": true - }, - "apify-client": { - "optional": true - }, - "assemblyai": { - "optional": true - }, - "better-sqlite3": { - "optional": true - }, - "cassandra-driver": { - "optional": true - }, - "cborg": { - "optional": true - }, - "cheerio": { - "optional": true - }, - "chromadb": { - "optional": true - }, - "closevector-common": { - "optional": true - }, - "closevector-node": { - "optional": true - }, - "closevector-web": { - "optional": true - }, - "cohere-ai": { - "optional": true - }, - "convex": { - "optional": true - }, - "crypto-js": { - "optional": true - }, - "d3-dsv": { - "optional": true - }, - "discord.js": { - "optional": true - }, - "dria": { - "optional": true - }, - "duck-duck-scrape": { - "optional": true - }, - "epub2": { - "optional": true - }, - "fast-xml-parser": { - "optional": true - }, - "firebase-admin": { - "optional": true - }, - "google-auth-library": { - "optional": true - }, - "googleapis": { - "optional": true - }, - "hnswlib-node": { - "optional": true - }, - "html-to-text": { - "optional": true - }, - "ignore": { - "optional": true - }, - "interface-datastore": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "it-all": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "jsonwebtoken": { - "optional": true - }, - "llmonitor": { - "optional": true - }, - "lodash": { - "optional": true - }, - "lunary": { - "optional": true - }, - "mammoth": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "neo4j-driver": { - "optional": true - }, - "notion-to-md": { - "optional": true - }, - "officeparser": { - "optional": true - }, - "pdf-parse": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-copy-streams": { - "optional": true - }, - "pickleparser": { - "optional": true - }, - "playwright": { - "optional": true - }, - "portkey-ai": { - "optional": true - }, - "puppeteer": { - "optional": true - }, - "pyodide": { - "optional": true - }, - "redis": { - "optional": true - }, - "replicate": { - "optional": true - }, - "sonix-speech-recognition": { - "optional": true - }, - "srt-parser-2": { - "optional": true - }, - "typeorm": { - "optional": true - }, - "typesense": { - "optional": true - }, - "usearch": { - "optional": true - }, - "voy-search": { - "optional": true - }, - "weaviate-ts-client": { - "optional": true - }, - "web-auth-library": { - "optional": true - }, - "word-extractor": { - "optional": true - }, - "ws": { - "optional": true - }, - "youtubei.js": { - "optional": true - } - } - }, "api/node_modules/@types/node": { "version": "18.19.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", @@ -14143,10 +13600,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", - "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", - "license": "MIT", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -18437,6 +17893,534 @@ "node": ">=18.0.0" } }, + "node_modules/@langchain/community": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.3.36.tgz", + "integrity": "sha512-4jBB4yqux8CGfCwlBbtXck5qP0yJPwDvtwI4KUN2j/At+zSZn1FyTL11G75ctG2b5GO7u+cR6QatDXIPooJphA==", + "dependencies": { + "@langchain/openai": ">=0.2.0 <0.5.0", + "binary-extensions": "^2.2.0", + "expr-eval": "^2.0.2", + "flat": "^5.0.2", + "js-yaml": "^4.1.0", + "langchain": ">=0.2.3 <0.3.0 || >=0.3.4 <0.4.0", + "langsmith": ">=0.2.8 <0.4.0", + "uuid": "^10.0.0", + "zod": "^3.22.3", + "zod-to-json-schema": "^3.22.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@arcjet/redact": "^v1.0.0-alpha.23", + "@aws-crypto/sha256-js": "^5.0.0", + "@aws-sdk/client-bedrock-agent-runtime": "^3.749.0", + "@aws-sdk/client-bedrock-runtime": "^3.749.0", + "@aws-sdk/client-dynamodb": "^3.749.0", + "@aws-sdk/client-kendra": "^3.749.0", + "@aws-sdk/client-lambda": "^3.749.0", + "@aws-sdk/client-s3": "^3.749.0", + "@aws-sdk/client-sagemaker-runtime": "^3.749.0", + "@aws-sdk/client-sfn": "^3.749.0", + "@aws-sdk/credential-provider-node": "^3.388.0", + "@azure/search-documents": "^12.0.0", + "@azure/storage-blob": "^12.15.0", + "@browserbasehq/sdk": "*", + "@browserbasehq/stagehand": "^1.0.0", + "@clickhouse/client": "^0.2.5", + "@cloudflare/ai": "*", + "@datastax/astra-db-ts": "^1.0.0", + "@elastic/elasticsearch": "^8.4.0", + "@getmetal/metal-sdk": "*", + "@getzep/zep-cloud": "^1.0.6", + "@getzep/zep-js": "^0.9.0", + "@gomomento/sdk": "^1.51.1", + "@gomomento/sdk-core": "^1.51.1", + "@google-ai/generativelanguage": "*", + "@google-cloud/storage": "^6.10.1 || ^7.7.0", + "@gradientai/nodejs-sdk": "^1.2.0", + "@huggingface/inference": "^2.6.4", + "@huggingface/transformers": "^3.2.3", + "@ibm-cloud/watsonx-ai": "*", + "@lancedb/lancedb": "^0.12.0", + "@langchain/core": ">=0.2.21 <0.4.0", + "@layerup/layerup-security": "^1.5.12", + "@libsql/client": "^0.14.0", + "@mendable/firecrawl-js": "^1.4.3", + "@mlc-ai/web-llm": "*", + "@mozilla/readability": "*", + "@neondatabase/serverless": "*", + "@notionhq/client": "^2.2.10", + "@opensearch-project/opensearch": "*", + "@pinecone-database/pinecone": "*", + "@planetscale/database": "^1.8.0", + "@premai/prem-sdk": "^0.3.25", + "@qdrant/js-client-rest": "^1.8.2", + "@raycast/api": "^1.55.2", + "@rockset/client": "^0.9.1", + "@smithy/eventstream-codec": "^2.0.5", + "@smithy/protocol-http": "^3.0.6", + "@smithy/signature-v4": "^2.0.10", + "@smithy/util-utf8": "^2.0.0", + "@spider-cloud/spider-client": "^0.0.21", + "@supabase/supabase-js": "^2.45.0", + "@tensorflow-models/universal-sentence-encoder": "*", + "@tensorflow/tfjs-converter": "*", + "@tensorflow/tfjs-core": "*", + "@upstash/ratelimit": "^1.1.3 || ^2.0.3", + "@upstash/redis": "^1.20.6", + "@upstash/vector": "^1.1.1", + "@vercel/kv": "*", + "@vercel/postgres": "*", + "@writerai/writer-sdk": "^0.40.2", + "@xata.io/client": "^0.28.0", + "@zilliz/milvus2-sdk-node": ">=2.3.5", + "apify-client": "^2.7.1", + "assemblyai": "^4.6.0", + "better-sqlite3": ">=9.4.0 <12.0.0", + "cassandra-driver": "^4.7.2", + "cborg": "^4.1.1", + "cheerio": "^1.0.0-rc.12", + "chromadb": "*", + "closevector-common": "0.1.3", + "closevector-node": "0.1.6", + "closevector-web": "0.1.6", + "cohere-ai": "*", + "convex": "^1.3.1", + "crypto-js": "^4.2.0", + "d3-dsv": "^2.0.0", + "discord.js": "^14.14.1", + "dria": "^0.0.3", + "duck-duck-scrape": "^2.2.5", + "epub2": "^3.0.1", + "fast-xml-parser": "*", + "firebase-admin": "^11.9.0 || ^12.0.0", + "google-auth-library": "*", + "googleapis": "*", + "hnswlib-node": "^3.0.0", + "html-to-text": "^9.0.5", + "ibm-cloud-sdk-core": "*", + "ignore": "^5.2.0", + "interface-datastore": "^8.2.11", + "ioredis": "^5.3.2", + "it-all": "^3.0.4", + "jsdom": "*", + "jsonwebtoken": "^9.0.2", + "llmonitor": "^0.5.9", + "lodash": "^4.17.21", + "lunary": "^0.7.10", + "mammoth": "^1.6.0", + "mariadb": "^3.4.0", + "mongodb": ">=5.2.0", + "mysql2": "^3.9.8", + "neo4j-driver": "*", + "notion-to-md": "^3.1.0", + "officeparser": "^4.0.4", + "openai": "*", + "pdf-parse": "1.1.1", + "pg": "^8.11.0", + "pg-copy-streams": "^6.0.5", + "pickleparser": "^0.2.1", + "playwright": "^1.32.1", + "portkey-ai": "^0.1.11", + "puppeteer": "*", + "pyodide": ">=0.24.1 <0.27.0", + "redis": "*", + "replicate": "*", + "sonix-speech-recognition": "^2.1.1", + "srt-parser-2": "^1.2.3", + "typeorm": "^0.3.20", + "typesense": "^1.5.3", + "usearch": "^1.1.1", + "voy-search": "0.6.2", + "weaviate-ts-client": "*", + "web-auth-library": "^1.0.3", + "word-extractor": "*", + "ws": "^8.14.2", + "youtubei.js": "*" + }, + "peerDependenciesMeta": { + "@arcjet/redact": { + "optional": true + }, + "@aws-crypto/sha256-js": { + "optional": true + }, + "@aws-sdk/client-bedrock-agent-runtime": { + "optional": true + }, + "@aws-sdk/client-bedrock-runtime": { + "optional": true + }, + "@aws-sdk/client-dynamodb": { + "optional": true + }, + "@aws-sdk/client-kendra": { + "optional": true + }, + "@aws-sdk/client-lambda": { + "optional": true + }, + "@aws-sdk/client-s3": { + "optional": true + }, + "@aws-sdk/client-sagemaker-runtime": { + "optional": true + }, + "@aws-sdk/client-sfn": { + "optional": true + }, + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@aws-sdk/dsql-signer": { + "optional": true + }, + "@azure/search-documents": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@browserbasehq/sdk": { + "optional": true + }, + "@clickhouse/client": { + "optional": true + }, + "@cloudflare/ai": { + "optional": true + }, + "@datastax/astra-db-ts": { + "optional": true + }, + "@elastic/elasticsearch": { + "optional": true + }, + "@getmetal/metal-sdk": { + "optional": true + }, + "@getzep/zep-cloud": { + "optional": true + }, + "@getzep/zep-js": { + "optional": true + }, + "@gomomento/sdk": { + "optional": true + }, + "@gomomento/sdk-core": { + "optional": true + }, + "@google-ai/generativelanguage": { + "optional": true + }, + "@google-cloud/storage": { + "optional": true + }, + "@gradientai/nodejs-sdk": { + "optional": true + }, + "@huggingface/inference": { + "optional": true + }, + "@huggingface/transformers": { + "optional": true + }, + "@lancedb/lancedb": { + "optional": true + }, + "@layerup/layerup-security": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@mendable/firecrawl-js": { + "optional": true + }, + "@mlc-ai/web-llm": { + "optional": true + }, + "@mozilla/readability": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@notionhq/client": { + "optional": true + }, + "@opensearch-project/opensearch": { + "optional": true + }, + "@pinecone-database/pinecone": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@premai/prem-sdk": { + "optional": true + }, + "@qdrant/js-client-rest": { + "optional": true + }, + "@raycast/api": { + "optional": true + }, + "@rockset/client": { + "optional": true + }, + "@smithy/eventstream-codec": { + "optional": true + }, + "@smithy/protocol-http": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "@smithy/util-utf8": { + "optional": true + }, + "@spider-cloud/spider-client": { + "optional": true + }, + "@supabase/supabase-js": { + "optional": true + }, + "@tensorflow-models/universal-sentence-encoder": { + "optional": true + }, + "@tensorflow/tfjs-converter": { + "optional": true + }, + "@tensorflow/tfjs-core": { + "optional": true + }, + "@upstash/ratelimit": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@upstash/vector": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@writerai/writer-sdk": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "@zilliz/milvus2-sdk-node": { + "optional": true + }, + "apify-client": { + "optional": true + }, + "assemblyai": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "cassandra-driver": { + "optional": true + }, + "cborg": { + "optional": true + }, + "cheerio": { + "optional": true + }, + "chromadb": { + "optional": true + }, + "closevector-common": { + "optional": true + }, + "closevector-node": { + "optional": true + }, + "closevector-web": { + "optional": true + }, + "cohere-ai": { + "optional": true + }, + "convex": { + "optional": true + }, + "crypto-js": { + "optional": true + }, + "d3-dsv": { + "optional": true + }, + "discord.js": { + "optional": true + }, + "dria": { + "optional": true + }, + "duck-duck-scrape": { + "optional": true + }, + "epub2": { + "optional": true + }, + "fast-xml-parser": { + "optional": true + }, + "firebase-admin": { + "optional": true + }, + "google-auth-library": { + "optional": true + }, + "googleapis": { + "optional": true + }, + "hnswlib-node": { + "optional": true + }, + "html-to-text": { + "optional": true + }, + "ignore": { + "optional": true + }, + "interface-datastore": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "it-all": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "jsonwebtoken": { + "optional": true + }, + "llmonitor": { + "optional": true + }, + "lodash": { + "optional": true + }, + "lunary": { + "optional": true + }, + "mammoth": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "neo4j-driver": { + "optional": true + }, + "notion-to-md": { + "optional": true + }, + "officeparser": { + "optional": true + }, + "pdf-parse": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-copy-streams": { + "optional": true + }, + "pickleparser": { + "optional": true + }, + "playwright": { + "optional": true + }, + "portkey-ai": { + "optional": true + }, + "puppeteer": { + "optional": true + }, + "pyodide": { + "optional": true + }, + "redis": { + "optional": true + }, + "replicate": { + "optional": true + }, + "sonix-speech-recognition": { + "optional": true + }, + "srt-parser-2": { + "optional": true + }, + "typeorm": { + "optional": true + }, + "typesense": { + "optional": true + }, + "usearch": { + "optional": true + }, + "voy-search": { + "optional": true + }, + "weaviate-ts-client": { + "optional": true + }, + "web-auth-library": { + "optional": true + }, + "word-extractor": { + "optional": true + }, + "ws": { + "optional": true + }, + "youtubei.js": { + "optional": true + } + } + }, + "node_modules/@langchain/community/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@langchain/core": { "version": "0.3.42", "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.42.tgz", @@ -18570,9 +18554,9 @@ } }, "node_modules/@langchain/langgraph": { - "version": "0.2.56", - "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.56.tgz", - "integrity": "sha512-/uiQL+SeEGNv6QZOilxdwNwDoYQ+t3pFr5tZyG3lvmgmO2Pfojp7SIV2y/yuVYkNlKPvKvpdfE6mmCkCT4G2aA==", + "version": "0.2.57", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-0.2.57.tgz", + "integrity": "sha512-SolqE+HzwbxEEiqAVgHwE11r9lzjZAnAfEe7MMBUE77TUCaWK3GC0VvDfJMNas53ndlc0KRutmpEa0ODWdhcRQ==", "dependencies": { "@langchain/langgraph-checkpoint": "~0.0.16", "@langchain/langgraph-sdk": "~0.0.32", @@ -18613,9 +18597,9 @@ } }, "node_modules/@langchain/langgraph-sdk": { - "version": "0.0.57", - "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.57.tgz", - "integrity": "sha512-xb1OgsQ7fq6zjWaspa/9yjZ35kf1/qIPBC8wkNUI0gelnZLeAnss9RhGKc8L2qMB1P3anxRB9IPGwqCoMUHimg==", + "version": "0.0.60", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.60.tgz", + "integrity": "sha512-7ndeAdw1afVY72HpKEGw7AyuDlD7U3e4jxaJflxA+PXaFPiE0d/hQYvlPT84YmvqNzJN605hv7YcrOju2573bQ==", "dependencies": { "@types/json-schema": "^7.0.15", "p-queue": "^6.6.2", @@ -18853,6 +18837,58 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@librechat/agents": { + "version": "2.3.94", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-2.3.94.tgz", + "integrity": "sha512-/hcojcTJiWADtcDMbgtNv8LZJnimORfDw0DPllqS+8rt2+PPhOEvBFoNFXibUWyW38S35CT9PQJCFIkidHfMsQ==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-sdk/credential-provider-node": "^3.613.0", + "@aws-sdk/types": "^3.609.0", + "@langchain/anthropic": "^0.3.14", + "@langchain/aws": "^0.1.7", + "@langchain/community": "^0.3.35", + "@langchain/core": "^0.3.40", + "@langchain/deepseek": "^0.0.1", + "@langchain/google-genai": "^0.1.11", + "@langchain/google-vertexai": "^0.2.2", + "@langchain/langgraph": "^0.2.49", + "@langchain/mistralai": "^0.0.26", + "@langchain/ollama": "^0.1.5", + "@langchain/openai": "^0.4.2", + "@langchain/xai": "^0.0.2", + "@smithy/eventstream-codec": "^2.2.0", + "@smithy/protocol-http": "^3.0.6", + "@smithy/signature-v4": "^2.0.10", + "@smithy/util-utf8": "^2.0.0", + "dotenv": "^16.4.7", + "https-proxy-agent": "^7.0.6", + "nanoid": "^3.3.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@librechat/agents/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@librechat/agents/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@librechat/backend": { "resolved": "api", "link": true @@ -19083,342 +19119,6 @@ "node-fetch": "^2.6.7" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.6.1.tgz", - "integrity": "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA==", - "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^4.1.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", - "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.5.2", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", - "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.0.1", - "content-disposition": "^1.0.0", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "^1.2.1", - "debug": "4.3.6", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "^2.0.0", - "fresh": "2.0.0", - "http-errors": "2.0.0", - "merge-descriptors": "^2.0.0", - "methods": "~1.1.2", - "mime-types": "^3.0.0", - "on-finished": "2.4.1", - "once": "1.4.0", - "parseurl": "~1.3.3", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "router": "^2.0.0", - "safe-buffer": "5.2.1", - "send": "^1.1.0", - "serve-static": "^2.1.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "^2.0.0", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", - "dependencies": { - "mime-db": "^1.53.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", - "dependencies": { - "debug": "^4.3.5", - "destroy": "^1.2.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^0.5.2", - "http-errors": "^2.0.0", - "mime-types": "^2.1.35", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", - "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", - "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", @@ -38274,6 +37974,17 @@ "node": ">= 0.6" } }, + "node_modules/rate-limit-redis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.0.tgz", + "integrity": "sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA==", + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express-rate-limit": ">= 6" + } + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -44316,7 +44027,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.73", + "version": "0.7.74", "license": "ISC", "dependencies": { "axios": "^1.8.2", @@ -44453,7 +44164,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.3", + "version": "0.0.5", "license": "MIT", "dependencies": { "mongoose": "^8.12.1" @@ -44682,7 +44393,7 @@ "version": "1.1.0", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", + "@modelcontextprotocol/sdk": "^1.7.0", "diff": "^7.0.0", "eventsource": "^3.0.2", "express": "^4.21.2" @@ -44719,6 +44430,144 @@ "keyv": "^4.5.4" } }, + "packages/mcp/node_modules/@modelcontextprotocol/sdk": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.7.0.tgz", + "integrity": "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^4.1.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "packages/mcp/node_modules/@modelcontextprotocol/sdk/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/mcp/node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", + "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.0.1", + "content-disposition": "^1.0.0", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "^1.2.1", + "debug": "4.3.6", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "^2.0.0", + "fresh": "2.0.0", + "http-errors": "2.0.0", + "merge-descriptors": "^2.0.0", + "methods": "~1.1.2", + "mime-types": "^3.0.0", + "on-finished": "2.4.1", + "once": "1.4.0", + "parseurl": "~1.3.3", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "router": "^2.0.0", + "safe-buffer": "5.2.1", + "send": "^1.1.0", + "serve-static": "^2.1.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "^2.0.0", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/mcp/node_modules/@modelcontextprotocol/sdk/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "packages/mcp/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/mcp/node_modules/body-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", + "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.5.2", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "packages/mcp/node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "packages/mcp/node_modules/body-parser/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "packages/mcp/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -44728,6 +44577,57 @@ "balanced-match": "^1.0.0" } }, + "packages/mcp/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/mcp/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "packages/mcp/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "packages/mcp/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "packages/mcp/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, "packages/mcp/node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -44748,6 +44648,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/mcp/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "packages/mcp/node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -44763,6 +44674,44 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "packages/mcp/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "packages/mcp/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/mcp/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "packages/mcp/node_modules/mime-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", + "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "dependencies": { + "mime-db": "^1.53.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "packages/mcp/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -44778,6 +44727,28 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/mcp/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "packages/mcp/node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "packages/mcp/node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -44792,6 +44763,82 @@ "funding": { "url": "https://github.com/sponsors/isaacs" } + }, + "packages/mcp/node_modules/send": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", + "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "dependencies": { + "debug": "^4.3.5", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "http-errors": "^2.0.0", + "mime-types": "^2.1.35", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/mcp/node_modules/send/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "packages/mcp/node_modules/send/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "packages/mcp/node_modules/send/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "packages/mcp/node_modules/serve-static": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", + "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/mcp/node_modules/type-is": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", + "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } } } } diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index 6f495a9aeb..ff7d4cb93c 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.73", + "version": "0.7.74", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 847943ddd1..89133435b8 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -500,11 +500,13 @@ export const intefaceSchema = z }); export type TInterfaceConfig = z.infer; +export type TBalanceConfig = z.infer; export type TStartupConfig = { appTitle: string; socialLogins?: string[]; interface?: TInterfaceConfig; + balance?: TBalanceConfig; discordLoginEnabled: boolean; facebookLoginEnabled: boolean; githubLoginEnabled: boolean; @@ -527,7 +529,6 @@ export type TStartupConfig = { socialLoginEnabled: boolean; passwordResetEnabled: boolean; emailEnabled: boolean; - checkBalance: boolean; showBirthdayIcon: boolean; helpAndFaqURL: string; customFooter?: string; @@ -551,6 +552,18 @@ export const ocrSchema = z.object({ strategy: z.nativeEnum(OCRStrategy).default(OCRStrategy.MISTRAL_OCR), }); +export const balanceSchema = z.object({ + enabled: z.boolean().optional().default(false), + startBalance: z.number().optional().default(20000), + autoRefillEnabled: z.boolean().optional().default(false), + refillIntervalValue: z.number().optional().default(30), + refillIntervalUnit: z + .enum(['seconds', 'minutes', 'hours', 'days', 'weeks', 'months']) + .optional() + .default('days'), + refillAmount: z.number().optional().default(10000), +}); + export const configSchema = z.object({ version: z.string(), cache: z.boolean().default(true), @@ -573,6 +586,7 @@ export const configSchema = z.object({ allowedDomains: z.array(z.string()).optional(), }) .default({ socialLogins: defaultSocialLogins }), + balance: balanceSchema.optional(), speech: z .object({ tts: ttsSchema.optional(), diff --git a/packages/data-provider/src/types/assistants.ts b/packages/data-provider/src/types/assistants.ts index 932fc02077..0ecf751961 100644 --- a/packages/data-provider/src/types/assistants.ts +++ b/packages/data-provider/src/types/assistants.ts @@ -150,11 +150,12 @@ export type File = { /* Agent types */ -export type AgentParameterValue = number | null; +export type AgentParameterValue = number | string | null; export type AgentModelParameters = { model?: string; temperature: AgentParameterValue; + maxContextTokens: AgentParameterValue; max_context_tokens: AgentParameterValue; max_output_tokens: AgentParameterValue; top_p: AgentParameterValue; diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index b1845b36a9..3add216e47 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -1,8 +1,8 @@ { "name": "@librechat/data-schemas", - "version": "0.0.3", - "type": "module", + "version": "0.0.5", "description": "Mongoose schemas and models for LibreChat", + "type": "module", "main": "dist/index.cjs", "module": "dist/index.es.js", "types": "./dist/types/index.d.ts", @@ -13,6 +13,9 @@ "types": "./dist/types/index.d.ts" } }, + "files": [ + "dist" + ], "scripts": { "clean": "rimraf dist", "build": "npm run clean && rollup -c --silent --bundleConfigAsCjs", @@ -55,14 +58,20 @@ "ts-node": "^10.9.2", "typescript": "^5.0.4" }, - "publishConfig": { - "registry": "https://registry.npmjs.org/", - "access": "public" - }, "dependencies": { "mongoose": "^8.12.1" }, "peerDependencies": { "keyv": "^4.5.4" - } + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "keywords": [ + "mongoose", + "schema", + "typescript", + "librechat" + ] } diff --git a/packages/data-schemas/rollup.config.js b/packages/data-schemas/rollup.config.js index 8b27ce81cb..c9f8838e77 100644 --- a/packages/data-schemas/rollup.config.js +++ b/packages/data-schemas/rollup.config.js @@ -1,25 +1,40 @@ import json from '@rollup/plugin-json'; import typescript from '@rollup/plugin-typescript'; import commonjs from '@rollup/plugin-commonjs'; +import nodeResolve from '@rollup/plugin-node-resolve'; +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; export default { input: 'src/index.ts', output: [ { - file: 'dist/index.cjs', // Changed from index.js to index.cjs - format: 'cjs', + file: 'dist/index.es.js', + format: 'es', sourcemap: true, - exports: 'named', }, { - file: 'dist/index.es.js', - format: 'esm', + file: 'dist/index.cjs', + format: 'cjs', sourcemap: true, - exports: 'named', }, ], - plugins: [json(), commonjs(), typescript({ tsconfig: './tsconfig.json' })], - external: [ - // list your external dependencies + plugins: [ + // Allow importing JSON files + json(), + // Automatically externalize peer dependencies + peerDepsExternal(), + // Resolve modules from node_modules + nodeResolve(), + // Convert CommonJS modules to ES6 + commonjs(), + // Compile TypeScript files and generate type declarations + typescript({ + tsconfig: './tsconfig.json', + declaration: true, + declarationDir: 'dist/types', + rootDir: 'src', + }), ], + // Do not bundle these external dependencies + external: ['mongoose'], }; diff --git a/packages/data-schemas/src/index.ts b/packages/data-schemas/src/index.ts index 2374afcac9..4b3af06b8e 100644 --- a/packages/data-schemas/src/index.ts +++ b/packages/data-schemas/src/index.ts @@ -1,49 +1,68 @@ -import actionSchema from './schema/action'; -import agentSchema from './schema/agent'; -import assistantSchema from './schema/assistant'; -import balanceSchema from './schema/balance'; -import bannerSchema from './schema/banner'; -import categoriesSchema from './schema/categories'; -import conversationTagSchema from './schema/conversationTag'; -import convoSchema from './schema/convo'; -import fileSchema from './schema/file'; -import keySchema from './schema/key'; -import messageSchema from './schema/message'; -import pluginAuthSchema from './schema/pluginAuth'; -import presetSchema from './schema/preset'; -import projectSchema from './schema/project'; -import promptSchema from './schema/prompt'; -import promptGroupSchema from './schema/promptGroup'; -import roleSchema from './schema/role'; -import sessionSchema from './schema/session'; -import shareSchema from './schema/share'; -import tokenSchema from './schema/token'; -import toolCallSchema from './schema/toolCall'; -import transactionSchema from './schema/transaction'; -import userSchema from './schema/user'; +export { default as actionSchema } from './schema/action'; +export type { IAction } from './schema/action'; -export { - actionSchema, - agentSchema, - assistantSchema, - balanceSchema, - bannerSchema, - categoriesSchema, - conversationTagSchema, - convoSchema, - fileSchema, - keySchema, - messageSchema, - pluginAuthSchema, - presetSchema, - projectSchema, - promptSchema, - promptGroupSchema, - roleSchema, - sessionSchema, - shareSchema, - tokenSchema, - toolCallSchema, - transactionSchema, - userSchema, -}; +export { default as agentSchema } from './schema/agent'; +export type { IAgent } from './schema/agent'; + +export { default as assistantSchema } from './schema/assistant'; +export type { IAssistant } from './schema/assistant'; + +export { default as balanceSchema } from './schema/balance'; +export type { IBalance } from './schema/balance'; + +export { default as bannerSchema } from './schema/banner'; +export type { IBanner } from './schema/banner'; + +export { default as categoriesSchema } from './schema/categories'; +export type { ICategory } from './schema/categories'; + +export { default as conversationTagSchema } from './schema/conversationTag'; +export type { IConversationTag } from './schema/conversationTag'; + +export { default as convoSchema } from './schema/convo'; +export type { IConversation } from './schema/convo'; + +export { default as fileSchema } from './schema/file'; +export type { IMongoFile } from './schema/file'; + +export { default as keySchema } from './schema/key'; +export type { IKey } from './schema/key'; + +export { default as messageSchema } from './schema/message'; +export type { IMessage } from './schema/message'; + +export { default as pluginAuthSchema } from './schema/pluginAuth'; +export type { IPluginAuth } from './schema/pluginAuth'; + +export { default as presetSchema } from './schema/preset'; +export type { IPreset } from './schema/preset'; + +export { default as projectSchema } from './schema/project'; +export type { IMongoProject } from './schema/project'; + +export { default as promptSchema } from './schema/prompt'; +export type { IPrompt } from './schema/prompt'; + +export { default as promptGroupSchema } from './schema/promptGroup'; +export type { IPromptGroup, IPromptGroupDocument } from './schema/promptGroup'; + +export { default as roleSchema } from './schema/role'; +export type { IRole } from './schema/role'; + +export { default as sessionSchema } from './schema/session'; +export type { ISession } from './schema/session'; + +export { default as shareSchema } from './schema/share'; +export type { ISharedLink } from './schema/share'; + +export { default as tokenSchema } from './schema/token'; +export type { IToken } from './schema/token'; + +export { default as toolCallSchema } from './schema/toolCall'; +export type { IToolCallData } from './schema/toolCall'; + +export { default as transactionSchema } from './schema/transaction'; +export type { ITransaction } from './schema/transaction'; + +export { default as userSchema } from './schema/user'; +export type { IUser } from './schema/user'; diff --git a/packages/data-schemas/src/schema/balance.ts b/packages/data-schemas/src/schema/balance.ts index 0878a57601..c02871dffe 100644 --- a/packages/data-schemas/src/schema/balance.ts +++ b/packages/data-schemas/src/schema/balance.ts @@ -3,6 +3,12 @@ import { Schema, Document, Types } from 'mongoose'; export interface IBalance extends Document { user: Types.ObjectId; tokenCredits: number; + // Automatic refill settings + autoRefillEnabled: boolean; + refillIntervalValue: number; + refillIntervalUnit: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months'; + lastRefill: Date; + refillAmount: number; } const balanceSchema = new Schema({ @@ -17,6 +23,29 @@ const balanceSchema = new Schema({ type: Number, default: 0, }, + // Automatic refill settings + autoRefillEnabled: { + type: Boolean, + default: false, + }, + refillIntervalValue: { + type: Number, + default: 30, + }, + refillIntervalUnit: { + type: String, + enum: ['seconds', 'minutes', 'hours', 'days', 'weeks', 'months'], + default: 'days', + }, + lastRefill: { + type: Date, + default: Date.now, + }, + // amount to add on each refill + refillAmount: { + type: Number, + default: 0, + }, }); export default balanceSchema; diff --git a/packages/data-schemas/tsconfig.json b/packages/data-schemas/tsconfig.json index 287f21089a..7c5cf16cb2 100644 --- a/packages/data-schemas/tsconfig.json +++ b/packages/data-schemas/tsconfig.json @@ -1,28 +1,19 @@ { "compilerOptions": { - "declaration": true, - "declarationDir": "./dist/types", - "module": "esnext", - "noImplicitAny": true, - "outDir": "./dist", - "target": "es2015", + "target": "ES2019", + "module": "ESNext", "moduleResolution": "node", - "lib": ["es2017", "dom", "ES2021.String"], - "skipLibCheck": true, - "esModuleInterop": true, + "declaration": true, + "declarationDir": "dist/types", + "outDir": "dist", "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": false, - "sourceMap": true, - "baseUrl": "." + "sourceMap": true }, - "ts-node": { - "experimentalSpecifierResolution": "node", - "transpileOnly": true, - "esm": true - }, - "exclude": ["node_modules", "dist", "types"], - "include": ["src/**/*", "types/index.d.ts", "types/react-query/index.d.ts"] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] } diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 086f701fdb..7af2780faf 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -68,7 +68,7 @@ "registry": "https://registry.npmjs.org/" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.6.1", + "@modelcontextprotocol/sdk": "^1.7.0", "diff": "^7.0.0", "eventsource": "^3.0.2", "express": "^4.21.2" diff --git a/packages/mcp/src/connection.ts b/packages/mcp/src/connection.ts index 4531ae777d..1bdd676de1 100644 --- a/packages/mcp/src/connection.ts +++ b/packages/mcp/src/connection.ts @@ -134,7 +134,12 @@ export class MCPConnection extends EventEmitter { } const url = new URL(options.url); this.logger?.info(`[MCP][${this.serverName}] Creating SSE transport: ${url.toString()}`); - const transport = new SSEClientTransport(url); + const abortController = new AbortController(); + const transport = new SSEClientTransport(url, { + requestInit: { + signal: abortController.signal, + }, + }); transport.onclose = () => { this.logger?.info(`[MCP][${this.serverName}] SSE transport closed`); @@ -175,6 +180,17 @@ export class MCPConnection extends EventEmitter { this.isInitializing = false; this.shouldStopReconnecting = false; this.reconnectAttempts = 0; + /** + * // FOR DEBUGGING + * // this.client.setRequestHandler(PingRequestSchema, async (request, extra) => { + * // this.logger?.info(`[MCP][${this.serverName}] PingRequest: ${JSON.stringify(request)}`); + * // if (getEventListeners && extra.signal) { + * // const listenerCount = getEventListeners(extra.signal, 'abort').length; + * // this.logger?.debug(`Signal has ${listenerCount} abort listeners`); + * // } + * // return {}; + * // }); + */ } else if (state === 'error' && !this.isReconnecting && !this.isInitializing) { this.handleReconnection().catch((error) => { this.logger?.error(`[MCP][${this.serverName}] Reconnection handler failed:`, error); @@ -269,7 +285,7 @@ export class MCPConnection extends EventEmitter { this.transport = this.constructTransport(this.options); this.setupTransportDebugHandlers(); - const connectTimeout = this.options.initTimeout ?? 10000; + const connectTimeout = this.options.initTimeout ?? 10000; await Promise.race([ this.client.connect(this.transport), new Promise((_resolve, reject) => @@ -304,6 +320,9 @@ export class MCPConnection extends EventEmitter { const originalSend = this.transport.send.bind(this.transport); this.transport.send = async (msg) => { + if ('result' in msg && !('method' in msg) && Object.keys(msg.result ?? {}).length === 0) { + throw new Error('Empty result'); + } this.logger?.debug(`[MCP][${this.serverName}] Transport sending: ${JSON.stringify(msg)}`); return originalSend(msg); }; diff --git a/packages/mcp/src/manager.ts b/packages/mcp/src/manager.ts index 9fccd9de08..2cda269562 100644 --- a/packages/mcp/src/manager.ts +++ b/packages/mcp/src/manager.ts @@ -1,4 +1,5 @@ import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { JsonSchemaType, MCPOptions } from 'librechat-data-provider'; import type { Logger } from 'winston'; import type * as t from './types/mcp'; @@ -192,12 +193,19 @@ export class MCPManager { } } - async callTool( - serverName: string, - toolName: string, - provider: t.Provider, - toolArguments?: Record, - ): Promise { + async callTool({ + serverName, + toolName, + provider, + toolArguments, + options, + }: { + serverName: string; + toolName: string; + provider: t.Provider; + toolArguments?: Record; + options?: RequestOptions; + }): Promise { const connection = this.connections.get(serverName); if (!connection) { throw new Error( @@ -213,7 +221,10 @@ export class MCPManager { }, }, CallToolResultSchema, - { timeout: connection.timeout }, + { + timeout: connection.timeout, + ...options, + }, ); return formatToolContent(result, provider); }