Build on the optional Headroom Token Saver from Carmelo Campos
(PR: feat: add optional Headroom token saver). Add managed start/stop
of the local headroom proxy from the dashboard, install detection,
status probing, and a simplified Token Saver UI.
- detect headroom CLI + python>=3.10, probe proxy /health
- spawn/stop proxy as a detached, pid-tracked process
- /api/headroom/{status,start,stop} routes, gated local-only in dashboardGuard
- one-click Start/Stop Headroom modal, no manual config needed
- claude<->openai shape conversion for /v1/compress via 9router translators
Thanks to Carmelo Campos (@carmelogunsroses) for the original Headroom integration.
Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
3.7 KiB
JavaScript
129 lines
3.7 KiB
JavaScript
import fs from "fs";
|
|
import path from "path";
|
|
import { spawn } from "child_process";
|
|
import { DATA_DIR } from "@/lib/dataDir.js";
|
|
import { findHeadroomBinary } from "./detect.js";
|
|
|
|
const HEADROOM_DIR = path.join(DATA_DIR, "headroom");
|
|
const PID_FILE = path.join(HEADROOM_DIR, "proxy.pid");
|
|
const LOG_FILE = path.join(HEADROOM_DIR, "proxy.log");
|
|
const DEFAULT_PORT = 8787;
|
|
const STARTUP_TIMEOUT_MS = 8000;
|
|
|
|
function ensureDir() {
|
|
if (!fs.existsSync(HEADROOM_DIR)) fs.mkdirSync(HEADROOM_DIR, { recursive: true });
|
|
}
|
|
|
|
function readPid() {
|
|
try {
|
|
if (fs.existsSync(PID_FILE)) return parseInt(fs.readFileSync(PID_FILE, "utf8"), 10);
|
|
} catch { /* ignore */ }
|
|
return null;
|
|
}
|
|
|
|
function writePid(pid) {
|
|
ensureDir();
|
|
fs.writeFileSync(PID_FILE, String(pid));
|
|
}
|
|
|
|
function clearPid() {
|
|
try { if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
}
|
|
|
|
// process.kill throws if pid is dead — use this to probe.
|
|
export function isPidAlive(pid) {
|
|
if (!pid || typeof pid !== "number") return false;
|
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
}
|
|
|
|
export function getManagedPid() {
|
|
const pid = readPid();
|
|
return pid && isPidAlive(pid) ? pid : null;
|
|
}
|
|
|
|
export async function startHeadroomProxy({ port = DEFAULT_PORT } = {}) {
|
|
const safePort = Number(port) > 0 && Number(port) < 65536 ? Number(port) : DEFAULT_PORT;
|
|
const binary = findHeadroomBinary();
|
|
if (!binary) {
|
|
const err = new Error("Headroom CLI not installed");
|
|
err.code = "NOT_INSTALLED";
|
|
throw err;
|
|
}
|
|
|
|
const existing = getManagedPid();
|
|
if (existing) return { pid: existing, alreadyRunning: true };
|
|
|
|
ensureDir();
|
|
// spawn stdio requires fd numbers, not WriteStream objects.
|
|
const outFd = fs.openSync(LOG_FILE, "a");
|
|
|
|
const child = spawn(binary, ["proxy", "--port", String(safePort)], {
|
|
stdio: ["ignore", outFd, outFd],
|
|
detached: true,
|
|
windowsHide: true,
|
|
env: { ...process.env },
|
|
});
|
|
|
|
if (!child.pid) {
|
|
fs.closeSync(outFd);
|
|
const err = new Error("Failed to spawn headroom proxy");
|
|
err.code = "SPAWN_FAILED";
|
|
throw err;
|
|
}
|
|
|
|
child.unref();
|
|
writePid(child.pid);
|
|
|
|
// Wait until the process either stays alive briefly (success) or exits fast (failure).
|
|
await new Promise((resolve, reject) => {
|
|
const startupTimer = setTimeout(() => {
|
|
if (isPidAlive(child.pid)) resolve();
|
|
else reject(new Error("headroom proxy exited during startup — see proxy.log"));
|
|
}, STARTUP_TIMEOUT_MS);
|
|
|
|
child.once("exit", (code) => {
|
|
clearTimeout(startupTimer);
|
|
clearPid();
|
|
fs.closeSync(outFd);
|
|
const e = new Error(`headroom proxy exited early (code=${code}) — see proxy.log`);
|
|
e.code = "EARLY_EXIT";
|
|
reject(e);
|
|
});
|
|
});
|
|
|
|
// Close parent's copy of the fd; child retains its own after unref.
|
|
fs.closeSync(outFd);
|
|
|
|
return { pid: child.pid, alreadyRunning: false };
|
|
}
|
|
|
|
export function stopHeadroomProxy() {
|
|
const pid = getManagedPid();
|
|
if (!pid) return { stopped: false, reason: "not_running" };
|
|
try {
|
|
process.kill(pid, "SIGTERM");
|
|
// Give it a moment, then force if still alive.
|
|
setTimeout(() => {
|
|
if (isPidAlive(pid)) {
|
|
try { process.kill(pid, "SIGKILL"); } catch { /* already gone */ }
|
|
}
|
|
}, 2000);
|
|
clearPid();
|
|
return { stopped: true, pid };
|
|
} catch (e) {
|
|
clearPid();
|
|
const err = new Error(`Failed to stop headroom proxy: ${e.message}`);
|
|
err.code = "STOP_FAILED";
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export function getHeadroomLogTail(maxLines = 200) {
|
|
try {
|
|
if (!fs.existsSync(LOG_FILE)) return "";
|
|
const content = fs.readFileSync(LOG_FILE, "utf8");
|
|
const lines = content.split(/\r?\n/).filter(Boolean);
|
|
return lines.slice(-maxLines).join("\n");
|
|
} catch { return ""; }
|
|
}
|