import { z } from "zod"; import { guarded, body, sse, json } from "@/lib/api"; import { adapterFor, estimateCost } from "@/lib/ai"; import { requireChatModel } from "@/lib/ai/registry"; import { recordUsage, assertWithinBudget } from "@/lib/usage"; import { sql } from "@/lib/db"; import { newId } from "@/lib/ids"; import { buildSystemPrompt } from "@/lib/chat/system"; import { readAttachment } from "@/lib/storage"; export const maxDuration = 900; /** POST: same prompt to N models concurrently; streams per-model events. GET: history. */ export const POST = guarded(async (req) => { const b = await body(req, z.object({ prompt: z.string().min(1).max(100_000), modelIds: z.array(z.string()).min(2).max(6), projectId: z.string().nullable().optional(), attachmentIds: z.array(z.string()).optional(), settings: z.object({ effort: z.enum(["low", "medium", "high", "xhigh", "max"]).optional(), maxOutputTokens: z.number().int().optional() }).optional(), system: z.string().max(20_000).optional() })); await assertWithinBudget(); const models = await Promise.all(b.modelIds.map(id => requireChatModel(id))); const id = newId("cmp"); const { system } = await buildSystemPrompt({ modelId: "(comparison)", projectId: b.projectId, query: b.prompt, toolsEnabled: false, conversationSettings: b.system ? { systemPrompt: b.system } : undefined }); const parts = [...(b.attachmentIds ?? []).map(a => ({ type: "image" as const, attachmentId: a, mime: "image/png" })), { type: "text" as const, text: b.prompt }]; return sse(async (emit, signal) => { emit("start", { id, models: models.map(m => ({ id: m.id, provider: m.provider })) }); const results = await Promise.all(models.map(async m => { const t0 = Date.now(); let ttft: number | null = null; let text = ""; let reasoning = ""; let usage = { input: 0, output: 0, cached: 0, reasoning: 0 }; let error: string | null = null; try { for await (const ev of adapterFor(m.provider).stream({ model: m, system, messages: [{ role: "user", parts }], settings: { ...b.settings, maxOutputTokens: b.settings?.maxOutputTokens ?? 8000 }, signal, loadAttachment: readAttachment })) { if (ev.type === "text") { if (ttft === null) ttft = Date.now() - t0; text += ev.delta; emit("text", { model: m.id, delta: ev.delta }); } else if (ev.type === "reasoning") { reasoning += ev.delta; emit("reasoning", { model: m.id, delta: ev.delta }); } else if (ev.type === "usage") usage = { input: usage.input + ev.usage.input, output: usage.output + ev.usage.output, cached: usage.cached + (ev.usage.cached ?? 0), reasoning: usage.reasoning + (ev.usage.reasoning ?? 0) }; else if (ev.type === "error") throw new Error(ev.message); } } catch (e) { error = (e as Error).message; emit("model_error", { model: m.id, message: error }); } const latencyMs = Date.now() - t0; const costUsd = estimateCost(m, usage); await recordUsage({ provider: m.provider, modelId: m.id, kind: "compare", projectId: b.projectId ?? null, usage, costEstimated: costUsd, latencyMs, ttftMs: ttft, status: error ? "error" : "ok", error, meta: { comparisonId: id } }); const r = { modelId: m.id, provider: m.provider, text, reasoning: reasoning || undefined, usage, costUsd, latencyMs, ttftMs: ttft, error }; emit("model_done", r); return r; })); await sql`INSERT INTO comparisons (id, project_id, prompt, model_ids, results) VALUES (${id}, ${b.projectId ?? null}, ${b.prompt}, ${b.modelIds}, ${sql.json(results as never)})`; emit("done", { id }); }, req.signal); }); export const GET = guarded(async (req) => { const id = req.nextUrl.searchParams.get("id"); if (id) { const r = await sql`SELECT * FROM comparisons WHERE id = ${id}`; return json({ comparison: r[0] ?? null }); } const rows = await sql`SELECT id, prompt, model_ids, preferred_model, created_at, (SELECT coalesce(sum((x->>'costUsd')::numeric),0) FROM jsonb_array_elements(results) x) AS cost FROM comparisons ORDER BY created_at DESC LIMIT 100`; return json({ comparisons: rows }); }); export const PATCH = guarded(async (req) => { const b = await body(req, z.object({ id: z.string(), preferredModel: z.string().optional() })); await sql`UPDATE comparisons SET preferred_model = COALESCE(${b.preferredModel ?? null}, preferred_model) WHERE id = ${b.id}`; return json({ ok: true }); });