SPB Git

spb/spboucher.ai Public

spboucher.ai — personal website of Simon-Pierre Boucher.

TypeScript 93.4% HTML 5.5% CSS 1%
3.1 KB · 120 lines typescript
Raw Blame History
1/*2  route.ts3  spboucher.ai Web4  Author: Simon-Pierre Boucher5  Mail: contact@spboucher.ai6*/78import Anthropic from "@anthropic-ai/sdk";910import { buildAgentSystemPrompt } from "@/lib/agent-profile";1112export const runtime = "nodejs";13export const dynamic = "force-dynamic";1415const MODEL = "claude-haiku-4-5";16const MAX_TURNS = 16;17const MAX_MESSAGE_CHARS = 4000;1819const SYSTEM_PROMPT = buildAgentSystemPrompt();2021interface ChatTurn {22  role: "user" | "assistant";23  content: string;24}2526/** Clamp and sanitize the client-provided history into a valid Messages array. */27function sanitizeMessages(raw: unknown): Anthropic.MessageParam[] | null {28  if (!Array.isArray(raw) || raw.length === 0) return null;2930  const turns: ChatTurn[] = [];31  for (const item of raw.slice(-MAX_TURNS)) {32    if (33      typeof item !== "object" ||34      item === null ||35      !("role" in item) ||36      !("content" in item)37    ) {38      return null;39    }40    const role = (item as ChatTurn).role;41    const content = (item as ChatTurn).content;42    if (role !== "user" && role !== "assistant") return null;43    if (typeof content !== "string" || content.trim().length === 0) return null;44    turns.push({ role, content: content.slice(0, MAX_MESSAGE_CHARS) });45  }4647  // Drop leading assistant turns; the API requires the first message be "user".48  while (turns.length > 0 && turns[0].role !== "user") turns.shift();49  if (turns.length === 0 || turns[turns.length - 1].role !== "user")50    return null;5152  return turns.map((t) => ({ role: t.role, content: t.content }));53}5455export async function POST(request: Request) {56  const apiKey = process.env.ANTHROPIC_API_KEY;57  if (!apiKey) {58    return Response.json(59      { error: "Agent is not configured on this server." },60      { status: 503 },61    );62  }6364  let messages: Anthropic.MessageParam[] | null = null;65  try {66    const body = await request.json();67    messages = sanitizeMessages(body?.messages);68  } catch {69    messages = null;70  }71  if (!messages) {72    return Response.json({ error: "Invalid messages." }, { status: 400 });73  }7475  const client = new Anthropic({ apiKey });7677  const stream = client.messages.stream({78    model: MODEL,79    max_tokens: 1024,80    system: [81      {82        type: "text",83        text: SYSTEM_PROMPT,84        cache_control: { type: "ephemeral" },85      },86    ],87    messages,88  });8990  const encoder = new TextEncoder();91  const body = new ReadableStream<Uint8Array>({92    start(controller) {93      stream.on("text", (delta) => {94        controller.enqueue(encoder.encode(delta));95      });96      stream.on("end", () => controller.close());97      stream.on("error", (err) => {98        console.error("SPB Agent stream error:", err);99        controller.enqueue(100          encoder.encode(101            "\n\n[The agent hit a temporary error — please try again.]",102          ),103        );104        controller.close();105      });106    },107    cancel() {108      stream.abort();109    },110  });111112  return new Response(body, {113    headers: {114      "Content-Type": "text/plain; charset=utf-8",115      "Cache-Control": "no-cache, no-transform",116      "X-Accel-Buffering": "no",117    },118  });119}120