import { NextResponse } from "next/server"; import { run } from "@/lib/runner"; import { FCC_BASE, FCC_TOKEN, probeReachable } from "@/lib/fcc"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const TIMEOUT_MS = 6 * 60 * 1000; async function callFcc(prompt: string): Promise<{ text: string; durationMs: number }> { const started = Date.now(); const ctl = new AbortController(); const tid = setTimeout(() => ctl.abort(), TIMEOUT_MS); try { const r = await fetch(`${FCC_BASE}/v1/messages`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": FCC_TOKEN, "anthropic-version": "2023-06-01", }, body: JSON.stringify({ model: "open_router/google/gemma-4-31b-it:free", messages: [{ role: "user", content: prompt }], max_tokens: 8192, }), signal: ctl.signal, }); clearTimeout(tid); if (!r.ok) { const errBody = await r.text().catch(() => ""); throw new Error(`FCC returned ${r.status}: ${errBody}`); } const body = await r.json(); const text = body?.content?.[0]?.text?.trim() || "(no response)"; return { text, durationMs: Date.now() - started }; } catch (e) { clearTimeout(tid); throw e; } } export async function POST(req: Request) { const { prompt } = await req.json(); if (typeof prompt !== "string" || prompt.length === 0) { return NextResponse.json({ error: "missing prompt" }, { status: 400 }); } if (prompt.length > 16_000) { return NextResponse.json({ error: "prompt too long" }, { status: 413 }); } const fccReachable = await probeReachable(); if (fccReachable) { try { const { text, durationMs } = await callFcc(prompt); return NextResponse.json({ ok: true, text, empty: !text, durationMs, exitCode: 0, timedOut: false, stderr: "", source: "fcc", }); } catch (e) { const errMsg = e instanceof Error ? e.message : String(e); return NextResponse.json({ ok: false, text: `FCC error: ${errMsg}`, empty: true, durationMs: 0, exitCode: -1, timedOut: false, stderr: errMsg, source: "fcc", }); } } // Fallback to Hermes CLI when FCC is not reachable const out = await run("hermes", ["-z", prompt, "--yolo", "--accept-hooks"], { timeoutMs: TIMEOUT_MS }); const ANSI_STRIP = /\x1b\[[0-9;?]*[a-zA-Z]|\x1b\]\d+;[^\x07\x1b]*(\x07|\x1b\\)/g; const text = out.stdout.replace(ANSI_STRIP, "").trim(); const stderrClean = out.stderr.replace(ANSI_STRIP, "").trim(); let diagnostic: string | null = null; if (!text) { const seconds = (out.durationMs / 1000).toFixed(1); const probableTimeout = out.durationMs >= TIMEOUT_MS - 2_000; const lines: string[] = []; lines.push(probableTimeout ? `Hermes was killed after ${seconds}s — the task likely needed longer than the ${Math.round(TIMEOUT_MS/60000)}-minute budget.` : `Hermes finished in ${seconds}s with exit ${out.code} but no stdout.` ); if (stderrClean) { lines.push("", "--- stderr ---"); lines.push(stderrClean.length > 4000 ? stderrClean.slice(-4000) : stderrClean); } else { lines.push("", "(no stderr either)"); } diagnostic = lines.join("\n"); } return NextResponse.json({ ok: out.ok && !!text, text: text || diagnostic || "(no response)", empty: !text, durationMs: out.durationMs, exitCode: out.code, timedOut: !text && out.durationMs >= TIMEOUT_MS - 2_000, stderr: stderrClean, source: "hermes", }); }