/* route.ts spboucher.ai Web Author: Simon-Pierre Boucher Mail: contact@spboucher.ai */ import Anthropic from "@anthropic-ai/sdk"; import { buildAgentSystemPrompt } from "@/lib/agent-profile"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const MODEL = "claude-haiku-4-5"; const MAX_TURNS = 16; const MAX_MESSAGE_CHARS = 4000; const SYSTEM_PROMPT = buildAgentSystemPrompt(); interface ChatTurn { role: "user" | "assistant"; content: string; } /** Clamp and sanitize the client-provided history into a valid Messages array. */ function sanitizeMessages(raw: unknown): Anthropic.MessageParam[] | null { if (!Array.isArray(raw) || raw.length === 0) return null; const turns: ChatTurn[] = []; for (const item of raw.slice(-MAX_TURNS)) { if ( typeof item !== "object" || item === null || !("role" in item) || !("content" in item) ) { return null; } const role = (item as ChatTurn).role; const content = (item as ChatTurn).content; if (role !== "user" && role !== "assistant") return null; if (typeof content !== "string" || content.trim().length === 0) return null; turns.push({ role, content: content.slice(0, MAX_MESSAGE_CHARS) }); } // Drop leading assistant turns; the API requires the first message be "user". while (turns.length > 0 && turns[0].role !== "user") turns.shift(); if (turns.length === 0 || turns[turns.length - 1].role !== "user") return null; return turns.map((t) => ({ role: t.role, content: t.content })); } export async function POST(request: Request) { const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { return Response.json( { error: "Agent is not configured on this server." }, { status: 503 }, ); } let messages: Anthropic.MessageParam[] | null = null; try { const body = await request.json(); messages = sanitizeMessages(body?.messages); } catch { messages = null; } if (!messages) { return Response.json({ error: "Invalid messages." }, { status: 400 }); } const client = new Anthropic({ apiKey }); const stream = client.messages.stream({ model: MODEL, max_tokens: 1024, system: [ { type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" }, }, ], messages, }); const encoder = new TextEncoder(); const body = new ReadableStream({ start(controller) { stream.on("text", (delta) => { controller.enqueue(encoder.encode(delta)); }); stream.on("end", () => controller.close()); stream.on("error", (err) => { console.error("SPB Agent stream error:", err); controller.enqueue( encoder.encode( "\n\n[The agent hit a temporary error — please try again.]", ), ); controller.close(); }); }, cancel() { stream.abort(); }, }); return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no", }, }); }