feat(headroom): add proxy lifecycle management + dashboard UI
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>
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
REACHABLE_MISS_THRESHOLD,
|
||||
CLIENT_PING_FAST_MS,
|
||||
CAVEMAN_LEVELS,
|
||||
PONYTAIL_LEVELS,
|
||||
} from "./endpointConstants";
|
||||
import { clientPingUrl, clientPingAny } from "./endpointPing";
|
||||
import EndpointRow from "./components/EndpointRow";
|
||||
@@ -33,8 +34,17 @@ export default function APIPageClient({ machineId }) {
|
||||
const [hasPassword, setHasPassword] = useState(true);
|
||||
const [tunnelDashboardAccess, setTunnelDashboardAccess] = useState(false);
|
||||
const [rtkEnabled, setRtkEnabledState] = useState(true);
|
||||
const [headroomEnabled, setHeadroomEnabled] = useState(false);
|
||||
const [headroomUrl, setHeadroomUrl] = useState("http://localhost:8787");
|
||||
const [headroomCompressUserMessages, setHeadroomCompressUserMessages] = useState(false);
|
||||
const [headroomStatus, setHeadroomStatus] = useState({ installed: false, running: false, python: null, loading: true });
|
||||
const [showHeadroomInstallModal, setShowHeadroomInstallModal] = useState(false);
|
||||
const [headroomActionLoading, setHeadroomActionLoading] = useState(false);
|
||||
const [headroomActionError, setHeadroomActionError] = useState("");
|
||||
const [cavemanEnabled, setCavemanEnabled] = useState(false);
|
||||
const [cavemanLevel, setCavemanLevel] = useState("full");
|
||||
const [ponytailEnabled, setPonytailEnabled] = useState(false);
|
||||
const [ponytailLevel, setPonytailLevel] = useState("full");
|
||||
const [locale, setLocale] = useState("en");
|
||||
|
||||
// Cloudflare Tunnel state
|
||||
@@ -232,8 +242,14 @@ export default function APIPageClient({ machineId }) {
|
||||
setHasPassword(data.hasPassword || false);
|
||||
setTunnelDashboardAccess(data.tunnelDashboardAccess || false);
|
||||
setRtkEnabledState(data.rtkEnabled !== false);
|
||||
setHeadroomEnabled(!!data.headroomEnabled);
|
||||
setHeadroomUrl(data.headroomUrl || "http://localhost:8787");
|
||||
setHeadroomCompressUserMessages(!!data.headroomCompressUserMessages);
|
||||
refreshHeadroomStatus();
|
||||
setCavemanEnabled(!!data.cavemanEnabled);
|
||||
setCavemanLevel(data.cavemanLevel || "full");
|
||||
setPonytailEnabled(!!data.ponytailEnabled);
|
||||
setPonytailLevel(data.ponytailLevel || "full");
|
||||
}
|
||||
if (statusRes.ok) {
|
||||
const data = await statusRes.json();
|
||||
@@ -313,11 +329,75 @@ export default function APIPageClient({ machineId }) {
|
||||
patchSetting({ cavemanEnabled: value });
|
||||
};
|
||||
|
||||
const handleHeadroomEnabled = (value) => {
|
||||
const nextUrl = headroomUrl.trim() || "http://localhost:8787";
|
||||
setHeadroomUrl(nextUrl);
|
||||
setHeadroomEnabled(value);
|
||||
patchSetting({ headroomEnabled: value, headroomUrl: nextUrl });
|
||||
};
|
||||
|
||||
const handleHeadroomUrlBlur = () => {
|
||||
const next = headroomUrl.trim() || "http://localhost:8787";
|
||||
setHeadroomUrl(next);
|
||||
patchSetting({ headroomUrl: next });
|
||||
};
|
||||
|
||||
const handleHeadroomCompressUserMessages = (value) => {
|
||||
setHeadroomCompressUserMessages(value);
|
||||
patchSetting({ headroomCompressUserMessages: value });
|
||||
};
|
||||
|
||||
const refreshHeadroomStatus = useCallback(async () => {
|
||||
setHeadroomStatus((s) => ({ ...s, loading: true }));
|
||||
try {
|
||||
const res = await fetch("/api/headroom/status", { headers: { "Cache-Control": "no-store" } });
|
||||
const data = await res.json();
|
||||
setHeadroomStatus({ ...data, loading: false });
|
||||
} catch {
|
||||
setHeadroomStatus({ installed: false, running: false, python: null, loading: false });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleHeadroomStart = useCallback(async () => {
|
||||
setHeadroomActionError("");
|
||||
setHeadroomActionLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/headroom/start", { method: "POST" });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || "Failed to start proxy");
|
||||
await refreshHeadroomStatus();
|
||||
} catch (e) {
|
||||
setHeadroomActionError(e.message);
|
||||
} finally {
|
||||
setHeadroomActionLoading(false);
|
||||
}
|
||||
}, [refreshHeadroomStatus]);
|
||||
|
||||
const handleHeadroomStop = useCallback(async () => {
|
||||
setHeadroomActionLoading(true);
|
||||
try {
|
||||
await fetch("/api/headroom/stop", { method: "POST" });
|
||||
await refreshHeadroomStatus();
|
||||
} finally {
|
||||
setHeadroomActionLoading(false);
|
||||
}
|
||||
}, [refreshHeadroomStatus]);
|
||||
|
||||
const handleCavemanLevel = (level) => {
|
||||
setCavemanLevel(level);
|
||||
patchSetting({ cavemanLevel: level });
|
||||
};
|
||||
|
||||
const handlePonytailEnabled = (value) => {
|
||||
setPonytailEnabled(value);
|
||||
patchSetting({ ponytailEnabled: value });
|
||||
};
|
||||
|
||||
const handlePonytailLevel = (level) => {
|
||||
setPonytailLevel(level);
|
||||
patchSetting({ ponytailLevel: level });
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const keysRes = await fetch("/api/keys");
|
||||
@@ -1043,6 +1123,47 @@ export default function APIPageClient({ machineId }) {
|
||||
onChange={() => handleRtkEnabled(!rtkEnabled)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-4 border-b border-border gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<p className="font-medium">
|
||||
Compress context{" "}
|
||||
<a
|
||||
href="https://github.com/chopratejas/headroom"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs font-normal text-primary underline hover:opacity-80"
|
||||
>
|
||||
(Headroom)
|
||||
</a>
|
||||
</p>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${headroomStatus.installed && headroomStatus.running ? "bg-success/15 text-success" : "bg-warning/15 text-warning"}`}>
|
||||
{headroomStatus.loading
|
||||
? "Checking…"
|
||||
: !headroomStatus.installed
|
||||
? "Not installed"
|
||||
: !headroomStatus.running
|
||||
? "Proxy off"
|
||||
: "Running"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowHeadroomInstallModal(true)}
|
||||
className="text-xs text-primary underline hover:opacity-80"
|
||||
>
|
||||
{headroomStatus.installed && headroomStatus.running ? "Manage" : "Setup"}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
Compress prompts via /v1/compress before routing to the model
|
||||
</p>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={headroomEnabled && headroomStatus.installed && headroomStatus.running}
|
||||
disabled={!headroomStatus.installed || !headroomStatus.running}
|
||||
onChange={() => handleHeadroomEnabled(!headroomEnabled)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-4 gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">
|
||||
@@ -1090,6 +1211,53 @@ export default function APIPageClient({ machineId }) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-4 mt-4 border-t border-border gap-4 flex-wrap">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium">
|
||||
Lazy senior dev{" "}
|
||||
<a
|
||||
href="https://github.com/DietrichGebert/ponytail"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs font-normal text-primary underline hover:opacity-80"
|
||||
>
|
||||
(Ponytail)
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-sm text-text-muted">
|
||||
Bias the model toward minimal code: YAGNI, reuse stdlib, deletion over addition
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
{ponytailEnabled && (
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{PONYTAIL_LEVELS.map((lvl) => (
|
||||
<button
|
||||
key={lvl.id}
|
||||
onClick={() => handlePonytailLevel(lvl.id)}
|
||||
className={`px-3 py-1.5 rounded text-xs font-medium border transition-colors ${
|
||||
ponytailLevel === lvl.id
|
||||
? "bg-primary text-white border-primary"
|
||||
: "bg-transparent border-border text-text-muted hover:bg-surface-2"
|
||||
}`}
|
||||
title={lvl.desc}
|
||||
>
|
||||
{lvl.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-primary">
|
||||
{PONYTAIL_LEVELS.find((lvl) => lvl.id === ponytailLevel)?.desc}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Toggle
|
||||
checked={ponytailEnabled}
|
||||
onChange={() => handlePonytailEnabled(!ponytailEnabled)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* API Keys */}
|
||||
@@ -1420,6 +1588,58 @@ export default function APIPageClient({ machineId }) {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Headroom Install Guide Modal */}
|
||||
<Modal
|
||||
isOpen={showHeadroomInstallModal}
|
||||
title={headroomStatus.installed ? "Headroom" : "Install Headroom"}
|
||||
onClose={() => setShowHeadroomInstallModal(false)}
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span>Status</span>
|
||||
<span className={headroomStatus.installed && headroomStatus.running ? "text-success" : "text-warning"}>
|
||||
{headroomStatus.loading
|
||||
? "Checking…"
|
||||
: !headroomStatus.installed
|
||||
? "Not installed"
|
||||
: !headroomStatus.running
|
||||
? "Proxy off"
|
||||
: "Running"}
|
||||
</span>
|
||||
</div>
|
||||
{headroomStatus.installed ? (
|
||||
headroomStatus.running ? (
|
||||
<Button onClick={handleHeadroomStop} variant="ghost" fullWidth disabled={headroomActionLoading}>
|
||||
{headroomActionLoading ? "Stopping…" : "Stop Headroom"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={handleHeadroomStart} fullWidth disabled={headroomActionLoading}>
|
||||
{headroomActionLoading ? "Starting…" : "Start Headroom"}
|
||||
</Button>
|
||||
)
|
||||
) : !headroomStatus.python ? (
|
||||
<p className="text-sm text-warning">Python ≥ 3.10 required. Install Python first.</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium">Install then click Start:</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<pre className="flex-1 rounded bg-black/5 dark:bg-white/5 p-2 text-xs font-mono overflow-x-auto">{`pip install "headroom-ai[proxy]"`}</pre>
|
||||
<Button size="sm" variant="ghost" onClick={() => copy(`pip install "headroom-ai[proxy]"`)}>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{headroomActionError && (
|
||||
<p className="text-sm text-warning">{headroomActionError}</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => refreshHeadroomStatus()} variant="ghost" fullWidth>Recheck</Button>
|
||||
<Button onClick={() => setShowHeadroomInstallModal(false)} fullWidth>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Confirm Modal */}
|
||||
<ConfirmModal
|
||||
isOpen={!!confirmState}
|
||||
|
||||
@@ -24,3 +24,9 @@ export const CAVEMAN_LEVELS = [
|
||||
{ id: "wenyan", label: "文 Full", desc: "Maximum 文言文, 80-90% reduction", wenyan: true },
|
||||
{ id: "wenyan-ultra", label: "文 Ultra", desc: "Extreme classical compression", wenyan: true },
|
||||
];
|
||||
|
||||
export const PONYTAIL_LEVELS = [
|
||||
{ id: "lite", label: "Lite", desc: "Build asked, name lazier option" },
|
||||
{ id: "full", label: "Full", desc: "Ladder enforced: stdlib/native first" },
|
||||
{ id: "ultra", label: "Ultra", desc: "YAGNI extremist, deletion first" },
|
||||
];
|
||||
|
||||
27
src/app/api/headroom/start/route.js
Normal file
27
src/app/api/headroom/start/route.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSettings } from "@/lib/localDb";
|
||||
import { startHeadroomProxy } from "@/lib/headroom/process";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function parsePortFromUrl(url) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const p = parseInt(u.port, 10);
|
||||
if (p > 0 && p < 65536) return p;
|
||||
} catch { /* ignore, fall through to default */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
const url = settings.headroomUrl || "http://localhost:8787";
|
||||
const port = parsePortFromUrl(url) || 8787;
|
||||
const result = await startHeadroomProxy({ port });
|
||||
return NextResponse.json({ success: true, ...result });
|
||||
} catch (error) {
|
||||
const status = error.code === "NOT_INSTALLED" ? 400 : 500;
|
||||
return NextResponse.json({ error: error.message, code: error.code || null }, { status });
|
||||
}
|
||||
}
|
||||
18
src/app/api/headroom/status/route.js
Normal file
18
src/app/api/headroom/status/route.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSettings } from "@/lib/localDb";
|
||||
import { getHeadroomStatus } from "@/lib/headroom/detect";
|
||||
import { getManagedPid } from "@/lib/headroom/process";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
const url = settings.headroomUrl || "http://localhost:8787";
|
||||
const status = await getHeadroomStatus(url);
|
||||
const managedPid = getManagedPid();
|
||||
return NextResponse.json({ ...status, url, managedPid });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
14
src/app/api/headroom/stop/route.js
Normal file
14
src/app/api/headroom/stop/route.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { stopHeadroomProxy } from "@/lib/headroom/process";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const result = stopHeadroomProxy();
|
||||
const status = result.stopped ? 200 : 409;
|
||||
return NextResponse.json({ ...result }, { status });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error.message, code: error.code || null }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user