mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
- Introduced GenerationJobManager to handle resumable LLM generation jobs independently of HTTP connections. - Added support for subscribing to ongoing generation jobs via SSE, allowing clients to reconnect and receive updates without losing progress. - Enhanced existing agent controllers and routes to integrate resumable functionality, including job creation, completion, and error handling. - Updated client-side hooks to manage adaptive SSE streams, switching between standard and resumable modes based on user settings. - Added UI components and settings for enabling/disabling resumable streams, improving user experience during unstable connections.
154 lines
4.5 KiB
JavaScript
154 lines
4.5 KiB
JavaScript
const express = require('express');
|
|
const { generateCheckAccess, skipAgentCheck, GenerationJobManager } = require('@librechat/api');
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider');
|
|
const {
|
|
setHeaders,
|
|
moderateText,
|
|
requireJwtAuth,
|
|
// validateModel,
|
|
validateConvoAccess,
|
|
buildEndpointOption,
|
|
canAccessAgentFromBody,
|
|
} = require('~/server/middleware');
|
|
const { initializeClient } = require('~/server/services/Endpoints/agents');
|
|
const AgentController = require('~/server/controllers/agents/request');
|
|
const addTitle = require('~/server/services/Endpoints/agents/title');
|
|
const { getRoleByName } = require('~/models/Role');
|
|
|
|
const router = express.Router();
|
|
|
|
router.use(moderateText);
|
|
|
|
const checkAgentAccess = generateCheckAccess({
|
|
permissionType: PermissionTypes.AGENTS,
|
|
permissions: [Permissions.USE],
|
|
skipCheck: skipAgentCheck,
|
|
getRoleByName,
|
|
});
|
|
const checkAgentResourceAccess = canAccessAgentFromBody({
|
|
requiredPermission: PermissionBits.VIEW,
|
|
});
|
|
|
|
/**
|
|
* @route GET /stream/:streamId
|
|
* @desc Subscribe to an ongoing generation job's SSE stream
|
|
* @access Private
|
|
*/
|
|
router.get('/stream/:streamId', requireJwtAuth, (req, res) => {
|
|
const { streamId } = req.params;
|
|
|
|
const job = GenerationJobManager.getJob(streamId);
|
|
if (!job) {
|
|
return res.status(404).json({
|
|
error: 'Stream not found',
|
|
message: 'The generation job does not exist or has expired.',
|
|
});
|
|
}
|
|
|
|
// Disable compression for SSE
|
|
res.setHeader('Content-Encoding', 'identity');
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
res.setHeader('Connection', 'keep-alive');
|
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
res.flushHeaders();
|
|
|
|
logger.debug(`[AgentStream] Client subscribed to ${streamId}`);
|
|
|
|
const unsubscribe = GenerationJobManager.subscribe(
|
|
streamId,
|
|
(event) => {
|
|
if (!res.writableEnded) {
|
|
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
|
|
if (typeof res.flush === 'function') {
|
|
res.flush();
|
|
}
|
|
}
|
|
},
|
|
(event) => {
|
|
if (!res.writableEnded) {
|
|
res.write(`event: message\ndata: ${JSON.stringify(event)}\n\n`);
|
|
if (typeof res.flush === 'function') {
|
|
res.flush();
|
|
}
|
|
res.end();
|
|
}
|
|
},
|
|
(error) => {
|
|
if (!res.writableEnded) {
|
|
res.write(`event: error\ndata: ${JSON.stringify({ error })}\n\n`);
|
|
if (typeof res.flush === 'function') {
|
|
res.flush();
|
|
}
|
|
res.end();
|
|
}
|
|
},
|
|
);
|
|
|
|
if (!unsubscribe) {
|
|
return res.status(404).json({ error: 'Failed to subscribe to stream' });
|
|
}
|
|
|
|
if (job.status === 'complete' || job.status === 'error' || job.status === 'aborted') {
|
|
res.write(`event: message\ndata: ${JSON.stringify({ final: true, status: job.status })}\n\n`);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
req.on('close', () => {
|
|
logger.debug(`[AgentStream] Client disconnected from ${streamId}`);
|
|
unsubscribe();
|
|
});
|
|
});
|
|
|
|
/**
|
|
* @route POST /abort
|
|
* @desc Abort an ongoing generation job
|
|
* @access Private
|
|
*/
|
|
router.post('/abort', (req, res) => {
|
|
const { streamId, abortKey } = req.body;
|
|
|
|
const jobStreamId = streamId || abortKey?.split(':')?.[0];
|
|
|
|
if (jobStreamId && GenerationJobManager.hasJob(jobStreamId)) {
|
|
GenerationJobManager.abortJob(jobStreamId);
|
|
logger.debug(`[AgentStream] Job aborted: ${jobStreamId}`);
|
|
return res.json({ success: true, aborted: jobStreamId });
|
|
}
|
|
|
|
res.status(404).json({ error: 'Job not found' });
|
|
});
|
|
|
|
router.use(checkAgentAccess);
|
|
router.use(checkAgentResourceAccess);
|
|
router.use(validateConvoAccess);
|
|
router.use(buildEndpointOption);
|
|
router.use(setHeaders);
|
|
|
|
const controller = async (req, res, next) => {
|
|
await AgentController(req, res, next, initializeClient, addTitle);
|
|
};
|
|
|
|
/**
|
|
* @route POST / (regular endpoint)
|
|
* @desc Chat with an assistant
|
|
* @access Public
|
|
* @param {express.Request} req - The request object, containing the request data.
|
|
* @param {express.Response} res - The response object, used to send back a response.
|
|
* @returns {void}
|
|
*/
|
|
router.post('/', controller);
|
|
|
|
/**
|
|
* @route POST /:endpoint (ephemeral agents)
|
|
* @desc Chat with an assistant
|
|
* @access Public
|
|
* @param {express.Request} req - The request object, containing the request data.
|
|
* @param {express.Response} res - The response object, used to send back a response.
|
|
* @returns {void}
|
|
*/
|
|
router.post('/:endpoint', controller);
|
|
|
|
module.exports = router;
|