From 1b8a0bfaee24a22409cf51ad1d1321843f0b982a Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sat, 4 Oct 2025 01:53:37 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20Resolve=20Build?= =?UTF-8?q?=20Warning,=20Package=20Cleanup,=20Robust=20Temp=20Chat=20Time?= =?UTF-8?q?=20(#9962)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⚙️ chore: Resolve Build Warning and `keyvMongo` types * 🔄 chore: Update mongodb version to ^6.14.2 in package.json and package-lock.json * chore: remove @langchain/openai dep * 🔄 refactor: Change log level from warn to debug for missing endpoint config * 🔄 refactor: Improve temp chat expiration date calculation in tests and implementation --- api/app/clients/OpenAIClient.js | 62 +------ api/app/clients/llm/createLLM.js | 81 --------- api/app/clients/llm/index.js | 2 - api/app/clients/memory/summaryBuffer.demo.js | 31 ---- .../clients/tools/util/handleTools.test.js | 4 - api/package.json | 1 - api/server/controllers/agents/client.js | 4 +- package-lock.json | 162 ++++++++++++------ packages/api/package.json | 3 +- packages/api/src/cache/keyvMongo.ts | 7 +- .../api/src/utils/tempChatRetention.spec.ts | 24 +-- packages/api/src/utils/tempChatRetention.ts | 4 +- 12 files changed, 131 insertions(+), 254 deletions(-) delete mode 100644 api/app/clients/llm/createLLM.js delete mode 100644 api/app/clients/memory/summaryBuffer.demo.js diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 17c4891dcd..1194474674 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -43,7 +43,6 @@ const { runTitleChain } = require('./chains'); const { extractBaseURL } = require('~/utils'); const { tokenSplit } = require('./document'); const BaseClient = require('./BaseClient'); -const { createLLM } = require('./llm'); class OpenAIClient extends BaseClient { constructor(apiKey, options = {}) { @@ -614,65 +613,8 @@ class OpenAIClient extends BaseClient { return (reply ?? '').trim(); } - initializeLLM({ - model = openAISettings.model.default, - modelName, - temperature = 0.2, - max_tokens, - streaming, - }) { - const modelOptions = { - modelName: modelName ?? model, - temperature, - user: this.user, - }; - - if (max_tokens) { - modelOptions.max_tokens = max_tokens; - } - - const configOptions = {}; - - if (this.langchainProxy) { - configOptions.basePath = this.langchainProxy; - } - - if (this.useOpenRouter) { - configOptions.basePath = 'https://openrouter.ai/api/v1'; - configOptions.baseOptions = { - headers: { - 'HTTP-Referer': 'https://librechat.ai', - 'X-Title': 'LibreChat', - }, - }; - } - - const { headers } = this.options; - if (headers && typeof headers === 'object' && !Array.isArray(headers)) { - configOptions.baseOptions = { - headers: resolveHeaders({ - headers: { - ...headers, - ...configOptions?.baseOptions?.headers, - }, - }), - }; - } - - if (this.options.proxy) { - configOptions.httpAgent = new HttpsProxyAgent(this.options.proxy); - configOptions.httpsAgent = new HttpsProxyAgent(this.options.proxy); - } - - const llm = createLLM({ - modelOptions, - configOptions, - openAIApiKey: this.apiKey, - azure: this.azure, - streaming, - }); - - return llm; + initializeLLM() { + throw new Error('Deprecated'); } /** diff --git a/api/app/clients/llm/createLLM.js b/api/app/clients/llm/createLLM.js deleted file mode 100644 index 846c4d8e9c..0000000000 --- a/api/app/clients/llm/createLLM.js +++ /dev/null @@ -1,81 +0,0 @@ -const { ChatOpenAI } = require('@langchain/openai'); -const { isEnabled, sanitizeModelName, constructAzureURL } = require('@librechat/api'); - -/** - * Creates a new instance of a language model (LLM) for chat interactions. - * - * @param {Object} options - The options for creating the LLM. - * @param {ModelOptions} options.modelOptions - The options specific to the model, including modelName, temperature, presence_penalty, frequency_penalty, and other model-related settings. - * @param {ConfigOptions} options.configOptions - Configuration options for the API requests, including proxy settings and custom headers. - * @param {Callbacks} [options.callbacks] - Callback functions for managing the lifecycle of the LLM, including token buffers, context, and initial message count. - * @param {boolean} [options.streaming=false] - Determines if the LLM should operate in streaming mode. - * @param {string} options.openAIApiKey - The API key for OpenAI, used for authentication. - * @param {AzureOptions} [options.azure={}] - Optional Azure-specific configurations. If provided, Azure configurations take precedence over OpenAI configurations. - * - * @returns {ChatOpenAI} An instance of the ChatOpenAI class, configured with the provided options. - * - * @example - * const llm = createLLM({ - * modelOptions: { modelName: 'gpt-4o-mini', temperature: 0.2 }, - * configOptions: { basePath: 'https://example.api/path' }, - * callbacks: { onMessage: handleMessage }, - * openAIApiKey: 'your-api-key' - * }); - */ -function createLLM({ - modelOptions, - configOptions, - callbacks, - streaming = false, - openAIApiKey, - azure = {}, -}) { - let credentials = { openAIApiKey }; - let configuration = { - apiKey: openAIApiKey, - ...(configOptions.basePath && { baseURL: configOptions.basePath }), - }; - - /** @type {AzureOptions} */ - let azureOptions = {}; - if (azure) { - const useModelName = isEnabled(process.env.AZURE_USE_MODEL_AS_DEPLOYMENT_NAME); - - credentials = {}; - configuration = {}; - azureOptions = azure; - - azureOptions.azureOpenAIApiDeploymentName = useModelName - ? sanitizeModelName(modelOptions.modelName) - : azureOptions.azureOpenAIApiDeploymentName; - } - - if (azure && process.env.AZURE_OPENAI_DEFAULT_MODEL) { - modelOptions.modelName = process.env.AZURE_OPENAI_DEFAULT_MODEL; - } - - if (azure && configOptions.basePath) { - const azureURL = constructAzureURL({ - baseURL: configOptions.basePath, - azureOptions, - }); - azureOptions.azureOpenAIBasePath = azureURL.split( - `/${azureOptions.azureOpenAIApiDeploymentName}`, - )[0]; - } - - return new ChatOpenAI( - { - streaming, - credentials, - configuration, - ...azureOptions, - ...modelOptions, - ...credentials, - callbacks, - }, - configOptions, - ); -} - -module.exports = createLLM; diff --git a/api/app/clients/llm/index.js b/api/app/clients/llm/index.js index d03e1cda4d..c7770ce103 100644 --- a/api/app/clients/llm/index.js +++ b/api/app/clients/llm/index.js @@ -1,7 +1,5 @@ -const createLLM = require('./createLLM'); const createCoherePayload = require('./createCoherePayload'); module.exports = { - createLLM, createCoherePayload, }; diff --git a/api/app/clients/memory/summaryBuffer.demo.js b/api/app/clients/memory/summaryBuffer.demo.js deleted file mode 100644 index fc575c3032..0000000000 --- a/api/app/clients/memory/summaryBuffer.demo.js +++ /dev/null @@ -1,31 +0,0 @@ -require('dotenv').config(); -const { ChatOpenAI } = require('@langchain/openai'); -const { getBufferString, ConversationSummaryBufferMemory } = require('langchain/memory'); - -const chatPromptMemory = new ConversationSummaryBufferMemory({ - llm: new ChatOpenAI({ modelName: 'gpt-4o-mini', temperature: 0 }), - maxTokenLimit: 10, - returnMessages: true, -}); - -(async () => { - await chatPromptMemory.saveContext({ input: 'hi my name\'s Danny' }, { output: 'whats up' }); - await chatPromptMemory.saveContext({ input: 'not much you' }, { output: 'not much' }); - await chatPromptMemory.saveContext( - { input: 'are you excited for the olympics?' }, - { output: 'not really' }, - ); - - // We can also utilize the predict_new_summary method directly. - const messages = await chatPromptMemory.chatHistory.getMessages(); - console.log('MESSAGES\n\n'); - console.log(JSON.stringify(messages)); - const previous_summary = ''; - const predictSummary = await chatPromptMemory.predictNewSummary(messages, previous_summary); - console.log('SUMMARY\n\n'); - console.log(JSON.stringify(getBufferString([{ role: 'system', content: predictSummary }]))); - - // const { history } = await chatPromptMemory.loadMemoryVariables({}); - // console.log('HISTORY\n\n'); - // console.log(JSON.stringify(history)); -})(); diff --git a/api/app/clients/tools/util/handleTools.test.js b/api/app/clients/tools/util/handleTools.test.js index b01aa2f7ef..b25372653c 100644 --- a/api/app/clients/tools/util/handleTools.test.js +++ b/api/app/clients/tools/util/handleTools.test.js @@ -30,7 +30,6 @@ jest.mock('~/server/services/Config', () => ({ }), })); -const { BaseLLM } = require('@langchain/openai'); const { Calculator } = require('@langchain/community/tools/calculator'); const { User } = require('~/db/models'); @@ -172,7 +171,6 @@ describe('Tool Handlers', () => { beforeAll(async () => { const toolMap = await loadTools({ user: fakeUser._id, - model: BaseLLM, tools: sampleTools, returnMap: true, useSpecs: true, @@ -266,7 +264,6 @@ describe('Tool Handlers', () => { it('returns an empty object when no tools are requested', async () => { toolFunctions = await loadTools({ user: fakeUser._id, - model: BaseLLM, returnMap: true, useSpecs: true, }); @@ -276,7 +273,6 @@ describe('Tool Handlers', () => { process.env.SD_WEBUI_URL = mockCredential; toolFunctions = await loadTools({ user: fakeUser._id, - model: BaseLLM, tools: ['stable-diffusion'], functions: true, returnMap: true, diff --git a/api/package.json b/api/package.json index 782d958102..28d29af8da 100644 --- a/api/package.json +++ b/api/package.json @@ -46,7 +46,6 @@ "@langchain/core": "^0.3.62", "@langchain/google-genai": "^0.2.13", "@langchain/google-vertexai": "^0.2.13", - "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", "@librechat/agents": "^2.4.82", "@librechat/api": "*", diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 5825257257..bf32385162 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1116,8 +1116,8 @@ class AgentClient extends BaseClient { appConfig.endpoints?.[endpoint] ?? titleProviderConfig.customEndpointConfig; if (!endpointConfig) { - logger.warn( - '[api/server/controllers/agents/client.js #titleConvo] Error getting endpoint config', + logger.debug( + `[api/server/controllers/agents/client.js #titleConvo] No endpoint config for "${endpoint}"`, ); } diff --git a/package-lock.json b/package-lock.json index 153120b9ad..04cb0e9aaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,6 @@ "@langchain/core": "^0.3.62", "@langchain/google-genai": "^0.2.13", "@langchain/google-vertexai": "^0.2.13", - "@langchain/openai": "^0.5.18", "@langchain/textsplitters": "^0.1.0", "@librechat/agents": "^2.4.82", "@librechat/api": "*", @@ -2364,6 +2363,54 @@ "mkdirp": "bin/cmd.js" } }, + "api/node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, "api/node_modules/multer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", @@ -21975,6 +22022,54 @@ "node": ">= 14" } }, + "node_modules/@librechat/agents/node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, "node_modules/@librechat/agents/node_modules/openai": { "version": "5.11.0", "resolved": "https://registry.npmjs.org/openai/-/openai-5.11.0.tgz", @@ -22623,9 +22718,10 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", - "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.1.tgz", + "integrity": "sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==", + "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" } @@ -41347,14 +41443,13 @@ } }, "node_modules/mongodb": { - "version": "6.17.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.17.0.tgz", - "integrity": "sha512-neerUzg/8U26cgruLysKEjJvoNSXhyID3RvzvdcpsIi2COYM3FS3o9nlH7fxFtefTb942dX3W9i37oPfCVj4wA==", - "devOptional": true, + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", + "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.4", + "bson": "^6.10.3", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -41510,52 +41605,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/mongodb": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.14.2.tgz", - "integrity": "sha512-kMEHNo0F3P6QKDq17zcDuPeaywK/YaJVCEQRzPF3TOM/Bl9MFg64YE5Tu7ifj37qZJMhwU1tl2Ioivws5gRG5Q==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.3", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, "node_modules/moo-color": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz", @@ -51235,7 +51284,7 @@ "keyv-file": "^5.1.2", "librechat-data-provider": "*", "memorystore": "^1.6.7", - "mongoose": "^8.12.1", + "mongodb": "^6.14.2", "rate-limit-redis": "^4.2.0", "rimraf": "^5.0.1", "rollup": "^4.22.4", @@ -51263,6 +51312,7 @@ "keyv-file": "^5.1.2", "librechat-data-provider": "*", "memorystore": "^1.6.7", + "mongoose": "^8.12.1", "node-fetch": "2.7.0", "rate-limit-redis": "^4.2.0", "tiktoken": "^1.0.15", diff --git a/packages/api/package.json b/packages/api/package.json index da11212c96..44f5ce9b22 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -69,7 +69,7 @@ "keyv-file": "^5.1.2", "librechat-data-provider": "*", "memorystore": "^1.6.7", - "mongoose": "^8.12.1", + "mongodb": "^6.14.2", "rate-limit-redis": "^4.2.0", "rimraf": "^5.0.1", "rollup": "^4.22.4", @@ -100,6 +100,7 @@ "keyv-file": "^5.1.2", "librechat-data-provider": "*", "memorystore": "^1.6.7", + "mongoose": "^8.12.1", "node-fetch": "2.7.0", "rate-limit-redis": "^4.2.0", "tiktoken": "^1.0.15", diff --git a/packages/api/src/cache/keyvMongo.ts b/packages/api/src/cache/keyvMongo.ts index 68c7262c6f..4c0a08bf63 100644 --- a/packages/api/src/cache/keyvMongo.ts +++ b/packages/api/src/cache/keyvMongo.ts @@ -1,7 +1,8 @@ import mongoose from 'mongoose'; import { EventEmitter } from 'events'; +import { GridFSBucket } from 'mongodb'; import { logger } from '@librechat/data-schemas'; -import { GridFSBucket, type Db, type ReadPreference, type Collection } from 'mongodb'; +import type { Db, ReadPreference, Collection } from 'mongodb'; interface KeyvMongoOptions { url?: string; @@ -103,7 +104,7 @@ class KeyvMongoCustom extends EventEmitter { const stream = client.bucket.openDownloadStreamByName(key); return new Promise((resolve) => { - const resp: Buffer[] = []; + const resp: Uint8Array[] = []; stream.on('error', () => { resolve(undefined); }); @@ -113,7 +114,7 @@ class KeyvMongoCustom extends EventEmitter { resolve(data); }); - stream.on('data', (chunk: Buffer) => { + stream.on('data', (chunk: Uint8Array) => { resp.push(chunk); }); }); diff --git a/packages/api/src/utils/tempChatRetention.spec.ts b/packages/api/src/utils/tempChatRetention.spec.ts index 8e924183c5..847088ab7c 100644 --- a/packages/api/src/utils/tempChatRetention.spec.ts +++ b/packages/api/src/utils/tempChatRetention.spec.ts @@ -92,14 +92,16 @@ describe('tempChatRetention', () => { describe('createTempChatExpirationDate', () => { it('should create expiration date with default retention period', () => { + const beforeCall = Date.now(); const result = createTempChatExpirationDate(); + const afterCall = Date.now(); - const expectedDate = new Date(); - expectedDate.setHours(expectedDate.getHours() + DEFAULT_RETENTION_HOURS); + const expectedMin = beforeCall + DEFAULT_RETENTION_HOURS * 60 * 60 * 1000; + const expectedMax = afterCall + DEFAULT_RETENTION_HOURS * 60 * 60 * 1000; - // Allow for small time differences in test execution - const timeDiff = Math.abs(result.getTime() - expectedDate.getTime()); - expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference + // Result should be between expectedMin and expectedMax + expect(result.getTime()).toBeGreaterThanOrEqual(expectedMin); + expect(result.getTime()).toBeLessThanOrEqual(expectedMax); }); it('should create expiration date with custom retention period', () => { @@ -109,14 +111,16 @@ describe('tempChatRetention', () => { }, }; + const beforeCall = Date.now(); const result = createTempChatExpirationDate(config?.interfaceConfig); + const afterCall = Date.now(); - const expectedDate = new Date(); - expectedDate.setHours(expectedDate.getHours() + 12); + const expectedMin = beforeCall + 12 * 60 * 60 * 1000; + const expectedMax = afterCall + 12 * 60 * 60 * 1000; - // Allow for small time differences in test execution - const timeDiff = Math.abs(result.getTime() - expectedDate.getTime()); - expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference + // Result should be between expectedMin and expectedMax + expect(result.getTime()).toBeGreaterThanOrEqual(expectedMin); + expect(result.getTime()).toBeLessThanOrEqual(expectedMax); }); it('should return a Date object', () => { diff --git a/packages/api/src/utils/tempChatRetention.ts b/packages/api/src/utils/tempChatRetention.ts index 6b78681891..3505a51a1b 100644 --- a/packages/api/src/utils/tempChatRetention.ts +++ b/packages/api/src/utils/tempChatRetention.ts @@ -73,7 +73,5 @@ export function getTempChatRetentionHours( */ export function createTempChatExpirationDate(interfaceConfig?: AppConfig['interfaceConfig']): Date { const retentionHours = getTempChatRetentionHours(interfaceConfig); - const expiredAt = new Date(); - expiredAt.setHours(expiredAt.getHours() + retentionHours); - return expiredAt; + return new Date(Date.now() + retentionHours * 60 * 60 * 1000); }