Configuration
All configuration options for the Headroom Python and TypeScript SDKs, proxy server, and per-request overrides.
Headroom can be configured via the SDK constructor, proxy command line, environment variables, or per-request overrides.
Runtime Rollout Channels
Headroom uses rollout channels to control which behaviors an already-installed artifact may expose. They do not select a package or released version.
| Variable | Default | Purpose |
|---|---|---|
HEADROOM_ROLLOUT_CHANNEL | stable | Selects stable, beta, canary, or dev. |
HEADROOM_FEATURES | unset | Comma-separated feature names to request explicitly. |
HEADROOM_DISABLE_FEATURES | unset | Comma-separated feature names to force off. Disable wins over every enable path. |
HEADROOM_UNSAFE_ALLOW_UNSTABLE_FEATURES | unset | Break-glass override for emergency mitigation only. |
See Runtime Rollouts for policy, provenance, and contributor rules. If Codex history disappeared after using an older wrapper, see Recover Codex State before wrapping Codex again.
SDK Modes (default_mode / headroom_mode)
These modes apply to SDK usage via HeadroomClient(default_mode=...) or per-request headroom_mode=.... They are not the same as the proxy --mode flag.
| Mode | Behavior | Use Case |
|---|---|---|
audit | Observes and logs, no modifications | Production monitoring, baseline measurement |
optimize | Applies safe, deterministic transforms | Production optimization |
simulate | Returns plan without API call | Testing, cost estimation |
Proxy
--modeis a separate axis:--mode cache(the default) freezes prior turns so the provider's prefix cache is never busted, which is where the savings come from on a real agent workload.--mode tokenmaximizes visible per-request compression at the cost of cache stability; prefercacheunless you are specifically diagnosing compression numbers. The proxy does not acceptaudit,optimize, orsimulate.
SDK Configuration
import { } from 'headroom-ai';
// Reads from HEADROOM_BASE_URL and HEADROOM_API_KEY automatically
const = new ();
// Or configure explicitly
const = new ({
: 'http://localhost:8787',
: 'your-api-key',
: 30_000,
: true,
: 2,
});Per-Request Overrides
Override configuration for individual requests:
import { } from 'headroom-ai';
const = await (messages, {
: 'gpt-4o',
: 100_000,
: 15_000,
});Proxy upstream override (x-headroom-base-url)
When using the proxy, send the x-headroom-base-url request header to route a
single request to a different upstream instead of the configured provider URL.
This lets a client that speaks a provider's wire format authenticate against a
compatible gateway (for example an OpenAI-compatible endpoint, or an
Anthropic-Messages gateway such as OpenCode Zen) without changing the proxy
configuration.
The header is honored by the OpenAI-compatible routes, the Anthropic Messages
route (POST /v1/messages), and the generic passthrough route. The proxy
forwards the request to <x-headroom-base-url> + the original request path
(e.g. /v1/messages). An empty or whitespace-only value is ignored and the
configured upstream is used.
curl http://127.0.0.1:8787/v1/messages \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-H "x-headroom-base-url: https://opencode.ai/zen/go" \
-H "x-api-key: <gateway-api-key>" \
-d '{"model":"glm-5.2","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'When HEADROOM_STRIP_INTERNAL_HEADERS is enabled (the default), the proxy
reads this header for routing and then strips it before forwarding upstream.
Configured secret headers are not sent to arbitrary upstreams
ANTHROPIC_TARGET_API_HEADERS / OPENAI_TARGET_API_HEADERS hold operator
secrets. Because x-headroom-base-url is chosen by the client, those headers
are only attached when the destination is one the operator designated:
- a host in the configured provider targets (
ANTHROPIC_TARGET_API_URL,OPENAI_TARGET_API_URL, and the Gemini/Vertex/Cloud Code equivalents), or - a host listed in
HEADROOM_UPSTREAM_ALLOWED_HOSTS(comma-separated).
A request to any other upstream is still proxied — it just does not carry
your configured headers, and the proxy logs
upstream_extra_headers_withheld host=<host> once per host. If you route to a
gateway via this header and need your configured headers to reach it, add its
host to HEADROOM_UPSTREAM_ALLOWED_HOSTS:
export HEADROOM_UPSTREAM_ALLOWED_HOSTS="gateway.internal,api.example-gateway.ai"Matching is on the parsed hostname and is exact — no wildcards — so
api.anthropic.com.evil.example and https://api.anthropic.com@evil.example
do not match api.anthropic.com.
x-headroom-base-url is refused when an outbound proxy is in the way
Because the destination is chosen by the client, the proxy validates it by
resolving the hostname and rejecting non-public addresses — and then dials the
address it validated, rather than resolving the name a second time at connect
time. Without that second half, a hostname whose authoritative DNS is
attacker-controlled can answer the check with a public address and the
connection with 169.254.169.254 or an RFC1918 host (DNS rebinding).
An outbound HTTP or SOCKS proxy (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) is
handed the target by name — in the request line, in CONNECT, in the SOCKS5
address — and resolves it itself, from its own network, after the check ran.
Headroom cannot enforce the validated address there, so a request carrying
x-headroom-base-url that would route through the proxy is refused rather
than forwarded on a verdict nothing enforces.
This affects only client-supplied upstreams. Every operator-configured provider
URL goes through the proxy exactly as before. If you need a specific endpoint
reachable via x-headroom-base-url through an outbound proxy, name it in
HEADROOM_ALLOWED_BASE_URLS — an explicit operator decision to admit that
destination by name, which also turns off the resolve-and-pin check for it.
SmartCrusher Configuration
Fine-tune JSON compression behavior:
from headroom.transforms import SmartCrusherConfig
config = SmartCrusherConfig(
# Maximum items to keep after compression
max_items_after_crush=15,
# Minimum tokens before applying compression
min_tokens_to_crush=200,
# Fraction of items always kept from the start/end
first_fraction=0.3,
last_fraction=0.15,
# Variance threshold for statistical analysis
variance_threshold=2.0,
)CacheAligner Configuration
Control prefix stabilization for provider cache hit rates:
from headroom import CacheAlignerConfig
config = CacheAlignerConfig(
# Enable/disable cache alignment
enabled=True,
# Use custom regex patterns instead of the built-in dynamic-content
# detector (use_dynamic_detector=False activates date_patterns)
use_dynamic_detector=False,
date_patterns=[
r"Today is \w+ \d+, \d{4}",
r"Current time: .*",
],
)Context Window Management
Context management is now automatic. Use per-request overrides to control behavior:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
# Reserve tokens for model output
headroom_output_buffer_tokens=4000,
# Keep last N turns uncompressed
headroom_keep_turns=3,
)The RollingWindowConfig, IntelligentContextConfig, and ScoringWeights classes are no longer part of Headroom. Context management now happens automatically inside the pipeline (live-zone-only compression).
Claude 1M context window (headroom wrap claude --1m)
headroom wrap claude --1m opts a Claude Code session into Anthropic's 1M-token context window by selecting a [1m]-suffixed model id, which makes Claude Code send the context-1m beta header. The model that --1m targets is resolved in this order:
- an explicit
--model/ANTHROPIC_MODELvalue (used as-is, with a[1m]suffix appended when missing), - otherwise
HEADROOM_1M_MODEL, when set, - otherwise the built-in default (currently
claude-opus-5).
Set HEADROOM_1M_MODEL to point --1m at a specific model without pinning ANTHROPIC_MODEL globally, so the default can follow a new Opus generation without a code change:
# Route --1m at a specific model for this shell / session
export HEADROOM_1M_MODEL=claude-opus-5
headroom wrap claude --1mHEADROOM_1M_MODEL is a fallback only: an explicit --model or ANTHROPIC_MODEL always wins. The value may be given with or without the [1m] suffix; both claude-opus-5 and claude-opus-5[1m] are accepted, and the suffix is added when absent.
For Claude Code's official VS Code extension, use the client-owned persisted
setting instead of ANTHROPIC_MODEL:
headroom wrap vscode-claude --1mThis writes the resolved model with exactly one [1m] suffix to the top-level
model setting in Claude Code's user settings and records the prior presence and
value in Headroom's reversible sidecar. Running the command without --1m
restores a model previously managed by Headroom; headroom unwrap vscode-claude
restores the model and proxy environment values together. Use
--no-configure --1m to print the setting without writing either file.
Claude Code and Anthropic own model support and account eligibility. Local tests prove settings persistence and restoration, not a live entitled VS Code session or a resulting 1M context window.
Pipeline Extensions
Use a headroom.pipeline_extension entry point when you need to normalize or annotate requests before they leave Headroom. The PRE_SEND stage is the right place for provider-specific request cleanup, such as turning content: null into content: "" for upstreams that reject OpenAI-spec tool-call messages.
from headroom.pipeline import PipelineEvent, PipelineStage
class NormalizeNullContent:
def on_pipeline_event(self, event: PipelineEvent) -> PipelineEvent:
if event.stage is not PipelineStage.PRE_SEND or not event.messages:
return event
for message in event.messages:
if (
message.get("role") == "assistant"
and message.get("content") is None
and message.get("tool_calls")
):
message["content"] = ""
return eventRegister it in pyproject.toml:
[project.entry-points."headroom.pipeline_extension"]
normalize_null_content = "my_pkg.normalize:NormalizeNullContent"If the upstream base URL itself must vary per request, use the x-headroom-base-url override header in addition to the normalization hook.
Proxy Configuration
Command Line Options
headroom proxy \
--port 8787 \ # Port to listen on
--host 127.0.0.1 \ # Host to bind to
--mode cache \ # default; freezes prior turns for prefix-cache stability
--budget 10.00 \ # Daily budget limit in USD
--log-file headroom.jsonl # Log file pathFeature Flags
# Disable optimization (passthrough mode)
headroom proxy --no-optimize
# Disable semantic caching
headroom proxy --no-cache
# Preserve provider prefix-cache stability instead of maximizing token removal
headroom proxy --mode cache
# Enable memory and live learning
headroom proxy --memory
headroom proxy --learn --min-evidence 3Environment Variables
| Variable | Description | Default |
|---|---|---|
HEADROOM_HOST | Proxy bind host | 127.0.0.1 |
HEADROOM_PORT | Proxy bind port | 8787 |
HEADROOM_MODE | Proxy optimization mode: token or cache | cache |
HEADROOM_WORKERS | Uvicorn worker count | 1 |
HEADROOM_LIMIT_CONCURRENCY | Maximum concurrent connections before 503 | 1000 |
HEADROOM_MAX_CONNECTIONS | Maximum upstream HTTP connections | 500 |
HEADROOM_MAX_KEEPALIVE | Maximum upstream keep-alive connections | 100 |
HEADROOM_KEEPALIVE_EXPIRY | Seconds an idle upstream keep-alive connection is kept open | 90 |
HEADROOM_HTTP_PROXY | HTTP proxy URL for upstream provider requests only; HTTPS provider APIs use CONNECT | -- |
HEADROOM_BUDGET | Daily budget limit in USD | -- |
HEADROOM_TELEMETRY | Set to on for local-only usage stats (powers your own /stats and dashboard; nothing is sent externally) | off |
HEADROOM_STATELESS | Set to true to disable filesystem writes | false |
HEADROOM_MODEL_LIMITS | Custom model config (JSON string or file path) | -- |
HEADROOM_LOSSLESS_COMPACTION | Set to 0 to disable the byte-reversible lossless folds (grep heading, log run-collapse, diff/config folds) while leaving the rest of the compression pipeline active. Read per request, so the proxy's /admin/runtime-env hot-sync applies without a restart. | 1 |
HEADROOM_BASE_URL | Base URL of the Headroom proxy (TypeScript SDK) | http://localhost:8787 |
HEADROOM_API_KEY | Optional API key for authenticated Headroom endpoints (TypeScript SDK) | -- |
HEADROOM_CONFIG_DIR | Canonical config (read-mostly) root. Derives models.json and per-plugin config paths when set. | ~/.headroom/config |
HEADROOM_WORKSPACE_DIR | Canonical workspace (read-write state) root. Derives savings, memory DB, logs, TOIN, subscription state, and more when set. | ~/.headroom |
HEADROOM_SAVINGS_PATH | Override persistent savings file location. Always wins when set. | derived from ${HEADROOM_WORKSPACE_DIR} |
HEADROOM_TOIN_PATH | Override TOIN telemetry file location. Always wins when set. | derived from ${HEADROOM_WORKSPACE_DIR} |
HEADROOM_SUBSCRIPTION_STATE_PATH | Override subscription tracker state file. Always wins when set. | derived from ${HEADROOM_WORKSPACE_DIR} |
HEADROOM_PERIODIC_TOIN_STATS | Controls periodic TOIN stats logging in long-lived proxy workers. Set to 0, false, off, or no to disable the 5-minute stats loop without disabling TOIN learning or request-time feedback. | true |
HEADROOM_MEMORY_INJECTION_MODE | Memory-context routing mode: live_zone_tail (default) or disabled. The legacy system_prompt mode was retired by PR-A2; supplying it raises. | live_zone_tail |
HEADROOM_PROXY_PYTHON_FORWARDER_MODE | Python forwarder serialization mode. byte_faithful (default) forwards original request bytes verbatim when no transform mutated the body and re-serializes canonically only when needed — keeps Anthropic prompt-cache hit-rate intact. legacy_json_kwarg is an explicit operator opt-in for emergency rollback to the historical httpx ... json=body behavior. NOT a fallback — only flip on explicit operator decision. | byte_faithful |
HEADROOM_STRIP_INTERNAL_HEADERS | Python proxy: whether to strip internal x-headroom-* request headers (e.g. x-headroom-bypass, x-headroom-mode, x-headroom-user-id, x-headroom-stack, x-headroom-base-url) before every upstream forwarder call (PR-A5, fixes P5-49). enabled (default) stops fingerprinting / leakage. disabled is an explicit operator opt-in for diagnostic shadow tracing — NOT a fallback. Inbound reads of these headers (bypass gating, memory user-id resolution) are unaffected because they read request.headers directly. | enabled |
HEADROOM_PROXY_STRIP_INTERNAL_HEADERS | Rust proxy: same policy as HEADROOM_STRIP_INTERNAL_HEADERS but for the Rust transparent proxy. Stripping happens inside build_forward_request_headers so both HTTP and WebSocket upstream calls are gated by one flag. enabled default; disabled operator opt-in for diagnostic shadow tracing. Response-side X-Headroom-* injection (e.g. x-headroom-tokens-saved) is unrelated and stays. | enabled |
HEADROOM_EMBEDDER_RUNTIME | Set to pytorch_mps to run the memory embedder via the torch sentence-transformers backend on the Apple GPU (MPS). Only engages when Apple MPS is actually available; otherwise it logs a warning and uses the existing default embedder selection path. pytorch_mps is the only accepted value. Requires the [pytorch-mps] extra. See Memory. | default embedder selection |
HEADROOM_KOMPRESS_BACKEND | Selects the Kompress (ModernBERT) inference backend, separate from HEADROOM_EMBEDDER_RUNTIME above. auto tries ONNX CPU then falls back to PyTorch; onnx/onnx_cpu and onnx_coreml force ONNX Runtime (CPU or CoreML); pytorch and pytorch_mps force PyTorch (pytorch_mps targets Apple's GPU, with mps/torch_mps accepted as aliases). | auto |
ORT_DYLIB_PATH | Path to the ONNX Runtime shared library loaded by the Rust core (magika detection, fastembed embeddings), which loads ORT dynamically on every platform. Auto-pinned at import headroom to the library inside the onnxruntime pip package (onnxruntime.dll / libonnxruntime.so* / libonnxruntime*.dylib); set it yourself to override. Without a pin, ML detection degrades to the non-ONNX tiers — and on Windows the bare DLL search can resolve to the Windows ML System32 build (1.17.x on Win11 24H2+), which deadlocks ONNX session init — see Troubleshooting. | auto-pinned |
HEADROOM_MAGIKA_INIT_TIMEOUT_SECS | Upper bound (integer seconds, > 0) on magika's one-time ONNX session init in the Rust detection chain. On timeout the init error is cached and detection uses the non-ML fallback tiers for the rest of the process; a warning is logged. Safety net for environments where the dylib pin above does not apply. | 5 |
HEADROOM_REQUEST_TIMEOUT | Request timeout in seconds | 300 |
HEADROOM_BETA_HEADER_STICKY | Controls per-session anthropic-beta / OpenAI-Beta re-echo. enabled (default): the proxy unions beta tokens across turns within a session — if the client sends a token in turn N and omits it in turn N+1, the proxy re-injects it to preserve prefix-cache stability. disabled: the client's value is forwarded verbatim with no accumulation. Any other value raises at request time. See Session Beta Header Tracking. | enabled |
HEADROOM_BETA_TRACKER_MAX_SESSIONS | LRU capacity of the in-memory session beta tracker. Once full, the oldest session entry is evicted. | 1000 |
HEADROOM_PROXY_BETA_HEADER_STICKY | Rust proxy: same per-conversation beta-token union as HEADROOM_BETA_HEADER_STICKY, applied to anthropic-beta / openai-beta on the intercepted /v1/messages, /v1/chat/completions, and /v1/responses routes. Requires the compression interceptor (HEADROOM_PROXY_COMPRESSION=1) — with it off the Rust proxy is a strict byte-pipe and this flag has no effect (startup warns). Unlike the Python tracker (keyed on model + system prompt), sessions are keyed per conversation, shared with the cache-drift detector — parallel conversations never inherit each other's tokens. enabled default; disabled forwards the client value verbatim and keeps no state. Tracker capacity is fixed at 1000 sessions. | enabled |
HEADROOM_LIVE_ZONE_DISABLE_ARMS | Rust proxy live-zone dispatcher: comma-separated content types whose compression arm is forced to a no-op, e.g. source_code,plain_text. Names are accepted in either the log-tag spelling (json_array, source_code, search, build, diff, html, text) or the natural spelling (search_results, build_output, git_diff, plain_text). Read once, on the first dispatch of the process, so a running proxy's behaviour cannot change mid-flight; unknown names are logged and ignored rather than failing the request. Shipped as the rollback switch for the SourceCode → CodeAwareCompressor and PlainText → Kompress arms, but applies to every arm. | -- |
HEADROOM_MODEL_ROUTER_ENABLED | Enable cost-aware model routing. 1/true/yes/on/enabled turns it on and requires HEADROOM_MODEL_ROUTES. See Cost-aware model routing. | off |
HEADROOM_MODEL_ROUTES | JSON array of ordered routing rules for cost-aware model routing (schema below). | -- |
HEADROOM_THINKING_COMPACT | Compact plain-text reasoning that models re-send every turn (Kimi/GLM/DeepSeek reasoning_content / inline <think>): Kompress it on warm turns, drop it on cold turns. No-op for Claude/Codex/OpenAI (encrypted reasoning). See Cold-prefix hook. | off |
HEADROOM_THINKING_COMPACT_KEEP_LAST | Most-recent assistant turns whose reasoning is left intact (the active reasoning the model still uses). Only applies with HEADROOM_THINKING_COMPACT. | 1 |
HEADROOM_COLD_RECOMPACT | On a confirmed-cold turn (idle past the real cache TTL), recompact the whole prefix — cross-turn dedupe + superseded-read drop + lossless folds — instead of forwarding a byte-identical prefix to a dead cache. Warm turns unchanged. Pair with HEADROOM_DEDUPE. See Cold-prefix hook. | off |
HEADROOM_DEDUPE | Whole-conversation verbatim cross-turn dedup in the router (cache-safe, information-preserving via retrieval markers). Superseded-read drop + lossless folds run without it; this adds verbatim dedup. | off |
HEADROOM_CACHE_TTL_LEARN | Append per-turn cache-outcome observations (provider, model, idle, hit/miss) to cache_ttl_observations.jsonl for the offline headroom-cache-ttl learner. Observation-only (no request-behavior change); respects HEADROOM_STATELESS; the log is size-bounded. | off |
HEADROOM_KOMPRESS_ENDPOINT / HEADROOM_KOMPRESS_ENDPOINT_TOKEN | Offload ML compression (Kompress) to a remote endpoint instead of the local ONNX model — used by reasoning compaction and the router when set. | -- |
HEADROOM_1M_MODEL | Fallback model that headroom wrap claude --1m targets when neither --model nor ANTHROPIC_MODEL is set. Accepts the id with or without the [1m] suffix (added when absent); an explicit --model / ANTHROPIC_MODEL always wins. See Claude 1M context window. | claude-opus-5 |
For provider-only proxying, prefer HEADROOM_HTTP_PROXY over process-wide variables such as HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, or NO_PROXY. HTTPX reads those global variables, but Headroom also passes them through to tool executions.
Cold-prefix hook & reasoning compaction
Two related optimizations target tokens that agent harnesses re-send every turn: model reasoning (re-billed as input on Kimi/GLM/DeepSeek and on Claude 4.6+), and the prefix itself once its prompt cache has lapsed. All flags below are off by default — the base proxy is unchanged until you opt in.
What to set for what:
| Goal | Set | Notes |
|---|---|---|
| Shrink Kimi/GLM/DeepSeek reasoning re-sent each turn | HEADROOM_THINKING_COMPACT=1 (+ HEADROOM_KOMPRESS_ENDPOINT for the best ratio) | Warm turns Kompress the reasoning; cold turns drop it. No-op for Claude/Codex/OpenAI (their reasoning is an encrypted handle, already cheap on resend). |
| Recompact a dead-cache prefix instead of re-sending it whole | HEADROOM_COLD_RECOMPACT=1 (+ HEADROOM_DEDUPE=1) | Fires only when the cache has genuinely lapsed. Works in both cache and token mode. |
| Learn each provider's real cache TTL | HEADROOM_CACHE_TTL_LEARN=1, then run the headroom-cache-ttl estimator periodically | Sharpens cold detection for providers that don't expose a TTL (Kimi/OpenAI). |
How cold detection knows the TTL. A wrong "cold" call would recompact a warm prefix and bust the cache, so the TTL must be right:
- Claude Code: read exactly, from the request's
cache_control.ttlplus CC's own controls —ENABLE_PROMPT_CACHING_1H(1h),FORCE_PROMPT_CACHING_5M(5m), andDISABLE_PROMPT_CACHING(+ per-modelDISABLE_PROMPT_CACHING_<FAMILY>), which turns caching off and makes every turn a free recompaction candidate. - Kimi / OpenAI / Codex: they don't expose a TTL, so detection uses a
conservative default until the learner (
HEADROOM_CACHE_TTL_LEARN+ theheadroom-cache-ttlplugin) fills in the real value from observed hits/misses.
Running the estimator. With HEADROOM_CACHE_TTL_LEARN=1 set on the proxy,
run the batch estimator periodically (cron / launchd — it is deliberately
out-of-process so it can never touch the request path):
headroom-cache-ttl # reads cache_ttl_observations.jsonl, writes cache_ttl_learned.json
headroom-cache-ttl --dry-run # print the estimate table without writingIt emits a TTL for a provider/model only when the observations bound it on
both sides — hits at some idle prove the cache was still alive (lower bound),
ttl_expiry misses at a larger idle prove it was dead (upper bound) — and it
writes the upper end of that interval: overestimating a TTL only skips a
recompaction, while underestimating one would bust a warm cache. Estimates are
never pooled across models: one model's expiry says nothing about another's
TTL, so a cross-model aggregate could close one model's interval with another's
death evidence. Keys without death-evidence beyond their last observed hit are
skipped, as is any model with no estimate of its own; both keep the
conservative static default. The proxy picks up a rewritten table by mtime; no
restart needed.
Can these be on by default?
HEADROOM_THINKING_COMPACT— stays opt-in. It rewrites model inputs (reasoning the model actively uses) and depends on Kompress, so its quality / latency trade should be a deliberate choice.HEADROOM_COLD_RECOMPACT— opt-in today; a candidate to default for Claude Code once TTL detection is field-validated. It only fires on confirmed-cold turns and recompacts losslessly, but a mis-read TTL would bust a warm cache (expensive), so it waits for confidence. For Kimi/OpenAI it should stay opt-in until the learner has data.HEADROOM_CACHE_TTL_LEARN— the safest to default on: observation-only, a size-bounded local log, and it respectsHEADROOM_STATELESS. Kept opt-in for now so nothing is written to disk unasked.
Cost-aware model routing
Complementary to content compression, Headroom can rewrite the upstream model per request to stretch quota and control spend, for example by sending small, tool-free requests to a cheaper model. Routing is opt-in and disabled by default, so behavior is unchanged unless you configure it.
Enable it with HEADROOM_MODEL_ROUTER_ENABLED=1 and declare ordered rules in HEADROOM_MODEL_ROUTES (a JSON array). The router evaluates rules top to bottom and the first rule whose conditions all match wins; every decision is logged with a reason so routing stays observable. Each rule object supports:
| Field | Type | Meaning |
|---|---|---|
to_model | string (required) | Model to route to when the rule matches. |
max_input_tokens | integer | Match only when the estimated input size is at or below this. |
min_input_tokens | integer | Match only when the estimated input size is at or above this. |
max_output_tokens | integer | Match only when the request's own max_tokens is at or below this. A request without max_tokens does not match. |
require_no_tools | boolean | Match only when the request declares no tools (a proxy for low-risk work). |
from_models | list of strings | Restrict the rule to these source models. Omit for any source model. |
name | string | Label surfaced in the decision log. |
export HEADROOM_MODEL_ROUTER_ENABLED=1
export HEADROOM_MODEL_ROUTES='[
{"name": "small-no-tools", "max_input_tokens": 4000, "require_no_tools": true,
"from_models": ["claude-sonnet-4-6"], "to_model": "claude-haiku-4-5"}
]'Notes:
- Combine
max_input_tokenswithmax_output_tokensto downshift only turns that are cheap on both sides: a tiny prompt that asks for a long answer (a largemax_tokens) stays on the original model. - Input size is a fast, tokenizer-free estimate over the messages, tools, and top-level
systemprompt, meant for tier selection rather than exact accounting. - A malformed rule fails open: it is skipped (never silently widened), and the rest of the rules still apply.
- Routing is skipped for byte-faithful passthrough requests (
x-headroom-bypass: trueorx-headroom-mode: passthrough), so those are never model-rewritten. - Routing currently applies on the Anthropic
/v1/messagespath.
Session Beta Header Tracking
When running as a proxy, Headroom maintains a per-session union of anthropic-beta (and OpenAI-Beta) tokens via SessionBetaTracker. The session key is derived from the x-headroom-session-id header if present, otherwise from md5(model + system_prompt[:500])[:16] — stable across turns of the same conversation.
Why: clients such as Claude Code and Codex CLI may drop a beta token between consecutive turns. Because anthropic-beta is part of the request bytes that determine the upstream prefix-cache key, a dropped token would bust the cache mid-conversation. The tracker re-injects any token seen earlier in the session so the cache key stays stable.
Trade-off: once the proxy has seen a beta token in a session it will continue re-sending it for the rest of that session, even if the client stops including it. Stopping the token on the client side alone is not sufficient — the proxy re-injects it. Set HEADROOM_BETA_HEADER_STICKY=disabled to pass the client's anthropic-beta value verbatim and bypass this accumulation.
# Disable sticky beta re-echo
export HEADROOM_BETA_HEADER_STICKY=disabled
headroom proxy ...Note: disabling sticky mode may reduce prefix-cache hit rates for clients that legitimately drop-and-re-add beta tokens across turns.
Filesystem Contract
Headroom resolves every on-disk resource through a two-root model
(HEADROOM_CONFIG_DIR + HEADROOM_WORKSPACE_DIR) with additive
precedence rules: explicit argument > per-resource env var > derived
from canonical root > default. Every legacy env var continues to work
unchanged.
See the Filesystem Contract
page for the full bucket table, plugin-author guidance, and the Docker
naming overlap note (HEADROOM_WORKSPACE is not the same as
HEADROOM_WORKSPACE_DIR).
Custom Model Configuration
Configure context limits and pricing for new or custom models:
{
"anthropic": {
"context_limits": {
"claude-4-opus-20250301": 200000,
"claude-custom-finetune": 128000
},
"pricing": {
"claude-4-opus-20250301": {
"input": 15.00,
"output": 75.00,
"cached_input": 1.50
}
}
},
"openai": {
"context_limits": {
"ft:gpt-4o:my-org": 128000,
"my-private-deployment": 200000
},
"pricing": {
"my-private-deployment": [5.00, 15.00]
}
}
}Save as ${HEADROOM_CONFIG_DIR}/models.json (defaults to
~/.headroom/config/models.json), or set HEADROOM_MODEL_LIMITS to a
JSON string or file path. Installs that still have
~/.headroom/models.json (the legacy location) continue to work.
Configure models Headroom can't already look up — fine-tunes, private deployments, gateway aliases. Public models are resolved from LiteLLM (see below), so pinning one here means maintaining a number yourself that would otherwise stay current on its own.
Where limits and prices come from
Headroom consults the installed LiteLLM model database (litellm.model_cost)
for public model metadata. That data ships with the LiteLLM package rather than
being fetched at runtime, so it is as current as your installed LiteLLM version
and refreshes when you upgrade it. Nothing here makes a network call.
Limits and prices do not use the same precedence. Both are first-match-wins:
Context limits
- Explicit configuration and the built-in table, checked together — they are merged into one mapping, so an exact match in either returns immediately, then partial/prefix matches.
- LiteLLM (
max_input_tokens) — reached only for models the previous step didn't match. - Pattern inference, then a generic default.
Pricing
- Explicit configuration — a value you configured is a decision, so it beats everything below.
- LiteLLM.
- Built-in table, then pattern inference, then a generic default.
The asymmetry is deliberate, not an oversight. For limits, a built-in entry
outranks LiteLLM because LiteLLM reports a model's maximum capability while
Headroom needs its effective default. claude-sonnet-4-20250514 is the
concrete case: LiteLLM reports 1,000,000, but that window is an opt-in beta, so
the built-in 200,000 is the safe assumption for a client that has not enabled
it. Pricing has no equivalent capability-vs-default split, so there LiteLLM
wins outright.
Limits are an input budget (max_input_tokens), not the total window: gpt-5
resolves to 272K input, not the 400K total (272K in + 128K out).
LiteLLM also resolves gateway-routed names — azure/..., bedrock/...,
vertex_ai/..., groq/... — which the built-in tables never covered.
The built-in tables additionally cover installs where LiteLLM is unavailable: the
dependency is gated python_version < '3.14'.
Pattern-Based Inference
Models that reach step 3 and match no table entry are inferred from naming patterns:
| Pattern | Inferred Settings |
|---|---|
*opus* | 200K context, Opus-tier pricing |
*sonnet* | 200K context, Sonnet-tier pricing |
*haiku* | 200K context, Haiku-tier pricing |
gpt-4o* | 128K context, GPT-4o pricing |
gpt-4.1* | ~1M context, GPT-4.1 pricing |
gpt-5* | 272K input, GPT-5 pricing |
o1*, o3*, o4* | 200K context, reasoning model pricing |
Matching prefers the most specific prefix, so gpt-4.1 is not treated as
gpt-4 (which would give it an 8K limit and legacy gpt-4 pricing).
Provider-Specific Settings
Provider classes configure token counting and context/pricing overrides, not
cache toggles -- provider-specific cache optimization (Anthropic
cache_control breakpoints, OpenAI prefix stabilization, Google
CachedContent) is controlled by enable_cache_optimizer on HeadroomClient
(see SDK Configuration above), which auto-detects the
right strategy from the provider you pass in.
from headroom import OpenAIProvider
provider = OpenAIProvider(
context_limits={"gpt-4o": 128000},
)Tool Profiles
Skip or customize compression for specific tools:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
headroom_tool_profiles={
"important_tool": {"skip_compression": True},
"search_tool": {"max_items_after_crush": 25},
},
)Configuration Precedence
Settings are applied in this order (later overrides earlier):
- Default values
- Environment variables
- SDK constructor arguments
- Per-request overrides
Validation
Validate your configuration at startup:
result = client.validate_setup()
if not result["valid"]:
print("Configuration issues:")
for check in ("provider", "storage", "config", "cache_optimizer"):
if result[check]["error"]:
print(f" - {check}: {result[check]['error']}")