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:
@@ -1,100 +1,9 @@
|
||||
// Caveman injector: appends a caveman-style instruction into the system message
|
||||
// of the final request body, just before it is dispatched to the provider executor.
|
||||
// Dispatches by format so it works for both translated and native-passthrough flows.
|
||||
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
import { injectSystemPrompt } from "./systemInject.js";
|
||||
import { CAVEMAN_PROMPTS } from "./cavemanPrompts.js";
|
||||
|
||||
const SEP = "\n\n";
|
||||
|
||||
export function injectCaveman(body, format, level) {
|
||||
const prompt = CAVEMAN_PROMPTS[level];
|
||||
if (!body || !prompt) return;
|
||||
|
||||
switch (format) {
|
||||
case FORMATS.CLAUDE:
|
||||
injectClaudeSystem(body, prompt);
|
||||
return;
|
||||
case FORMATS.GEMINI:
|
||||
case FORMATS.GEMINI_CLI:
|
||||
case FORMATS.VERTEX:
|
||||
case FORMATS.ANTIGRAVITY:
|
||||
// Antigravity wraps Gemini shape in body.request → injectGeminiSystem handles it
|
||||
injectGeminiSystem(body, prompt);
|
||||
return;
|
||||
default:
|
||||
// OpenAI and OpenAI-shaped formats (responses/codex/cursor/kiro/ollama)
|
||||
injectMessagesSystem(body, prompt);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI-shaped: messages[] (chat) or input[] (responses) or instructions (responses string)
|
||||
function injectMessagesSystem(body, prompt) {
|
||||
// OpenAI Responses API: top-level string field
|
||||
if (typeof body.instructions === "string") {
|
||||
body.instructions = body.instructions
|
||||
? `${body.instructions}${SEP}${prompt}`
|
||||
: prompt;
|
||||
return;
|
||||
}
|
||||
|
||||
const arr = Array.isArray(body.messages) ? body.messages
|
||||
: Array.isArray(body.input) ? body.input
|
||||
: null;
|
||||
if (!arr) return;
|
||||
|
||||
const idx = arr.findIndex(m => m && (m.role === "system" || m.role === "developer"));
|
||||
if (idx >= 0) {
|
||||
appendToOpenAIMessage(arr[idx], prompt);
|
||||
} else {
|
||||
arr.unshift({ role: "system", content: prompt });
|
||||
}
|
||||
}
|
||||
|
||||
function appendToOpenAIMessage(msg, prompt) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = `${msg.content}${SEP}${prompt}`;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Responses-style array of parts {type:"input_text"|"text", text}
|
||||
msg.content.push({ type: "input_text", text: prompt });
|
||||
} else {
|
||||
msg.content = prompt;
|
||||
}
|
||||
}
|
||||
|
||||
// Claude shape: body.system as string | array of {type:"text", text}
|
||||
// Insert before the last cache_control block to keep caveman inside the cached prefix.
|
||||
function injectClaudeSystem(body, prompt) {
|
||||
if (typeof body.system === "string" && body.system.length > 0) {
|
||||
body.system = `${body.system}${SEP}${prompt}`;
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(body.system)) {
|
||||
const block = { type: "text", text: prompt };
|
||||
let lastCacheIdx = -1;
|
||||
for (let i = body.system.length - 1; i >= 0; i--) {
|
||||
if (body.system[i]?.cache_control) { lastCacheIdx = i; break; }
|
||||
}
|
||||
if (lastCacheIdx >= 0) {
|
||||
body.system.splice(lastCacheIdx, 0, block);
|
||||
} else {
|
||||
body.system.push(block);
|
||||
}
|
||||
return;
|
||||
}
|
||||
body.system = prompt;
|
||||
}
|
||||
|
||||
// Gemini shape: body.system_instruction | body.systemInstruction | body.request.systemInstruction
|
||||
// Each shape: { parts: [{ text }] }
|
||||
function injectGeminiSystem(body, prompt) {
|
||||
const target = body.request && typeof body.request === "object" ? body.request : body;
|
||||
const useSnake = Object.prototype.hasOwnProperty.call(target, "system_instruction");
|
||||
const key = useSnake ? "system_instruction" : "systemInstruction";
|
||||
const sys = target[key];
|
||||
if (sys && Array.isArray(sys.parts)) {
|
||||
sys.parts.push({ text: prompt });
|
||||
return;
|
||||
}
|
||||
target[key] = { parts: [{ text: prompt }] };
|
||||
injectSystemPrompt(body, format, CAVEMAN_PROMPTS[level]);
|
||||
}
|
||||
|
||||
63
open-sse/rtk/headroom.js
Normal file
63
open-sse/rtk/headroom.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { claudeToOpenAIRequest } from "../translator/request/claude-to-openai.js";
|
||||
import { openaiToClaudeRequest } from "../translator/request/openai-to-claude.js";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 3000;
|
||||
|
||||
// POST messages to Headroom /v1/compress; returns compressed messages + stats or null.
|
||||
async function callCompress(url, messages, model, timeoutMs, compressUserMessages) {
|
||||
const endpoint = `${String(url).replace(/\/$/, "")}/v1/compress`;
|
||||
const payload = { messages, model };
|
||||
if (compressUserMessages) payload.config = { compress_user_messages: true };
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
if (!Array.isArray(data?.messages)) return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Compress request body via Headroom proxy. Fail-open: returns null on any error.
|
||||
// /v1/compress only understands OpenAI shape, so Claude bodies are translated
|
||||
// to OpenAI, compressed, then translated back using 9Router's own translators.
|
||||
export async function compressWithHeadroom(body, { enabled, url, model, format, compressUserMessages, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
||||
if (!enabled || !url || !body) return null;
|
||||
|
||||
try {
|
||||
// Claude shape: translate → OpenAI → compress → translate back.
|
||||
if (format === "claude") {
|
||||
const oai = claudeToOpenAIRequest(model, body, false);
|
||||
if (!Array.isArray(oai?.messages)) return null;
|
||||
const data = await callCompress(url, oai.messages, model, timeoutMs, compressUserMessages);
|
||||
if (!data) return null;
|
||||
const claudeBody = openaiToClaudeRequest(model, { ...oai, messages: data.messages }, false);
|
||||
if (Array.isArray(claudeBody?.messages)) body.messages = claudeBody.messages;
|
||||
if (claudeBody?.system !== undefined) body.system = claudeBody.system;
|
||||
return data;
|
||||
}
|
||||
|
||||
// OpenAI shape: messages/input go straight to the proxy.
|
||||
const key = Array.isArray(body.messages) ? "messages"
|
||||
: Array.isArray(body.input) ? "input"
|
||||
: null;
|
||||
if (!key) return null;
|
||||
const data = await callCompress(url, body[key], model, timeoutMs, compressUserMessages);
|
||||
if (!data) return null;
|
||||
body[key] = data.messages;
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatHeadroomLog(stats) {
|
||||
if (!stats) return null;
|
||||
const before = stats.tokens_before || 0;
|
||||
const after = stats.tokens_after || 0;
|
||||
const saved = stats.tokens_saved || 0;
|
||||
const pct = before > 0 ? ((saved / before) * 100).toFixed(1) : "0";
|
||||
return `saved ${saved} tokens / ${before} (${pct}%) ${after ? `after=${after}` : ""}`.trim();
|
||||
}
|
||||
9
open-sse/rtk/ponytail.js
Normal file
9
open-sse/rtk/ponytail.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// Ponytail injector: appends the "lazy senior dev" instruction into the system
|
||||
// message of the final request body, just before dispatch to the provider executor.
|
||||
|
||||
import { injectSystemPrompt } from "./systemInject.js";
|
||||
import { PONYTAIL_PROMPTS } from "./ponytailPrompt.js";
|
||||
|
||||
export function injectPonytail(body, format, level) {
|
||||
injectSystemPrompt(body, format, PONYTAIL_PROMPTS[level]);
|
||||
}
|
||||
52
open-sse/rtk/ponytailPrompt.js
Normal file
52
open-sse/rtk/ponytailPrompt.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// Ponytail intensity-level prompts injected into system message to bias toward minimal code.
|
||||
// Adapted from ponytail skill (https://github.com/DietrichGebert/ponytail).
|
||||
|
||||
export const PONYTAIL_LEVELS = {
|
||||
LITE: "lite",
|
||||
FULL: "full",
|
||||
ULTRA: "ultra",
|
||||
};
|
||||
|
||||
const SHARED_PERSONA = "You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.";
|
||||
|
||||
const SHARED_LADDER = "Before writing code, stop at the first rung that holds: 1) Does this need to exist at all? (YAGNI) 2) Stdlib does it? Use it. 3) Native platform feature covers it? Use it (CSS over JS, DB constraint over app code). 4) Already-installed dependency solves it? Use it; never add a new one for what a few lines can do. 5) Can it be one line? One line. 6) Only then: the minimum code that works.";
|
||||
|
||||
const SHARED_RULES = "No unrequested abstractions (no interface with one implementation, no factory for one product, no config for a value that never changes). No boilerplate or scaffolding \"for later\". Deletion over addition. Boring over clever. Fewest files possible; shortest working diff wins. Two stdlib options the same size: take the edge-case-correct one. Mark deliberate simplifications with a `ponytail:` comment naming the ceiling and upgrade path.";
|
||||
|
||||
const SHARED_OUTPUT = "Code first. Then at most three short lines: what was skipped, when to add it. No essays or design notes. Pattern: `[code] → skipped: [X], add when [Y].`";
|
||||
|
||||
const SHARED_NOT_LAZY = "Never simplify away: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, anything explicitly requested. Non-trivial logic leaves ONE runnable check behind (an assert-based self-check or one small test file; no frameworks). Trivial one-liners need no test.";
|
||||
|
||||
const SHARED_PERSISTENCE = "ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure.";
|
||||
|
||||
export const PONYTAIL_PROMPTS = {
|
||||
[PONYTAIL_LEVELS.LITE]: [
|
||||
SHARED_PERSONA,
|
||||
"Lite: build what's asked, but name the lazier alternative in one line. User picks.",
|
||||
SHARED_LADDER,
|
||||
SHARED_RULES,
|
||||
SHARED_OUTPUT,
|
||||
SHARED_NOT_LAZY,
|
||||
SHARED_PERSISTENCE,
|
||||
].join(" "),
|
||||
|
||||
[PONYTAIL_LEVELS.FULL]: [
|
||||
SHARED_PERSONA,
|
||||
"Full: the ladder enforced. Stdlib and native first. Shortest diff, shortest explanation.",
|
||||
SHARED_LADDER,
|
||||
SHARED_RULES,
|
||||
SHARED_OUTPUT,
|
||||
SHARED_NOT_LAZY,
|
||||
SHARED_PERSISTENCE,
|
||||
].join(" "),
|
||||
|
||||
[PONYTAIL_LEVELS.ULTRA]: [
|
||||
SHARED_PERSONA,
|
||||
"Ultra: YAGNI extremist. Deletion before addition. Ship the one-liner and challenge the rest of the requirement in the same response.",
|
||||
SHARED_LADDER,
|
||||
SHARED_RULES,
|
||||
SHARED_OUTPUT,
|
||||
SHARED_NOT_LAZY,
|
||||
SHARED_PERSISTENCE,
|
||||
].join(" "),
|
||||
};
|
||||
98
open-sse/rtk/systemInject.js
Normal file
98
open-sse/rtk/systemInject.js
Normal file
@@ -0,0 +1,98 @@
|
||||
// Shared system-prompt injector: appends an instruction into the system message of
|
||||
// the final request body, dispatching by format so it works for translated and
|
||||
// native-passthrough flows. Used by caveman.js and ponytail.js.
|
||||
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
|
||||
const SEP = "\n\n";
|
||||
|
||||
export function injectSystemPrompt(body, format, prompt) {
|
||||
if (!body || !prompt) return;
|
||||
|
||||
switch (format) {
|
||||
case FORMATS.CLAUDE:
|
||||
injectClaudeSystem(body, prompt);
|
||||
return;
|
||||
case FORMATS.GEMINI:
|
||||
case FORMATS.GEMINI_CLI:
|
||||
case FORMATS.VERTEX:
|
||||
case FORMATS.ANTIGRAVITY:
|
||||
// Antigravity wraps Gemini shape in body.request → injectGeminiSystem handles it
|
||||
injectGeminiSystem(body, prompt);
|
||||
return;
|
||||
default:
|
||||
// OpenAI and OpenAI-shaped formats (responses/codex/cursor/kiro/ollama)
|
||||
injectMessagesSystem(body, prompt);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI-shaped: messages[] (chat) or input[] (responses) or instructions (responses string)
|
||||
function injectMessagesSystem(body, prompt) {
|
||||
// OpenAI Responses API: top-level string field
|
||||
if (typeof body.instructions === "string") {
|
||||
body.instructions = body.instructions
|
||||
? `${body.instructions}${SEP}${prompt}`
|
||||
: prompt;
|
||||
return;
|
||||
}
|
||||
|
||||
const arr = Array.isArray(body.messages) ? body.messages
|
||||
: Array.isArray(body.input) ? body.input
|
||||
: null;
|
||||
if (!arr) return;
|
||||
|
||||
const idx = arr.findIndex(m => m && (m.role === "system" || m.role === "developer"));
|
||||
if (idx >= 0) {
|
||||
appendToOpenAIMessage(arr[idx], prompt);
|
||||
} else {
|
||||
arr.unshift({ role: "system", content: prompt });
|
||||
}
|
||||
}
|
||||
|
||||
function appendToOpenAIMessage(msg, prompt) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = `${msg.content}${SEP}${prompt}`;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Responses-style array of parts {type:"input_text"|"text", text}
|
||||
msg.content.push({ type: "input_text", text: prompt });
|
||||
} else {
|
||||
msg.content = prompt;
|
||||
}
|
||||
}
|
||||
|
||||
// Claude shape: body.system as string | array of {type:"text", text}
|
||||
// Insert before the last cache_control block to keep injection inside the cached prefix.
|
||||
function injectClaudeSystem(body, prompt) {
|
||||
if (typeof body.system === "string" && body.system.length > 0) {
|
||||
body.system = `${body.system}${SEP}${prompt}`;
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(body.system)) {
|
||||
const block = { type: "text", text: prompt };
|
||||
let lastCacheIdx = -1;
|
||||
for (let i = body.system.length - 1; i >= 0; i--) {
|
||||
if (body.system[i]?.cache_control) { lastCacheIdx = i; break; }
|
||||
}
|
||||
if (lastCacheIdx >= 0) {
|
||||
body.system.splice(lastCacheIdx, 0, block);
|
||||
} else {
|
||||
body.system.push(block);
|
||||
}
|
||||
return;
|
||||
}
|
||||
body.system = prompt;
|
||||
}
|
||||
|
||||
// Gemini shape: body.system_instruction | body.systemInstruction | body.request.systemInstruction
|
||||
// Each shape: { parts: [{ text }] }
|
||||
function injectGeminiSystem(body, prompt) {
|
||||
const target = body.request && typeof body.request === "object" ? body.request : body;
|
||||
const useSnake = Object.prototype.hasOwnProperty.call(target, "system_instruction");
|
||||
const key = useSnake ? "system_instruction" : "systemInstruction";
|
||||
const sys = target[key];
|
||||
if (sys && Array.isArray(sys.parts)) {
|
||||
sys.parts.push({ text: prompt });
|
||||
return;
|
||||
}
|
||||
target[key] = { parts: [{ text: prompt }] };
|
||||
}
|
||||
Reference in New Issue
Block a user