118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
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",
|
|
});
|
|
}
|