diff --git a/.env.example b/.env.example index 142fe882023..71185686cac 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,11 @@ DOMAIN_CLIENT=http://localhost:3080 DOMAIN_SERVER=http://localhost:3080 NO_INDEX=true +# Use the address that is at most n number of hops away from the Express application. +# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left. +# A value of 0 means that the first untrusted address would be req.socket.remoteAddress, i.e. there is no reverse proxy. +# Defaulted to 1. +TRUST_PROXY=1 #===============# # JSON Logging # @@ -292,6 +297,10 @@ MEILI_NO_ANALYTICS=true MEILI_HOST=http://0.0.0.0:7700 MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt +# Optional: Disable indexing, useful in a multi-node setup +# where only one instance should perform an index sync. +# MEILI_NO_SYNC=true + #==================================================# # Speech to Text & Text to Speech # #==================================================# @@ -495,6 +504,16 @@ HELP_AND_FAQ_URL=https://librechat.ai # Google tag manager id #ANALYTICS_GTM_ID=user provided google tag manager id +#===============# +# REDIS Options # +#===============# + +# REDIS_URI=10.10.10.10:6379 +# USE_REDIS=true + +# USE_REDIS_CLUSTER=true +# REDIS_CA=/path/to/ca.crt + #==================================================# # Others # #==================================================# @@ -502,9 +521,6 @@ HELP_AND_FAQ_URL=https://librechat.ai # NODE_ENV= -# REDIS_URI= -# USE_REDIS= - # E2E_USER_EMAIL= # E2E_USER_PASSWORD= diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yml b/.github/ISSUE_TEMPLATE/QUESTION.yml deleted file mode 100644 index c66e6baa3b0..00000000000 --- a/.github/ISSUE_TEMPLATE/QUESTION.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Question -description: Ask your question -title: "[Question]: " -labels: ["❓ question"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill this! - - type: textarea - id: what-is-your-question - attributes: - label: What is your question? - description: Please give as many details as possible - placeholder: Please give as many details as possible - validations: - required: true - - type: textarea - id: more-details - attributes: - label: More Details - description: Please provide more details if needed. - placeholder: Please provide more details if needed. - validations: - required: true - - type: dropdown - id: browsers - attributes: - label: What is the main subject of your question? - multiple: true - options: - - Documentation - - Installation - - UI - - Endpoints - - User System/OAuth - - Other - - type: textarea - id: screenshots - attributes: - label: Screenshots - description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them. - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md) - options: - - label: I agree to follow this project's Code of Conduct - required: true diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js index d544b50a11e..816dcd29b2e 100644 --- a/api/cache/keyvRedis.js +++ b/api/cache/keyvRedis.js @@ -1,15 +1,81 @@ +const fs = require('fs'); +const ioredis = require('ioredis'); const KeyvRedis = require('@keyv/redis'); const { isEnabled } = require('~/server/utils'); const logger = require('~/config/winston'); -const { REDIS_URI, USE_REDIS } = process.env; +const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_KEY_PREFIX, REDIS_MAX_LISTENERS } = + process.env; let keyvRedis; +const redis_prefix = REDIS_KEY_PREFIX || ''; +const redis_max_listeners = REDIS_MAX_LISTENERS || 10; + +function mapURI(uri) { + const regex = + /^(?:(?\w+):\/\/)?(?:(?[^:@]+)(?::(?[^@]+))?@)?(?[\w.-]+)(?::(?\d{1,5}))?$/; + const match = uri.match(regex); + + if (match) { + const { scheme, user, password, host, port } = match.groups; + + return { + scheme: scheme || 'none', + user: user || null, + password: password || null, + host: host || null, + port: port || null, + }; + } else { + const parts = uri.split(':'); + if (parts.length === 2) { + return { + scheme: 'none', + user: null, + password: null, + host: parts[0], + port: parts[1], + }; + } + + return { + scheme: 'none', + user: null, + password: null, + host: uri, + port: null, + }; + } +} if (REDIS_URI && isEnabled(USE_REDIS)) { - keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false }); + let redisOptions = null; + let keyvOpts = { + useRedisSets: false, + keyPrefix: redis_prefix, + }; + + if (REDIS_CA) { + const ca = fs.readFileSync(REDIS_CA); + redisOptions = { tls: { ca } }; + } + + if (isEnabled(USE_REDIS_CLUSTER)) { + const hosts = REDIS_URI.split(',').map((item) => { + var value = mapURI(item); + + return { + host: value.host, + port: value.port, + }; + }); + const cluster = new ioredis.Cluster(hosts, { redisOptions }); + keyvRedis = new KeyvRedis(cluster, keyvOpts); + } else { + keyvRedis = new KeyvRedis(REDIS_URI, keyvOpts); + } keyvRedis.on('error', (err) => logger.error('KeyvRedis connection error:', err)); - keyvRedis.setMaxListeners(20); + keyvRedis.setMaxListeners(redis_max_listeners); logger.info( '[Optional] Redis initialized. Note: Redis support is experimental. If you have issues, disable it. Cache needs to be flushed for values to refresh.', ); diff --git a/api/lib/db/indexSync.js b/api/lib/db/indexSync.js index 86c909419d6..9c40e684d30 100644 --- a/api/lib/db/indexSync.js +++ b/api/lib/db/indexSync.js @@ -1,9 +1,11 @@ const { MeiliSearch } = require('meilisearch'); const Conversation = require('~/models/schema/convoSchema'); const Message = require('~/models/schema/messageSchema'); +const { isEnabled } = require('~/server/utils'); const { logger } = require('~/config'); -const searchEnabled = process.env?.SEARCH?.toLowerCase() === 'true'; +const searchEnabled = isEnabled(process.env.SEARCH); +const indexingDisabled = isEnabled(process.env.MEILI_NO_SYNC); let currentTimeout = null; class MeiliSearchClient { @@ -23,8 +25,7 @@ class MeiliSearchClient { } } -// eslint-disable-next-line no-unused-vars -async function indexSync(req, res, next) { +async function indexSync() { if (!searchEnabled) { return; } @@ -33,10 +34,15 @@ async function indexSync(req, res, next) { const client = MeiliSearchClient.getInstance(); const { status } = await client.health(); - if (status !== 'available' || !process.env.SEARCH) { + if (status !== 'available') { throw new Error('Meilisearch not available'); } + if (indexingDisabled === true) { + logger.info('[indexSync] Indexing is disabled, skipping...'); + return; + } + const messageCount = await Message.countDocuments(); const convoCount = await Conversation.countDocuments(); const messages = await client.index('messages').getStats(); @@ -71,7 +77,6 @@ async function indexSync(req, res, next) { logger.info('[indexSync] Meilisearch not configured, search will be disabled.'); } else { logger.error('[indexSync] error', err); - // res.status(500).json({ error: 'Server error' }); } } } diff --git a/api/server/index.js b/api/server/index.js index 30d36d9a9fd..a98178145c7 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -22,10 +22,11 @@ const staticCache = require('./utils/staticCache'); const noIndex = require('./middleware/noIndex'); const routes = require('./routes'); -const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION } = process.env ?? {}; +const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {}; const port = Number(PORT) || 3080; const host = HOST || 'localhost'; +const trusted_proxy = Number(TRUST_PROXY) || 1; /* trust first proxy by default */ const startServer = async () => { if (typeof Bun !== 'undefined') { @@ -53,7 +54,7 @@ const startServer = async () => { app.use(staticCache(app.locals.paths.dist)); app.use(staticCache(app.locals.paths.fonts)); app.use(staticCache(app.locals.paths.assets)); - app.set('trust proxy', 1); /* trust first proxy */ + app.set('trust proxy', trusted_proxy); app.use(cors()); app.use(cookieParser());