mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
feat(api): initial Redis support; fix(SearchBar): proper debounce (#1039)
* refactor: use keyv for search caching with 1 min expirations * feat: keyvRedis; chore: bump keyv, bun.lockb, add jsconfig for vscode file resolution * feat: api/search redis support * refactor(redis) use ioredis cluster for keyv fix(OpenID): when redis is configured, use redis memory store for express-session * fix: revert using uri for keyvredis * fix(SearchBar): properly debounce search queries, fix weird render behaviors * refactor: add authentication to search endpoint and show error messages in results * feat: redis support for violation logs * fix(logViolation): ensure a number is always being stored in cache * feat(concurrentLimiter): uses clearPendingReq, clears pendingReq on abort, redis support * fix(api/search/enable): query only when authenticated * feat(ModelService): redis support * feat(checkBan): redis support * refactor(api/search): consolidate keyv logic * fix(ci): add default empty value for REDIS_URI * refactor(keyvRedis): use condition to initialize keyvRedis assignment * refactor(connectDb): handle disconnected state (should create a new conn) * fix(ci/e2e): handle case where cleanUp did not successfully run * fix(getDefaultEndpoint): return endpoint from localStorage if defined and endpointsConfig is default * ci(e2e): remove afterAll messages as startup/cleanUp will clear messages * ci(e2e): remove teardown for CI until further notice * chore: bump playwright/test * ci(e2e): reinstate teardown as CI issue is specific to github env * fix(ci): click settings menu trigger by testid
This commit is contained in:
parent
4ac0c04e83
commit
5145121eb7
29 changed files with 461 additions and 171 deletions
45
api/cache/clearPendingReq.js
vendored
45
api/cache/clearPendingReq.js
vendored
|
@ -1,29 +1,48 @@
|
|||
const Keyv = require('keyv');
|
||||
const { pendingReqFile } = require('./keyvFiles');
|
||||
const { LIMIT_CONCURRENT_MESSAGES } = process.env ?? {};
|
||||
|
||||
const keyv = new Keyv({ store: pendingReqFile, namespace: 'pendingRequests' });
|
||||
const getLogStores = require('./getLogStores');
|
||||
const { isEnabled } = require('../server/utils');
|
||||
const { USE_REDIS, LIMIT_CONCURRENT_MESSAGES } = process.env ?? {};
|
||||
const ttl = 1000 * 60 * 1;
|
||||
|
||||
/**
|
||||
* Clear pending requests from the cache.
|
||||
* Clear or decrement pending requests from the cache.
|
||||
* Checks the environmental variable LIMIT_CONCURRENT_MESSAGES;
|
||||
* if the rule is enabled ('true'), pending requests in the cache are cleared.
|
||||
* if the rule is enabled ('true'), it either decrements the count of pending requests
|
||||
* or deletes the key if the count is less than or equal to 1.
|
||||
*
|
||||
* @module clearPendingReq
|
||||
* @requires keyv
|
||||
* @requires keyvFiles
|
||||
* @requires ./getLogStores
|
||||
* @requires ../server/utils
|
||||
* @requires process
|
||||
*
|
||||
* @async
|
||||
* @function
|
||||
* @returns {Promise<void>} A promise that either clears 'pendingRequests' from store or resolves with no value.
|
||||
* @param {Object} params - The parameters object.
|
||||
* @param {string} params.userId - The user ID for which the pending requests are to be cleared or decremented.
|
||||
* @param {Object} [params.cache] - An optional cache object to use. If not provided, a default cache will be fetched using getLogStores.
|
||||
* @returns {Promise<void>} A promise that either decrements the 'pendingRequests' count, deletes the key from the store, or resolves with no value.
|
||||
*/
|
||||
const clearPendingReq = async () => {
|
||||
if (LIMIT_CONCURRENT_MESSAGES?.toLowerCase() !== 'true') {
|
||||
const clearPendingReq = async ({ userId, cache: _cache }) => {
|
||||
if (!userId) {
|
||||
return;
|
||||
} else if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await keyv.clear();
|
||||
const namespace = 'pending_req';
|
||||
const cache = _cache ?? getLogStores(namespace);
|
||||
|
||||
if (!cache) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `${USE_REDIS ? namespace : ''}:${userId ?? ''}`;
|
||||
const currentReq = +((await cache.get(key)) ?? 0);
|
||||
|
||||
if (currentReq && currentReq >= 1) {
|
||||
await cache.set(key, currentReq - 1, ttl);
|
||||
} else {
|
||||
await cache.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = clearPendingReq;
|
||||
|
|
40
api/cache/getLogStores.js
vendored
40
api/cache/getLogStores.js
vendored
|
@ -1,26 +1,37 @@
|
|||
const Keyv = require('keyv');
|
||||
const keyvMongo = require('./keyvMongo');
|
||||
const { math } = require('../server/utils');
|
||||
const keyvRedis = require('./keyvRedis');
|
||||
const { math, isEnabled } = require('../server/utils');
|
||||
const { logFile, violationFile } = require('./keyvFiles');
|
||||
const { BAN_DURATION } = process.env ?? {};
|
||||
const { BAN_DURATION, USE_REDIS } = process.env ?? {};
|
||||
|
||||
const duration = math(BAN_DURATION, 7200000);
|
||||
|
||||
const createViolationInstance = (namespace) => {
|
||||
const config = isEnabled(USE_REDIS) ? { store: keyvRedis } : { store: violationFile, namespace };
|
||||
return new Keyv(config);
|
||||
};
|
||||
|
||||
// Serve cache from memory so no need to clear it on startup/exit
|
||||
const pending_req = isEnabled(USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: 'pending_req' });
|
||||
|
||||
const namespaces = {
|
||||
ban: new Keyv({ store: keyvMongo, ttl: duration, namespace: 'bans' }),
|
||||
pending_req,
|
||||
ban: new Keyv({ store: keyvMongo, namespace: 'bans', duration }),
|
||||
general: new Keyv({ store: logFile, namespace: 'violations' }),
|
||||
concurrent: new Keyv({ store: violationFile, namespace: 'concurrent' }),
|
||||
non_browser: new Keyv({ store: violationFile, namespace: 'non_browser' }),
|
||||
message_limit: new Keyv({ store: violationFile, namespace: 'message_limit' }),
|
||||
token_balance: new Keyv({ store: violationFile, namespace: 'token_balance' }),
|
||||
registrations: new Keyv({ store: violationFile, namespace: 'registrations' }),
|
||||
logins: new Keyv({ store: violationFile, namespace: 'logins' }),
|
||||
concurrent: createViolationInstance('concurrent'),
|
||||
non_browser: createViolationInstance('non_browser'),
|
||||
message_limit: createViolationInstance('message_limit'),
|
||||
token_balance: createViolationInstance('token_balance'),
|
||||
registrations: createViolationInstance('registrations'),
|
||||
logins: createViolationInstance('logins'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns either the logs of violations specified by type if a type is provided
|
||||
* or it returns the general log if no type is specified. If an invalid type is passed,
|
||||
* an error will be thrown.
|
||||
* Returns the keyv cache specified by type.
|
||||
* If an invalid type is passed, an error will be thrown.
|
||||
*
|
||||
* @module getLogStores
|
||||
* @requires keyv - a simple key-value storage that allows you to easily switch out storage adapters.
|
||||
|
@ -31,11 +42,10 @@ const namespaces = {
|
|||
* @throws Will throw an error if an invalid violation type is passed.
|
||||
*/
|
||||
const getLogStores = (type) => {
|
||||
if (!type) {
|
||||
if (!type || !namespaces[type]) {
|
||||
throw new Error(`Invalid store type: ${type}`);
|
||||
}
|
||||
const logs = namespaces[type];
|
||||
return logs;
|
||||
return namespaces[type];
|
||||
};
|
||||
|
||||
module.exports = getLogStores;
|
||||
|
|
3
api/cache/index.js
vendored
3
api/cache/index.js
vendored
|
@ -1,6 +1,5 @@
|
|||
const keyvFiles = require('./keyvFiles');
|
||||
const getLogStores = require('./getLogStores');
|
||||
const logViolation = require('./logViolation');
|
||||
const clearPendingReq = require('./clearPendingReq');
|
||||
|
||||
module.exports = { ...keyvFiles, getLogStores, logViolation, clearPendingReq };
|
||||
module.exports = { ...keyvFiles, getLogStores, logViolation };
|
||||
|
|
14
api/cache/keyvRedis.js
vendored
Normal file
14
api/cache/keyvRedis.js
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
const KeyvRedis = require('@keyv/redis');
|
||||
|
||||
const { REDIS_URI } = process.env;
|
||||
|
||||
let keyvRedis;
|
||||
|
||||
if (REDIS_URI) {
|
||||
keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false });
|
||||
keyvRedis.on('error', (err) => console.error('KeyvRedis connection error:', err));
|
||||
} else {
|
||||
// console.log('REDIS_URI not provided. Redis module will not be initialized.');
|
||||
}
|
||||
|
||||
module.exports = keyvRedis;
|
12
api/cache/logViolation.js
vendored
12
api/cache/logViolation.js
vendored
|
@ -1,5 +1,6 @@
|
|||
const getLogStores = require('./getLogStores');
|
||||
const banViolation = require('./banViolation');
|
||||
const { isEnabled } = require('../server/utils');
|
||||
|
||||
/**
|
||||
* Logs the violation.
|
||||
|
@ -17,10 +18,11 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => {
|
|||
}
|
||||
const logs = getLogStores('general');
|
||||
const violationLogs = getLogStores(type);
|
||||
const key = isEnabled(process.env.USE_REDIS) ? `${type}:${userId}` : userId;
|
||||
|
||||
const userViolations = (await violationLogs.get(userId)) ?? 0;
|
||||
const violationCount = userViolations + score;
|
||||
await violationLogs.set(userId, violationCount);
|
||||
const userViolations = (await violationLogs.get(key)) ?? 0;
|
||||
const violationCount = +userViolations + +score;
|
||||
await violationLogs.set(key, violationCount);
|
||||
|
||||
errorMessage.user_id = userId;
|
||||
errorMessage.prev_count = userViolations;
|
||||
|
@ -28,10 +30,10 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => {
|
|||
errorMessage.date = new Date().toISOString();
|
||||
|
||||
await banViolation(req, res, errorMessage);
|
||||
const userLogs = (await logs.get(userId)) ?? [];
|
||||
const userLogs = (await logs.get(key)) ?? [];
|
||||
userLogs.push(errorMessage);
|
||||
delete errorMessage.user_id;
|
||||
await logs.set(userId, userLogs);
|
||||
await logs.set(key, userLogs);
|
||||
};
|
||||
|
||||
module.exports = logViolation;
|
||||
|
|
4
api/cache/redis.js
vendored
Normal file
4
api/cache/redis.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
const Redis = require('ioredis');
|
||||
const { REDIS_URI } = process.env ?? {};
|
||||
const redis = new Redis.Cluster(REDIS_URI);
|
||||
module.exports = redis;
|
13
api/jsconfig.json
Normal file
13
api/jsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "CommonJS",
|
||||
// "checkJs": true, // Report errors in JavaScript files
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"*": ["*", "node_modules/*"],
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -18,11 +18,12 @@ if (!cached) {
|
|||
}
|
||||
|
||||
async function connectDb() {
|
||||
if (cached.conn) {
|
||||
if (cached.conn && cached.conn?._readyState === 1) {
|
||||
return cached.conn;
|
||||
}
|
||||
|
||||
if (!cached.promise) {
|
||||
const disconnected = cached.conn && cached.conn?._readyState !== 1;
|
||||
if (!cached.promise || disconnected) {
|
||||
const opts = {
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
|
|
|
@ -24,11 +24,13 @@
|
|||
"@anthropic-ai/sdk": "^0.5.4",
|
||||
"@azure/search-documents": "^11.3.2",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@keyv/redis": "^2.8.0",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.37.2",
|
||||
"axios": "^1.3.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cohere-ai": "^6.0.0",
|
||||
"connect-redis": "^7.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
|
@ -39,10 +41,11 @@
|
|||
"googleapis": "^118.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"html": "^1.0.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jose": "^4.15.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"keyv": "^4.5.3",
|
||||
"keyv": "^4.5.4",
|
||||
"keyv-file": "^0.2.0",
|
||||
"langchain": "^0.0.153",
|
||||
"lodash": "^4.17.21",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const { sendMessage, sendError, countTokens, isEnabled } = require('../utils');
|
||||
const { saveMessage, getConvo, getConvoTitle } = require('../../models');
|
||||
const { sendMessage, sendError, countTokens } = require('../utils');
|
||||
const clearPendingReq = require('../../cache/clearPendingReq');
|
||||
const spendTokens = require('../../models/spendTokens');
|
||||
const abortControllers = require('./abortControllers');
|
||||
|
||||
|
@ -20,6 +21,9 @@ async function abortMessage(req, res) {
|
|||
const handleAbort = () => {
|
||||
return async (req, res) => {
|
||||
try {
|
||||
if (isEnabled(process.env.LIMIT_CONCURRENT_MESSAGES)) {
|
||||
await clearPendingReq({ userId: req.user.id });
|
||||
}
|
||||
return await abortMessage(req, res);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
|
|
@ -3,8 +3,11 @@ const uap = require('ua-parser-js');
|
|||
const { getLogStores } = require('../../cache');
|
||||
const denyRequest = require('./denyRequest');
|
||||
const { isEnabled, removePorts } = require('../utils');
|
||||
const keyvRedis = require('../../cache/keyvRedis');
|
||||
|
||||
const banCache = new Keyv({ namespace: 'bans', ttl: 0 });
|
||||
const banCache = isEnabled(process.env.USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: 'bans', ttl: 0 });
|
||||
const message = 'Your account has been temporarily banned due to violations of our service.';
|
||||
|
||||
/**
|
||||
|
@ -50,9 +53,11 @@ const checkBan = async (req, res, next = () => {}) => {
|
|||
|
||||
req.ip = removePorts(req);
|
||||
const userId = req.user?.id ?? req.user?._id ?? null;
|
||||
const ipKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:ip:${req.ip}` : req.ip;
|
||||
const userKey = isEnabled(process.env.USE_REDIS) ? `ban_cache:user:${userId}` : userId;
|
||||
|
||||
const cachedIPBan = await banCache.get(req.ip);
|
||||
const cachedUserBan = await banCache.get(userId);
|
||||
const cachedIPBan = await banCache.get(ipKey);
|
||||
const cachedUserBan = await banCache.get(userKey);
|
||||
const cachedBan = cachedIPBan || cachedUserBan;
|
||||
|
||||
if (cachedBan) {
|
||||
|
@ -78,13 +83,13 @@ const checkBan = async (req, res, next = () => {}) => {
|
|||
const timeLeft = Number(isBanned.expiresAt) - Date.now();
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
await banLogs.delete(req.ip);
|
||||
await banLogs.delete(userId);
|
||||
await banLogs.delete(ipKey);
|
||||
await banLogs.delete(userKey);
|
||||
return next();
|
||||
}
|
||||
|
||||
banCache.set(req.ip, isBanned, timeLeft);
|
||||
banCache.set(userId, isBanned, timeLeft);
|
||||
banCache.set(ipKey, isBanned, timeLeft);
|
||||
banCache.set(userKey, isBanned, timeLeft);
|
||||
req.banned = true;
|
||||
return await banResponse(req, res);
|
||||
};
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
const Keyv = require('keyv');
|
||||
const { logViolation } = require('../../cache');
|
||||
|
||||
const clearPendingReq = require('../../cache/clearPendingReq');
|
||||
const { logViolation, getLogStores } = require('../../cache');
|
||||
const denyRequest = require('./denyRequest');
|
||||
|
||||
// Serve cache from memory so no need to clear it on startup/exit
|
||||
const pendingReqCache = new Keyv({ namespace: 'pendingRequests' });
|
||||
const {
|
||||
USE_REDIS,
|
||||
CONCURRENT_MESSAGE_MAX = 1,
|
||||
CONCURRENT_VIOLATION_SCORE: score,
|
||||
} = process.env ?? {};
|
||||
const ttl = 1000 * 60 * 1;
|
||||
|
||||
/**
|
||||
* Middleware to limit concurrent requests for a user.
|
||||
|
@ -12,7 +15,7 @@ const pendingReqCache = new Keyv({ namespace: 'pendingRequests' });
|
|||
* This middleware checks if a user has exceeded a specified concurrent request limit.
|
||||
* If the user exceeds the limit, an error is returned. If the user is within the limit,
|
||||
* their request count is incremented. After the request is processed, the count is decremented.
|
||||
* If the `pendingReqCache` store is not available, the middleware will skip its logic.
|
||||
* If the `cache` store is not available, the middleware will skip its logic.
|
||||
*
|
||||
* @function
|
||||
* @param {Object} req - Express request object containing user information.
|
||||
|
@ -21,7 +24,9 @@ const pendingReqCache = new Keyv({ namespace: 'pendingRequests' });
|
|||
* @throws {Error} Throws an error if the user exceeds the concurrent request limit.
|
||||
*/
|
||||
const concurrentLimiter = async (req, res, next) => {
|
||||
if (!pendingReqCache) {
|
||||
const namespace = 'pending_req';
|
||||
const cache = getLogStores(namespace);
|
||||
if (!cache) {
|
||||
return next();
|
||||
}
|
||||
|
||||
|
@ -29,12 +34,12 @@ const concurrentLimiter = async (req, res, next) => {
|
|||
return next();
|
||||
}
|
||||
|
||||
const { CONCURRENT_MESSAGE_MAX = 1, CONCURRENT_VIOLATION_SCORE: score } = process.env;
|
||||
const userId = req.user?.id ?? req.user?._id ?? '';
|
||||
const limit = Math.max(CONCURRENT_MESSAGE_MAX, 1);
|
||||
const type = 'concurrent';
|
||||
|
||||
const userId = req.user?.id ?? req.user?._id ?? null;
|
||||
const pendingRequests = (await pendingReqCache.get(userId)) ?? 0;
|
||||
const key = `${USE_REDIS ? namespace : ''}:${userId}`;
|
||||
const pendingRequests = +((await cache.get(key)) ?? 0);
|
||||
|
||||
if (pendingRequests >= limit) {
|
||||
const errorMessage = {
|
||||
|
@ -46,22 +51,17 @@ const concurrentLimiter = async (req, res, next) => {
|
|||
await logViolation(req, res, type, errorMessage, score);
|
||||
return await denyRequest(req, res, errorMessage);
|
||||
} else {
|
||||
await pendingReqCache.set(userId, pendingRequests + 1);
|
||||
await cache.set(key, pendingRequests + 1, ttl);
|
||||
}
|
||||
|
||||
// Ensure the requests are removed from the store once the request is done
|
||||
let cleared = false;
|
||||
const cleanUp = async () => {
|
||||
if (!pendingReqCache) {
|
||||
if (cleared) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRequests = await pendingReqCache.get(userId);
|
||||
|
||||
if (currentRequests && currentRequests >= 1) {
|
||||
await pendingReqCache.set(userId, currentRequests - 1);
|
||||
} else {
|
||||
await pendingReqCache.delete(userId);
|
||||
}
|
||||
cleared = true;
|
||||
await clearPendingReq({ userId, cache });
|
||||
};
|
||||
|
||||
if (pendingRequests < limit) {
|
||||
|
@ -72,10 +72,4 @@ const concurrentLimiter = async (req, res, next) => {
|
|||
next();
|
||||
};
|
||||
|
||||
// if cache is not served from memory, clear it on exit
|
||||
// process.on('exit', async () => {
|
||||
// console.log('Clearing all pending requests before exiting...');
|
||||
// await pendingReqCache.clear();
|
||||
// });
|
||||
|
||||
module.exports = concurrentLimiter;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const Keyv = require('keyv');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { MeiliSearch } = require('meilisearch');
|
||||
|
@ -6,8 +7,15 @@ const { Conversation, getConvosQueried } = require('../../models/Conversation');
|
|||
const { reduceHits } = require('../../lib/utils/reduceHits');
|
||||
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
|
||||
const requireJwtAuth = require('../middleware/requireJwtAuth');
|
||||
const keyvRedis = require('../../cache/keyvRedis');
|
||||
const { isEnabled } = require('../utils');
|
||||
|
||||
const cache = new Map();
|
||||
const expiration = 60 * 1000;
|
||||
const cache = isEnabled(process.env.USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: 'search', ttl: expiration });
|
||||
|
||||
router.use(requireJwtAuth);
|
||||
|
||||
router.get('/sync', async function (req, res) {
|
||||
await Message.syncWithMeili();
|
||||
|
@ -15,24 +23,20 @@ router.get('/sync', async function (req, res) {
|
|||
res.send('synced');
|
||||
});
|
||||
|
||||
router.get('/', requireJwtAuth, async function (req, res) {
|
||||
router.get('/', async function (req, res) {
|
||||
try {
|
||||
let user = req.user.id;
|
||||
user = user ?? null;
|
||||
let user = req.user.id ?? '';
|
||||
const { q } = req.query;
|
||||
const pageNumber = req.query.pageNumber || 1;
|
||||
const key = `${user || ''}${q}`;
|
||||
|
||||
if (cache.has(key)) {
|
||||
const key = `${user}:search:${q}`;
|
||||
const cached = await cache.get(key);
|
||||
if (cached) {
|
||||
console.log('cache hit', key);
|
||||
const cached = cache.get(key);
|
||||
const { pages, pageSize, messages } = cached;
|
||||
res
|
||||
.status(200)
|
||||
.send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages });
|
||||
return;
|
||||
} else {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
// const message = await Message.meiliSearch(q);
|
||||
|
@ -67,7 +71,7 @@ router.get('/', requireJwtAuth, async function (req, res) {
|
|||
if (message.conversationId.includes('--')) {
|
||||
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
|
||||
}
|
||||
if (result.convoMap[message.conversationId] && !message.error) {
|
||||
if (result.convoMap[message.conversationId]) {
|
||||
const convo = result.convoMap[message.conversationId];
|
||||
const { title, chatGptLabel, model } = convo;
|
||||
message = { ...message, ...{ title, chatGptLabel, model } };
|
||||
|
@ -77,7 +81,7 @@ router.get('/', requireJwtAuth, async function (req, res) {
|
|||
result.messages = activeMessages;
|
||||
if (result.cache) {
|
||||
result.cache.messages = activeMessages;
|
||||
cache.set(key, result.cache);
|
||||
cache.set(key, result.cache, expiration);
|
||||
delete result.cache;
|
||||
}
|
||||
delete result.convoMap;
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
const Keyv = require('keyv');
|
||||
const axios = require('axios');
|
||||
const { isEnabled } = require('../utils');
|
||||
const keyvRedis = require('../../cache/keyvRedis');
|
||||
// const { getAzureCredentials, genAzureChatCompletion } = require('../../utils/');
|
||||
const { openAIApiKey, userProvidedOpenAI } = require('./EndpointService').config;
|
||||
|
||||
const modelsCache = new Keyv({ namespace: 'models' });
|
||||
const modelsCache = isEnabled(process.env.USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: 'models' });
|
||||
|
||||
const { OPENROUTER_API_KEY, OPENAI_REVERSE_PROXY, CHATGPT_MODELS, ANTHROPIC_MODELS } =
|
||||
process.env ?? {};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const session = require('express-session');
|
||||
const RedisStore = require('connect-redis').default;
|
||||
const passport = require('passport');
|
||||
const {
|
||||
googleLogin,
|
||||
|
@ -7,6 +8,7 @@ const {
|
|||
facebookLogin,
|
||||
setupOpenId,
|
||||
} = require('../strategies');
|
||||
const client = require('../cache/redis');
|
||||
|
||||
const configureSocialLogins = (app) => {
|
||||
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||
|
@ -28,13 +30,15 @@ const configureSocialLogins = (app) => {
|
|||
process.env.OPENID_SCOPE &&
|
||||
process.env.OPENID_SESSION_SECRET
|
||||
) {
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.OPENID_SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
}),
|
||||
);
|
||||
const sessionOptions = {
|
||||
secret: process.env.OPENID_SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
};
|
||||
if (process.env.USE_REDIS) {
|
||||
sessionOptions.store = new RedisStore({ client, prefix: 'librechat' });
|
||||
}
|
||||
app.use(session(sessionOptions));
|
||||
app.use(passport.session());
|
||||
setupOpenId();
|
||||
}
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -7,7 +7,6 @@ const isJson = (str: string) => {
|
|||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -170,7 +170,7 @@ export default function Message({
|
|||
text={text ?? ''}
|
||||
message={message}
|
||||
enterEdit={enterEdit}
|
||||
error={error ?? false}
|
||||
error={!!(error && !searchResult)}
|
||||
isSubmitting={isSubmitting}
|
||||
unfinished={unfinished ?? false}
|
||||
isCreatedByUser={isCreatedByUser ?? true}
|
||||
|
|
|
@ -13,7 +13,6 @@ import { Panel, Spinner } from '~/components';
|
|||
import { Conversations, Pages } from '../Conversations';
|
||||
import {
|
||||
useAuthContext,
|
||||
useDebounce,
|
||||
useMediaQuery,
|
||||
useLocalize,
|
||||
useConversation,
|
||||
|
@ -67,14 +66,8 @@ export default function Nav({ navVisible, setNavVisible }) {
|
|||
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
|
||||
const debouncedSearchTerm = useDebounce(searchQuery, 750);
|
||||
const searchQueryFn = useSearchQuery(debouncedSearchTerm, pageNumber + '', {
|
||||
enabled: !!(
|
||||
!!debouncedSearchTerm &&
|
||||
debouncedSearchTerm.length > 0 &&
|
||||
isSearchEnabled &&
|
||||
isSearching
|
||||
),
|
||||
const searchQueryFn = useSearchQuery(searchQuery, pageNumber + '', {
|
||||
enabled: !!(!!searchQuery && searchQuery.length > 0 && isSearchEnabled && isSearching),
|
||||
});
|
||||
|
||||
const onSearchSuccess = useCallback((data: TSearchResults, expectedPage?: number) => {
|
||||
|
|
|
@ -55,6 +55,7 @@ export default function NavLinks() {
|
|||
'group-ui-open:bg-gray-800 flex w-full items-center gap-2.5 rounded-md px-3 py-3 text-sm transition-colors duration-200 hover:bg-gray-800',
|
||||
open ? 'bg-gray-800' : '',
|
||||
)}
|
||||
data-testid="nav-user"
|
||||
>
|
||||
<div className="-ml-0.9 -mt-0.8 h-9 w-8 flex-shrink-0">
|
||||
<div className="relative flex">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { forwardRef, useState, useEffect, Ref } from 'react';
|
||||
import { forwardRef, useState, useCallback, useMemo, Ref } from 'react';
|
||||
import { Search, X } from 'lucide-react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
|
@ -10,33 +11,35 @@ type SearchBarProps = {
|
|||
|
||||
const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { clearSearch } = props;
|
||||
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
|
||||
const setSearchQuery = useSetRecoilState(store.searchQuery);
|
||||
const [showClearIcon, setShowClearIcon] = useState(false);
|
||||
const [text, setText] = useState('');
|
||||
const localize = useLocalize();
|
||||
|
||||
const clearText = useCallback(() => {
|
||||
setShowClearIcon(false);
|
||||
setSearchQuery('');
|
||||
clearSearch();
|
||||
setText('');
|
||||
}, [setSearchQuery, clearSearch]);
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target as HTMLInputElement;
|
||||
/* TODO: deprecated keyCode */
|
||||
if (e.keyCode === 8 && value === '') {
|
||||
setSearchQuery('');
|
||||
clearSearch();
|
||||
if (e.key === 'Backspace' && value === '') {
|
||||
clearText();
|
||||
}
|
||||
};
|
||||
|
||||
const sendRequest = useCallback((value: string) => setSearchQuery(value), [setSearchQuery]);
|
||||
const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]);
|
||||
|
||||
const onChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target as HTMLInputElement;
|
||||
setSearchQuery(value);
|
||||
setShowClearIcon(value.length > 0);
|
||||
setText(value);
|
||||
debouncedSendRequest(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery.length === 0) {
|
||||
setShowClearIcon(false);
|
||||
} else {
|
||||
setShowClearIcon(true);
|
||||
}
|
||||
}, [searchQuery]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
@ -46,7 +49,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
<input
|
||||
type="text"
|
||||
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight outline-none"
|
||||
value={searchQuery}
|
||||
value={text}
|
||||
onChange={onChange}
|
||||
onKeyDown={(e) => {
|
||||
e.code === 'Space' ? e.stopPropagation() : null;
|
||||
|
@ -58,10 +61,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
|
|||
className={`absolute right-3 h-5 w-5 cursor-pointer ${
|
||||
showClearIcon ? 'opacity-100' : 'opacity-0'
|
||||
} transition-opacity duration-1000`}
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
clearSearch();
|
||||
}}
|
||||
onClick={clearText}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -29,8 +29,8 @@ export default function Root() {
|
|||
const setEndpointsConfig = useSetRecoilState(store.endpointsConfig);
|
||||
const setModelsConfig = useSetRecoilState(store.modelsConfig);
|
||||
|
||||
const searchEnabledQuery = useGetSearchEnabledQuery();
|
||||
const endpointsQuery = useGetEndpointsQuery();
|
||||
const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated });
|
||||
const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated });
|
||||
const presetsQuery = useGetPresetsQuery({ enabled: !!user });
|
||||
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { atom, selector } from 'recoil';
|
||||
import { TEndpointsConfig } from 'librechat-data-provider';
|
||||
|
||||
const defaultConfig: TEndpointsConfig = {
|
||||
azureOpenAI: null,
|
||||
openAI: null,
|
||||
bingAI: null,
|
||||
chatGPTBrowser: null,
|
||||
gptPlugins: null,
|
||||
google: null,
|
||||
anthropic: null,
|
||||
};
|
||||
|
||||
const endpointsConfig = atom<TEndpointsConfig>({
|
||||
key: 'endpointsConfig',
|
||||
default: {
|
||||
azureOpenAI: null,
|
||||
openAI: null,
|
||||
bingAI: null,
|
||||
chatGPTBrowser: null,
|
||||
gptPlugins: null,
|
||||
google: null,
|
||||
anthropic: null,
|
||||
},
|
||||
default: defaultConfig,
|
||||
});
|
||||
|
||||
const plugins = selector({
|
||||
|
@ -58,4 +60,5 @@ export default {
|
|||
endpointsConfig,
|
||||
endpointsFilter,
|
||||
availableEndpoints,
|
||||
defaultConfig,
|
||||
};
|
||||
|
|
|
@ -28,11 +28,18 @@ const getEndpointFromSetup = (convoSetup: TConvoSetup, endpointsConfig: TEndpoin
|
|||
const getEndpointFromLocalStorage = (endpointsConfig: TEndpointsConfig) => {
|
||||
try {
|
||||
const { lastConversationSetup } = getLocalStorageItems();
|
||||
const { endpoint } = lastConversationSetup;
|
||||
const isDefaultConfig = Object.values(endpointsConfig ?? {})?.every((value) => !value);
|
||||
|
||||
return (
|
||||
lastConversationSetup.endpoint &&
|
||||
(endpointsConfig[lastConversationSetup.endpoint] ? lastConversationSetup.endpoint : null)
|
||||
);
|
||||
if (isDefaultConfig && endpoint) {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
if (isDefaultConfig && endpoint) {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
return endpoint && endpointsConfig[endpoint] ? endpoint : null;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { Page, FullConfig, chromium } from '@playwright/test';
|
||||
import cleanupUser from './cleanupUser';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
type User = { email: string; name: string; password: string };
|
||||
const timeout = 3500;
|
||||
|
||||
async function register(page: Page, user: User) {
|
||||
await page.getByRole('link', { name: 'Sign up' }).click();
|
||||
|
@ -52,18 +54,31 @@ async function authenticate(config: FullConfig, user: User) {
|
|||
});
|
||||
console.log('🤖: ✔️ localStorage: set Nav as Visible', storageState);
|
||||
|
||||
await page.goto(baseURL, { timeout: 5000 });
|
||||
await page.goto(baseURL, { timeout });
|
||||
await register(page, user);
|
||||
await page.waitForURL(`${baseURL}/chat/new`);
|
||||
try {
|
||||
await page.waitForURL(`${baseURL}/chat/new`, { timeout });
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
const userExists = page.getByTestId('registration-error');
|
||||
if (userExists) {
|
||||
console.log('🤖: 🚨 user already exists');
|
||||
await cleanupUser(user);
|
||||
await page.goto(baseURL, { timeout });
|
||||
await register(page, user);
|
||||
} else {
|
||||
throw new Error('🤖: 🚨 user failed to register');
|
||||
}
|
||||
}
|
||||
console.log('🤖: ✔️ user successfully registered');
|
||||
|
||||
// Logout
|
||||
await logout(page, user);
|
||||
await page.waitForURL(`${baseURL}/login`);
|
||||
await page.waitForURL(`${baseURL}/login`, { timeout });
|
||||
console.log('🤖: ✔️ user successfully logged out');
|
||||
|
||||
await login(page, user);
|
||||
await page.waitForURL(`${baseURL}/chat/new`);
|
||||
await page.waitForURL(`${baseURL}/chat/new`, { timeout });
|
||||
console.log('🤖: ✔️ user successfully authenticated');
|
||||
|
||||
await page.context().storageState({ path: storageState as string });
|
||||
|
|
|
@ -38,14 +38,6 @@ test.beforeAll(async ({ browser }) => {
|
|||
await page.close();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
console.log('🤖: clearing conversations after message tests.');
|
||||
const page = await beforeAfterAllContext.newPage();
|
||||
await clearConvos(page);
|
||||
await page.close();
|
||||
await beforeAfterAllContext.close();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(initialUrl, { timeout: 5000 });
|
||||
});
|
||||
|
|
|
@ -4,14 +4,14 @@ test.describe('Navigation suite', () => {
|
|||
test('Navigation bar', async ({ page }) => {
|
||||
await page.goto('http://localhost:3080/', { timeout: 5000 });
|
||||
|
||||
await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click();
|
||||
const navBar = await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').isVisible();
|
||||
expect(navBar).toBeTruthy();
|
||||
await page.getByTestId('nav-user').click();
|
||||
const navSettings = await page.getByTestId('nav-user').isVisible();
|
||||
expect(navSettings).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Settings modal', async ({ page }) => {
|
||||
await page.goto('http://localhost:3080/', { timeout: 5000 });
|
||||
await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click();
|
||||
await page.getByTestId('nav-user').click();
|
||||
await page.getByText('Settings').click();
|
||||
|
||||
const modal = await page.getByRole('dialog', { name: 'Settings' }).isVisible();
|
||||
|
|
232
package-lock.json
generated
232
package-lock.json
generated
|
@ -15,7 +15,7 @@
|
|||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.32.1",
|
||||
"@playwright/test": "^1.38.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"cross-env": "^7.0.3",
|
||||
|
@ -45,11 +45,13 @@
|
|||
"@anthropic-ai/sdk": "^0.5.4",
|
||||
"@azure/search-documents": "^11.3.2",
|
||||
"@keyv/mongo": "^2.1.8",
|
||||
"@keyv/redis": "^2.8.0",
|
||||
"@waylaidwanderer/chatgpt-api": "^1.37.2",
|
||||
"axios": "^1.3.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cohere-ai": "^6.0.0",
|
||||
"connect-redis": "^7.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
|
@ -60,10 +62,11 @@
|
|||
"googleapis": "^118.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"html": "^1.0.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jose": "^4.15.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"keyv": "^4.5.3",
|
||||
"keyv": "^4.5.4",
|
||||
"keyv-file": "^0.2.0",
|
||||
"langchain": "^0.0.153",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -4850,6 +4853,11 @@
|
|||
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
||||
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
|
@ -5419,6 +5427,17 @@
|
|||
"@mongodb-js/saslprep": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@keyv/redis": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@keyv/redis/-/redis-2.8.0.tgz",
|
||||
"integrity": "sha512-6k7wG/KKSIGpruKlsEB4sFjECJEyQsuJbWoWdoq9Uv2L6Mm/SEqEidekRZI/QljE1A4WQkFsIE8hHl1Oc3UNGg==",
|
||||
"dependencies": {
|
||||
"ioredis": "^5.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@librechat/backend": {
|
||||
"resolved": "api",
|
||||
"link": true
|
||||
|
@ -5531,12 +5550,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz",
|
||||
"integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==",
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
|
||||
"integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.38.0"
|
||||
"playwright": "1.38.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
@ -6500,6 +6519,78 @@
|
|||
"node": ">=8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
|
||||
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "1.5.11",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.11.tgz",
|
||||
"integrity": "sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2",
|
||||
"generic-pool": "3.9.0",
|
||||
"yallist": "4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@redis/graph": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
|
||||
"integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/json": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz",
|
||||
"integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/search": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.5.tgz",
|
||||
"integrity": "sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/time-series": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz",
|
||||
"integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@remix-run/router": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz",
|
||||
|
@ -9682,6 +9773,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
|
@ -9856,6 +9955,17 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/connect-redis": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-7.1.0.tgz",
|
||||
"integrity": "sha512-UaqO1EirWjON2ENsyau7N5lbkrdYBpS6mYlXSeff/OYXsd6EGZ+SXSmNPoljL2PSua8fgjAEaldSA73PMZQ9Eg==",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express-session": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "2.15.3",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
|
||||
|
@ -10399,6 +10509,14 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
|
@ -12368,6 +12486,16 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/generic-pool": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
|
||||
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
|
@ -13636,6 +13764,29 @@
|
|||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
|
||||
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "^1.1.1",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"denque": "^2.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.isarguments": "^3.1.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
|
@ -15222,9 +15373,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz",
|
||||
"integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==",
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
|
@ -15583,11 +15734,21 @@
|
|||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
},
|
||||
"node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
|
@ -18422,12 +18583,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.38.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz",
|
||||
"integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==",
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
|
||||
"integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.38.0"
|
||||
"playwright-core": "1.38.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
@ -18440,9 +18601,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.38.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz",
|
||||
"integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==",
|
||||
"version": "1.38.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
|
||||
"integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
@ -20348,6 +20509,40 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "4.6.10",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-4.6.10.tgz",
|
||||
"integrity": "sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@redis/bloom": "1.2.0",
|
||||
"@redis/client": "1.5.11",
|
||||
"@redis/graph": "1.1.0",
|
||||
"@redis/json": "1.0.6",
|
||||
"@redis/search": "1.1.5",
|
||||
"@redis/time-series": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
|
||||
|
@ -21397,6 +21592,11 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/danny-avila/LibreChat#readme",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.32.1",
|
||||
"@playwright/test": "^1.38.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue