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:
Danny Avila 2023-10-11 17:05:47 -04:00 committed by GitHub
parent 4ac0c04e83
commit 5145121eb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 461 additions and 171 deletions

View file

@ -1,29 +1,48 @@
const Keyv = require('keyv'); const getLogStores = require('./getLogStores');
const { pendingReqFile } = require('./keyvFiles'); const { isEnabled } = require('../server/utils');
const { LIMIT_CONCURRENT_MESSAGES } = process.env ?? {}; const { USE_REDIS, LIMIT_CONCURRENT_MESSAGES } = process.env ?? {};
const ttl = 1000 * 60 * 1;
const keyv = new Keyv({ store: pendingReqFile, namespace: 'pendingRequests' });
/** /**
* Clear pending requests from the cache. * Clear or decrement pending requests from the cache.
* Checks the environmental variable LIMIT_CONCURRENT_MESSAGES; * 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 * @module clearPendingReq
* @requires keyv * @requires ./getLogStores
* @requires keyvFiles * @requires ../server/utils
* @requires process * @requires process
* *
* @async * @async
* @function * @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 () => { const clearPendingReq = async ({ userId, cache: _cache }) => {
if (LIMIT_CONCURRENT_MESSAGES?.toLowerCase() !== 'true') { if (!userId) {
return;
} else if (!isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
return; 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; module.exports = clearPendingReq;

View file

@ -1,26 +1,37 @@
const Keyv = require('keyv'); const Keyv = require('keyv');
const keyvMongo = require('./keyvMongo'); 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 { logFile, violationFile } = require('./keyvFiles');
const { BAN_DURATION } = process.env ?? {}; const { BAN_DURATION, USE_REDIS } = process.env ?? {};
const duration = math(BAN_DURATION, 7200000); 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 = { 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' }), general: new Keyv({ store: logFile, namespace: 'violations' }),
concurrent: new Keyv({ store: violationFile, namespace: 'concurrent' }), concurrent: createViolationInstance('concurrent'),
non_browser: new Keyv({ store: violationFile, namespace: 'non_browser' }), non_browser: createViolationInstance('non_browser'),
message_limit: new Keyv({ store: violationFile, namespace: 'message_limit' }), message_limit: createViolationInstance('message_limit'),
token_balance: new Keyv({ store: violationFile, namespace: 'token_balance' }), token_balance: createViolationInstance('token_balance'),
registrations: new Keyv({ store: violationFile, namespace: 'registrations' }), registrations: createViolationInstance('registrations'),
logins: new Keyv({ store: violationFile, namespace: 'logins' }), logins: createViolationInstance('logins'),
}; };
/** /**
* Returns either the logs of violations specified by type if a type is provided * Returns the keyv cache specified by type.
* or it returns the general log if no type is specified. If an invalid type is passed, * If an invalid type is passed, an error will be thrown.
* an error will be thrown.
* *
* @module getLogStores * @module getLogStores
* @requires keyv - a simple key-value storage that allows you to easily switch out storage adapters. * @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. * @throws Will throw an error if an invalid violation type is passed.
*/ */
const getLogStores = (type) => { const getLogStores = (type) => {
if (!type) { if (!type || !namespaces[type]) {
throw new Error(`Invalid store type: ${type}`); throw new Error(`Invalid store type: ${type}`);
} }
const logs = namespaces[type]; return namespaces[type];
return logs;
}; };
module.exports = getLogStores; module.exports = getLogStores;

3
api/cache/index.js vendored
View file

@ -1,6 +1,5 @@
const keyvFiles = require('./keyvFiles'); const keyvFiles = require('./keyvFiles');
const getLogStores = require('./getLogStores'); const getLogStores = require('./getLogStores');
const logViolation = require('./logViolation'); 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
View 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;

View file

@ -1,5 +1,6 @@
const getLogStores = require('./getLogStores'); const getLogStores = require('./getLogStores');
const banViolation = require('./banViolation'); const banViolation = require('./banViolation');
const { isEnabled } = require('../server/utils');
/** /**
* Logs the violation. * Logs the violation.
@ -17,10 +18,11 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => {
} }
const logs = getLogStores('general'); const logs = getLogStores('general');
const violationLogs = getLogStores(type); const violationLogs = getLogStores(type);
const key = isEnabled(process.env.USE_REDIS) ? `${type}:${userId}` : userId;
const userViolations = (await violationLogs.get(userId)) ?? 0; const userViolations = (await violationLogs.get(key)) ?? 0;
const violationCount = userViolations + score; const violationCount = +userViolations + +score;
await violationLogs.set(userId, violationCount); await violationLogs.set(key, violationCount);
errorMessage.user_id = userId; errorMessage.user_id = userId;
errorMessage.prev_count = userViolations; errorMessage.prev_count = userViolations;
@ -28,10 +30,10 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => {
errorMessage.date = new Date().toISOString(); errorMessage.date = new Date().toISOString();
await banViolation(req, res, errorMessage); await banViolation(req, res, errorMessage);
const userLogs = (await logs.get(userId)) ?? []; const userLogs = (await logs.get(key)) ?? [];
userLogs.push(errorMessage); userLogs.push(errorMessage);
delete errorMessage.user_id; delete errorMessage.user_id;
await logs.set(userId, userLogs); await logs.set(key, userLogs);
}; };
module.exports = logViolation; module.exports = logViolation;

4
api/cache/redis.js vendored Normal file
View 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
View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
// "checkJs": true, // Report errors in JavaScript files
"baseUrl": "./",
"paths": {
"*": ["*", "node_modules/*"],
"~/*": ["./*"]
}
},
"exclude": ["node_modules"]
}

View file

@ -18,11 +18,12 @@ if (!cached) {
} }
async function connectDb() { async function connectDb() {
if (cached.conn) { if (cached.conn && cached.conn?._readyState === 1) {
return cached.conn; return cached.conn;
} }
if (!cached.promise) { const disconnected = cached.conn && cached.conn?._readyState !== 1;
if (!cached.promise || disconnected) {
const opts = { const opts = {
useNewUrlParser: true, useNewUrlParser: true,
useUnifiedTopology: true, useUnifiedTopology: true,

View file

@ -24,11 +24,13 @@
"@anthropic-ai/sdk": "^0.5.4", "@anthropic-ai/sdk": "^0.5.4",
"@azure/search-documents": "^11.3.2", "@azure/search-documents": "^11.3.2",
"@keyv/mongo": "^2.1.8", "@keyv/mongo": "^2.1.8",
"@keyv/redis": "^2.8.0",
"@waylaidwanderer/chatgpt-api": "^1.37.2", "@waylaidwanderer/chatgpt-api": "^1.37.2",
"axios": "^1.3.4", "axios": "^1.3.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"cohere-ai": "^6.0.0", "cohere-ai": "^6.0.0",
"connect-redis": "^7.1.0",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@ -39,10 +41,11 @@
"googleapis": "^118.0.0", "googleapis": "^118.0.0",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"html": "^1.0.0", "html": "^1.0.0",
"ioredis": "^5.3.2",
"jose": "^4.15.2", "jose": "^4.15.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"keyv": "^4.5.3", "keyv": "^4.5.4",
"keyv-file": "^0.2.0", "keyv-file": "^0.2.0",
"langchain": "^0.0.153", "langchain": "^0.0.153",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View file

@ -1,5 +1,6 @@
const { sendMessage, sendError, countTokens, isEnabled } = require('../utils');
const { saveMessage, getConvo, getConvoTitle } = require('../../models'); const { saveMessage, getConvo, getConvoTitle } = require('../../models');
const { sendMessage, sendError, countTokens } = require('../utils'); const clearPendingReq = require('../../cache/clearPendingReq');
const spendTokens = require('../../models/spendTokens'); const spendTokens = require('../../models/spendTokens');
const abortControllers = require('./abortControllers'); const abortControllers = require('./abortControllers');
@ -20,6 +21,9 @@ async function abortMessage(req, res) {
const handleAbort = () => { const handleAbort = () => {
return async (req, res) => { return async (req, res) => {
try { try {
if (isEnabled(process.env.LIMIT_CONCURRENT_MESSAGES)) {
await clearPendingReq({ userId: req.user.id });
}
return await abortMessage(req, res); return await abortMessage(req, res);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View file

@ -3,8 +3,11 @@ const uap = require('ua-parser-js');
const { getLogStores } = require('../../cache'); const { getLogStores } = require('../../cache');
const denyRequest = require('./denyRequest'); const denyRequest = require('./denyRequest');
const { isEnabled, removePorts } = require('../utils'); 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.'; 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); req.ip = removePorts(req);
const userId = req.user?.id ?? req.user?._id ?? null; 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 cachedIPBan = await banCache.get(ipKey);
const cachedUserBan = await banCache.get(userId); const cachedUserBan = await banCache.get(userKey);
const cachedBan = cachedIPBan || cachedUserBan; const cachedBan = cachedIPBan || cachedUserBan;
if (cachedBan) { if (cachedBan) {
@ -78,13 +83,13 @@ const checkBan = async (req, res, next = () => {}) => {
const timeLeft = Number(isBanned.expiresAt) - Date.now(); const timeLeft = Number(isBanned.expiresAt) - Date.now();
if (timeLeft <= 0) { if (timeLeft <= 0) {
await banLogs.delete(req.ip); await banLogs.delete(ipKey);
await banLogs.delete(userId); await banLogs.delete(userKey);
return next(); return next();
} }
banCache.set(req.ip, isBanned, timeLeft); banCache.set(ipKey, isBanned, timeLeft);
banCache.set(userId, isBanned, timeLeft); banCache.set(userKey, isBanned, timeLeft);
req.banned = true; req.banned = true;
return await banResponse(req, res); return await banResponse(req, res);
}; };

View file

@ -1,10 +1,13 @@
const Keyv = require('keyv'); const clearPendingReq = require('../../cache/clearPendingReq');
const { logViolation } = require('../../cache'); const { logViolation, getLogStores } = require('../../cache');
const denyRequest = require('./denyRequest'); const denyRequest = require('./denyRequest');
// Serve cache from memory so no need to clear it on startup/exit const {
const pendingReqCache = new Keyv({ namespace: 'pendingRequests' }); 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. * 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. * 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, * 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. * 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 * @function
* @param {Object} req - Express request object containing user information. * @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. * @throws {Error} Throws an error if the user exceeds the concurrent request limit.
*/ */
const concurrentLimiter = async (req, res, next) => { const concurrentLimiter = async (req, res, next) => {
if (!pendingReqCache) { const namespace = 'pending_req';
const cache = getLogStores(namespace);
if (!cache) {
return next(); return next();
} }
@ -29,12 +34,12 @@ const concurrentLimiter = async (req, res, next) => {
return 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 limit = Math.max(CONCURRENT_MESSAGE_MAX, 1);
const type = 'concurrent'; const type = 'concurrent';
const userId = req.user?.id ?? req.user?._id ?? null; const key = `${USE_REDIS ? namespace : ''}:${userId}`;
const pendingRequests = (await pendingReqCache.get(userId)) ?? 0; const pendingRequests = +((await cache.get(key)) ?? 0);
if (pendingRequests >= limit) { if (pendingRequests >= limit) {
const errorMessage = { const errorMessage = {
@ -46,22 +51,17 @@ const concurrentLimiter = async (req, res, next) => {
await logViolation(req, res, type, errorMessage, score); await logViolation(req, res, type, errorMessage, score);
return await denyRequest(req, res, errorMessage); return await denyRequest(req, res, errorMessage);
} else { } 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 // Ensure the requests are removed from the store once the request is done
let cleared = false;
const cleanUp = async () => { const cleanUp = async () => {
if (!pendingReqCache) { if (cleared) {
return; return;
} }
cleared = true;
const currentRequests = await pendingReqCache.get(userId); await clearPendingReq({ userId, cache });
if (currentRequests && currentRequests >= 1) {
await pendingReqCache.set(userId, currentRequests - 1);
} else {
await pendingReqCache.delete(userId);
}
}; };
if (pendingRequests < limit) { if (pendingRequests < limit) {
@ -72,10 +72,4 @@ const concurrentLimiter = async (req, res, next) => {
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; module.exports = concurrentLimiter;

View file

@ -1,3 +1,4 @@
const Keyv = require('keyv');
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { MeiliSearch } = require('meilisearch'); const { MeiliSearch } = require('meilisearch');
@ -6,8 +7,15 @@ const { Conversation, getConvosQueried } = require('../../models/Conversation');
const { reduceHits } = require('../../lib/utils/reduceHits'); const { reduceHits } = require('../../lib/utils/reduceHits');
const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc'); const { cleanUpPrimaryKeyValue } = require('../../lib/utils/misc');
const requireJwtAuth = require('../middleware/requireJwtAuth'); 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) { router.get('/sync', async function (req, res) {
await Message.syncWithMeili(); await Message.syncWithMeili();
@ -15,24 +23,20 @@ router.get('/sync', async function (req, res) {
res.send('synced'); res.send('synced');
}); });
router.get('/', requireJwtAuth, async function (req, res) { router.get('/', async function (req, res) {
try { try {
let user = req.user.id; let user = req.user.id ?? '';
user = user ?? null;
const { q } = req.query; const { q } = req.query;
const pageNumber = req.query.pageNumber || 1; const pageNumber = req.query.pageNumber || 1;
const key = `${user || ''}${q}`; const key = `${user}:search:${q}`;
const cached = await cache.get(key);
if (cache.has(key)) { if (cached) {
console.log('cache hit', key); console.log('cache hit', key);
const cached = cache.get(key);
const { pages, pageSize, messages } = cached; const { pages, pageSize, messages } = cached;
res res
.status(200) .status(200)
.send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages }); .send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages });
return; return;
} else {
cache.clear();
} }
// const message = await Message.meiliSearch(q); // const message = await Message.meiliSearch(q);
@ -67,7 +71,7 @@ router.get('/', requireJwtAuth, async function (req, res) {
if (message.conversationId.includes('--')) { if (message.conversationId.includes('--')) {
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId); message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
} }
if (result.convoMap[message.conversationId] && !message.error) { if (result.convoMap[message.conversationId]) {
const convo = result.convoMap[message.conversationId]; const convo = result.convoMap[message.conversationId];
const { title, chatGptLabel, model } = convo; const { title, chatGptLabel, model } = convo;
message = { ...message, ...{ title, chatGptLabel, model } }; message = { ...message, ...{ title, chatGptLabel, model } };
@ -77,7 +81,7 @@ router.get('/', requireJwtAuth, async function (req, res) {
result.messages = activeMessages; result.messages = activeMessages;
if (result.cache) { if (result.cache) {
result.cache.messages = activeMessages; result.cache.messages = activeMessages;
cache.set(key, result.cache); cache.set(key, result.cache, expiration);
delete result.cache; delete result.cache;
} }
delete result.convoMap; delete result.convoMap;

View file

@ -1,9 +1,13 @@
const Keyv = require('keyv'); const Keyv = require('keyv');
const axios = require('axios'); const axios = require('axios');
const { isEnabled } = require('../utils');
const keyvRedis = require('../../cache/keyvRedis');
// const { getAzureCredentials, genAzureChatCompletion } = require('../../utils/'); // const { getAzureCredentials, genAzureChatCompletion } = require('../../utils/');
const { openAIApiKey, userProvidedOpenAI } = require('./EndpointService').config; 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 } = const { OPENROUTER_API_KEY, OPENAI_REVERSE_PROXY, CHATGPT_MODELS, ANTHROPIC_MODELS } =
process.env ?? {}; process.env ?? {};

View file

@ -1,4 +1,5 @@
const session = require('express-session'); const session = require('express-session');
const RedisStore = require('connect-redis').default;
const passport = require('passport'); const passport = require('passport');
const { const {
googleLogin, googleLogin,
@ -7,6 +8,7 @@ const {
facebookLogin, facebookLogin,
setupOpenId, setupOpenId,
} = require('../strategies'); } = require('../strategies');
const client = require('../cache/redis');
const configureSocialLogins = (app) => { const configureSocialLogins = (app) => {
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { 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_SCOPE &&
process.env.OPENID_SESSION_SECRET process.env.OPENID_SESSION_SECRET
) { ) {
app.use( const sessionOptions = {
session({
secret: process.env.OPENID_SESSION_SECRET, secret: process.env.OPENID_SESSION_SECRET,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
}), };
); if (process.env.USE_REDIS) {
sessionOptions.store = new RedisStore({ client, prefix: 'librechat' });
}
app.use(session(sessionOptions));
app.use(passport.session()); app.use(passport.session());
setupOpenId(); setupOpenId();
} }

BIN
bun.lockb

Binary file not shown.

View file

@ -7,7 +7,6 @@ const isJson = (str: string) => {
try { try {
JSON.parse(str); JSON.parse(str);
} catch (e) { } catch (e) {
console.error(e);
return false; return false;
} }
return true; return true;

View file

@ -170,7 +170,7 @@ export default function Message({
text={text ?? ''} text={text ?? ''}
message={message} message={message}
enterEdit={enterEdit} enterEdit={enterEdit}
error={error ?? false} error={!!(error && !searchResult)}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
unfinished={unfinished ?? false} unfinished={unfinished ?? false}
isCreatedByUser={isCreatedByUser ?? true} isCreatedByUser={isCreatedByUser ?? true}

View file

@ -13,7 +13,6 @@ import { Panel, Spinner } from '~/components';
import { Conversations, Pages } from '../Conversations'; import { Conversations, Pages } from '../Conversations';
import { import {
useAuthContext, useAuthContext,
useDebounce,
useMediaQuery, useMediaQuery,
useLocalize, useLocalize,
useConversation, useConversation,
@ -67,14 +66,8 @@ export default function Nav({ navVisible, setNavVisible }) {
const [isFetching, setIsFetching] = useState(false); const [isFetching, setIsFetching] = useState(false);
const debouncedSearchTerm = useDebounce(searchQuery, 750); const searchQueryFn = useSearchQuery(searchQuery, pageNumber + '', {
const searchQueryFn = useSearchQuery(debouncedSearchTerm, pageNumber + '', { enabled: !!(!!searchQuery && searchQuery.length > 0 && isSearchEnabled && isSearching),
enabled: !!(
!!debouncedSearchTerm &&
debouncedSearchTerm.length > 0 &&
isSearchEnabled &&
isSearching
),
}); });
const onSearchSuccess = useCallback((data: TSearchResults, expectedPage?: number) => { const onSearchSuccess = useCallback((data: TSearchResults, expectedPage?: number) => {

View file

@ -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', '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' : '', 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="-ml-0.9 -mt-0.8 h-9 w-8 flex-shrink-0">
<div className="relative flex"> <div className="relative flex">

View file

@ -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 { Search, X } from 'lucide-react';
import { useRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import debounce from 'lodash/debounce';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import store from '~/store'; import store from '~/store';
@ -10,33 +11,35 @@ type SearchBarProps = {
const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => { const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) => {
const { clearSearch } = props; const { clearSearch } = props;
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery); const setSearchQuery = useSetRecoilState(store.searchQuery);
const [showClearIcon, setShowClearIcon] = useState(false); const [showClearIcon, setShowClearIcon] = useState(false);
const [text, setText] = useState('');
const localize = useLocalize(); const localize = useLocalize();
const clearText = useCallback(() => {
setShowClearIcon(false);
setSearchQuery('');
clearSearch();
setText('');
}, [setSearchQuery, clearSearch]);
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
const { value } = e.target as HTMLInputElement; const { value } = e.target as HTMLInputElement;
/* TODO: deprecated keyCode */ if (e.key === 'Backspace' && value === '') {
if (e.keyCode === 8 && value === '') { clearText();
setSearchQuery('');
clearSearch();
} }
}; };
const sendRequest = useCallback((value: string) => setSearchQuery(value), [setSearchQuery]);
const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]);
const onChange = (e: React.FormEvent<HTMLInputElement>) => { const onChange = (e: React.FormEvent<HTMLInputElement>) => {
const { value } = e.target as HTMLInputElement; const { value } = e.target as HTMLInputElement;
setSearchQuery(value);
setShowClearIcon(value.length > 0); setShowClearIcon(value.length > 0);
setText(value);
debouncedSendRequest(value);
}; };
useEffect(() => {
if (searchQuery.length === 0) {
setShowClearIcon(false);
} else {
setShowClearIcon(true);
}
}, [searchQuery]);
return ( return (
<div <div
ref={ref} ref={ref}
@ -46,7 +49,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref<HTMLDivElement>) =
<input <input
type="text" type="text"
className="m-0 mr-0 w-full border-none bg-transparent p-0 pl-7 text-sm leading-tight outline-none" 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} onChange={onChange}
onKeyDown={(e) => { onKeyDown={(e) => {
e.code === 'Space' ? e.stopPropagation() : null; 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 ${ className={`absolute right-3 h-5 w-5 cursor-pointer ${
showClearIcon ? 'opacity-100' : 'opacity-0' showClearIcon ? 'opacity-100' : 'opacity-0'
} transition-opacity duration-1000`} } transition-opacity duration-1000`}
onClick={() => { onClick={clearText}
setSearchQuery('');
clearSearch();
}}
/> />
</div> </div>
); );

View file

@ -29,8 +29,8 @@ export default function Root() {
const setEndpointsConfig = useSetRecoilState(store.endpointsConfig); const setEndpointsConfig = useSetRecoilState(store.endpointsConfig);
const setModelsConfig = useSetRecoilState(store.modelsConfig); const setModelsConfig = useSetRecoilState(store.modelsConfig);
const searchEnabledQuery = useGetSearchEnabledQuery();
const endpointsQuery = useGetEndpointsQuery(); const endpointsQuery = useGetEndpointsQuery();
const searchEnabledQuery = useGetSearchEnabledQuery({ enabled: isAuthenticated });
const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated }); const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated });
const presetsQuery = useGetPresetsQuery({ enabled: !!user }); const presetsQuery = useGetPresetsQuery({ enabled: !!user });

View file

@ -1,9 +1,7 @@
import { atom, selector } from 'recoil'; import { atom, selector } from 'recoil';
import { TEndpointsConfig } from 'librechat-data-provider'; import { TEndpointsConfig } from 'librechat-data-provider';
const endpointsConfig = atom<TEndpointsConfig>({ const defaultConfig: TEndpointsConfig = {
key: 'endpointsConfig',
default: {
azureOpenAI: null, azureOpenAI: null,
openAI: null, openAI: null,
bingAI: null, bingAI: null,
@ -11,7 +9,11 @@ const endpointsConfig = atom<TEndpointsConfig>({
gptPlugins: null, gptPlugins: null,
google: null, google: null,
anthropic: null, anthropic: null,
}, };
const endpointsConfig = atom<TEndpointsConfig>({
key: 'endpointsConfig',
default: defaultConfig,
}); });
const plugins = selector({ const plugins = selector({
@ -58,4 +60,5 @@ export default {
endpointsConfig, endpointsConfig,
endpointsFilter, endpointsFilter,
availableEndpoints, availableEndpoints,
defaultConfig,
}; };

View file

@ -28,11 +28,18 @@ const getEndpointFromSetup = (convoSetup: TConvoSetup, endpointsConfig: TEndpoin
const getEndpointFromLocalStorage = (endpointsConfig: TEndpointsConfig) => { const getEndpointFromLocalStorage = (endpointsConfig: TEndpointsConfig) => {
try { try {
const { lastConversationSetup } = getLocalStorageItems(); const { lastConversationSetup } = getLocalStorageItems();
const { endpoint } = lastConversationSetup;
const isDefaultConfig = Object.values(endpointsConfig ?? {})?.every((value) => !value);
return ( if (isDefaultConfig && endpoint) {
lastConversationSetup.endpoint && return endpoint;
(endpointsConfig[lastConversationSetup.endpoint] ? lastConversationSetup.endpoint : null) }
);
if (isDefaultConfig && endpoint) {
return endpoint;
}
return endpoint && endpointsConfig[endpoint] ? endpoint : null;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return null; return null;

View file

@ -1,8 +1,10 @@
import { Page, FullConfig, chromium } from '@playwright/test'; import { Page, FullConfig, chromium } from '@playwright/test';
import cleanupUser from './cleanupUser';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
type User = { email: string; name: string; password: string }; type User = { email: string; name: string; password: string };
const timeout = 3500;
async function register(page: Page, user: User) { async function register(page: Page, user: User) {
await page.getByRole('link', { name: 'Sign up' }).click(); 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); console.log('🤖: ✔️ localStorage: set Nav as Visible', storageState);
await page.goto(baseURL, { timeout: 5000 }); await page.goto(baseURL, { timeout });
await register(page, user); 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'); console.log('🤖: ✔️ user successfully registered');
// Logout // Logout
await logout(page, user); await logout(page, user);
await page.waitForURL(`${baseURL}/login`); await page.waitForURL(`${baseURL}/login`, { timeout });
console.log('🤖: ✔️ user successfully logged out'); console.log('🤖: ✔️ user successfully logged out');
await login(page, user); await login(page, user);
await page.waitForURL(`${baseURL}/chat/new`); await page.waitForURL(`${baseURL}/chat/new`, { timeout });
console.log('🤖: ✔️ user successfully authenticated'); console.log('🤖: ✔️ user successfully authenticated');
await page.context().storageState({ path: storageState as string }); await page.context().storageState({ path: storageState as string });

View file

@ -38,14 +38,6 @@ test.beforeAll(async ({ browser }) => {
await page.close(); 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 }) => { test.beforeEach(async ({ page }) => {
await page.goto(initialUrl, { timeout: 5000 }); await page.goto(initialUrl, { timeout: 5000 });
}); });

View file

@ -4,14 +4,14 @@ test.describe('Navigation suite', () => {
test('Navigation bar', async ({ page }) => { test('Navigation bar', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 }); await page.goto('http://localhost:3080/', { timeout: 5000 });
await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').click(); await page.getByTestId('nav-user').click();
const navBar = await page.locator('[id="headlessui-menu-button-\\:r0\\:"]').isVisible(); const navSettings = await page.getByTestId('nav-user').isVisible();
expect(navBar).toBeTruthy(); expect(navSettings).toBeTruthy();
}); });
test('Settings modal', async ({ page }) => { test('Settings modal', async ({ page }) => {
await page.goto('http://localhost:3080/', { timeout: 5000 }); 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(); await page.getByText('Settings').click();
const modal = await page.getByRole('dialog', { name: 'Settings' }).isVisible(); const modal = await page.getByRole('dialog', { name: 'Settings' }).isVisible();

232
package-lock.json generated
View file

@ -15,7 +15,7 @@
"packages/*" "packages/*"
], ],
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.32.1", "@playwright/test": "^1.38.1",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^5.62.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
@ -45,11 +45,13 @@
"@anthropic-ai/sdk": "^0.5.4", "@anthropic-ai/sdk": "^0.5.4",
"@azure/search-documents": "^11.3.2", "@azure/search-documents": "^11.3.2",
"@keyv/mongo": "^2.1.8", "@keyv/mongo": "^2.1.8",
"@keyv/redis": "^2.8.0",
"@waylaidwanderer/chatgpt-api": "^1.37.2", "@waylaidwanderer/chatgpt-api": "^1.37.2",
"axios": "^1.3.4", "axios": "^1.3.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cheerio": "^1.0.0-rc.12", "cheerio": "^1.0.0-rc.12",
"cohere-ai": "^6.0.0", "cohere-ai": "^6.0.0",
"connect-redis": "^7.1.0",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@ -60,10 +62,11 @@
"googleapis": "^118.0.0", "googleapis": "^118.0.0",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"html": "^1.0.0", "html": "^1.0.0",
"ioredis": "^5.3.2",
"jose": "^4.15.2", "jose": "^4.15.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"keyv": "^4.5.3", "keyv": "^4.5.4",
"keyv-file": "^0.2.0", "keyv-file": "^0.2.0",
"langchain": "^0.0.153", "langchain": "^0.0.153",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@ -4850,6 +4853,11 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true "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": { "node_modules/@isaacs/cliui": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -5419,6 +5427,17 @@
"@mongodb-js/saslprep": "^1.1.0" "@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": { "node_modules/@librechat/backend": {
"resolved": "api", "resolved": "api",
"link": true "link": true
@ -5531,12 +5550,12 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.38.0", "version": "1.38.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.0.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz",
"integrity": "sha512-xis/RXXsLxwThKnlIXouxmIvvT3zvQj1JE39GsNieMUrMpb3/GySHDh2j8itCG22qKVD4MYLBp7xB73cUW/UUw==", "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright": "1.38.0" "playwright": "1.38.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -6500,6 +6519,78 @@
"node": ">=8.x" "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": { "node_modules/@remix-run/router": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz",
@ -9682,6 +9773,14 @@
"node": ">=6" "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": { "node_modules/co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -9856,6 +9955,17 @@
"node": ">=0.8" "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": { "node_modules/consola": {
"version": "2.15.3", "version": "2.15.3",
"resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz",
@ -10399,6 +10509,14 @@
"node": ">=0.4.0" "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -12368,6 +12486,16 @@
"node": ">=12" "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": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -13636,6 +13764,29 @@
"loose-envify": "^1.0.0" "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": { "node_modules/ip": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@ -15222,9 +15373,9 @@
} }
}, },
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.3", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dependencies": { "dependencies": {
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
} }
@ -15583,11 +15734,21 @@
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true "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": { "node_modules/lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" "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": { "node_modules/lodash.isboolean": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
@ -18422,12 +18583,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.38.0", "version": "1.38.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.0.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz",
"integrity": "sha512-fJGw+HO0YY+fU/F1N57DMO+TmXHTrmr905J05zwAQE9xkuwP/QLDk63rVhmyxh03dYnEhnRbsdbH9B0UVVRB3A==", "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"playwright-core": "1.38.0" "playwright-core": "1.38.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -18440,9 +18601,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.38.0", "version": "1.38.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.0.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz",
"integrity": "sha512-f8z1y8J9zvmHoEhKgspmCvOExF2XdcxMW8jNRuX4vkQFrzV4MlZ55iwb5QeyiFQgOFCUolXiRHgpjSEnqvO48g==", "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==",
"devOptional": true, "devOptional": true,
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -20348,6 +20509,40 @@
"node": ">=8" "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": { "node_modules/reflect.getprototypeof": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@ -21397,6 +21592,11 @@
"node": ">=8" "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": { "node_modules/statuses": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",

View file

@ -66,7 +66,7 @@
}, },
"homepage": "https://github.com/danny-avila/LibreChat#readme", "homepage": "https://github.com/danny-avila/LibreChat#readme",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.32.1", "@playwright/test": "^1.38.1",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/parser": "^5.62.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",