mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
✨ feat: Implement Resumable Generation Jobs with SSE Support
- 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.
This commit is contained in:
parent
5bfebc7c9d
commit
0e850a5d5f
17 changed files with 1212 additions and 37 deletions
|
|
@ -1,9 +1,11 @@
|
|||
const express = require('express');
|
||||
const { generateCheckAccess, skipAgentCheck } = require('@librechat/api');
|
||||
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,
|
||||
|
|
@ -28,6 +30,97 @@ 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue