spb/chat-spboucher-ai
Public
1import { z } from "zod";2import { guarded, body, sse, json } from "@/lib/api";3import { adapterFor, estimateCost } from "@/lib/ai";4import { requireChatModel } from "@/lib/ai/registry";5import { recordUsage, assertWithinBudget } from "@/lib/usage";6import { sql } from "@/lib/db";7import { newId } from "@/lib/ids";8import { buildSystemPrompt } from "@/lib/chat/system";9import { readAttachment } from "@/lib/storage";1011export const maxDuration = 900;1213/** POST: same prompt to N models concurrently; streams per-model events. GET: history. */14export const POST = guarded(async (req) => {15 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() }));16 await assertWithinBudget();17 const models = await Promise.all(b.modelIds.map(id => requireChatModel(id)));18 const id = newId("cmp");19 const { system } = await buildSystemPrompt({ modelId: "(comparison)", projectId: b.projectId, query: b.prompt, toolsEnabled: false, conversationSettings: b.system ? { systemPrompt: b.system } : undefined });20 const parts = [...(b.attachmentIds ?? []).map(a => ({ type: "image" as const, attachmentId: a, mime: "image/png" })), { type: "text" as const, text: b.prompt }];21 return sse(async (emit, signal) => {22 emit("start", { id, models: models.map(m => ({ id: m.id, provider: m.provider })) });23 const results = await Promise.all(models.map(async m => {24 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;25 try {26 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 })) {27 if (ev.type === "text") { if (ttft === null) ttft = Date.now() - t0; text += ev.delta; emit("text", { model: m.id, delta: ev.delta }); }28 else if (ev.type === "reasoning") { reasoning += ev.delta; emit("reasoning", { model: m.id, delta: ev.delta }); }29 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) };30 else if (ev.type === "error") throw new Error(ev.message);31 }32 } catch (e) { error = (e as Error).message; emit("model_error", { model: m.id, message: error }); }33 const latencyMs = Date.now() - t0; const costUsd = estimateCost(m, usage);34 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 } });35 const r = { modelId: m.id, provider: m.provider, text, reasoning: reasoning || undefined, usage, costUsd, latencyMs, ttftMs: ttft, error };36 emit("model_done", r);37 return r;38 }));39 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)})`;40 emit("done", { id });41 }, req.signal);42});4344export const GET = guarded(async (req) => {45 const id = req.nextUrl.searchParams.get("id");46 if (id) { const r = await sql`SELECT * FROM comparisons WHERE id = ${id}`; return json({ comparison: r[0] ?? null }); }47 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`;48 return json({ comparisons: rows });49});5051export const PATCH = guarded(async (req) => {52 const b = await body(req, z.object({ id: z.string(), preferredModel: z.string().optional() }));53 await sql`UPDATE comparisons SET preferred_model = COALESCE(${b.preferredModel ?? null}, preferred_model) WHERE id = ${b.id}`;54 return json({ ok: true });55});56