Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions HEADROOM.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Headroom is a local context-compression proxy that sits between AI Watchtower and
your LiteLLM endpoint. It intercepts each chat request, runs a compression pass
over the message history using an on-device ONNX model (kompress-base), and
over the message history using an on-device ONNX model (kompress-v2-base), and
forwards the compressed prompt to LiteLLM. Only the compressed text leaves the
machine. The full, uncompressed conversation is never sent anywhere.

Expand Down Expand Up @@ -37,7 +37,7 @@ find each piece of the implementation in the source tree.
1. You send a message in the Headroom tab.
2. The AI Watchtower server appends it to the conversation transcript and POSTs
the full history to http://localhost:8787/v1/chat/completions (the proxy).
3. The proxy loads the kompress-base ONNX model (already on disk after first run)
3. The proxy loads the kompress-v2-base ONNX model (already on disk after first run)
and runs a semantic compression pass over the message history.
4. Compression only fires when the context window exceeds 500 tokens. Shorter
conversations pass through unchanged.
Expand All @@ -63,7 +63,7 @@ AI Watchtower server (apps/server/src/routes/litellm.ts)
v
Headroom proxy (http://localhost:8787)
|
| semantic compression (kompress-base, ONNX, local CPU/GPU)
| semantic compression (kompress-v2-base, ONNX, local CPU/GPU)
| POST /v1/chat/completions
v
LiteLLM (LITELLM_BASE_URL)
Expand Down Expand Up @@ -92,8 +92,8 @@ The script (scripts/dev) does the following in order:
3. Runs pnpm install if node_modules is absent.
4. Checks whether headroom is on PATH.
- If not, prompts to install it: pip install headroom-ai httpx[http2]
5. Checks whether kompress-base is cached at
~/.cache/huggingface/hub/models--chopratejas--kompress-base
5. Checks whether kompress-v2-base is cached at
~/.cache/huggingface/hub/models--chopratejas--kompress-v2-base
- If not cached, starts the proxy online so it can download the model
from HuggingFace (one-time, ~several hundred MB).
- If already cached, sets HF_HUB_OFFLINE=1 before starting the proxy.
Expand Down Expand Up @@ -338,8 +338,8 @@ to the headroom-ai documentation (https://github.com/chopratejas/headroom).

All compression runs entirely on your machine:

- The kompress-base model is a local ONNX file cached at
~/.cache/huggingface/hub/models--chopratejas--kompress-base.
- The kompress-v2-base model is a local ONNX file cached at
~/.cache/huggingface/hub/models--chopratejas--kompress-v2-base.
- After the first run (model download), HF_HUB_OFFLINE=1 is set so no further
HuggingFace network calls are made.
- HEADROOM_TELEMETRY=off is set unconditionally by scripts/dev; the proxy sends
Expand Down Expand Up @@ -380,7 +380,7 @@ What stays local:
| Dev runner (install + launch) | scripts/dev |
| Proxy log file | data/headroom.log (runtime, gitignored) |
| Persistent savings | ~/.headroom/proxy_savings.json (outside repo) |
| Model cache | ~/.cache/huggingface/hub/models--chopratejas--kompress-base |
| Model cache | ~/.cache/huggingface/hub/models--chopratejas--kompress-v2-base |


## Known limitations and gotchas
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ Headroom is a transparent context compression proxy that reduces tokens sent to

**Setup:** `pnpm dev:one` handles everything automatically:
- Installs `headroom-ai` via pip if not present (prompts once)
- Downloads the `kompress-base` compression model from HuggingFace (one-time, ~first run)
- Downloads the `kompress-v2-base` compression model from HuggingFace (one-time, ~first run)
- Starts the proxy on `http://localhost:8787` before the app
- Routes all Headroom tab calls through the proxy → your `LITELLM_BASE_URL`

Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { dashboardRouter } from "./routes/dashboard";
import { analyticsRouter } from "./routes/analytics";
import { aiCoachAnalyticsRouter } from "./routes/aiCoachAnalytics";
import { getValidModelIds } from "./litellm";
import { startHeadroomSnapshotPoller } from "./headroom-snapshot-poller";

export type AgentsFleetServer = {
app: Express;
Expand Down Expand Up @@ -42,5 +43,7 @@ export function createApp(): AgentsFleetServer {
app.use("/api", analyticsRouter());
app.use("/api", aiCoachAnalyticsRouter());

startHeadroomSnapshotPoller();

return { app, hub, processManager };
}
16 changes: 16 additions & 0 deletions apps/server/src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,22 @@ export function getDb(): Db {

CREATE INDEX IF NOT EXISTS idx_session_analytics_harness
ON session_analytics(harness);

-- Periodic poll of the headroom proxy's live-only metrics
-- (/subscription-window, /quota, /stats). Headroom keeps these in
-- memory and in its own JSON files under ~/.headroom, which can be
-- lost (restarts, history caps); this table is our durable copy so the
-- dashboard can show trends even after headroom forgets.
CREATE TABLE IF NOT EXISTS headroom_snapshots (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
subscription_window TEXT NULL,
quota TEXT NULL,
stats TEXT NULL
);

CREATE INDEX IF NOT EXISTS idx_headroom_snapshots_created_at
ON headroom_snapshots(created_at);
`);

// Migrations (sessions columns)
Expand Down
59 changes: 59 additions & 0 deletions apps/server/src/headroom-snapshot-poller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import crypto from "node:crypto";
import { getDb } from "./db";

const HEADROOM_PROXY_BASE = process.env.HEADROOM_PROXY_URL ?? "http://localhost:8787";
const POLL_INTERVAL_MS = 5 * 60 * 1000;
const MAX_ROWS = 5000;
const FETCH_TIMEOUT_MS = 3000;

async function fetchJson(url: string): Promise<unknown | null> {
try {
const res = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}

async function pollOnce(): Promise<void> {
const base = HEADROOM_PROXY_BASE.replace(/\/$/, "");
const [subscriptionWindow, quota, stats] = await Promise.all([
fetchJson(`${base}/subscription-window`),
fetchJson(`${base}/quota`),
fetchJson(`${base}/stats?cached=1`),
]);

// Proxy isn't running / unreachable -- nothing to persist this tick.
if (subscriptionWindow === null && quota === null && stats === null) return;

const db = getDb();
db.prepare(
`INSERT INTO headroom_snapshots (id, created_at, subscription_window, quota, stats)
VALUES (?, ?, ?, ?, ?)`,
).run(
crypto.randomUUID(),
new Date().toISOString(),
subscriptionWindow ? JSON.stringify(subscriptionWindow) : null,
quota ? JSON.stringify(quota) : null,
stats ? JSON.stringify(stats) : null,
);

// Best-effort cap so this table can't grow unbounded on a long-running server.
db.prepare(
`DELETE FROM headroom_snapshots WHERE id NOT IN (
SELECT id FROM headroom_snapshots ORDER BY created_at DESC LIMIT ?
)`,
).run(MAX_ROWS);
}

let pollerStarted = false;

export function startHeadroomSnapshotPoller(): void {
if (pollerStarted) return;
pollerStarted = true;
pollOnce().catch(() => {});
setInterval(() => {
pollOnce().catch(() => {});
}, POLL_INTERVAL_MS);
}
66 changes: 66 additions & 0 deletions apps/server/src/routes/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,5 +444,71 @@ export function dashboardRouter(): Router {
}
});

/**
* GET /api/dashboard/headroom/quota?url=<proxyUrl>
* Proxies to {url}/subscription-window and {url}/quota on the local headroom
* proxy. Returns { configured: false } if no url provided.
*/
router.get("/dashboard/headroom/quota", async (req: Request, res: Response) => {
const url = typeof req.query.url === "string" ? req.query.url.trim() : "";
if (!url) return res.json({ configured: false });

try {
new URL(url);
} catch {
return res.status(400).json({ error: { message: "Invalid headroom proxy URL" } });
}

try {
const base = url.replace(/\/$/, "");
const [subRes, quotaRes] = await Promise.all([
fetch(`${base}/subscription-window`, { signal: AbortSignal.timeout(3000) }),
fetch(`${base}/quota`, { signal: AbortSignal.timeout(3000) }),
]);
const subscriptionWindow = subRes.ok ? await subRes.json() as Record<string, unknown> : null;
const quota = quotaRes.ok ? await quotaRes.json() as Record<string, unknown> : null;
return res.json({ configured: true, subscriptionWindow, quota });
} catch (e) {
return res.status(502).json({ error: { message: `Could not reach headroom proxy: ${String(e)}` } });
}
});

/**
* GET /api/dashboard/headroom/snapshots?limit=200
* Returns our own durable poll history of headroom's live-only metrics
* (subscription window, quota, stats), so trends survive even if
* headroom's own in-memory/JSON state resets or caps its history.
*/
router.get("/dashboard/headroom/snapshots", (req: Request, res: Response) => {
const limit = Math.min(2000, Math.max(1, Number(req.query.limit) || 200));
const db = getDb();
const rows = db
.prepare(
`SELECT id, created_at, subscription_window, quota, stats
FROM headroom_snapshots
ORDER BY created_at DESC
LIMIT ?`,
)
.all(limit) as Array<{
id: string;
created_at: string;
subscription_window: string | null;
quota: string | null;
stats: string | null;
}>;

const snapshots = rows
.map((r) => ({
id: r.id,
createdAt: r.created_at,
subscriptionWindow: r.subscription_window ? JSON.parse(r.subscription_window) as unknown : null,
quota: r.quota ? JSON.parse(r.quota) as unknown : null,
stats: r.stats ? JSON.parse(r.stats) as unknown : null,
}))
.reverse();

return res.json({ snapshots });
});

return router;
}
Loading
Loading