Troubleshooting
Solutions for common Headroom issues including proxy startup, connection errors, no token savings, high latency, and installation problems.
Solutions for common Headroom issues.
Proxy Server Issues
Proxy will not start
Symptom: headroom proxy fails or hangs.
# Check if port is already in use
lsof -i :8787
# Try a different port
headroom proxy --port 8788
# Check for missing dependencies
pip install "headroom-ai[proxy]"
# Run with debug logging
headroom proxy --log-file ~/.headroom/logs/proxy.jsonl --log-messagesConnection refused when calling proxy
Symptom: curl: (7) Failed to connect to localhost port 8787
# Verify proxy is running
curl http://localhost:8787/health
# Check if proxy started on a different port
ps aux | grep headroomProxy returns errors for some requests
Symptom: Some requests work, others fail with 502/503.
# Check proxy logs for the actual error
headroom proxy --log-file ~/.headroom/logs/proxy.jsonl --log-messages
# Verify API key is set
echo $OPENAI_API_KEY # or ANTHROPIC_API_KEY
# Test the underlying API directly
curl https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY"Windows: ML content detection hangs or silently falls back
Symptom: On Windows 11 24H2+, every proxied request stalls (historically
Optimization failed: TimeoutError), or the first detection in each process
burns ~5 seconds and compression quality drops because detection runs on the
non-ML fallback tiers. The proxy log may show
magika ONNX session init timed out.
Cause: The Rust core loads ONNX Runtime dynamically. Without
ORT_DYLIB_PATH, the bare Windows DLL search resolves onnxruntime.dll to
C:\Windows\System32\onnxruntime.dll — the Windows ML OS component (1.17.x),
which deadlocks ort session initialization instead of returning an error.
Fix: Headroom pins ORT_DYLIB_PATH automatically at import time to the
DLL inside the onnxruntime pip package (included in headroom-ai[proxy]).
Confirm in the startup log:
Pinned ORT_DYLIB_PATH to bundled ONNX Runtime: ...\onnxruntime\capi\onnxruntime.dllIf the pin is skipped (library install without onnxruntime), either install
it or point the variable at any modern ONNX Runtime yourself:
pip install onnxruntime
# or
$env:ORT_DYLIB_PATH = "C:\path\to\onnxruntime.dll"HEADROOM_MAGIKA_INIT_TIMEOUT_SECS (default 5) bounds the init as a safety
net; on timeout detection degrades to non-ML tiers for the process lifetime.
No Token Savings
Symptom: stats['session']['tokens_saved_total'] is 0.
Diagnosis:
stats = client.get_stats()
print(f"Mode: {stats['config']['mode']}") # Should be "optimize"
print(f"SmartCrusher: {stats['transforms']['smart_crusher_enabled']}")Common causes:
- Mode is
audit(observation only, no modifications) - Messages do not contain tool outputs
- Tool outputs are below the 200-token threshold
- Data is not compressible (high uniqueness, code, grep results)
Solutions:
import { } from 'headroom-ai';
// Ensure the proxy is running in optimize mode
// (default, unless --no-optimize was passed)
const = await (messages, { : 'gpt-4o' });
.(`Saved: ${.tokensSaved} tokens`);
.(`Compressed: ${.compressed}`);# 1. Ensure mode is "optimize"
client = HeadroomClient(
original_client=OpenAI(),
provider=OpenAIProvider(),
default_mode="optimize", # NOT "audit"
)
# 2. Or override per-request
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
headroom_mode="optimize",
)
# 3. Lower the compression threshold
config = HeadroomConfig()
config.smart_crusher.min_tokens_to_crush = 100 # Default is 200Dashboard shows 0 compressed/saved tokens after upgrading to 0.31.0
Symptom: After upgrading from 0.27.0 to 0.31.0, the dashboard's compression / "Tokens Saved" figures read ~0, even though total token spend is the same or lower than before.
Cause: This is a default-mode change, not a regression. 0.31.0 ships the coding savings profile as the out-of-box default, and coding runs the proxy in cache mode (see Savings profiles). Cache mode freezes the provider prefix and compresses only the newest turn delta — this deliberately avoids busting the provider's prompt cache. So the compression figure is small, and the savings shift to cheaper prefix-cache reads (cached input tokens are billed at a fraction of list price). On a short prompt there is little delta to compress, so the compression tile can read ~0 while your actual cost still drops.
Where the savings show up: Look at the Prefix Cache Impact panel and the Compression vs Cache tile on the dashboard — these reflect cache-read savings rather than per-request compression. The headline "Tokens Saved" tile only counts compression, so in cache mode it understates the real benefit.
To get 0.27.0-style compression numbers back: run the proxy in token mode, which prioritizes visible compression:
# Per run
headroom proxy --mode token
# Or pick a token-mode profile
HEADROOM_SAVINGS_PROFILE=balanced headroom proxy # ~70% target
HEADROOM_SAVINGS_PROFILE=agent-90 headroom proxy # ~90% targetTrade-off: token mode maximizes visible compression but rewrites prior turns, which can reduce provider prefix-cache hits — so raw compression goes up while cache-read savings go down. Cache mode is the default because, for long coding sessions, preserving the prefix cache usually wins overall.
Claude Code context window is larger through the proxy
Symptom: After pointing Claude Code at Headroom (ANTHROPIC_BASE_URL), /context all
shows more tokens used than a direct session — the "System tools" and "MCP tools"
lines grow by tens of thousands of tokens, before you send any message.
Cause: Claude Code normally defers most tool schemas behind its server-side
Tool Search Tool (it sends only tool names and loads full schemas on demand).
It enables this only when it believes it is talking directly to api.anthropic.com.
The moment ANTHROPIC_BASE_URL is a custom host, Claude Code can't assume the endpoint
supports the feature, so it falls back to eagerly materializing every tool schema
into the local context window. This is a Claude Code client-side decision made before
the request reaches the proxy — no proxy header can reverse it.
Solution: set ENABLE_TOOL_SEARCH so Claude Code keeps deferring tools through the
proxy. The proxy forwards the tool_reference blocks correctly, so deferral works
end-to-end (both streaming and non-streaming, subscription and API-key auth).
# Easiest: `headroom wrap claude` sets ENABLE_TOOL_SEARCH=true automatically.
headroom wrap claude
# Choose the mode (true = always defer, the default; auto / auto:N = defer only
# when tool definitions exceed N% of the budget; false = off):
headroom wrap claude --tool-search auto
# Running `claude` manually instead of via wrap? Set it yourself:
ENABLE_TOOL_SEARCH=true ANTHROPIC_BASE_URL=http://localhost:8787 claudeVerify (before / after) with /context all in a fresh session, no messages sent:
| Section | Eager (no ENABLE_TOOL_SEARCH) | Deferred (ENABLE_TOOL_SEARCH=true) |
|---|---|---|
| System tools | fully materialized | deferred subset |
| MCP tools | every tool shows a token cost | (loaded on-demand), 0 tokens |
When deferral is off, the proxy log also prints a one-time hint naming the fix.
See issue #746 for the full analysis.
Claude Code VSCode extension caveat
Anthropic's VSCode extension webview does not currently render the deferred-tool
content blocks that ENABLE_TOOL_SEARCH=true enables through Headroom. Tool
results can show up as unsupported content type in the extension even though
the standalone claude CLI works correctly. If you use Claude Code inside
VSCode, set ENABLE_TOOL_SEARCH=false for that target and restart the Headroom
deployment. See issue #2028.
Remote Control unavailable through custom ANTHROPIC_BASE_URL
Symptom: When Claude Code runs with ANTHROPIC_BASE_URL set to a custom host (for example, Headroom), the Remote Control menu is absent.
Cause: This is a Claude-side gate. Headroom only receives normal API traffic and can still compress it, but Claude evaluates Remote Control availability before proxy traffic reaches the server.
Fix: Use Headroom for normal proxied API sessions, and launch Claude directly (without ANTHROPIC_BASE_URL) when you need Claude Remote Control.
ENABLE_TOOL_SEARCH is unaffected and can stay enabled for context-window savings while routing through Headroom.
Server-managed settings unavailable through custom ANTHROPIC_BASE_URL
Symptom: Settings pushed from Admin Settings > Claude Code > Managed settings in the claude.ai console (server-managed settings) don't apply to sessions running through Headroom, even though they apply fine without the proxy.
Cause: This is a Claude-side gate, not a Headroom limitation. Per Anthropic's docs, server-managed settings require a direct connection to api.anthropic.com; if ANTHROPIC_BASE_URL is set to any non-default host — which is exactly what wrapping via Headroom does — Claude Code skips the settings fetch entirely for that session. The request never reaches Headroom, so there is no endpoint for Headroom to implement or proxy.
This is separate from the OS-level managed-settings.json file (macOS /Library/Application Support/ClaudeCode/, Linux /etc/claude-code/, Windows C:\Program Files\ClaudeCode\): that file is read straight from local disk at startup and is unaffected by ANTHROPIC_BASE_URL or Headroom. If that file isn't taking effect, the cause is unrelated to proxying (path, permissions, or JSON syntax) — check claude --debug-file <path> and search the log for Remote settings.
Fix: None available on the Headroom side — this is an intentional Anthropic security boundary (a proxy in the path could otherwise forge org policy). If your org relies on server-managed settings, deploy the same policy as endpoint-managed settings (MDM profile, Windows registry, or a local managed-settings.json) instead, since those are read locally and unaffected by proxying.
See Server-managed settings platform availability and issue #3074.
Compression Too Aggressive
Symptom: LLM responses are missing information that was in tool outputs.
# 1. Keep more items
config = HeadroomConfig()
config.smart_crusher.max_items_after_crush = 50 # Default: 15
# 2. Skip compression for specific tools
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
headroom_tool_profiles={
"important_tool": {"skip_compression": True},
},
)
# 3. Disable SmartCrusher entirely
config.smart_crusher.enabled = FalseHigh Latency
Symptom: Requests take longer than expected.
Diagnosis:
import time
import logging
logging.basicConfig(level=logging.DEBUG)
start = time.time()
response = client.chat.completions.create(...)
print(f"Total time: {time.time() - start:.2f}s")Solutions:
# 1. Use BM25 instead of embeddings (faster)
config = HeadroomConfig()
config.smart_crusher.relevance.tier = "bm25"
# 2. Increase threshold to skip small payloads
config.smart_crusher.min_tokens_to_crush = 500
# 3. Disable transforms you don't need
config.cache_aligner.enabled = False
config.rolling_window.enabled = FalseInstallation Issues
pipx installs an older Headroom version
Symptom: PyPI shows a newer headroom-ai release, but pipx install or
pipx upgrade keeps an older version. A pinned install can also fail with
No matching distribution found.
Cause: pipx resolves packages inside its app virtual environment. If that
environment uses a Python version that Headroom does not publish wheels for yet,
pip may skip newer releases and choose the newest compatible build it can use.
Check the interpreter:
pipx listInstall with a supported Python explicitly:
pipx install --python python3.13 "headroom-ai[all]"For a pinned release:
pipx install --python python3.13 "headroom-ai[all]==0.21.4"If you already have Headroom installed under pipx, uninstall it first or
reinstall it with the supported interpreter.
pip install fails with C++ compilation error
Symptom: RuntimeError: Unsupported compiler -- at least C++11 support is needed!
# Linux / Debian-based (including Docker)
apt-get install -y build-essential && pip install headroom-ai
# macOS (Xcode command line tools)
xcode-select --install && pip install headroom-aiFor Docker, install and remove build tools in one layer:
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/*ModuleNotFoundError: No module named 'headroom'
# Check it is installed in the right environment
pip show headroom-ai
# If using virtual environment, ensure it is activated
source venv/bin/activate
# Reinstall
pip install --upgrade headroom-aiMissing optional dependency
# For proxy server
pip install "headroom-ai[proxy]"
# For embedding-based relevance scoring
pip install "headroom-ai[relevance]"
# For code compression (tree-sitter)
pip install "headroom-ai[code]"
# For everything
pip install "headroom-ai[all]"Windows: Defender blocks ast-grep-cli (sg.exe) during install
Symptom: On Windows, uv tool install "headroom-ai[all]" (or pip install) fails while installing the ast-grep-cli wheel, and Windows Defender flags sg.exe:
error: Failed to install: ast_grep_cli-0.44.1-py3-none-win_amd64.whl (ast-grep-cli==0.44.1)
Caused by: failed to open file ...\ast_grep_cli-0.44.1.data\scripts\sg.exe:
The operation did not complete successfully because the file contains a virus
or potentially unwanted software. (os error 225)
Threat: Trojan:Win64/Lazy!MTBCause: A known false positive in the upstream ast-grep-cli wheel's bundled sg.exe (ast-grep/ast-grep#2799), not a Headroom issue. ast-grep is a base dependency, so the block also affects the [proxy] extra. Headroom uses ast-grep only for optional AST-based Read-output outlining and runs normally without it — the only impact is the install-time quarantine.
Workarounds (safest first):
-
Run the proxy in Docker — no local wheel is installed, so Defender is never triggered. See Docker install; the image is
ghcr.io/headroomlabs-ai/headroom. -
Restore the file from quarantine and retry — open Windows Security → Virus & threat protection → Protection history, select the
sg.exedetection, choose Restore, then re-run the install command. This changes no persistent settings. -
Add a temporary, scoped Defender exclusion during install (last resort; requires an elevated PowerShell). Only do this if you accept excluding a known false positive, and remove the exclusion afterward:
# Scope the exclusion to uv's tools directory, install, then remove it $uvTools = (uv tool dir) Add-MpPreference -ExclusionPath $uvTools uv tool install "headroom-ai[all]" Remove-MpPreference -ExclusionPath $uvToolsDo not disable Defender wholesale — keep the exclusion narrow and temporary.
-
Report the false positive to Microsoft so a corrected signature ships for everyone: submit
sg.exeat the Microsoft Security Intelligence sample submission page.
uv build errors: "src does not appear to be a Python project"
Symptom: uv tool install "headroom-ai[all]" fails to build a dependency (commonly litellm or cryptography) with:
error: Failed to build: <package>==<version>
Caused by: `src does not appear to be a Python project, as neither `pyproject.toml`
nor `setup.py` are present`or a wheel install fails with Unknown wheel data type: .DS_Store.
Cause: Corrupted or stale entries in uv's local build/wheel cache, not a Headroom dependency-pin problem. Headroom does not pin exact versions of litellm or cryptography that would trigger this.
Fix: clear uv's cache and reinstall:
uv cache clean
uv tool install "headroom-ai[all]"To clear the cache for just one package instead:
uv cache clean litellmIf the failure is specifically for ast-grep-cli==0.44.1, that release is already excluded by Headroom's dependency pin (ast-grep-cli>=0.30.0,!=0.44.1) due to a compromised supply-chain build (ast-grep/ast-grep#2799) — uv cache clean and a plain reinstall should pick up a safe version automatically.
Provider-Specific Issues
OpenAI: Invalid API key
import os
from openai import OpenAI
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
raise ValueError("OPENAI_API_KEY not set")
client = HeadroomClient(
original_client=OpenAI(api_key=api_key),
provider=OpenAIProvider(),
)Anthropic: Authentication error
import os
from anthropic import Anthropic
api_key = os.environ.get("ANTHROPIC_API_KEY")
client = HeadroomClient(
original_client=Anthropic(api_key=api_key),
provider=AnthropicProvider(),
)Unknown model warnings
# For custom/fine-tuned models, specify context limit
client = HeadroomClient(
original_client=OpenAI(),
provider=OpenAIProvider(),
model_context_limits={
"ft:gpt-4o-2024-08-06:my-org::abc123": 128000,
"my-custom-model": 32000,
},
)ValidationError on Setup
result = client.validate_setup()
print(result)
# Common issues:
# {"provider": {"ok": False, "error": "No API key"}}
# -> Set OPENAI_API_KEY or pass api_key to OpenAI()
#
# {"storage": {"ok": False, "error": "unable to open database"}}
# -> Check path permissions, use :memory: for testing
#
# {"config": {"ok": False, "error": "Invalid mode"}}
# -> Use "audit" or "optimize" onlyFor testing, use in-memory storage:
client = HeadroomClient(
original_client=OpenAI(),
provider=OpenAIProvider(),
store_url="sqlite:///:memory:",
)Debugging Techniques
Enable Full Logging
import logging
# See everything
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(name)s %(levelname)s %(message)s",
)
# Or just Headroom logs
logging.getLogger("headroom").setLevel(logging.DEBUG)Use Simulation to Inspect Transforms
plan = client.chat.completions.simulate(
model="gpt-4o",
messages=messages,
)
print(f"Tokens: {plan.tokens_before} -> {plan.tokens_after}")
print(f"Transforms: {plan.transforms_applied}")
print(f"Waste signals: {plan.waste_signals}")
import json
print(json.dumps(plan.messages_optimized, indent=2))Test Transforms Directly
from headroom import SmartCrusher, Tokenizer
from headroom.config import SmartCrusherConfig
import json
config = SmartCrusherConfig()
crusher = SmartCrusher(config)
tokenizer = Tokenizer()
messages = [
{
"role": "tool",
"content": json.dumps({"items": list(range(100))}),
"tool_call_id": "1",
}
]
result = crusher.apply(messages, tokenizer)
print(f"Tokens: {result.tokens_before} -> {result.tokens_after}")Getting Help
- Enable debug logging and check the output
- Use
simulate()to see what transforms would apply - Run
validate_setup()for configuration issues - File an issue at github.com/headroomlabs-ai/headroom with your Headroom version, Python version, provider, debug log output, and minimal reproduction code