require('dotenv').config(); const fs = require('fs'); const path = require('path'); require('module-alias')({ base: path.resolve(__dirname, '..') }); const cors = require('cors'); const axios = require('axios'); const express = require('express'); const passport = require('passport'); const compression = require('compression'); const cookieParser = require('cookie-parser'); const { logger } = require('@librechat/data-schemas'); const mongoSanitize = require('express-mongo-sanitize'); const { isEnabled, ErrorController, performStartupChecks, handleJsonParseError, initializeFileStorage, GenerationJobManager, createStreamServices, } = require('@librechat/api'); const { connectDb, indexSync } = require('~/db'); const initializeOAuthReconnectManager = require('./services/initializeOAuthReconnectManager'); const createValidateImageRequest = require('./middleware/validateImageRequest'); const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies'); const { updateInterfacePermissions } = require('~/models/interface'); const { checkMigrations } = require('./services/start/migration'); const initializeMCPs = require('./services/initializeMCPs'); const configureSocialLogins = require('./socialLogins'); const { getAppConfig } = require('./services/Config'); const staticCache = require('./utils/staticCache'); const noIndex = require('./middleware/noIndex'); const { seedDatabase } = require('~/models'); const routes = require('./routes'); const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {}; // Allow PORT=0 to be used for automatic free port assignment const port = isNaN(Number(PORT)) ? 3080 : Number(PORT); const host = HOST || 'localhost'; const trusted_proxy = Number(TRUST_PROXY) || 1; /* trust first proxy by default */ const app = express(); const startServer = async () => { if (typeof Bun !== 'undefined') { axios.defaults.headers.common['Accept-Encoding'] = 'gzip'; } await connectDb(); logger.info('Connected to MongoDB'); indexSync().catch((err) => { logger.error('[indexSync] Background sync failed:', err); }); app.disable('x-powered-by'); app.set('trust proxy', trusted_proxy); await seedDatabase(); const appConfig = await getAppConfig(); initializeFileStorage(appConfig); await performStartupChecks(appConfig); await updateInterfacePermissions(appConfig); const indexPath = path.join(appConfig.paths.dist, 'index.html'); let indexHTML = fs.readFileSync(indexPath, 'utf8'); // In order to provide support to serving the application in a sub-directory // We need to update the base href if the DOMAIN_CLIENT is specified and not the root path if (process.env.DOMAIN_CLIENT) { const clientUrl = new URL(process.env.DOMAIN_CLIENT); const baseHref = clientUrl.pathname.endsWith('/') ? clientUrl.pathname : `${clientUrl.pathname}/`; if (baseHref !== '/') { logger.info(`Setting base href to ${baseHref}`); indexHTML = indexHTML.replace(/base href="\/"/, `base href="${baseHref}"`); } } app.get('/health', (_req, res) => res.status(200).send('OK')); /* Middleware */ app.use(noIndex); app.use(express.json({ limit: '3mb' })); app.use(express.urlencoded({ extended: true, limit: '3mb' })); app.use(handleJsonParseError); /** * Express 5 Compatibility: Make req.query writable for mongoSanitize * In Express 5, req.query is read-only by default, but express-mongo-sanitize needs to modify it */ app.use((req, _res, next) => { Object.defineProperty(req, 'query', { ...Object.getOwnPropertyDescriptor(req, 'query'), value: req.query, writable: true, }); next(); }); app.use(mongoSanitize()); app.use(cors()); app.use(cookieParser()); if (!isEnabled(DISABLE_COMPRESSION)) { app.use(compression()); } else { console.warn('Response compression has been disabled via DISABLE_COMPRESSION.'); } app.use(staticCache(appConfig.paths.dist)); app.use(staticCache(appConfig.paths.fonts)); app.use(staticCache(appConfig.paths.assets)); if (!ALLOW_SOCIAL_LOGIN) { console.warn('Social logins are disabled. Set ALLOW_SOCIAL_LOGIN=true to enable them.'); } /* OAUTH */ app.use(passport.initialize()); passport.use(jwtLogin()); passport.use(passportLogin()); /* LDAP Auth */ if (process.env.LDAP_URL && process.env.LDAP_USER_SEARCH_BASE) { passport.use(ldapLogin); } if (isEnabled(ALLOW_SOCIAL_LOGIN)) { await configureSocialLogins(app); } app.use('/oauth', routes.oauth); /* API Endpoints */ app.use('/api/auth', routes.auth); app.use('/api/actions', routes.actions); app.use('/api/keys', routes.keys); app.use('/api/user', routes.user); app.use('/api/search', routes.search); app.use('/api/messages', routes.messages); app.use('/api/convos', routes.convos); app.use('/api/presets', routes.presets); app.use('/api/prompts', routes.prompts); app.use('/api/categories', routes.categories); app.use('/api/endpoints', routes.endpoints); app.use('/api/balance', routes.balance); app.use('/api/models', routes.models); app.use('/api/config', routes.config); app.use('/api/assistants', routes.assistants); app.use('/api/files', await routes.files.initialize()); app.use('/images/', createValidateImageRequest(appConfig.secureImageLinks), routes.staticRoute); app.use('/api/share', routes.share); app.use('/api/roles', routes.roles); app.use('/api/agents', routes.agents); app.use('/api/banner', routes.banner); app.use('/api/memories', routes.memories); app.use('/api/permissions', routes.accessPermissions); app.use('/api/tags', routes.tags); app.use('/api/mcp', routes.mcp); app.use(ErrorController); app.get('/api/healthSylar', (req, res) => { try { const healthStatus = { status: 'ok', timestamp: new Date().toISOString(), service: 'librechat-backend', version: process.env.LIBRECHAT_VERSION || 'unknown', uptime: process.uptime(), environment: process.env.NODE_ENV || 'production', // Information sur la disponibilité du serveur GPU Sylar gpu_server: { status: 'online', // 'online', 'offline', 'maintenance' // Vous pouvez ajouter ici une vérification réelle de disponibilité du serveur GPU // en appelant votre endpoint GPU ou en utilisant une variable d'environnement base_url: process.env.CUSTOM_API_BASE_URL || process.env.VITE_API_URL || 'unknown', }, // Ressources système memory: { used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024), unit: 'MB' } }; res.status(200).json(healthStatus); } catch (error) { logger.error('[HealthSylar Check] Error:', error); res.status(503).json({ status: 'error', message: 'Service temporarily unavailable', timestamp: new Date().toISOString() }); } }); // Health probe for upcoming GPU1 endpoint (returns offline while upstream is down) app.get('/api/healthGPU1', async (_req, res) => { const started = Date.now(); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); try { const response = await fetch('https://cpu.lsbuchet.com/healthGPU1', { signal: controller.signal }); const responseTime = Date.now() - started; if (response.ok) { const data = await response.json().catch(() => ({})); return res.status(200).json({ status: data.status || 'ok', gpu_status: data.gpu_status ?? true, responseTime, source: 'cpu.lsbuchet.com/healthGPU1', payload: data, }); } const body = await response.text().catch(() => ''); return res.status(503).json({ status: 'offline', gpu_status: false, responseTime, source: 'cpu.lsbuchet.com/healthGPU1', message: `HTTP ${response.status}`, body, }); } catch (error) { const responseTime = Date.now() - started; logger.warn('[healthGPU1] fetch failed:', error?.message || error); return res.status(503).json({ status: 'offline', gpu_status: false, responseTime, source: 'cpu.lsbuchet.com/healthGPU1', error: error?.message || 'Unknown error', }); } finally { clearTimeout(timeout); } }); app.get('/api/credits', async (req, res) => { const started = Date.now(); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); try { const response = await fetch('https://cpu.lsbuchet.com/v1/users/credits', { signal: controller.signal, }); const responseTime = Date.now() - started; if (response.ok) { const data = await response.json().catch(() => ({})); return res.status(200).json({ ...data, responseTime, source: 'cpu.lsbuchet.com/v1/users/credits', }); } return res.status(response.status).json({ error: 'Failed to fetch credits', message: `HTTP ${response.status}`, responseTime, source: 'cpu.lsbuchet.com/v1/users/credits', }); } catch (error) { const responseTime = Date.now() - started; logger.warn('[credits] fetch failed:', error?.message || error); return res.status(503).json({ error: error?.message || 'Unknown error', responseTime, source: 'cpu.lsbuchet.com/v1/users/credits', }); } finally { clearTimeout(timeout); } }); // Proxy endpoint for server toggle app.post('/api/server/toggle', async (req, res) => { const { state } = req.body; const started = Date.now(); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); try { const response = await fetch('https://cpu.lsbuchet.com/v1/server/toggle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ state }), signal: controller.signal, }); const responseTime = Date.now() - started; if (response.ok) { const data = await response.json().catch(() => ({})); return res.status(200).json({ ...data, responseTime, source: 'cpu.lsbuchet.com/v1/server/toggle', }); } const errorData = await response.json().catch(() => ({})); return res.status(response.status).json({ ...errorData, responseTime, source: 'cpu.lsbuchet.com/v1/server/toggle', }); } catch (error) { const responseTime = Date.now() - started; logger.warn('[server/toggle] fetch failed:', error?.message || error); return res.status(503).json({ error: error?.message || 'Unknown error', responseTime, source: 'cpu.lsbuchet.com/v1/server/toggle', }); } finally { clearTimeout(timeout); } }); // Proxy endpoint for CPU status check (conversation-aware) app.get('/api/statusCPU', async (req, res) => { const conversationId = req.query.conversation_id; const started = Date.now(); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); try { const url = conversationId ? `https://cpu.lsbuchet.com/v1/status/cpu?conversation_id=${encodeURIComponent(conversationId)}` : 'https://cpu.lsbuchet.com/v1/status/cpu'; const response = await fetch(url, { signal: controller.signal }); const responseTime = Date.now() - started; if (response.ok) { const data = await response.json().catch(() => ({})); return res.status(200).json({ ...data, responseTime, source: 'cpu.lsbuchet.com/v1/status/cpu', }); } return res.status(response.status).json({ status: 'error', message: `HTTP ${response.status}`, responseTime, source: 'cpu.lsbuchet.com/v1/status/cpu', }); } catch (error) { const responseTime = Date.now() - started; logger.warn('[statusCPU] fetch failed:', error?.message || error); return res.status(503).json({ status: 'error', error: error?.message || 'Unknown error', responseTime, source: 'cpu.lsbuchet.com/v1/status/cpu', }); } finally { clearTimeout(timeout); } }); app.use((req, res) => { res.set({ 'Cache-Control': process.env.INDEX_CACHE_CONTROL || 'no-cache, no-store, must-revalidate', Pragma: process.env.INDEX_PRAGMA || 'no-cache', Expires: process.env.INDEX_EXPIRES || '0', }); const lang = req.cookies.lang || req.headers['accept-language']?.split(',')[0] || 'en-US'; const saneLang = lang.replace(/"/g, '"'); let updatedIndexHtml = indexHTML.replace(/lang="en-US"/g, `lang="${saneLang}"`); res.type('html'); res.send(updatedIndexHtml); }); app.listen(port, host, async (err) => { if (err) { logger.error('Failed to start server:', err); process.exit(1); } if (host === '0.0.0.0') { logger.info( `Server listening on all interfaces at port ${port}. Use http://localhost:${port} to access it`, ); } else { logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`); } await initializeMCPs(); await initializeOAuthReconnectManager(); await checkMigrations(); // Configure stream services (auto-detects Redis from USE_REDIS env var) const streamServices = createStreamServices(); GenerationJobManager.configure(streamServices); GenerationJobManager.initialize(); }); }; startServer(); let messageCount = 0; process.on('uncaughtException', (err) => { if (!err.message.includes('fetch failed')) { logger.error('There was an uncaught error:', err); } if (err.message && err.message?.toLowerCase()?.includes('abort')) { logger.warn('There was an uncatchable abort error.'); return; } if (err.message.includes('GoogleGenerativeAI')) { logger.warn( '\n\n`GoogleGenerativeAI` errors cannot be caught due to an upstream issue, see: https://github.com/google-gemini/generative-ai-js/issues/303', ); return; } if (err.message.includes('fetch failed')) { if (messageCount === 0) { logger.warn('Meilisearch error, search will be disabled'); messageCount++; } return; } if (err.message.includes('OpenAIError') || err.message.includes('ChatCompletionMessage')) { logger.error( '\n\nAn Uncaught `OpenAIError` error may be due to your reverse-proxy setup or stream configuration, or a bug in the `openai` node package.', ); return; } if (err.stack && err.stack.includes('@librechat/agents')) { logger.error( '\n\nAn error occurred in the agents system. The error has been logged and the app will continue running.', { message: err.message, stack: err.stack, }, ); return; } process.exit(1); }); /** Export app for easier testing purposes */ module.exports = app;