add response versioning support in messages for regeneration improvements

This commit is contained in:
2026-09-22 14:48:13 +02:00
parent 7602692341
commit 72e429b1b3
15 changed files with 591 additions and 86 deletions
+53 -3
View File
@@ -105,6 +105,14 @@ const preferredModelValidator = (body) => {
export function createChatsRouter({repositories, chatService, modelService, config, chatRateLimiters}) {
const router = express.Router();
router.use(requireLogin);
router.use(["/chats/:id/messages/by-index/:index", "/chats/:id/messages/:messageId"], (req, res, next) => {
const chatId = Number(req.params.id);
const messageId = req.params.messageId ? Number(req.params.messageId) : chatService.getChatMessageByIndex(chatId, req.params.index)?.id;
if (["PUT", "DELETE", "POST"].includes(req.method) && chatService.isRegeneratingMessage(chatId, messageId)) {
return res.status(409).json({error: "This response is being regenerated. Wait for it to finish."});
}
next();
});
router.get("/models", async (req, res) => {
try {
@@ -475,7 +483,21 @@ export function createChatsRouter({repositories, chatService, modelService, conf
}
});
router.post("/chats/:id/messages/:messageId/retry", ...chatRateLimiters, async (req, res) => {
router.post("/chats/:id/messages/:messageId/versions/:index", (req, res) => {
const chatId = Number(req.params.id), messageId = Number(req.params.messageId);
if (!repositories.getChat(chatId, req.session.user)) return res.status(404).json({error: "Chat not found"});
const message = repositories.getChatMessage(chatId, messageId);
if (!message || message.role !== "bot") return res.status(404).json({error: "Assistant message not found"});
if (chatService.isGenerating(chatId)) return res.status(409).json({error: "Wait for generation to finish"});
const index = Number(req.params.index);
const versions = repositories.listMessageVersions(messageId);
if (!Number.isInteger(index) || index < 0 || index >= versions.length) return res.status(400).json({error: "Version is out of range"});
repositories.saveMessageVersion(chatId, messageId, null, index);
repositories.updateChatContextSummary(chatId, req.session.user, null, 0);
res.json({message: chatService.formatChatMessage(repositories.getChatMessage(chatId, messageId))});
});
router.post(["/chats/:id/messages/:messageId/retry", "/chats/:id/messages/:messageId/retry/stream"], ...chatRateLimiters, async (req, res) => {
const chatId = Number(req.params.id);
const messageId = Number(req.params.messageId);
const chat = repositories.getChat(chatId, req.session.user);
@@ -484,19 +506,45 @@ export function createChatsRouter({repositories, chatService, modelService, conf
if (!targetMessage) return res.status(404).json({error: "Message not found"});
if (targetMessage.role !== "bot") return res.status(400).json({error: "Only assistant messages can be regenerated"});
const streaming = req.path.endsWith("/stream");
const controller = new AbortController();
let finished = false;
const sendEvent = (event, data) => {
if (!res.destroyed) res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
};
if (streaming) {
res.once("close", () => { if (!finished) controller.abort(); });
res.writeHead(200, {"Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", "X-Accel-Buffering": "no"});
}
try {
const result = await chatService.retryMessage({
user: req.session.user,
chatId,
selectedModel: String(req.body?.model || "qwen3:14b"),
targetMessage,
retryStyle: String(req.body?.style || "").trim() || null
retryStyle: String(req.body?.style || "").trim() || null,
webSearch: req.body?.webSearch === true,
reasoning: req.body?.reasoning === true,
signal: controller.signal,
...(streaming ? {
onChunk: (chunk, fullReply) => sendEvent("chunk", {chunk, fullReply}),
onThinking: (chunk, fullThinking) => sendEvent("thinking", {chunk, fullThinking}),
onStatus: status => sendEvent("status", status)
} : {})
});
if (streaming) {
sendEvent(result.error ? "error" : "done", result.error ? {error: result.error, searchState: result.searchState} : {...result, reply: result.message.content});
return;
}
if (result.error) return res.status(result.status).json({error: result.error});
return res.json(result);
} catch (error) {
const mapped = modelService.mapError(error);
if (streaming) { sendEvent("error", mapped.body); return; }
return res.status(mapped.status).json(mapped.body);
} finally {
finished = true;
if (streaming) res.end();
}
});
@@ -514,7 +562,9 @@ export function createChatsRouter({repositories, chatService, modelService, conf
chatId,
selectedModel: String(req.body?.model || "qwen3:14b"),
targetMessage,
retryStyle: String(req.body?.style || "").trim() || null
retryStyle: String(req.body?.style || "").trim() || null,
webSearch: req.body?.webSearch === true,
reasoning: req.body?.reasoning === true
});
if (result.error) return res.status(result.status).json({error: result.error});
return res.json(result);