Proxy Server
Run the Headroom proxy to compress LLM traffic for any client — Claude Code, Cursor, OpenAI SDK, or custom apps.
The Headroom proxy is a standalone HTTP server that compresses all LLM traffic passing through it. Point any client at the proxy and get automatic context optimization.
Running a local OpenAI-compatible model? See Local LLM prefill benchmarking for a baseline-vs-optimized workflow that measures prompt-processing savings with the dashboard.
Starting the proxy
# Basic usage
headroom proxy
# Custom host and port
headroom proxy --host 0.0.0.0 --port 8080
# With logging and budget
headroom proxy \
--log-file /var/log/headroom.jsonl \
--budget 100.0Telemetry is local-only and off by default. HEADROOM_TELEMETRY=on (or --telemetry) turns on in-process usage stats that power your own /stats, /metrics, and dashboard — nothing is sent to Headroom Labs. (The anonymous aggregate beacon that older versions shipped has been removed from the code.)
CLI options
Core
| Option | Default | Description |
|---|---|---|
--host | 127.0.0.1 | Host to bind to |
--port | 8787 | Port to bind to |
--workers | 1 | Number of Uvicorn worker processes |
--limit-concurrency | 1000 | Maximum concurrent connections before Uvicorn returns 503 |
--max-connections | 500 | Maximum upstream HTTP connections |
--max-keepalive | 100 | Maximum upstream keep-alive connections |
--http-proxy | None | HTTP proxy URL for upstream provider requests only; HTTPS provider APIs use CONNECT |
--mode | cache | Optimization mode: token prioritizes compression, cache preserves provider prefix-cache stability. Default is cache (see Savings profiles) |
--no-optimize | false | Disable optimization (passthrough mode) |
--no-cache | false | Disable semantic caching |
--no-rate-limit | false | Disable rate limiting |
--log-file | None | Path to JSONL log file |
--log-messages | false | Store full request/response content for the live feed |
--budget | None | Daily budget limit in USD |
--openai-api-url | https://api.openai.com | Custom OpenAI API URL |
--provider-name | Detected from --openai-api-url | Display name for the OpenAI-compatible upstream on the dashboard (e.g. OpenRouter). Well-known hosts (OpenRouter, Groq, Together, Azure OpenAI, …) are detected automatically; this overrides them. Routing and pricing are unaffected. |
--anthropic-api-url | Anthropic default | Custom Anthropic API URL |
--gemini-api-url | Gemini default | Custom Gemini API URL |
--backend | anthropic | Backend: anthropic, bedrock, openrouter, anyllm, or litellm-<provider> |
--bedrock-api-url | None | Bedrock InvokeModel upstream for the /model/{id}/invoke passthrough routes (see Bedrock via a local gateway) |
--telemetry | false | Enable local, in-process usage stats (for your own /stats and dashboard; nothing leaves the machine) |
--no-telemetry | false | Force local telemetry off (already the default) |
--stateless | false | Disable filesystem writes and keep runtime state in memory |
Use --http-proxy or HEADROOM_HTTP_PROXY when only provider API traffic should go through a proxy:
headroom proxy --http-proxy http://proxy.internal:8080Avoid setting process-wide variables such as HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, or NO_PROXY for this use case. HTTPX reads those variables too, but Headroom also inherits them into tool executions, so they can proxy unrelated tool traffic.
Context management
| Option | Default | Description |
|---|---|---|
--mode token | Prioritize token compression; prior turns may be rewritten for maximum savings. | |
--mode cache | default | Freeze prior turns to maximize provider prefix-cache hit rate. This is the effective default (see Savings profiles). |
--intercept-tool-results | false | Opt into canary tool-result interceptors such as ast-grep Read outlining. Requires HEADROOM_ROLLOUT_CHANNEL=canary (or dev). |
--no-read-lifecycle | false | Disable stale/superseded Read-output compression. |
--code-aware / --no-code-aware | disabled | Enable or disable AST-based code compression. Requires headroom-ai[code]. |
--code-graph | false | Enable the proxy's live code-graph file watcher for the current project. |
Code-memory MCP (Serena)
headroom wrap registers Serena as the code-memory MCP for semantic, symbol-level code navigation. Serena runs on demand via uvx — Headroom downloads and executes no binary of its own — and indexes the current project locally. Pass --code-memory none to register no code-memory MCP.
Upgrading from tokensave? Earlier releases registered a
tokensaveMCP server (a downloaded Rust binary). tokensave has been retired in favour of Serena. On your nextheadroom wrap/headroom unwrap, Headroom removes thetokensaveMCP entry it installed and switches you to Serena — nothing to migrate, since both are just indexes rebuilt from your source. The leftovertokensavebinary in~/.local/binand any.tokensave/folders are unused and safe to delete.
By default, the proxy uses the shared ContentRouter pipeline. It routes text, logs, JSON, code, images, and tool outputs through the currently enabled compressors and preserves reversible CCR markers where applicable.
# Maximize compression
headroom proxy --mode token
# Preserve provider prefix cache stability
headroom proxy --mode cacheSavings profiles
HEADROOM_SAVINGS_PROFILE selects a named profile that seeds Headroom's whole compression posture — proxy mode, keep-ratio, which messages are compressed, and force_kompress — at proxy startup. It is read by headroom proxy and by the headroom wrap subprocesses. When unset, the default profile is coding.
| Profile | Target savings | Mode | Notes |
|---|---|---|---|
coding | emergent (~50%) | cache | Default. Delta-only compression at ~0 prefix-cache busts; never lossy-compresses file reads. |
balanced | ~70% | token | Moderate compression with structural compaction. Also the fallback for an unknown profile name. |
agent-90 | ~90% | token | Aggressive; pins a 0.10 keep-ratio and forces Kompress. |
general | emergent (~60%) | token | Non-coding workloads. |
An unrecognized HEADROOM_SAVINGS_PROFILE value logs a warning and falls back to balanced — the proxy never fails to start over a bad profile name. See headroom/agent_savings.py for each profile's full set of knobs.
Because the default coding profile uses cache mode (and the proxy's own default mode is also cache), Headroom runs in cache mode out of the box. Mode precedence: an explicit --mode wins, otherwise HEADROOM_MODE (which a profile seeds), otherwise the cache default. To run token mode, pass --mode token or choose a token-mode profile:
# Aggressive ~90% token-savings profile
HEADROOM_SAVINGS_PROFILE=agent-90 headroom proxy --port 8787Optional features
| Option | Default | Description |
|---|---|---|
--memory | false | Enable persistent user memory and provider-appropriate memory tools |
--memory-db-path | {cwd}/.headroom/memory.db | Override the memory SQLite path |
--no-memory-tools | false | Disable automatic memory tool injection |
--no-memory-context | false | Disable automatic memory context injection |
--memory-top-k | 10 | Number of memories to inject as context |
--learn | false | Enable live traffic learning; implies --memory |
--no-learn | false | Explicitly disable traffic learning |
--min-evidence | 5 | Minimum observations before a learned pattern is persisted |
--codex-wire-debug | false | Write local Codex wire snapshots and matching proxy log traces |
--compress-passthrough | false | Also compress custom proxy paths that fall through to the catch-all handler (OpenAI Responses-shaped bodies, path ends in /responses). Also HEADROOM_COMPRESS_PASSTHROUGH=1 |
headroom proxy --memory
headroom proxy --learn --min-evidence 3
headroom proxy --codex-wire-debug
headroom proxy --compress-passthroughLLMLingua removed from the proxy CLI
The old LLMLingua proxy toggles are no longer part of the CLI. Headroom's proxy compression path uses ContentRouter plus the current built-in compressors, including Kompress where applicable.
Savings profiles
The proxy uses a savings profile to control compression behavior — which messages get compressed, how aggressively, and whether to prioritize provider prefix-cache stability or raw savings. Only the env var survives across related tools (headroom wrap passes it to the proxy it launches).
# Switch to a different profile
HEADROOM_SAVINGS_PROFILE=agent-90 headroom proxyBuilt-in profiles
| Profile | Target savings | proxy_mode | force_kompress | Best for |
|---|---|---|---|---|
coding (default) | ~50% (emergent) | cache | No | Coding agents — preserves Anthropic prefix-cache stability |
agent-90 | 90% | token | Yes | Non-coding, cost-sensitive, or high-volume workloads |
balanced | 70% | token | No | General-purpose moderate compression |
general | ~60% (emergent) | token | No | Non-coding chat, little code in context |
coding (default) — Optimizes for coding-agent workloads with Anthropic. Uses cache mode (proxy_mode="cache"): compresses only the newest delta in each turn so the provider's prefix-cache is never busted. User messages are compressed, system prompts preserved (hottest cache). Protects the 2 most recent turns verbatim. Lossless-first with lossy fallback; tool search and cross-turn dedup enabled. This is the profile that headroom wrap uses.
agent-90 — Forces ML-based (Kompress) compression with a 10% keep-ratio, ignoring the lossless path. Compresses both user and system messages. Designed for non-coding or cost-sensitive workloads where maximum compression is the goal.
balanced — Token-mode compression with a 30% keep-ratio. Uses the standard lossless pipeline (does not force Kompress). Protects 4 recent turns. A safe general-purpose profile.
general — Token-mode compression for non-coding conversations. No turn protection (protect_recent=0 — nothing code-positional to preserve) and does not compress user or system messages. Uses the standard lossless pipeline.
Profiles override CLI flags
A profile's proxy_mode setting overrides the --mode flag. The coding profile sets proxy_mode="cache", so --mode token has no effect when coding is active:
# These are equivalent — coding's cache mode always wins
headroom proxy
headroom proxy --mode token # --mode token is silently overriddenTo run in token mode, switch to a profile that uses it:
HEADROOM_SAVINGS_PROFILE=agent-90 headroom proxy --mode tokenExtending a profile with env overrides
Profile defaults are applied only when the corresponding env var is not already set. You can start from a named profile and override individual settings:
# Start from coding but force Kompress on
HEADROOM_SAVINGS_PROFILE=coding HEADROOM_FORCE_KOMPRESS=1 headroom proxy
# Start from balanced but lower the keep-ratio
HEADROOM_SAVINGS_PROFILE=balanced HEADROOM_TARGET_RATIO=0.15 headroom proxyCustom profiles
For permanent custom profiles, see the profile definitions in headroom/agent_savings.py. Each profile is an AgentSavingsProfile dataclass with fields for compression mode, target ratio, turn protection, and pipeline toggles.
Configuration in depth
Proxy behavior is set by three layers, each overriding the one before:
- Savings profile (
HEADROOM_SAVINGS_PROFILE) — seeds a whole posture (mode, keep-ratio, which roles get compressed, Kompress on/off). Defaultcoding. See Savings profiles. - Environment variables — nearly every CLI flag has an
HEADROOM_*twin, which is what you'll use in Docker, systemd, or CI. - CLI flags — the most explicit; they win over env and profile.
The tables below group the knobs by what they control. They aren't exhaustive (headroom proxy --help prints the full list), but they cover what real deployments actually touch. Unless noted, every option is off/unset by default and safe to ignore.
Compression tuning
Fine-grained control over what gets compressed and how hard. Most users pick a profile instead and never touch these.
| Flag / env | Default | Effect |
|---|---|---|
--mode / HEADROOM_MODE | cache | cache compresses only the newest delta (prefix-cache safe); token maximizes removal. |
--target-ratio / HEADROOM_TARGET_RATIO | unset | Keep-ratio for ML text compression; lower = more aggressive (e.g. 0.10). |
HEADROOM_MIN_TOKENS | 500 | Minimum block size before a tool output is compressed. |
--compress-user-messages / HEADROOM_COMPRESS_USER_MESSAGES | false | Compress content inside user-role messages (tool results live there). The coding profile turns this on. |
HEADROOM_COMPRESS_SYSTEM_MESSAGES | unset | Compress system prompts. Off by default to keep the hottest cache prefix stable. |
HEADROOM_PROTECT_RECENT | profile | Never compress the N most recent turns. |
--protect-tool-results / HEADROOM_PROTECT_TOOL_RESULTS | empty | Comma-separated tool names whose output is never lossy-compressed. |
--compressor (repeatable) / HEADROOM_COMPRESSORS | all | Restrict to specific compressors: smart_crusher,kompress,code_aware,search,log,tabular,config,html,image. |
--code-aware / --no-code-aware | off | AST-based code compression. Requires headroom-ai[code]. |
Kompress (ML compression)
Kompress is the ModernBERT/ONNX compressor that ContentRouter falls back to for prose and unstructured text. It can run in-process or be offloaded to a hosted endpoint.
| Flag / env | Default | Effect |
|---|---|---|
--disable-kompress / HEADROOM_DISABLE_KOMPRESS | false | Turn off ML compression; keep the structural compressors. |
--disable-kompress-anthropic / --disable-kompress-openai | inherit | Per-provider override. |
--force-kompress-all / HEADROOM_FORCE_KOMPRESS_ALL | false | Route all content through Kompress, bypassing per-type selection. |
HEADROOM_KOMPRESS_ENDPOINT | none | Offload ML compression to a remote /compress endpoint (e.g. a Modal deployment) instead of running the model locally. |
HEADROOM_KOMPRESS_ENDPOINT_TOKEN | none | Bearer token for the remote endpoint. |
HEADROOM_KOMPRESS_BACKEND | auto | Compute backend: auto, onnx_cpu, onnx_coreml, pytorch, pytorch_mps. |
Reversible compression (CCR) and lossless mode
By default Headroom stores originals so the model can recover them via headroom_retrieve. See Reversible Compression.
| Flag / env | Default | Effect |
|---|---|---|
--no-ccr / HEADROOM_NO_CCR | CCR on | Disable retrieval markers and the injected headroom_retrieve tool. |
--lossless / HEADROOM_LOSSLESS | false | Format-native lossless compaction only — no CCR marker, no retrieval tool. |
--no-ccr-proactive-expansion | expansion on | Stop proactively re-expanding compressed content when the model appears to need it. |
File-read handling
Coding agents re-read the same files repeatedly; these control how stale reads are handled without busting the prefix cache.
| Flag / env | Default | Effect |
|---|---|---|
--no-read-lifecycle | lifecycle on | Stop replacing stale/superseded file reads with CCR markers. |
--read-maturation / HEADROOM_READ_MATURATION | false | (Beta) Hold freshly-read files out of the prefix cache until the file quiesces. Requires HEADROOM_ROLLOUT_CHANNEL=beta (or dev). |
--read-maturation-quiesce-turns | 5 | Turns of no change before a held read is admitted. |
Reliability: timeouts, retries, limits
| Flag / env | Default | Effect |
|---|---|---|
--request-timeout-seconds / HEADROOM_REQUEST_TIMEOUT | 300 | Upstream request timeout (seconds). |
--connect-timeout-seconds | 10 | Upstream connect timeout (seconds). |
--retry-max-attempts | 3 | Upstream retries on transient failure. |
--limit-concurrency | 1000 | Concurrent connections before returning 503. |
--rpm / --tpm | 60 / 100000 | Requests- and tokens-per-minute rate limits (disable with --no-rate-limit). |
--budget / --budget-period | none / daily | Spend cap in USD per period; over-budget requests get 429. |
--workers / HEADROOM_WORKERS | 1 | Uvicorn worker processes. |
Tool search and MCP
Defers large tool schemas so they don't sit in every request. See MCP.
| Env | Scope | Effect |
|---|---|---|
HEADROOM_TOOL_SEARCH | proxy (server-side) | Defer MCP/system tool schemas behind a search_tools tool. On by default for Anthropic requests carrying enough tools to be worth it; set HEADROOM_TOOL_SEARCH=0 to opt out. |
ENABLE_TOOL_SEARCH | client (Claude Code) | Keep Claude Code's own deferred tool-loading active behind a custom base URL (issue #746). Set automatically by headroom wrap. |
Cost-aware model routing
Rewrite the upstream model per request — for example, send small, tool-free calls to a cheaper model. Opt-in and off by default. Configure with HEADROOM_MODEL_ROUTER_ENABLED plus HEADROOM_MODEL_ROUTES; see Cost-aware model routing.
Observability
| Flag / env | Default | Effect |
|---|---|---|
--telemetry / HEADROOM_TELEMETRY | off | Local-only usage stats for your own /stats, /metrics, and dashboard. Nothing leaves the machine. |
--log-file / HEADROOM_LOG_FILE | none | JSONL request/response log. |
--log-messages | false | Include full message bodies in the log (may contain sensitive data). |
HEADROOM_OTEL_METRICS_ENABLED | false | Export OpenTelemetry metrics (HEADROOM_OTEL_METRICS_ENDPOINT, …). See OTLP export. |
HEADROOM_LANGFUSE_ENABLED | false | Emit Langfuse traces (LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY). |
See Metrics for the Prometheus and Grafana setup.
Security and networking
| Flag / env | Default | Effect |
|---|---|---|
HEADROOM_PROXY_TOKEN | none | Require a bearer token (X-Headroom-Proxy-Token) from non-loopback callers. |
HEADROOM_COMPRESS_ALLOW_REMOTE | false | Allow non-loopback callers to reach POST /v1/compress. Required to run Headroom as a gateway/sidecar; without it remote callers get 404. |
--offline / HEADROOM_OFFLINE | false | Air-gap mode: hard-disable all egress (telemetry, update checks, license reporting, model downloads). |
--stateless / HEADROOM_STATELESS | false | Keep all state in memory; no filesystem writes (disables logs, memory, TOIN). |
HEADROOM_STRIP_INTERNAL_HEADERS | enabled | Strip internal x-headroom-* headers before forwarding upstream. |
HEADROOM_TLS_STRICT | strict | Set 0 to relax CA-constraint checks behind a corporate TLS-inspection proxy. |
Performance
| Flag / env | Default | Effect |
|---|---|---|
--embedding-server / HEADROOM_EMBEDDING_SERVER | off | Share one ONNX embedder across workers (~600 MB RSS saved). |
--compression-max-workers / HEADROOM_COMPRESSION_MAX_WORKERS | CPU count | Bound the CPU-bound compression threadpool. |
Full JSON config
For programmatic deployment you can pass an entire proxy config as JSON via HEADROOM_PROXY_CONFIG_JSON, or point HEADROOM_CONFIG_DIR / HEADROOM_WORKSPACE_DIR at custom roots (see Filesystem Contract).
API endpoints
GET /health
curl http://localhost:8787/health{
"status": "healthy",
"optimize": true,
"stats": {
"total_requests": 42,
"tokens_saved": 15000,
"savings_percent": 45.2
}
}GET /stats
Live session statistics plus durable persistent_savings totals. Stored at ~/.headroom/proxy_savings.json (override with HEADROOM_SAVINGS_PATH).
curl http://localhost:8787/statsGET /stats-history
Durable history with hourly, daily, weekly, and monthly rollups. Powers the /dashboard view.
curl http://localhost:8787/stats-history
curl "http://localhost:8787/stats-history?format=csv&series=weekly"GET /metrics
Prometheus-format metrics for monitoring.
curl http://localhost:8787/metricsheadroom_requests_total{mode="optimize"} 1234
headroom_tokens_saved_total 5678900
headroom_persistent_savings_tokens_saved_total 5678900
headroom_compression_ratio_bucket{le="0.5"} 890
headroom_latency_seconds_bucket{le="0.01"} 800
headroom_cache_hits_total 456headroom_tokens_saved_total is the runtime counter for the current proxy process. Use headroom_persistent_savings_tokens_saved_total for durable lifetime savings that match /stats.persistent_savings.
POST /v1/messages
Anthropic API format. The proxy compresses messages, forwards to Anthropic, and returns the response.
POST /v1/chat/completions
OpenAI API format. The proxy compresses messages, forwards to OpenAI, and returns the response.
POST /v1/responses
OpenAI Responses API format. The proxy compresses input payloads where applicable, forwards the request, and returns the response.
For Codex-compatible clients, the proxy also accepts these alias paths and routes them through the same handler:
POST /v1/codex/responsesPOST /backend-api/responsesPOST /backend-api/codex/responses
Matching WebSocket and subpath aliases are also supported for Codex flows.
Codex Live voice WebSocket
The proxy relays Codex Live voice frames without parsing or transforming them. These paths use the same transparent transport:
ws://localhost:8787/v1/livews://localhost:8787/v1/codex/livews://localhost:8787/backend-api/livews://localhost:8787/backend-api/codex/live
Subscription authentication uses the derived ChatGPT backend path. API-key
authentication preserves the selected OpenAI-compatible base URL and inbound
path. The backend Live suffix defaults to /live and can be corrected with
HEADROOM_CODEX_LIVE_WS_PATH if the upstream contract changes. The exact
ChatGPT backend path is not confirmed by this proxy documentation.
POST /v1internal:streamGenerateContent
Google Cloud Code Assist / Antigravity compatibility endpoint used by Pi-style google-gemini-cli and google-antigravity providers.
The proxy also accepts:
POST /v1/v1internal:streamGenerateContent
POST /v1/compress
Compression-only endpoint. Compresses messages and returns them without ever making a completion request to an LLM provider — no generation, no provider API key, no upstream chat call. Used by the TypeScript SDK, by LiteLLM's headroom guardrail, and by API gateways running Headroom as a sidecar.
It does run local ML models
"No LLM call" means no generative request to a provider. Compression itself is ML-backed: Kompress is a ModernBERT encoder that scores tokens for retention (classification, not generation), and Magika classifies content types. Both run in-process by default, so budget CPU and memory for the sidecar accordingly.
If HEADROOM_KOMPRESS_ENDPOINT is set, Kompress inference is offloaded over HTTP to that model server — real egress from the sidecar, which matters if you deployed it expecting none. Only inference goes remote: the CCR store and retrieval markers stay proxy-local, and original content never persists off-box. Leave the variable unset to keep everything in-process, or run with HEADROOM_DISABLE_KOMPRESS=1 for structural compression only.
Loopback-only by default
This route is restricted to loopback callers and answers everyone else with 404, not 403 — deliberately, so it stays invisible to external scanners. A gateway calling it from another host or pod therefore sees what looks like a missing route.
Both the client IP and the inbound Host: header must name loopback. To allow remote callers, set HEADROOM_COMPRESS_ALLOW_REMOTE=1. HEADROOM_PROXY_TOKEN still applies if set.
Message format
The endpoint does no format conversion. Whatever shape you send in messages is the shape you get back, and both wire formats are compressed natively:
- OpenAI shape —
role: "tool"messages withtool_call_id, assistanttool_calls - Anthropic shape — content-block lists with
tool_use/tool_result/thinkingblocks
So an Anthropic-native caller does not need to convert to OpenAI format first. Block types, tool_use_ids and message order are all preserved.
model selects the tokenizer (per-model, from Headroom's tokenizer registry) and the context limit. Send the real model name — including gateway-prefixed forms like bedrock/anthropic.claude-3-5-sonnet or vertex_ai/claude-sonnet-4@20250514 — so token counts and compression aggressiveness are right.
Request
| Field | Type | Required | Description |
|---|---|---|---|
messages | array | yes | Messages to compress, in either wire format. 400 if missing. Empty array returns immediately with zero metrics. |
model | string | yes | Model name. Drives tokenizer + context-limit resolution. 400 if missing. |
token_budget | integer | no | Overrides the model's context limit. Used by callers that need to fit a tighter budget. |
config | object | no | Compression options, below. A non-object value is ignored rather than rejected. |
system and tools are ignored
Only the four fields above are read. Anthropic sends system and tools out of band, alongside messages — this endpoint accepts them without complaint (you get a 200, no warning) and returns neither, so neither is compressed.
Keep carrying both yourself and send them upstream unchanged. Two consequences worth knowing:
- An Anthropic system prompt is not compressed here, even though it is resent on every request.
- Tool-schema compaction and tool-search deferral are not reachable through this endpoint — on tool-heavy traffic those can be the largest share of available savings. Run Headroom as the proxy (rather than calling
/v1/compress) if you need them.
config fields:
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | unset | ccr, lossy_inline, or lossless_then_lossy. Unset selects the default marker-free pipeline. Any other value is a 400. |
frozen_message_count | integer | unset | Pin a prefix: the first N messages are returned byte-for-byte unchanged while staying visible to cross-message transforms like dedup. Set it to the number of messages the provider has already cached so compression cannot rewrite the prefix and bust that cache. Must be a non-negative integer; anything else is a 400. |
compress_user_messages | boolean | false | Also compress user-role messages. |
target_ratio | number | unset | Target compression ratio. |
protect_recent | integer | unset | Leave the last N messages uncompressed. |
protect_analysis_context | boolean | unset | Preserve analysis context blocks. |
config.mode values:
- unset (default) — marker-free. Emits no
<<ccr:…>>retrieval markers and writes nothing to the CCR store, so you can forward the returned messages straight to a provider. This is the right mode for a gateway or guardrail that just swapsmessagesand forwards. ccr— emits CCR markers and writes to the store. Only for callers that also inject theheadroom_retrievetool and can reach/v1/retrieve(itself loopback-only). Markers are a dangling pointer for the model otherwise.lossy_inline(aliaslossless_then_lossy) — runs the lossless byte/data fold first, then compresses the folded remainder. Marker-free.
Response
| Field | Type | Description |
|---|---|---|
messages | array | Compressed messages, in the shape you sent. |
tokens_before | integer | Token count before compression. |
tokens_after | integer | Token count after compression. |
tokens_saved | integer | tokens_before - tokens_after. |
compression_ratio | number | tokens_after / tokens_before — so lower is better. A ratio of 0.23 means a 77% reduction, not 23%. 1.0 when nothing was compressed. |
transforms_applied | array | Transform labels that ran. |
transforms_summary | object | Per-transform counts. |
ccr_hashes | array | Retrieval hashes for markers inserted (empty unless mode: "ccr"). |
{
"messages": [{ "role": "user", "content": "..." }],
"tokens_before": 15000,
"tokens_after": 3500,
"tokens_saved": 11500,
"compression_ratio": 0.23,
"transforms_applied": ["router:smart_crusher:0.35"],
"transforms_summary": { "router:smart_crusher:0.35": 1 },
"ccr_hashes": []
}Headers
x-headroom-bypass: true (case-insensitive) skips compression entirely and echoes your messages back with zeroed metrics. The bypass and empty-messages responses omit transforms_summary.
Errors and fail-open
| Status | Body | When |
|---|---|---|
400 | error.type = "invalid_request" | Missing messages or model, malformed JSON, invalid config.mode, or invalid config.frozen_message_count. |
401 | — | HEADROOM_PROXY_TOKEN is set and the bearer token is missing or wrong. |
404 | — | Non-loopback caller without HEADROOM_COMPRESS_ALLOW_REMOTE=1. |
503 | error.type = "compression_error" | Compression failed unexpectedly. |
Compression fails open on timeout: you get 200 with your original messages, zeroed metrics, plus compression_skipped: true and skip_reason: "compression_timeout". Always check compression_skipped if you need to know whether compression actually ran.
Requests are recorded under provider="compress" in /stats and /metrics.
Multi-turn usage: keeping the prefix cache
This is the single most important thing to get right, and the default is not safe for an agent loop.
When Headroom proxies a request itself it watches the provider's cache hit rate turn over turn and freezes the already-cached prefix. /v1/compress cannot do that — it is stateless. It sees one isolated call and has no idea what the provider already cached.
The provider caches the bytes you forwarded. Compression changed those bytes, so your original messages and the ones the provider cached are no longer the same thing — and it is the forwarded version you have to keep reproducing. Send the pristine originals again next turn and the provider sees a different prefix and re-reads it from scratch. On Anthropic a cache read is ~90% cheaper than fresh input, so that can easily cost more than the compression saves.
Compression is also not uniform over a conversation: how hard a message is compressed depends partly on how far it now sits from the end, so an older tool result can fall outside the recent-read protection window as the conversation grows and be compressed harder than it was last turn. Another reason not to rely on re-compression reproducing earlier output.
Two rules:
- Pass
config.frozen_message_count— how many leading messages the provider has already cached. - Send back your own previous output, not the original messages.
frozen_message_countreturns those leading messages exactly as you passed them in — it pins whatever you hand it. Hand it pristine originals and you get pristine originals back, which is precisely the prefix the provider does not have.
# Keep what you FORWARDED, not what you started with.
forwarded: list[dict] = []
def next_turn(new_messages: list[dict]) -> list[dict]:
body = {
"messages": forwarded + new_messages,
"model": "claude-sonnet-4-6",
# Everything already forwarded is already cached upstream — pin it.
"config": {"frozen_message_count": len(forwarded)},
}
result = requests.post(f"{proxy}/v1/compress", json=body).json()
forwarded[:] = result["messages"] # becomes next turn's frozen prefix
return forwardedRe-sending pristine messages every turn silently busts the cache
Compressing the full original conversation on each turn looks correct — you get a 200 and a positive tokens_saved — but the leading messages come back different from the ones the provider cached. You pay for compression and for a cache miss. Nothing in the response tells you this happened; watch your provider's cache-read tokens.
Also for multi-turn callers:
- Leave
config.modeunset. The default is marker-free, which is what a forward-only caller wants. - Send the real model name so the tokenizer and context limit resolve correctly — including gateway-prefixed forms.
protect_recentis not a substitute. It guards the newest messages;frozen_message_countguards the oldest, which is the cached end.
Not the same as HEADROOM_KOMPRESS_ENDPOINT
HEADROOM_KOMPRESS_ENDPOINT points outbound at a remote Kompress ML model server that happens to expose a /compress path. It is unrelated to this inbound endpoint.
Agent wrapping
Use headroom wrap to launch supported CLI agents through the local proxy:
# Claude Code
headroom wrap claude
# Claude Code extension in VS Code (configures settings, then starts the proxy)
headroom wrap vscode-claude
# OpenAI Codex
headroom wrap codex
# Aider
headroom wrap aider
# Cursor (starts the proxy and prints settings to paste into Cursor)
headroom wrap cursor
# Grok Build (updates ~/.grok/config.toml and starts the proxy)
headroom wrap grok-buildCursor reads model endpoints from its settings UI, so headroom wrap cursor
does not rewrite Cursor configuration or launch the app. After it starts the
proxy, copy the printed base URL into Cursor's model settings.
Grok Build reads model endpoints from ~/.grok/config.toml. headroom wrap grok-build
injects or updates [model.grok-build] base_url to point at the local proxy, then
run grok from the same project directory. See Grok Build Integration.
The official Claude Code extension reads Claude Code's user settings rather than
the terminal environment. Use headroom wrap vscode-claude, reload VS Code after
the first run, and keep the wrapper running. See the
VS Code Claude Code guide for verification and undo
steps.
For environment-driven clients, you can also set the base URL manually:
# Claude Code
ANTHROPIC_BASE_URL=http://localhost:8787 claude
# Any OpenAI-compatible CLI client that reads OPENAI_BASE_URL
OPENAI_BASE_URL=http://localhost:8787/v1 your-clientCloud providers
# AWS Bedrock
headroom proxy --backend bedrock --region us-east-1
# Google Vertex AI
headroom proxy --backend vertex_ai --region us-central1
# Azure OpenAI
headroom proxy --backend azure
# OpenRouter (400+ models)
OPENROUTER_API_KEY=sk-or-... headroom proxy --backend openrouterGoogle Vertex AI
--backend vertex_ai delegates to LiteLLM,
which brings two requirements that are easy to miss:
1. Install the Vertex SDK. google-cloud-aiplatform is not included in any
Headroom extra ([proxy], [all], …) or Docker image variant, so install it
alongside Headroom:
pip install "headroom-ai[proxy]" "google-cloud-aiplatform>=1.38"Without it, the first Vertex request fails with
litellm.BadRequestError: … vertexai import failed … No module named 'vertexai'.
2. Set the LiteLLM project/location variables. LiteLLM reads the GCP project
and region from VERTEXAI_PROJECT / VERTEXAI_LOCATION — these are not the
standard Google Cloud variables (GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION)
used by gcloud and current Google SDKs:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json # or use ADC
export VERTEXAI_PROJECT=<your-gcp-project>
export VERTEXAI_LOCATION=us-central1
headroom proxy --backend vertex_ai --region us-central1If VERTEXAI_PROJECT is unset, requests do not fail loudly — they can silently
resolve against your Application Default Credentials' default quota project,
billing a different GCP project than you intended. Set it explicitly even if
GOOGLE_CLOUD_PROJECT is already exported.
Backend name aliases. vertex_ai, vertex, google-vertex, googlevertex,
litellm-vertex, and litellm-vertex_ai are all normalized to the same
LiteLLM-backed vertex_ai backend — CLI help text and older docs use these
spellings interchangeably.
Running Claude Code against Claude models on Vertex? See Claude Code on Vertex AI for the recommended native Vertex-mode flow that reuses Claude Code's own GCP auth.
Not the same as the LiteLLM callback
--backend vertex_ai runs Headroom as a proxy that itself calls Vertex via
LiteLLM. The LiteLLM integration page documents the inverse:
adding Headroom as a compression callback inside your own LiteLLM app. Despite
the shared name, they are different mechanisms.
Native Vertex passthrough routes
Separately from --backend vertex_ai, the proxy always registers routes that
mirror Vertex's native REST shape verbatim — no backend flag needed:
/{api_version}/projects/{project}/locations/{location}/publishers/{publisher}/models/{model}:generateContent
/{api_version}/projects/{project}/locations/{location}/publishers/{publisher}/models/{model}:streamGenerateContent
/{api_version}/projects/{project}/locations/{location}/publishers/{publisher}/models/{model}:countTokens
/{api_version}/projects/{project}/locations/{location}/publishers/{publisher}/models/{model}:rawPredict
/{api_version}/projects/{project}/locations/{location}/publishers/{publisher}/models/{model}:streamRawPredictRequests with publisher=google (Gemini models) go through Headroom's full
Gemini optimization handler; publisher=anthropic (Claude on Vertex) routes
through the same LiteLLM-Vertex path as --backend vertex_ai. Any client that
already speaks the native Vertex REST API can simply point its endpoint at the
proxy — this is the mechanism headroom wrap claude uses in
Vertex mode.
Bedrock via a local gateway
--backend bedrock accepts Anthropic input (/v1/messages) and re-signs to AWS. Some setups are the other way around: the client already speaks Bedrock (e.g. Claude Code with CLAUDE_CODE_USE_BEDROCK=1, or any AWS SDK pointed at a custom endpoint), sending POST /model/{id}/invoke to a local gateway that re-signs and forwards to AWS (LiteLLM, LocalStack, a corporate Bedrock proxy).
--bedrock-api-url lets Headroom sit in that chain. It registers passthrough routes for /model/{id}/invoke and /model/{id}/invoke-with-response-stream, compresses the request body with the same pipeline as /v1/messages, and forwards to the gateway:
headroom proxy --bedrock-api-url http://127.0.0.1:4000
# then point the client's Bedrock endpoint at Headroom:
AWS_ENDPOINT_URL_BEDROCK_RUNTIME=http://127.0.0.1:8787 your-bedrock-clientThe routes are registered only when --bedrock-api-url (or BEDROCK_TARGET_API_URL) is set — otherwise Bedrock requests fall through unchanged.
Rewriting the request body invalidates the caller's SigV4 signature (it covers a hash of the body). Point --bedrock-api-url at a gateway that re-signs or does not verify the inbound signature — never raw AWS, which would reject the request with 403. For direct-to-AWS compression, use --backend bedrock (which re-signs). The two are complementary.
Environment variables
export HEADROOM_HOST=0.0.0.0
export HEADROOM_PORT=8787
export HEADROOM_BUDGET=100.0
# Route OpenAI passthrough requests to a custom endpoint
export OPENAI_TARGET_API_URL=https://custom.openai.endpoint.com
# Route Anthropic passthrough requests to a custom endpoint
export ANTHROPIC_TARGET_API_URL=https://litellm.company.internal
# Compress Bedrock InvokeModel traffic, forwarding to a re-signing gateway
export BEDROCK_TARGET_API_URL=http://127.0.0.1:4000
headroom proxyProduction deployment
gunicorn
pip install gunicorn
gunicorn headroom.proxy.server:app \
--workers 4 \
--bind 0.0.0.0:8787 \
--worker-class uvicorn.workers.UvicornWorkerDocker
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends build-essential \
&& pip install "headroom-ai[proxy]" \
&& apt-get purge -y build-essential && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*
EXPOSE 8787
CMD ["headroom", "proxy", "--host", "0.0.0.0"]Build dependencies
build-essential is required at install time because headroom-ai includes hnswlib, a C++ extension compiled from source. It is removed after installation to keep the image slim.
Failure Learning
Offline failure analysis for coding agents. Analyzes past sessions, finds what went wrong, correlates with what fixed it, and writes project-level learnings.
Local LLM Prefill Benchmark
Measure local LLM prompt-processing savings by running Headroom in passthrough and optimized proxy modes against an OpenAI-compatible local server.