import { z } from "zod"; import { guarded, body, sse } from "@/lib/api"; import { createConversation, getConversation, insertMessage, getActiveMessages } from "@/lib/chat/store"; import { runAssistant } from "@/lib/chat/engine"; import { sql } from "@/lib/db"; import type { Part } from "@/lib/ai/types"; export const maxDuration = 900; const schema = z.object({ conversationId: z.string().optional(), projectId: z.string().nullable().optional(), text: z.string().max(200_000), attachmentIds: z.array(z.string()).max(20).optional(), modelId: z.string().optional(), routingMode: z.enum(["manual", "fast", "quality", "economy", "auto"]).optional(), settings: z.object({ effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(), maxOutputTokens: z.number().int().min(256).max(128_000).optional(), temperature: z.number().min(0).max(2).optional(), reasoningSummary: z.boolean().optional() }).optional(), toolsEnabled: z.boolean().optional(), toolNames: z.array(z.string()).optional(), }); export const POST = guarded(async (req) => { const b = await body(req, schema); let conv = b.conversationId ? await getConversation(b.conversationId) : null; if (b.conversationId && !conv) return Response.json({ error: "Conversation introuvable" }, { status: 404 }); if (!conv) conv = await createConversation({ projectId: b.projectId ?? null, modelId: b.modelId ?? null, settings: { routingMode: b.routingMode ?? (b.modelId ? "manual" : "auto"), toolsEnabled: b.toolsEnabled ?? true } }); const parts: Part[] = []; if (b.attachmentIds?.length) { const atts = await sql<{ id: string; mime: string; filename: string; size_bytes: number; page_count: number | null }[]>`SELECT id, mime, filename, size_bytes, page_count FROM attachments WHERE id = ANY(${b.attachmentIds})`; for (const a of atts) parts.push(a.mime.startsWith("image/") ? { type: "image", attachmentId: a.id, mime: a.mime, name: a.filename } : { type: "file", attachmentId: a.id, mime: a.mime, name: a.filename, size: a.size_bytes, pages: a.page_count ?? undefined }); } if (b.text.trim()) parts.push({ type: "text", text: b.text }); if (!parts.length) return Response.json({ error: "Message vide" }, { status: 400 }); const prev = await getActiveMessages(conv.id); const user = await insertMessage({ conversationId: conv.id, parentId: prev.at(-1)?.id ?? null, role: "user", parts }); if (b.attachmentIds?.length) await sql`UPDATE attachments SET conversation_id = ${conv.id}, message_id = ${user.id} WHERE id = ANY(${b.attachmentIds})`; if (b.modelId && b.modelId !== conv.model_id) await sql`UPDATE conversations SET model_id = ${b.modelId} WHERE id = ${conv.id}`; const convId = conv.id; return sse(async (emit, signal) => { emit("conversation", { id: convId, userMessageId: user.id, created: !b.conversationId }); await runAssistant({ conversationId: convId, modelId: b.modelId, routingMode: b.routingMode, settings: b.settings, toolsEnabled: b.toolsEnabled, toolNames: b.toolNames, parentMessageId: user.id, signal, emit }); }, req.signal); });