spb/spboucher.ai Public
spboucher.ai — personal website of Simon-Pierre Boucher.
TypeScript 93.4%
HTML 5.5%
CSS 1%
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