SPB Git
3.0 KB · 38 lines typescript
1import { z } from "zod";2import { handler, parseBody } from "@/lib/api";3import { callAI } from "@/lib/ai/providers";4import { getModel } from "@/lib/ai/catalog";5import { recordUsage } from "@/lib/ai/usage";6import { db, schema } from "@/db";7import { and, desc, eq } from "drizzle-orm";8import { ASSISTANT_SYSTEM } from "@/lib/ai/prompts";9import { sse } from "@/lib/ai/chat";10export const maxDuration = 600;11/** POST { prompt, models[] } → SSE: one `result` event per model as they finish; then `saved` with the comparison id. */12export const POST = handler(async ({ req, ws }) => {13  const b = await parseBody(req, z.object({ prompt: z.string().min(1).max(60_000), models: z.array(z.string()).min(1).max(6), system: z.string().max(4000).optional() }));14  async function* gen() {15    const results: any[] = [];16    const tasks = b.models.map(async (id) => {17      const m = await getModel(id); if (!m) return { model: id, error: "unknown model" };18      try {19        const r = await callAI({ model: id, provider: m.provider as any, system: b.system ?? ASSISTANT_SYSTEM, messages: [{ role: "user", content: b.prompt }], maxTokens: 6000 });20        await recordUsage({ workspaceId: ws.id, provider: m.provider as any, model: id, task: "compare", usage: r.usage, costUsd: r.costUsd, latencyMs: r.totalMs, ttftMs: r.ttftMs, ok: true });21        return { model: id, provider: m.provider, text: r.text, usage: r.usage, costUsd: r.costUsd, latencyMs: Math.round(r.totalMs), ttftMs: r.ttftMs == null ? null : Math.round(r.ttftMs), refusal: r.refusal ?? null };22      } catch (e: any) { await recordUsage({ workspaceId: ws.id, provider: m.provider as any, model: id, task: "compare", ok: false, error: e.message }); return { model: id, provider: m.provider, error: String(e.message).slice(0, 300) }; }23    });24    const queue = tasks.map((p) => p.then((r) => { results.push(r); return r; }));25    for (const p of queue) { /* yield in completion order */ }26    const pending = new Set(queue);27    while (pending.size) { const r = await Promise.race([...pending].map((p) => p.then((v) => [p, v] as const))); pending.delete(r[0]); yield { type: "result", ...r[1] }; }28    const [saved] = await db.insert(schema.aiComparisons).values({ workspaceId: ws.id, prompt: b.prompt, models: b.models, results }).returning({ id: schema.aiComparisons.id });29    yield { type: "saved", id: saved.id };30  }31  return sse(gen(), req.signal);32});33export const GET = handler(async ({ req, ws }) => {34  const id = new URL(req.url).searchParams.get("id");35  if (id) { const [c] = await db.select().from(schema.aiComparisons).where(and(eq(schema.aiComparisons.id, id), eq(schema.aiComparisons.workspaceId, ws.id))); return Response.json(c ?? null); }36  return Response.json(await db.select({ id: schema.aiComparisons.id, prompt: schema.aiComparisons.prompt, models: schema.aiComparisons.models, createdAt: schema.aiComparisons.createdAt }).from(schema.aiComparisons).where(eq(schema.aiComparisons.workspaceId, ws.id)).orderBy(desc(schema.aiComparisons.createdAt)).limit(50));37});38
Files · main
Menu

Theme