SPB Git
4.3 KB · 56 lines typescript
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
Files · main
Menu

Theme