SPB Git
2.6 KB · 31 lines typescript
1import { z } from "zod";2import { handler, parseBody } from "@/lib/api";3import { streamAI } from "@/lib/ai/providers";4import { route } from "@/lib/ai/router";5import { ACTION_SYSTEMS, INJECTION_GUARD } from "@/lib/ai/prompts";6import { recordUsage } from "@/lib/ai/usage";7import { costUsd } from "@/lib/ai/catalog";8import { sse } from "@/lib/ai/chat";9import type { TaskClass } from "@/lib/ai/types";10export const maxDuration = 300;11const TASK_FOR: Record<string, TaskClass> = { explain_math: "reason", contradictions: "reason", outline: "write", study_notes: "write", table: "extract", action_items: "extract" };12/** Streaming inline AI actions on a selection or a whole page (rewrite, summarize, translate, …) or a free instruction. */13export const POST = handler(async ({ req, ws }) => {14  const b = await parseBody(req, z.object({ action: z.string().max(40), text: z.string().max(120_000), instruction: z.string().max(4000).optional(), model: z.string().nullable().optional(), language: z.string().max(8).optional() }));15  const system = ACTION_SYSTEMS[b.action] ?? `Follow the user's instruction on the given text. Return only the result, no preamble. ${b.language ? `Answer in ${b.language}.` : "Keep the language of the text."}`;16  const task: TaskClass = TASK_FOR[b.action] ?? (b.action === "custom" ? "chat" : "write");17  const dec = await route({ task, preferred: b.model ?? null, estTokens: Math.ceil(b.text.length / 3.6) + 2000 });18  async function* gen() {19    yield { type: "meta", model: dec.model.id, provider: dec.provider, reason: dec.reason };20    let text = ""; let usage: any = null; let ms = 0; let ttft: number | null = null; let err: string | null = null;21    const user = `${b.instruction ? `Instruction: ${b.instruction}\n\n` : ""}<workspace kind="text">\n${b.text}\n</workspace>`;22    for await (const ev of streamAI({ model: dec.model.id, provider: dec.provider, system: `${system}\n${INJECTION_GUARD}`, messages: [{ role: "user", content: user }], effort: dec.effort, maxTokens: 8000, signal: req.signal })) {23      if (ev.type === "text") { text += ev.delta; yield { type: "text", delta: ev.delta }; } else if (ev.type === "done") { usage = ev.usage; ms = ev.totalMs; ttft = ev.ttftMs; } else if (ev.type === "error") err = ev.message;24    }25    const cost = usage ? costUsd(dec.model, usage) : null;26    await recordUsage({ workspaceId: ws.id, provider: dec.provider, model: dec.model.id, task: `action:${b.action}`, usage, costUsd: cost, latencyMs: ms, ttftMs: ttft, ok: !err, error: err });27    if (err && !text) yield { type: "error", message: err }; else yield { type: "done", usage, costUsd: cost, latencyMs: Math.round(ms) };28  }29  return sse(gen(), req.signal);30});31
Files · main
Menu

Theme