import { withUser, parseBody } from "@/lib/api"; import { LIMITS } from "@/lib/rate-limit"; import { chatRequestSchema } from "@/lib/chat/schemas"; import { prepareTurn, runTurn, toPublicMessage } from "@/lib/chat/service"; import { sseResponse } from "@/lib/chat/sse"; export const dynamic = "force-dynamic"; export const maxDuration = 900; /** * POST /api/chat — streams a model turn as Server-Sent Events. * Events: meta → (text-delta | reasoning-delta | tool-* | citation | server-tool)* → done | error * * `meta` carries `ephemeral` (temporary chat: conversationId is the "ephemeral" sentinel and nothing * is persisted except the usage record) and `requestId` (shown in the error details sheet). */ export const POST = withUser( async (ctx) => { const input = await parseBody(ctx.req, chatRequestSchema, 2_500_000); const turn = await prepareTurn({ userId: ctx.user.id, requestId: ctx.requestId, ip: ctx.ip }, input); return sseResponse( async (emit, signal) => { // Abort provider work when the client disconnects. const onAbort = () => controller.abort(); const controller = new AbortController(); signal.addEventListener("abort", onAbort, { once: true }); ctx.req.signal?.addEventListener("abort", onAbort, { once: true }); emit({ type: "meta", conversationId: turn.conversation.id, isNewConversation: turn.isNewConversation, userMessage: turn.userMessage ? toPublicMessage(turn.userMessage) : null, assistantMessageId: turn.assistantMessage.id, modelKey: turn.model.key, clientId: input.clientId, ephemeral: turn.ephemeral, requestId: ctx.requestId, }); const outcome = await runTurn({ userId: ctx.user.id, requestId: ctx.requestId, ip: ctx.ip }, turn, emit, controller.signal); if (outcome.error && !outcome.message.content) emit({ type: "error", error: outcome.error }); emit({ type: "done", message: toPublicMessage(outcome.message), usage: outcome.usage, costUsd: outcome.costUsd, latencyMs: outcome.latencyMs, ttftMs: outcome.ttftMs, status: outcome.status, title: outcome.title, error: outcome.error }); }, { requestId: ctx.requestId }, ); }, { limit: { ...LIMITS.chat, key: "chat" } }, );