From c2f1fe400b0bd048fb4c3ae04a30331e93f04ef3 Mon Sep 17 00:00:00 2001 From: <> Date: Sat, 11 Apr 2026 16:05:01 +0000 Subject: [PATCH] Deployed fb953eb with MkDocs version: 1.6.1 --- getting-started/index.html | 7 +- index.html | 9 +- integration-guide/index.html | 109 +++++++------ proxy/index.html | 306 ++++++++++++++++++++--------------- quickstart/index.html | 15 +- search/search_index.json | 2 +- sitemap.xml | 62 +++---- sitemap.xml.gz | Bin 470 -> 470 bytes 8 files changed, 285 insertions(+), 225 deletions(-) diff --git a/getting-started/index.html b/getting-started/index.html index bf9da0b68..d6437870b 100644 --- a/getting-started/index.html +++ b/getting-started/index.html @@ -2068,8 +2068,11 @@
# Claude Code
ANTHROPIC_BASE_URL=http://localhost:8787 claude
-# OpenAI-compatible clients
-OPENAI_BASE_URL=http://localhost:8787/v1 your-app
+# GitHub Copilot CLI (default Anthropic-style proxy route)
+headroom wrap copilot -- --model claude-sonnet-4-20250514
+
+# OpenAI-compatible clients
+OPENAI_BASE_URL=http://localhost:8787/v1 your-app
That's it! All your requests now go through Headroom and get optimized automatically.
headroom wrap claude # Claude Code
-headroom wrap codex # OpenAI Codex CLI
-headroom wrap aider # Aider
-headroom wrap cursor # Cursor
-headroom wrap openclaw # OpenClaw plugin bootstrap
+headroom wrap copilot -- --model claude-sonnet-4-20250514
+headroom wrap codex # OpenAI Codex CLI
+headroom wrap aider # Aider
+headroom wrap cursor # Cursor
+headroom wrap openclaw # OpenClaw plugin bootstrap
Starts the proxy, points your tool at it, compresses everything automatically.
x-headroom-tokens-saved: 1234 — tokens removed
The Headroom proxy is a standalone HTTP server. Best for non-Python apps or tools that only support base URL configuration (Claude Code, Cursor).
+The Headroom proxy is a standalone HTTP server. Best for non-Python apps or tools that only support base URL configuration (Claude Code, Cursor, GitHub Copilot CLI).
# Claude Code
ANTHROPIC_BASE_URL=http://localhost:8787 claude
-# Cursor / Any OpenAI client
-OPENAI_BASE_URL=http://localhost:8787/v1 cursor
+# GitHub Copilot CLI
+headroom wrap copilot -- --model claude-sonnet-4-20250514
+
+# Cursor / Any OpenAI client
+OPENAI_BASE_URL=http://localhost:8787/v1 cursor
For translated backends, the Copilot wrapper can switch to Headroom's OpenAI-compatible route:
+ +By default, headroom wrap copilot installs rtk and appends token-optimized shell guidance to .github/copilot-instructions.md so Copilot sessions reuse the same command-saving conventions as other wrapped agent CLIs. Use --no-rtk to skip that step.
# 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 openrouter
+# 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 openrouter
See Proxy Documentation for all options.
Agno¶
Full integration with the Agno agent framework.
-from agno.agent import Agent
-from agno.models.anthropic import Claude
-from headroom.integrations.agno import HeadroomAgnoModel
-
-model = HeadroomAgnoModel(Claude(id="claude-sonnet-4-20250514"))
-agent = Agent(model=model, tools=[your_tools])
-response = agent.run("Investigate the issue")
-
-print(f"Tokens saved: {model.total_tokens_saved}")
+from agno.agent import Agent
+from agno.models.anthropic import Claude
+from headroom.integrations.agno import HeadroomAgnoModel
+
+model = HeadroomAgnoModel(Claude(id="claude-sonnet-4-20250514"))
+agent = Agent(model=model, tools=[your_tools])
+response = agent.run("Investigate the issue")
+
+print(f"Tokens saved: {model.total_tokens_saved}")
See Agno Guide for hooks, multi-provider, and streaming.
LangChain¶
Full integration with LangChain — chat models, memory, retrievers, tool wrappers, and streaming.
-from langchain_openai import ChatOpenAI
-from headroom.integrations import HeadroomChatModel
-
-llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
-response = llm.invoke("Hello!")
+from langchain_openai import ChatOpenAI
+from headroom.integrations import HeadroomChatModel
+
+llm = HeadroomChatModel(ChatOpenAI(model="gpt-4o"))
+response = llm.invoke("Hello!")
See LangChain Guide for details and known limitations.
TypeScript SDK¶
For Node.js, Next.js, and any TypeScript/JavaScript application.
-npm install headroom-ai
+
See the TypeScript SDK Guide for full documentation including Vercel AI SDK middleware, OpenAI SDK wrapper, and Anthropic SDK wrapper.
OpenClaw¶
Context compression plugin for OpenClaw agents.
-pip install "headroom-ai[proxy]"
-openclaw plugins install headroom-openclaw
+
Configure as context engine:
-
{ "plugins": { "slots": { "contextEngine": "headroom" } } }
+
The plugin auto-detects a running Headroom proxy or starts one. Compression happens in assemble() — zero changes to the agent's behavior.
See the OpenClaw plugin documentation for full setup.
Compression Hooks (Advanced)¶
Customize compression behavior without modifying Headroom's code:
-from headroom import compress, CompressionHooks, CompressContext
-
-class MyHooks(CompressionHooks):
- def pre_compress(self, messages, ctx):
- # Modify messages before compression (dedup, filter, inject)
- return messages
-
- def compute_biases(self, messages, ctx):
- # Per-message compression aggressiveness
- # >1.0 = keep more, <1.0 = compress more
- return {5: 1.5, 6: 0.5} # Keep message 5, compress message 6
-
- def post_compress(self, event):
- # Observe results (logging, analytics, learning)
- print(f"Saved {event.tokens_saved} tokens")
-
-result = compress(messages, model="gpt-4o", hooks=MyHooks())
+from headroom import compress, CompressionHooks, CompressContext
+
+class MyHooks(CompressionHooks):
+ def pre_compress(self, messages, ctx):
+ # Modify messages before compression (dedup, filter, inject)
+ return messages
+
+ def compute_biases(self, messages, ctx):
+ # Per-message compression aggressiveness
+ # >1.0 = keep more, <1.0 = compress more
+ return {5: 1.5, 6: 0.5} # Keep message 5, compress message 6
+
+ def post_compress(self, event):
+ # Observe results (logging, analytics, learning)
+ print(f"Saved {event.tokens_saved} tokens")
+
+result = compress(messages, model="gpt-4o", hooks=MyHooks())
See Architecture for how hooks integrate with the pipeline.
diff --git a/proxy/index.html b/proxy/index.html
index 23ba719c5..3e788fb24 100644
--- a/proxy/index.html
+++ b/proxy/index.html
@@ -776,6 +776,23 @@
+
+
@@ -2145,6 +2162,23 @@
+
+
@@ -2479,19 +2513,30 @@
--log-file /var/log/headroom.jsonl \
--budget 100.0
+Common agent CLI entrypoints¶
+# Claude Code
+ANTHROPIC_BASE_URL=http://localhost:8787 claude
+
+# GitHub Copilot CLI
+headroom wrap copilot -- --model claude-sonnet-4-20250514
+
+# OpenAI-compatible clients
+OPENAI_BASE_URL=http://localhost:8787/v1 your-app
+
+headroom wrap copilot uses Copilot CLI's BYOK provider settings under the hood. In provider-type=auto, it chooses Headroom's Anthropic route for the default proxy backend and the OpenAI-compatible /v1 route for translated backends such as anyllm and LiteLLM.
Anonymous aggregate telemetry is enabled by default. Opt out with HEADROOM_TELEMETRY=off or headroom proxy --no-telemetry. Downstream apps can set HEADROOM_SDK=headroom-app to override the anonymous telemetry sdk label; the default remains proxy.
Operational OTEL metrics are configured separately and are off by default. Install headroom-ai[proxy,otel] and set:
-HEADROOM_OTEL_METRICS_ENABLED=1
-HEADROOM_OTEL_METRICS_EXPORTER=otlp_http
-HEADROOM_OTEL_METRICS_ENDPOINT=http://127.0.0.1:4318/v1/metrics
-HEADROOM_OTEL_SERVICE_NAME=headroom-proxy
+HEADROOM_OTEL_METRICS_ENABLED=1
+HEADROOM_OTEL_METRICS_EXPORTER=otlp_http
+HEADROOM_OTEL_METRICS_ENDPOINT=http://127.0.0.1:4318/v1/metrics
+HEADROOM_OTEL_SERVICE_NAME=headroom-proxy
Use HEADROOM_OTEL_METRICS_EXPORTER=console for local smoke testing. HEADROOM_TELEMETRY controls the anonymous data-flywheel beacon only; it does not disable or enable OTEL export.
Langfuse can be enabled alongside this OTEL path for trace ingestion. Langfuse does not ingest OTEL metrics, so Headroom keeps metrics and Langfuse traces as complementary signals:
-HEADROOM_LANGFUSE_ENABLED=1
-LANGFUSE_PUBLIC_KEY=pk-lf-...
-LANGFUSE_SECRET_KEY=sk-lf-...
-LANGFUSE_BASE_URL=https://cloud.langfuse.com
+HEADROOM_LANGFUSE_ENABLED=1
+LANGFUSE_PUBLIC_KEY=pk-lf-...
+LANGFUSE_SECRET_KEY=sk-lf-...
+LANGFUSE_BASE_URL=https://cloud.langfuse.com
When configured, Headroom emits OTLP traces for the shared compression pipeline to Langfuse while continuing to expose metrics through /metrics and OTEL metric exporters.
Command Line Options¶
@@ -2559,8 +2604,8 @@
cache mode: prioritize provider prefix cache stability. Prior turns are frozen; only the newest turn is mutable.
Set via CLI or env:
-headroom proxy --mode token
-HEADROOM_MODE=cache headroom proxy
+
When to pick each:
@@ -2597,11 +2642,11 @@
By default, the proxy uses IntelligentContextManager which scores messages by multiple factors (recency, semantic similarity, TOIN-learned patterns, error indicators, forward references) and drops lowest-scored messages first. This is smarter than simple age-based truncation.
CCR Integration: When messages are dropped, they're stored in CCR so the LLM can retrieve them if needed. The inserted marker includes the CCR reference. Drops are also recorded to TOIN, so the system learns which message patterns are important across all users.
-# Use legacy RollingWindow (drops oldest first)
-headroom proxy --no-intelligent-context
-
-# Disable semantic scoring (faster, but less intelligent)
-headroom proxy --no-intelligent-scoring
+# Use legacy RollingWindow (drops oldest first)
+headroom proxy --no-intelligent-context
+
+# Disable semantic scoring (faster, but less intelligent)
+headroom proxy --no-intelligent-scoring
LLMLingua Options (ML Compression)¶
@@ -2631,71 +2676,72 @@
Note: LLMLingua requires additional dependencies: pip install headroom-ai[llmlingua]
-# Enable LLMLingua with GPU acceleration
-headroom proxy --llmlingua --llmlingua-device cuda
-
-# More aggressive compression (keep only 20%)
-headroom proxy --llmlingua --llmlingua-rate 0.2
-
-# Conservative compression for code (keep 50%)
-headroom proxy --llmlingua --llmlingua-rate 0.5
+# Enable LLMLingua with GPU acceleration
+headroom proxy --llmlingua --llmlingua-device cuda
+
+# More aggressive compression (keep only 20%)
+headroom proxy --llmlingua --llmlingua-rate 0.2
+
+# Conservative compression for code (keep 50%)
+headroom proxy --llmlingua --llmlingua-rate 0.5
API Endpoints¶
Liveness¶
-curl http://localhost:8787/livez
+
Response:
-
{
- "service": "headroom-proxy",
- "status": "healthy",
- "alive": true,
- "version": "0.5.21",
- "timestamp": "2026-04-10T16:36:25Z",
- "uptime_seconds": 12.483
-}
+{
+ "service": "headroom-proxy",
+ "status": "healthy",
+ "alive": true,
+ "version": "0.5.21",
+ "timestamp": "2026-04-10T16:36:25Z",
+ "uptime_seconds": 12.483
+}
Readiness¶
-curl http://localhost:8787/readyz
+
Response:
-
{
- "service": "headroom-proxy",
- "status": "healthy",
- "ready": true,
- "version": "0.5.21",
- "timestamp": "2026-04-10T16:36:25Z",
- "uptime_seconds": 12.483,
- "checks": {
- "startup": {"enabled": true, "ready": true, "status": "healthy"},
- "http_client": {"enabled": true, "ready": true, "status": "healthy"},
- "cache": {"enabled": true, "ready": true, "status": "healthy"},
- "rate_limiter": {"enabled": true, "ready": true, "status": "healthy"},
- "memory": {"enabled": false, "ready": true, "status": "disabled"}
- }
-}
+{
+ "service": "headroom-proxy",
+ "status": "healthy",
+ "ready": true,
+ "version": "0.5.21",
+ "timestamp": "2026-04-10T16:36:25Z",
+ "uptime_seconds": 12.483,
+ "checks": {
+ "startup": {"enabled": true, "ready": true, "status": "healthy"},
+ "http_client": {"enabled": true, "ready": true, "status": "healthy"},
+ "cache": {"enabled": true, "ready": true, "status": "healthy"},
+ "rate_limiter": {"enabled": true, "ready": true, "status": "healthy"},
+ "memory": {"enabled": false, "ready": true, "status": "disabled"}
+ }
+}
/readyz returns HTTP 503 when Headroom has not completed startup or a required enabled subsystem is unavailable. This is the endpoint used by the container health checks.
Aggregate Health¶
-curl http://localhost:8787/health
+
Response:
-
{
- "status": "healthy",
- "ready": true,
- "version": "0.5.21",
- "config": {
- "optimize": true,
- "cache": true,
- "rate_limit": true
- },
- "checks": {
- "startup": {"enabled": true, "ready": true, "status": "healthy"},
- "http_client": {"enabled": true, "ready": true, "status": "healthy"}
- }
-}
+{
+ "status": "healthy",
+ "ready": true,
+ "version": "0.5.21",
+ "config": {
+ "backend": "anthropic",
+ "optimize": true,
+ "cache": true,
+ "rate_limit": true
+ },
+ "checks": {
+ "startup": {"enabled": true, "ready": true, "status": "healthy"},
+ "http_client": {"enabled": true, "ready": true, "status": "healthy"}
+ }
+}
Detailed Statistics¶
-curl http://localhost:8787/stats
+
/stats remains the live/session-oriented endpoint and now also includes a
persistent_savings block with durable proxy compression lifetime totals plus a
@@ -2711,7 +2757,7 @@ observed TTL breakdowns under prefix_cache:
These are provider-reported observations, not configured TTL and not remaining
expiration time.
Historical Savings¶
-curl http://localhost:8787/stats-history
+
/stats-history exposes durable proxy compression history for dashboards and
other Headroom frontends. It returns:
@@ -2725,49 +2771,49 @@ other Headroom frontends. It returns:
Set HEADROOM_SAVINGS_PATH to override the location.
/dashboard uses this endpoint directly for its historical view, including the
daily/weekly/monthly rollups and built-in JSON / CSV export buttons.
-curl "http://localhost:8787/stats-history?format=csv&series=weekly"
-curl "http://localhost:8787/stats-history?format=csv&series=monthly"
+curl "http://localhost:8787/stats-history?format=csv&series=weekly"
+curl "http://localhost:8787/stats-history?format=csv&series=monthly"
Prometheus Metrics¶
-curl http://localhost:8787/metrics
+
/metrics remains the built-in Prometheus-formatted operational view. The proxy now also emits the same operational events through the OTEL facade when OTEL metrics are configured.
LLM APIs¶
The proxy supports both Anthropic and OpenAI API formats:
-# Anthropic format
-POST /v1/messages
-
-# OpenAI format
-POST /v1/chat/completions
+
POST /v1/compress¶
Compression-only endpoint. Compresses messages without calling any LLM. Used by the TypeScript SDK and any HTTP client that wants compression as a service.
Request:
-
{
- "messages": [...], // OpenAI chat format
- "model": "gpt-4o" // model name (for token counting)
-}
+{
+ "messages": [...], // OpenAI chat format
+ "model": "gpt-4o" // model name (for token counting)
+}
Response:
-
{
- "messages": [...], // compressed messages
- "tokens_before": 15000,
- "tokens_after": 3500,
- "tokens_saved": 11500,
- "compression_ratio": 0.23,
- "transforms_applied": ["router:smart_crusher:0.35"],
- "ccr_hashes": ["a1b2c3"]
-}
+{
+ "messages": [...], // compressed messages
+ "tokens_before": 15000,
+ "tokens_after": 3500,
+ "tokens_saved": 11500,
+ "compression_ratio": 0.23,
+ "transforms_applied": ["router:smart_crusher:0.35"],
+ "ccr_hashes": ["a1b2c3"]
+}
Headers:
- x-headroom-bypass: true — skip compression, return messages as-is
Error responses: 400 (missing fields), 401 (bad API key), 503 (compression failed)
Using with Claude Code¶
-# Start proxy
-headroom proxy --port 8787
-
-# In another terminal
-ANTHROPIC_BASE_URL=http://localhost:8787 claude
+# Start proxy
+headroom proxy --port 8787
+
+# In another terminal
+ANTHROPIC_BASE_URL=http://localhost:8787 claude
Using with Cursor¶
@@ -2775,31 +2821,31 @@ daily/weekly/monthly rollups and built-in JSON / CSV export buttons.
- In Cursor settings, set the base URL to
http://localhost:8787
Using with OpenAI SDK¶
-from openai import OpenAI
-
-client = OpenAI(
- base_url="http://localhost:8787/v1",
- api_key="your-api-key", # Still needed for upstream
-)
+from openai import OpenAI
+
+client = OpenAI(
+ base_url="http://localhost:8787/v1",
+ api_key="your-api-key", # Still needed for upstream
+)
Features¶
LLMLingua ML Compression (Opt-In)¶
When enabled, the proxy uses Microsoft's LLMLingua-2 model for ML-based token compression:
-headroom proxy --llmlingua
+
How it works:
- LLMLinguaCompressor is added to the transform pipeline (before RollingWindow)
- Automatically detects content type (JSON, code, text) and adjusts compression
- Stores original content in CCR for retrieval if needed
Startup feedback:
-# When enabled and available:
-LLMLingua: ENABLED (device=cuda, rate=0.3)
-
-# When installed but not enabled (helpful hint):
-LLMLingua: available (enable with --llmlingua for ML compression)
-
-# When enabled but not installed:
-WARNING: LLMLingua requested but not installed. Install with: pip install headroom-ai[llmlingua]
+# When enabled and available:
+LLMLingua: ENABLED (device=cuda, rate=0.3)
+
+# When installed but not enabled (helpful hint):
+LLMLingua: available (enable with --llmlingua for ML compression)
+
+# When enabled but not installed:
+WARNING: LLMLingua requested but not installed. Install with: pip install headroom-ai[llmlingua]
Why opt-in?
| Concern | Default Proxy | With LLMLingua |
@@ -2832,37 +2878,37 @@ daily/weekly/monthly rollups and built-in JSON / CSV export buttons.
Prometheus Metrics¶
Export metrics for monitoring:
-headroom_requests_total
-headroom_tokens_saved_total
-headroom_cost_usd_total
-headroom_latency_ms_sum
+headroom_requests_total
+headroom_tokens_saved_total
+headroom_cost_usd_total
+headroom_latency_ms_sum
Configuration via Environment¶
-export HEADROOM_HOST=0.0.0.0
-export HEADROOM_PORT=8787
-export HEADROOM_BUDGET=100.0
-export OPENAI_TARGET_API_URL=https://custom.openai.endpoint.com
-headroom proxy
+export HEADROOM_HOST=0.0.0.0
+export HEADROOM_PORT=8787
+export HEADROOM_BUDGET=100.0
+export OPENAI_TARGET_API_URL=https://custom.openai.endpoint.com
+headroom proxy
Running in Production¶
For production deployments:
-# Use a process manager
-pip install gunicorn
-
-# Run with gunicorn
-gunicorn headroom.proxy.server:app \
- --workers 4 \
- --bind 0.0.0.0:8787 \
- --worker-class uvicorn.workers.UvicornWorker
+# Use a process manager
+pip install gunicorn
+
+# Run with gunicorn
+gunicorn headroom.proxy.server:app \
+ --workers 4 \
+ --bind 0.0.0.0:8787 \
+ --worker-class uvicorn.workers.UvicornWorker
Or with Docker:
-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"]
+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"]
Note: build-essential is required at install time because headroom-ai includes hnswlib, a C++ extension that must be compiled from source. It is removed after installation to keep the image slim.
diff --git a/quickstart/index.html b/quickstart/index.html
index f9bb27c69..eb2cac3ae 100644
--- a/quickstart/index.html
+++ b/quickstart/index.html
@@ -2487,18 +2487,21 @@
Step 2: Verify It's Running¶
curl http://localhost:8787/health
-# Expected: {"status": "healthy", "mode": "optimize", ...}
+# Expected: {"status":"healthy","ready":true,"config":{"backend":"anthropic",...},...}
Step 3: Point Your Client¶
# Claude Code
ANTHROPIC_BASE_URL=http://localhost:8787 claude
-# Cursor / Continue / any OpenAI client
-OPENAI_BASE_URL=http://localhost:8787/v1 your-app
+# GitHub Copilot CLI (default Anthropic-style proxy route)
+headroom wrap copilot -- --model claude-sonnet-4-20250514
-# Python
-export OPENAI_BASE_URL=http://localhost:8787/v1
-python your_script.py
+# Cursor / Continue / any OpenAI client
+OPENAI_BASE_URL=http://localhost:8787/v1 your-app
+
+# Python
+export OPENAI_BASE_URL=http://localhost:8787/v1
+python your_script.py
Step 4: Check Savings¶
curl http://localhost:8787/stats
diff --git a/search/search_index.json b/search/search_index.json
index b57cb60cd..7ef2cb947 100644
--- a/search/search_index.json
+++ b/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Headroom","text":"The Context Optimization Layer for LLM Applications
Compress everything your AI agent reads. Same answers, fraction of the tokens.
87% Avg Token Reduction 100% Answer Accuracy 6 Compression Algorithms 100+ LLM Providers"},{"location":"#what-it-does","title":"What It Does","text":"Every tool call, DB query, file read, and RAG retrieval your agent makes is 70-95% boilerplate. Headroom compresses it away before it hits the model. The LLM sees less noise, responds faster, and costs less.
Your Agent / App\n \u2502\n \u2502 tool outputs, logs, DB reads, RAG results, file reads, API responses\n \u25bc\n Headroom \u2190 proxy, Python library, or framework integration\n \u2502\n \u25bc\n LLM Provider (OpenAI, Anthropic, Google, Bedrock, 100+ via LiteLLM)\n
Headroom works as a transparent proxy (zero code changes), a Python function (compress()), or a framework integration (LangChain, Agno, Strands, LiteLLM, MCP).
"},{"location":"#quick-start","title":"Quick Start","text":"Proxy (Zero Code Changes)Python SDKCoding AgentsTypeScript SDKLiteLLM Callback pip install \"headroom-ai[all]\"\nheadroom proxy\n
# Point any tool at the proxy\nANTHROPIC_BASE_URL=http://localhost:8787 claude\nOPENAI_BASE_URL=http://localhost:8787/v1 your-app\n
That's it. Your existing code works unchanged, with 40-90% fewer tokens.
from headroom import compress\n\nresult = compress(messages, model=\"claude-sonnet-4-5-20250929\")\nresponse = client.messages.create(\n model=\"claude-sonnet-4-5-20250929\",\n messages=result.messages,\n)\nprint(f\"Saved {result.tokens_saved} tokens ({result.compression_ratio:.0%})\")\n
Works with any Python LLM client. Full SDK guide \u2192
headroom wrap claude # Claude Code\nheadroom wrap codex # OpenAI Codex CLI\nheadroom wrap aider # Aider\nheadroom wrap cursor # Cursor\nheadroom wrap openclaw # OpenClaw plugin bootstrap\n
Starts the proxy, points your tool at it, compresses everything automatically.
import { compress } from 'headroom-ai';\n\nconst result = await compress(messages, { model: 'claude-sonnet-4-5-20250929' });\n// Use result.messages with any LLM client\nconsole.log(`Saved ${result.tokensSaved} tokens`);\n
Works with Vercel AI SDK, OpenAI Node SDK, and Anthropic TS SDK. Full TS guide \u2192
import litellm\nfrom headroom.integrations.litellm_callback import HeadroomCallback\n\nlitellm.callbacks = [HeadroomCallback()]\n# All 100+ providers now compressed automatically\n
"},{"location":"#framework-integrations","title":"Framework Integrations","text":"All integration patterns \u2192
"},{"location":"#langchain","title":"LangChain","text":"Wrap any chat model. Supports memory, retrievers, tools, streaming, async.
from headroom.integrations import HeadroomChatModel\n\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n
LangChain Guide \u2192
"},{"location":"#agno","title":"Agno","text":"Full agent framework integration with observability hooks.
from headroom.integrations.agno import HeadroomAgnoModel\n\nmodel = HeadroomAgnoModel(Claude(id=\"claude-sonnet-4-20250514\"))\nagent = Agent(model=model)\n
Agno Guide \u2192
"},{"location":"#strands","title":"Strands","text":"Model wrapping + tool output hook provider for Strands Agents.
from headroom.integrations.strands import HeadroomStrandsModel\n\nmodel = HeadroomStrandsModel(wrapped_model=bedrock_model)\nagent = Agent(model=model)\n
Strands Guide \u2192
"},{"location":"#mcp-tools","title":"MCP Tools","text":"Three tools for Claude Code, Cursor, or any MCP client: headroom_compress, headroom_retrieve, headroom_stats.
headroom mcp install && claude\n
MCP Guide \u2192
"},{"location":"#typescript-sdk","title":"TypeScript SDK","text":"compress(), Vercel AI SDK middleware, OpenAI and Anthropic client wrappers.
npm install headroom-ai\n
TypeScript SDK Guide \u2192
"},{"location":"#openclaw","title":"OpenClaw","text":"ContextEngine plugin for OpenClaw agents. Auto-compresses context in assemble().
headroom wrap openclaw\n
OpenClaw Plugin \u2192
"},{"location":"#how-it-works","title":"How It Works","text":"Headroom runs a three-stage pipeline on every request:
graph LR\n A[Your Prompt] --> B[CacheAligner]\n B --> C[ContentRouter]\n C --> D[IntelligentContext]\n D --> E[LLM Provider]\n\n C -->|JSON| F[SmartCrusher]\n C -->|Code| G[CodeCompressor]\n C -->|Text| H[Kompress]\n C -->|Logs| I[LogCompressor]\n\n F --> D\n G --> D\n H --> D\n I --> D
Stage 1: CacheAligner \u2014 Stabilizes message prefixes so the provider's KV cache actually hits. Claude offers a 90% read discount on cached prefixes; CacheAligner makes that work.
Stage 2: ContentRouter \u2014 Auto-detects content type (JSON, code, logs, search results, diffs, HTML, plain text) and routes each to the optimal compressor:
Content Type Compressor How It Works JSON arrays SmartCrusher Statistical analysis: keeps errors, anomalies, boundaries. No hardcoded rules. Source code CodeCompressor AST-aware (tree-sitter). Preserves function signatures, collapses bodies. Plain text Kompress ModernBERT token classification. Removes redundant tokens while preserving meaning. Build/test logs LogCompressor Keeps failures, errors, warnings. Drops passing noise. Search results SearchCompressor Ranks by relevance to user query, keeps top matches. Git diffs DiffCompressor Preserves change hunks, drops unchanged context. HTML HTMLExtractor Strips markup, extracts readable content. Stage 3: IntelligentContext \u2014 If the conversation still exceeds the model's context limit, scores each message by importance (recency, references, density) and drops the lowest-value ones.
Nothing is lost. Compressed content goes into the CCR store (Compress-Cache-Retrieve). The LLM gets a headroom_retrieve tool and can fetch full originals when it needs more detail.
Full architecture deep dive \u2192
"},{"location":"#results","title":"Results","text":"100 production log entries. One critical error buried at position 67.
Metric Baseline Headroom Input tokens 10,144 1,260 Correct answers 4/4 4/4 87.6% fewer tokens. Same answer. The FATAL error was automatically preserved \u2014 not by keyword matching, but by statistical analysis of field variance.
"},{"location":"#real-workloads","title":"Real Workloads","text":"Scenario Before After Savings Code search (100 results) 17,765 1,408 92% SRE incident debugging 65,694 5,118 92% Codebase exploration 78,502 41,254 47% GitHub issue triage 54,174 14,761 73%"},{"location":"#accuracy-benchmarks","title":"Accuracy Benchmarks","text":"Benchmark Category N Accuracy Compression GSM8K Math 100 0.870 0.000 delta TruthfulQA Factual 100 0.560 +0.030 delta SQuAD v2 QA 100 97% 19% reduction BFCL Tool/Function 100 97% 32% reduction CCR Needle Lossless 50 100% 77% reduction Full benchmark methodology \u2192 | Known limitations \u2192
"},{"location":"#key-features","title":"Key Features","text":""},{"location":"#lossless-compression-ccr","title":"Lossless Compression (CCR)","text":"Compresses aggressively, stores originals, gives the LLM a tool to retrieve full details. Nothing is thrown away. Learn more \u2192
"},{"location":"#smart-content-detection","title":"Smart Content Detection","text":"Auto-detects JSON, code, logs, text, diffs, HTML. Routes each to the best compressor. Zero configuration needed. Learn more \u2192
"},{"location":"#cache-optimization","title":"Cache Optimization","text":"Stabilizes prefixes so provider KV caches hit. Tracks frozen messages to preserve the 90% read discount. Learn more \u2192
"},{"location":"#image-compression","title":"Image Compression","text":"40-90% token reduction via trained ML router. Automatically selects resize/quality tradeoff per image. Learn more \u2192
"},{"location":"#persistent-memory","title":"Persistent Memory","text":"Hierarchical memory (user/session/agent/turn) with SQLite + HNSW backends. Survives across conversations. Learn more \u2192
"},{"location":"#failure-learning","title":"Failure Learning","text":"Reads past sessions, finds failed tool calls, correlates with what succeeded, writes learnings to CLAUDE.md. Learn more \u2192
"},{"location":"#multi-agent-context","title":"Multi-Agent Context","text":"Compress what moves between agents. Any framework.
ctx = SharedContext()\nctx.put(\"research\", big_output)\nsummary = ctx.get(\"research\") # ~80% smaller\n
Learn more \u2192"},{"location":"#metrics-observability","title":"Metrics & Observability","text":"Prometheus endpoint, per-request logging, cost tracking, budget limits, pipeline timing breakdowns. Learn more \u2192
"},{"location":"#cloud-providers","title":"Cloud Providers","text":"Works with any LLM provider out of the box:
headroom proxy # Direct Anthropic/OpenAI\nheadroom proxy --backend bedrock --region us-east-1 # AWS Bedrock\nheadroom proxy --backend vertex_ai --region us-central1 # Google Vertex AI\nheadroom proxy --backend azure # Azure OpenAI\nheadroom proxy --backend openrouter # OpenRouter (400+ models)\n
Or via LiteLLM for 100+ providers (Together, Groq, Fireworks, Ollama, vLLM, etc.).
"},{"location":"#installation","title":"Installation","text":"pip install headroom-ai # Core library (Python)\npip install \"headroom-ai[all]\" # Everything (recommended)\nnpm install headroom-ai # TypeScript / Node.js\npip install \"headroom-ai[proxy]\" # Proxy server + MCP tools\npip install \"headroom-ai[ml]\" # ML compression (Kompress, requires torch)\npip install \"headroom-ai[langchain]\" # LangChain integration\npip install \"headroom-ai[agno]\" # Agno integration\npip install \"headroom-ai[evals]\" # Evaluation framework\n
Requires Python 3.10+.
"},{"location":"#next-steps","title":"Next Steps","text":" - Quickstart \u2014 Running in 5 minutes
- Integration Guide \u2014 Every way to add Headroom to your stack
- Architecture \u2014 How the pipeline works under the hood
- Benchmarks \u2014 Accuracy and latency data
- Limitations \u2014 When compression helps and when it doesn't
Apache 2.0 \u2014 Free for commercial use. GitHub | PyPI | Discord
"},{"location":"ARCHITECTURE/","title":"Headroom SDK: A Complete Explanation","text":""},{"location":"ARCHITECTURE/#architecture-overview","title":"Architecture Overview","text":"flowchart TB\n subgraph Entry[\"Entry Points\"]\n Proxy[\"Proxy Mode<br/><i>Zero code changes</i>\"]\n SDK[\"SDK Mode<br/><i>HeadroomClient</i>\"]\n Integrations[\"Integrations<br/><i>LangChain / Agno</i>\"]\n end\n\n subgraph Pipeline[\"Transform Pipeline\"]\n direction TB\n\n CA[\"Cache Aligner<br/>\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501<br/>Extracts dynamic content<br/>(dates, UUIDs, tokens)<br/>Stable prefix for caching\"]\n\n SC[\"Smart Crusher<br/>\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501<br/>Analyzes tool outputs<br/>Keeps: first, last, errors, outliers<br/>70-95% reduction\"]\n\n CM[\"Context Manager<br/>\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501<br/>Enforces token limits<br/>Scores by recency and relevance<br/>Fits context window\"]\n\n CA --> SC --> CM\n end\n\n subgraph Cache[\"Provider Cache Optimization\"]\n direction LR\n Anthropic[\"Anthropic<br/><i>cache_control blocks</i><br/>90% savings\"]\n OpenAI[\"OpenAI<br/><i>Prefix alignment</i><br/>50% savings\"]\n Google[\"Google<br/><i>CachedContent API</i><br/>75% savings\"]\n end\n\n subgraph CCR[\"CCR: Compress-Cache-Retrieve\"]\n Store[(\"Compression<br/>Store\")]\n Tool[\"Retrieve Tool<br/><i>LLM requests original</i>\"]\n Store <--> Tool\n end\n\n LLM[\"LLM API<br/><i>OpenAI / Anthropic / Google</i>\"]\n\n Entry --> Pipeline\n Pipeline --> Cache\n Cache --> LLM\n SC -.->|\"Stores original\"| Store\n LLM -.->|\"If needed\"| Tool
"},{"location":"ARCHITECTURE/#what-problem-does-headroom-solve","title":"What Problem Does Headroom Solve?","text":"When you use AI models like GPT-4 or Claude, you pay for tokens - the pieces of text you send (input) and receive (output). The problem is:
- Tool outputs are HUGE: When an AI agent calls tools (search, database queries, APIs), the responses are often massive JSON blobs with thousands of tokens
- Most of that data is REDUNDANT: 60 metric data points showing
cpu: 45% repeated, or 50 log entries with the same error message - You're paying for waste: Every token costs money and adds latency
- Context windows fill up: Models have limits (128K tokens), and bloated tool outputs eat into your available space
Headroom creates \"headroom\" - it intelligently compresses your input tokens so you have more room (and budget) for what matters.
"},{"location":"ARCHITECTURE/#how-headroom-works-the-big-picture","title":"How Headroom Works: The Big Picture","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 YOUR APPLICATION \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 HEADROOM CLIENT \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 ANALYZE \u2502\u2192 \u2502 TRANSFORM \u2502\u2192 \u2502 CALL \u2502 \u2502\n\u2502 \u2502 (Parser) \u2502 \u2502 (Pipeline) \u2502 \u2502 (API) \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502\n\u2502 \u25bc \u25bc \u25bc \u2502\n\u2502 Count tokens Apply compressions Send to OpenAI/Claude \u2502\n\u2502 Detect waste Preserve meaning Log metrics \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 OPENAI / ANTHROPIC API \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"ARCHITECTURE/#the-core-components-in-simple-terms","title":"The Core Components (In Simple Terms)","text":""},{"location":"ARCHITECTURE/#1-headroomclient-clientpy-the-wrapper","title":"1. HeadroomClient (client.py) - The Wrapper","text":"This is what you interact with. It wraps your existing OpenAI or Anthropic client:
# Before (normal OpenAI)\nclient = OpenAI(api_key=\"...\")\nresponse = client.chat.completions.create(model=\"gpt-4o\", messages=[...])\n\n# After (with Headroom)\nbase = OpenAI(api_key=\"...\")\nclient = HeadroomClient(original_client=base, provider=OpenAIProvider())\nresponse = client.chat.completions.create(model=\"gpt-4o\", messages=[...])\n
What it does: - Intercepts your API calls - Runs messages through the transform pipeline - Calls the real API with optimized messages - Logs metrics to a database - Returns the response unchanged
Two modes: - audit: Just observe and log (no changes) - optimize: Apply transforms to reduce tokens
"},{"location":"ARCHITECTURE/#2-providers-providers-model-specific-knowledge","title":"2. Providers (providers/) - Model-Specific Knowledge","text":"Different AI providers have different rules:
class OpenAIProvider:\n # Knows GPT-4o has 128K context\n # Knows how to count tokens (tiktoken)\n # Knows pricing ($2.50 per million input tokens)\n\nclass AnthropicProvider:\n # Knows Claude has 200K context\n # Uses different tokenization (~4 chars per token)\n # Different pricing structure\n
Why this matters: Token counting is model-specific. GPT-4 uses different tokenization than Claude. Headroom needs accurate counts to know how much to compress.
"},{"location":"ARCHITECTURE/#3-parser-parserpy-understanding-your-messages","title":"3. Parser (parser.py) - Understanding Your Messages","text":"Before optimizing, Headroom needs to understand what's in your messages:
messages = [\n {\"role\": \"system\", \"content\": \"You are helpful...\"},\n {\"role\": \"user\", \"content\": \"Search for X\"},\n {\"role\": \"assistant\", \"tool_calls\": [...]},\n {\"role\": \"tool\", \"content\": \"{huge JSON}\"},\n]\n\n# Parser breaks this into \"blocks\":\nblocks = [\n Block(kind=\"system\", tokens=50, ...),\n Block(kind=\"user\", tokens=10, ...),\n Block(kind=\"tool_call\", tokens=20, ...),\n Block(kind=\"tool_result\", tokens=5000, ...), # \u2190 This is the problem!\n]\n
It also detects waste signals: - Large JSON blobs (>500 tokens) - HTML tags and comments - Base64 encoded data - Excessive whitespace
"},{"location":"ARCHITECTURE/#4-transforms-transforms-the-compression-magic","title":"4. Transforms (transforms/) - The Compression Magic","text":"This is where the real work happens. Headroom has 4 transforms that run in sequence:
"},{"location":"ARCHITECTURE/#transform-1-cache-aligner","title":"Transform 1: Cache Aligner","text":"Problem: LLM providers cache your prompts, but only if they're byte-identical. If your system prompt has today's date, every day is a cache miss.
# Before:\n\"You are helpful. Current Date: 2024-12-15\" # Changes daily = no cache\n\n# After:\n\"You are helpful.\" # Static = cacheable\n\"[Context: Current Date: 2024-12-15]\" # Dynamic part moved to end\n
How it works: 1. Find date patterns in system prompt 2. Extract them 3. Move to end of message 4. Now the PREFIX is stable \u2192 cache hits!
"},{"location":"ARCHITECTURE/#transform-2-tool-crusher-naive-disabled-by-default","title":"Transform 2: Tool Crusher (Naive) - DISABLED BY DEFAULT","text":"This was our first approach - simple but limited:
# Before: 60 items\n[{\"ts\": 1, \"cpu\": 45}, {\"ts\": 2, \"cpu\": 45}, ..., {\"ts\": 60, \"cpu\": 95}]\n\n# After: First 10 items only\n[{\"ts\": 1, \"cpu\": 45}, ..., {\"ts\": 10, \"cpu\": 45}, {\"__truncated\": 50}]\n
Problem: If the important data (CPU spike) is at position 45, it gets thrown away!
"},{"location":"ARCHITECTURE/#transform-3-smart-crusher-new-default","title":"Transform 3: Smart Crusher (NEW DEFAULT)","text":"This is the intelligent approach using statistical analysis:
# Analyzes the data first:\nanalysis = {\n \"ts\": {\"type\": \"sequential\", \"unique_ratio\": 1.0},\n \"host\": {\"type\": \"constant\", \"value\": \"prod-1\"}, # \u2190 Same everywhere!\n \"cpu\": {\"variance\": 892, \"change_points\": [45]}, # \u2190 Spike detected!\n}\n\n# Smart compression:\n{\n \"__headroom_constants\": {\"host\": \"prod-1\"}, # Factor out\n \"__headroom_summary\": \"items 0-44: cpu stable at ~45\", # Summarize boring part\n \"data\": [\n {\"ts\": 45, \"cpu\": 92}, # Keep the spike!\n {\"ts\": 46, \"cpu\": 95},\n ...\n ]\n}\n
Strategies it uses: 1. TIME_SERIES: Detect variance spikes, keep change points 2. CLUSTER: Group similar log messages, keep 1-2 per cluster 3. TOP_N: For search results, keep highest scored 4. SMART_SAMPLE: Statistical sampling with constant extraction
"},{"location":"ARCHITECTURE/#transform-4-llmlingua-compressor-optional","title":"Transform 4: LLMLingua Compressor (Optional)","text":"When to use: Maximum compression needed and latency is acceptable.
# Opt-in ML-based compression using Microsoft's LLMLingua-2\n# BERT-based token classifier trained via GPT-4 distillation\n\n# Before: Long tool output text\n\"The function processUserData takes a user object and validates all fields...\"\n\n# After: Compressed while preserving semantic meaning\n\"function processUserData validates user fields...\"\n
Key characteristics: - Uses microsoft/llmlingua-2-xlm-roberta-large-meetingbank model - Auto-detects content type (code, JSON, text) for optimal compression rates - Stores original in CCR for retrieval if needed - Adds 50-200ms latency per request - Requires ~1GB RAM when loaded
Proxy integration (opt-in):
headroom proxy --llmlingua --llmlingua-device cuda\n
"},{"location":"ARCHITECTURE/#transform-5-rolling-window","title":"Transform 5: Rolling Window","text":"Problem: Even after compression, you might exceed the model's context limit.
# Model limit: 128K tokens\n# Your messages: 150K tokens\n# Need to drop 22K tokens\n\n# Rolling Window drops OLDEST messages first:\n# - Keeps system prompt (always)\n# - Keeps last 2 turns (always)\n# - Drops old tool calls + their responses as atomic units\n
Safety rule: If we drop a tool CALL, we MUST drop its RESPONSE too (or vice versa). Otherwise the model sees orphaned data.
"},{"location":"ARCHITECTURE/#transform-6-intelligent-context-manager-advanced","title":"Transform 6: Intelligent Context Manager (Advanced)","text":"Problem: Rolling Window drops by position (oldest first), but position doesn't equal importance.
# Scenario: Error at turn 3, verbose success at turn 10\n# Rolling Window: Drops turn 3 error (oldest first)\n# Intelligent Context: Keeps turn 3 error (high TOIN error score)\n
The Solution: Multi-factor importance scoring using TOIN-learned patterns:
# Message scores (all learned, no hardcodes):\nscores = {\n \"recency\": 0.20, # Exponential decay from end\n \"semantic_similarity\": 0.20, # Embedding similarity to recent context\n \"toin_importance\": 0.25, # TOIN retrieval_rate (high = important)\n \"error_indicator\": 0.15, # TOIN field_semantics.inferred_type\n \"forward_reference\": 0.15, # Referenced by later messages\n \"token_density\": 0.05, # Unique tokens / total tokens\n}\n\n# Drops lowest-scored messages first\n# Preserves critical errors even if old\n
Key principle: No hardcoded patterns. Error detection uses TOIN's learned field_semantics.inferred_type == \"error_indicator\", not keyword matching like \"error\" or \"fail\".
TOIN + CCR Integration:
IntelligentContext is a message-level compressor \u2014 just like SmartCrusher compresses items in an array, IntelligentContext \"compresses\" messages in a conversation. This means full CCR integration:
# When messages are dropped:\n# 1. Store dropped messages in CCR for potential retrieval\nccr_ref = store.store(\n original=json.dumps(dropped_messages),\n compressed=\"[60 messages dropped]\",\n tool_name=\"intelligent_context_drop\",\n)\n\n# 2. Record drop to TOIN for cross-user learning\ntoin.record_compression(\n tool_signature=message_signature, # Pattern of roles, tools, errors\n original_count=len(dropped_messages),\n compressed_count=1, # The marker\n strategy=\"intelligent_context_drop\",\n)\n\n# 3. Insert marker with CCR reference\nmarker = f\"[Earlier context compressed: 60 messages dropped. Retrieve: {ccr_ref}]\"\n
The feedback loop: - If users retrieve dropped messages via CCR, TOIN learns those patterns are important - Future drops of similar message patterns get higher importance scores - The system gets smarter across all users, not just within one session
"},{"location":"ARCHITECTURE/#5-storage-storage-metrics-database","title":"5. Storage (storage/) - Metrics Database","text":"Every request is logged:
CREATE TABLE requests (\n id TEXT PRIMARY KEY,\n timestamp TEXT,\n model TEXT,\n mode TEXT, -- audit or optimize\n tokens_input_before INTEGER, -- Before Headroom\n tokens_input_after INTEGER, -- After Headroom\n tokens_saved INTEGER, -- The win!\n transforms_applied TEXT, -- What we did\n ...\n);\n
This lets you: - See how much you're saving - Generate reports - Track trends over time
"},{"location":"ARCHITECTURE/#the-data-flow-step-by-step","title":"The Data Flow (Step by Step)","text":"Let's trace a real request:
"},{"location":"ARCHITECTURE/#step-1-you-call-the-api","title":"Step 1: You call the API","text":"response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[\n {\"role\": \"system\", \"content\": \"You are an SRE. Date: 2024-12-15\"},\n {\"role\": \"user\", \"content\": \"Check the metrics\"},\n {\"role\": \"assistant\", \"tool_calls\": [...]},\n {\"role\": \"tool\", \"content\": \"{60 metric points...}\"}, # 5000 tokens!\n {\"role\": \"user\", \"content\": \"What's wrong?\"},\n ],\n headroom_mode=\"optimize\",\n)\n
"},{"location":"ARCHITECTURE/#step-2-headroomclient-intercepts","title":"Step 2: HeadroomClient intercepts","text":"# In client.py:\ndef _create(self, messages, ...):\n # 1. Parse messages into blocks\n blocks, breakdown, waste = parse_messages(messages, tokenizer)\n # breakdown = {\"system\": 50, \"user\": 20, \"tool_result\": 5000, ...}\n\n # 2. Count original tokens\n tokens_before = 5100\n
"},{"location":"ARCHITECTURE/#step-3-transform-pipeline-runs","title":"Step 3: Transform Pipeline runs","text":"# In pipeline.py:\ndef apply(self, messages, ...):\n # Transform 1: Cache Aligner\n # - Extracts \"Date: 2024-12-15\" from system prompt\n # - Moves to end\n\n # Transform 2: Smart Crusher\n # - Analyzes 60 metric points\n # - Detects CPU spike at point 45\n # - Compresses to 17 points (preserving spike)\n # - Factors out constant \"host\" field\n\n # Transform 3: LLMLingua (if enabled via --llmlingua)\n # - ML-based compression on remaining long text\n # - Auto-detects content type for optimal rate\n # - Stores original in CCR for retrieval\n\n # Transform 4: Rolling Window\n # - Checks if we're under limit (we are)\n # - No drops needed\n\n return TransformResult(\n messages=optimized,\n tokens_before=5100,\n tokens_after=1200, # 76% reduction!\n transforms=[\"cache_align\", \"smart_crush:1\"]\n )\n
"},{"location":"ARCHITECTURE/#step-4-call-real-api","title":"Step 4: Call real API","text":"# In client.py:\nresponse = self._original.chat.completions.create(\n model=\"gpt-4o\",\n messages=optimized_messages, # Only 1200 tokens now!\n)\n
"},{"location":"ARCHITECTURE/#step-5-log-metrics-and-return","title":"Step 5: Log metrics and return","text":"# Save to database\nmetrics = RequestMetrics(\n tokens_input_before=5100,\n tokens_input_after=1200,\n tokens_saved=3900, # 76%!\n ...\n)\nstorage.save(metrics)\n\nreturn response # Unchanged from API\n
"},{"location":"ARCHITECTURE/#the-smart-crusher-deep-dive","title":"The Smart Crusher Deep Dive","text":"This is the most sophisticated part. Here's how it analyzes data:
"},{"location":"ARCHITECTURE/#field-analysis","title":"Field Analysis","text":"def analyze_field(key, items):\n values = [item[key] for item in items]\n\n return {\n \"unique_ratio\": len(set(values)) / len(values),\n # 0.0 = all same (constant)\n # 1.0 = all different (unique IDs)\n\n \"variance\": statistics.variance(values), # For numbers\n # Low = stable\n # High = changing\n\n \"change_points\": detect_spikes(values),\n # Indices where value jumps significantly\n }\n
"},{"location":"ARCHITECTURE/#pattern-detection","title":"Pattern Detection","text":"def detect_pattern(field_stats):\n # Has timestamp + numeric variance? \u2192 TIME_SERIES\n if has_timestamp and has_numeric_variance:\n return \"time_series\"\n\n # Has message field + level field? \u2192 LOGS\n if has_message_field and has_level_field:\n return \"logs\"\n\n # Has score/rank field? \u2192 SEARCH_RESULTS\n if has_score_field:\n return \"search_results\"\n\n return \"generic\"\n
"},{"location":"ARCHITECTURE/#compression-strategy","title":"Compression Strategy","text":"def compress(items, analysis):\n if analysis.pattern == \"time_series\":\n # Keep points around change points\n # Summarize stable regions\n return time_series_compress(items, analysis.change_points)\n\n elif analysis.pattern == \"logs\":\n # Cluster similar messages\n # Keep 1-2 per cluster\n return cluster_compress(items, analysis.clusters)\n\n elif analysis.pattern == \"search_results\":\n # Sort by score\n # Keep top N\n return top_n_compress(items, analysis.score_field)\n
"},{"location":"ARCHITECTURE/#ccr-architecture-compress-cache-retrieve","title":"CCR Architecture: Compress-Cache-Retrieve","text":""},{"location":"ARCHITECTURE/#the-key-insight","title":"The Key Insight","text":"\"Prefer raw > Compaction > Summarization only when compaction no longer yields enough space. Compaction (Reversible) strips out information that is redundant because it exists in the environment\u2014if the agent needs to read the data later, it can use a tool to retrieve it.\" \u2014 Phil Schmid, Context Engineering
The problem with traditional compression: If we guess wrong about what's important, we've permanently lost data. The LLM might need something we threw away.
CCR's solution: Make compression reversible. When SmartCrusher compresses, the original data is cached. If the LLM needs more, it can retrieve instantly.
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 TOOL OUTPUT (1000 items) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 HEADROOM CCR LAYER \u2502\n\u2502 \u2502\n\u2502 1. COMPRESS: Keep 20 items (errors, anomalies, relevant) \u2502\n\u2502 2. CACHE: Store full 1000 items in fast local cache \u2502\n\u2502 3. INJECT: Add retrieval capability to LLM context \u2502\n\u2502 \u2502\n\u2502 \"20 items shown. Use /v1/retrieve?hash=xxx for more.\" \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LLM PROCESSING \u2502\n\u2502 \u2502\n\u2502 Option A: LLM solves task with 20 items \u2192 Done \u2502\n\u2502 Option B: LLM needs more \u2192 retrieves via API \u2502\n\u2502 \u2192 We fetch from cache \u2192 Return instantly \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 FEEDBACK LOOP \u2502\n\u2502 \u2502\n\u2502 Track: What did the LLM retrieve? What queries? \u2502\n\u2502 Learn: \"For this tool, keep items matching common queries\" \u2502\n\u2502 Improve: Next compression uses learned patterns \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"ARCHITECTURE/#ccr-phase-1-compression-store","title":"CCR Phase 1: Compression Store","text":"Location: headroom/cache/compression_store.py
When SmartCrusher compresses, the original content is stored for on-demand retrieval:
@dataclass\nclass CompressionEntry:\n hash: str # 16-char SHA256 for retrieval\n original_content: str # Full JSON before compression\n compressed_content: str # Compressed JSON\n original_item_count: int\n compressed_item_count: int\n tool_name: str | None # For feedback tracking\n created_at: float\n ttl: int = 300 # 5 minute default\n
Features: - Thread-safe in-memory storage - TTL-based expiration (default 5 minutes) - LRU-style eviction when capacity reached - Built-in BM25 search within cached content
Usage:
store = get_compression_store()\n\n# Store compressed content\nhash_key = store.store(\n original=original_json,\n compressed=compressed_json,\n original_item_count=1000,\n compressed_item_count=20,\n tool_name=\"search_api\",\n)\n\n# Retrieve later\nentry = store.retrieve(hash_key)\n\n# Or search within cached content\nresults = store.search(hash_key, \"user query\")\n
"},{"location":"ARCHITECTURE/#ccr-phase-2-retrieval-api","title":"CCR Phase 2: Retrieval API","text":"Endpoints:
Endpoint Method Description /v1/retrieve POST Retrieve original content by hash /v1/retrieve?query=X POST Search within cached content Retrieval Request:
{\n \"hash\": \"abc123def456...\",\n \"query\": \"find errors\" // Optional: search within\n}\n
Response (full retrieval):
{\n \"hash\": \"abc123def456...\",\n \"original_content\": \"[{...}, {...}, ...]\",\n \"original_item_count\": 1000,\n \"tool_name\": \"search_api\"\n}\n
Response (search):
{\n \"hash\": \"abc123def456...\",\n \"query\": \"find errors\",\n \"results\": [{...}, {...}, ...],\n \"count\": 15\n}\n
"},{"location":"ARCHITECTURE/#ccr-phase-3-tool-injection","title":"CCR Phase 3: Tool Injection","text":"When compression happens, Headroom injects retrieval instructions into the LLM context.
Method A: System Message Injection
## Compressed Context Available\nThe following tool outputs have been compressed. If you need more detail,\ncall the retrieve_compressed tool with the hash.\n\nAvailable: hash=abc123 (1000\u219220 items from search_api)\n
Method B: MCP Tool Registration (Hybrid) When running as MCP server, Headroom exposes retrieval as a tool:
{\n \"name\": \"headroom_retrieve\",\n \"description\": \"Retrieve more items from compressed tool output\",\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"hash\": {\"type\": \"string\"},\n \"query\": {\"type\": \"string\"}\n }\n }\n}\n
Marker Injection: Compressed content includes retrieval markers:
{\n \"__headroom_compressed\": true,\n \"__headroom_hash\": \"abc123def456\",\n \"__headroom_stats\": {\n \"original_items\": 1000,\n \"kept_items\": 20,\n \"errors_preserved\": 5\n },\n \"data\": [...]\n}\n
"},{"location":"ARCHITECTURE/#ccr-phase-4-feedback-loop","title":"CCR Phase 4: Feedback Loop","text":"Location: headroom/cache/compression_feedback.py
The feedback system learns from retrieval patterns to improve future compression.
Tracked Patterns per Tool:
@dataclass\nclass ToolPattern:\n tool_name: str\n total_compressions: int # Times we compressed this tool\n total_retrievals: int # Times LLM asked for more\n full_retrievals: int # Retrieved everything\n search_retrievals: int # Used search query\n common_queries: dict[str, int] # Query frequency\n queried_fields: dict[str, int] # Fields mentioned in queries\n
Key Metrics: - Retrieval Rate: total_retrievals / total_compressions - High (>50%) \u2192 Compressing too aggressively - Low (<20%) \u2192 Compression is effective - Full Retrieval Rate: full_retrievals / total_retrievals - High (>80%) \u2192 Data is unique, consider skipping compression
Compression Hints:
@dataclass\nclass CompressionHints:\n max_items: int = 15 # Target item count\n suggested_items: int | None # Calculated optimal\n skip_compression: bool # Don't compress at all\n preserve_fields: list[str] # Always keep these fields\n aggressiveness: float # 0.0 = aggressive, 1.0 = conservative\n reason: str # Explanation\n
Feedback-Driven Adjustment:
# In SmartCrusher._crush_array()\nif self.config.use_feedback_hints and tool_name:\n feedback = get_compression_feedback()\n hints = feedback.get_compression_hints(tool_name)\n\n if hints.skip_compression:\n return items, f\"skip:feedback({hints.reason})\", None\n\n if hints.suggested_items is not None:\n self.config.max_items_after_crush = hints.suggested_items\n
Feedback Endpoints:
Endpoint Method Description /v1/feedback GET Get all learned patterns /v1/feedback/{tool_name} GET Get hints for specific tool Example Response:
{\n \"total_compressions\": 150,\n \"total_retrievals\": 23,\n \"global_retrieval_rate\": 0.15,\n \"tools_tracked\": 5,\n \"tool_patterns\": {\n \"search_api\": {\n \"compressions\": 50,\n \"retrievals\": 5,\n \"retrieval_rate\": 0.10,\n \"full_rate\": 0.20,\n \"search_rate\": 0.80,\n \"common_queries\": [\"status:error\", \"level:critical\"],\n \"queried_fields\": [\"status\", \"level\", \"message\"]\n }\n }\n}\n
"},{"location":"ARCHITECTURE/#ccr-phase-5-response-handler-automatic-tool-call-handling","title":"CCR Phase 5: Response Handler (Automatic Tool Call Handling)","text":"Location: headroom/ccr/response_handler.py
The Problem: When the proxy injects the headroom_retrieve tool, the LLM might call it. But who handles that tool call? Without response handling, the tool call would go back to the client unhandled.
The Solution: The Response Handler intercepts LLM responses, detects CCR tool calls, executes retrievals automatically, and continues the conversation until the LLM produces a final response.
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 RESPONSE HANDLER FLOW \u2502\n\u2502 \u2502\n\u2502 1. LLM Response arrives \u2502\n\u2502 \u2514\u2500 Contains: tool_use(headroom_retrieve, hash=abc123) \u2502\n\u2502 \u2502\n\u2502 2. Handler detects CCR tool call \u2502\n\u2502 \u2514\u2500 Extracts hash and optional query \u2502\n\u2502 \u2502\n\u2502 3. Handler executes retrieval \u2502\n\u2502 \u2514\u2500 Full retrieval: store.retrieve(hash) \u2502\n\u2502 \u2514\u2500 Search: store.search(hash, query) \u2502\n\u2502 \u2502\n\u2502 4. Handler continues conversation \u2502\n\u2502 \u2514\u2500 Adds tool result to messages \u2502\n\u2502 \u2514\u2500 Makes another API call \u2502\n\u2502 \u2502\n\u2502 5. Repeat until no CCR tool calls \u2502\n\u2502 \u2514\u2500 Max 3 rounds (configurable) \u2502\n\u2502 \u2502\n\u2502 6. Return final response to client \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Key Classes:
@dataclass\nclass CCRToolCall:\n tool_call_id: str # For matching response\n hash_key: str # CCR hash to retrieve\n query: str | None # Optional search query\n\n@dataclass\nclass CCRToolResult:\n tool_call_id: str\n content: str # Retrieved data as JSON\n success: bool\n items_retrieved: int\n was_search: bool # True if search, False if full retrieval\n\nclass CCRResponseHandler:\n async def handle_response(\n self,\n response: dict, # Initial LLM response\n messages: list, # Conversation history\n tools: list, # Tool definitions\n api_call_fn: Callable, # Function to make API calls\n provider: str, # \"anthropic\" or \"openai\"\n ) -> dict:\n \"\"\"Handle CCR tool calls until final response.\"\"\"\n
Streaming Support:
The handler also supports streaming responses via StreamingCCRHandler:
class StreamingCCRBuffer:\n \"\"\"Buffers streaming chunks to detect CCR tool calls.\"\"\"\n chunks: list[bytes]\n detected_ccr: bool\n\nclass StreamingCCRHandler:\n \"\"\"Handles CCR in streaming responses.\"\"\"\n async def process_stream(self, stream, messages, tools, api_call_fn):\n \"\"\"Yields chunks, switching to buffered mode if CCR detected.\"\"\"\n
"},{"location":"ARCHITECTURE/#ccr-phase-6-context-tracker-multi-turn-awareness","title":"CCR Phase 6: Context Tracker (Multi-Turn Awareness)","text":"Location: headroom/ccr/context_tracker.py
The Problem: In multi-turn conversations, earlier compressed data might become relevant later. Without tracking, the LLM has \"context amnesia\" - it can't reference data that was compressed in turn 1 when answering a question in turn 5.
The Solution: The Context Tracker maintains awareness of all compressed content across the conversation and can proactively expand relevant data when a new query might need it.
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CONTEXT TRACKER FLOW \u2502\n\u2502 \u2502\n\u2502 Turn 1: Search returns 100 files \u2192 compressed to 10 \u2502\n\u2502 Tracker stores: hash=abc123, sample=\"auth.py, db.py\" \u2502\n\u2502 \u2502\n\u2502 Turn 5: User asks \"What about the authentication middleware?\" \u2502\n\u2502 Tracker analyzes query: \u2502\n\u2502 - \"authentication\" matches \"auth.py\" in sample \u2502\n\u2502 - Relevance score: 0.7 (above threshold) \u2502\n\u2502 \u2502\n\u2502 Proactive Expansion: \u2502\n\u2502 - Retrieves abc123 before LLM responds \u2502\n\u2502 - Adds expanded context to request \u2502\n\u2502 \u2502\n\u2502 Result: LLM sees full file list, can answer accurately \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Key Classes:
@dataclass\nclass CompressedContext:\n hash_key: str # CCR hash\n turn_number: int # When compression happened\n timestamp: float # For age-based filtering\n tool_name: str | None # Which tool was compressed\n original_item_count: int\n compressed_item_count: int\n query_context: str # User query at compression time\n sample_content: str # Preview for relevance matching\n\n@dataclass\nclass ExpansionRecommendation:\n hash_key: str\n reason: str # Human-readable explanation\n relevance_score: float # 0-1, higher = more relevant\n expand_full: bool # True = full retrieval\n search_query: str | None # If expand_full=False\n\nclass ContextTracker:\n def track_compression(self, hash_key, turn_number, ...):\n \"\"\"Track a compression event.\"\"\"\n\n def analyze_query(self, query: str) -> list[ExpansionRecommendation]:\n \"\"\"Find relevant compressed contexts for a query.\"\"\"\n\n def execute_expansions(self, recommendations) -> list[dict]:\n \"\"\"Execute recommended expansions.\"\"\"\n
Relevance Calculation:
The tracker uses simple but effective heuristics:
- Keyword overlap with sample content - Extract keywords from query, match against compressed content preview
- Keyword overlap with original query - Match against the query that triggered compression
- Tool name relevance - File operations more likely to need expansion for \"file\", \"where\", \"find\" queries
- Age discount - Older contexts get lower scores
Configuration:
@dataclass\nclass ContextTrackerConfig:\n enabled: bool = True\n max_tracked_contexts: int = 100 # LRU eviction\n relevance_threshold: float = 0.3 # Min score to recommend\n max_context_age_seconds: float = 300 # 5 minutes\n proactive_expansion: bool = True\n max_proactive_expansions: int = 2 # Per query\n
"},{"location":"ARCHITECTURE/#why-ccr-is-a-moat","title":"Why CCR is a Moat","text":" - Reversible: No permanent information loss. Worst case = retrieve everything.
- Transparent: LLM knows it can ask for more data.
- Automatic: Response Handler executes retrievals without client intervention.
- Context-Aware: Context Tracker prevents multi-turn amnesia.
- Feedback Loop: Learn from actual needs, not guesses.
- Network Effect: Retrieval patterns across users improve compression for everyone.
- Zero-Risk: If compression fails, instant fallback to original data.
"},{"location":"ARCHITECTURE/#image-compression-architecture","title":"Image Compression Architecture","text":"Vision models charge by the token, and images are expensive (765-2900 tokens for a typical image). Headroom's image compression uses a trained ML router to automatically select the optimal compression technique.
"},{"location":"ARCHITECTURE/#the-key-insight_1","title":"The Key Insight","text":"Not all image queries need full resolution: - \"What is this?\" \u2192 Low detail is fine (87% savings) - \"Count the whiskers\" \u2192 Need full detail (0% savings) - \"Read the sign\" \u2192 Could convert to text (99% savings)
"},{"location":"ARCHITECTURE/#how-it-works","title":"How It Works","text":"User: [image] + \"What animal is this?\"\n \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 1. Query Analysis \u2502\n\u2502 TrainedRouter (MiniLM) \u2502\n\u2502 Classifies \u2192 full_low \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 2. Image Analysis (Optional) \u2502\n\u2502 SigLIP checks: \u2502\n\u2502 - Has text? Is complex? \u2502\n\u2502 - Fine details needed? \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 3. Apply Compression \u2502\n\u2502 OpenAI: detail=\"low\" \u2502\n\u2502 Anthropic: Resize to 512px \u2502\n\u2502 Google: Resize to 768px \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2193\nCompressed request \u2192 LLM \u2192 Response\n
"},{"location":"ARCHITECTURE/#the-trained-router","title":"The Trained Router","text":"A fine-tuned MiniLM classifier hosted on HuggingFace:
- Model:
chopratejas/technique-router - Size: ~128MB (downloaded once, cached)
- Accuracy: 93.7% on 1,157 training examples
- Latency: ~10ms CPU, ~2ms GPU
The router learns from examples like: | Query | Technique | |-------|-----------| | \"What is this?\" | full_low | | \"Count the items\" | preserve | | \"Read the text\" | transcode | | \"What's in the corner?\" | crop |
"},{"location":"ARCHITECTURE/#provider-specific-compression","title":"Provider-Specific Compression","text":"Each provider handles images differently:
Provider Method Savings OpenAI detail=\"low\" parameter ~87% Anthropic PIL resize to 512px ~75% Google PIL resize to 768px (tile-optimized) ~75%"},{"location":"ARCHITECTURE/#integration-points","title":"Integration Points","text":"Image compression runs in the proxy before text compression:
Request arrives\n \u2193\n[Image Compression] \u2190 NEW\n \u2193\n[Transform Pipeline: Cache Aligner \u2192 Smart Crusher \u2192 ...]\n \u2193\nForward to LLM\n
This ensures images are compressed first, then text compression (CCR, SmartCrusher) handles the rest.
"},{"location":"ARCHITECTURE/#code-location","title":"Code Location","text":"headroom/\n\u251c\u2500\u2500 image/\n\u2502 \u251c\u2500\u2500 __init__.py # Public API\n\u2502 \u251c\u2500\u2500 compressor.py # ImageCompressor class\n\u2502 \u2514\u2500\u2500 trained_router.py # TrainedRouter (HuggingFace model)\n\u251c\u2500\u2500 proxy/\n\u2502 \u2514\u2500\u2500 server.py # Integration point\n
"},{"location":"ARCHITECTURE/#file-structure-explained","title":"File Structure Explained","text":"headroom/\n\u251c\u2500\u2500 __init__.py # Public exports\n\u251c\u2500\u2500 client.py # HeadroomClient - the main wrapper\n\u251c\u2500\u2500 config.py # All configuration dataclasses\n\u251c\u2500\u2500 parser.py # Message \u2192 Block decomposition\n\u251c\u2500\u2500 tokenizer.py # Token counting abstraction\n\u251c\u2500\u2500 utils.py # Hashing, markers, helpers\n\u2502\n\u251c\u2500\u2500 providers/\n\u2502 \u251c\u2500\u2500 base.py # Provider/TokenCounter protocols\n\u2502 \u251c\u2500\u2500 openai.py # OpenAI-specific (tiktoken)\n\u2502 \u2514\u2500\u2500 anthropic.py # Anthropic-specific\n\u2502\n\u251c\u2500\u2500 transforms/\n\u2502 \u251c\u2500\u2500 base.py # Transform protocol\n\u2502 \u251c\u2500\u2500 pipeline.py # Orchestrates all transforms\n\u2502 \u251c\u2500\u2500 cache_aligner.py # Date extraction for caching\n\u2502 \u251c\u2500\u2500 tool_crusher.py # Naive compression (disabled)\n\u2502 \u251c\u2500\u2500 smart_crusher.py # Statistical compression (default)\n\u2502 \u251c\u2500\u2500 rolling_window.py # Token limit enforcement (position-based)\n\u2502 \u251c\u2500\u2500 intelligent_context.py # Semantic context management (score-based)\n\u2502 \u251c\u2500\u2500 scoring.py # Message importance scoring\n\u2502 \u2514\u2500\u2500 llmlingua_compressor.py # ML-based compression (opt-in)\n\u2502\n\u251c\u2500\u2500 cache/ # CCR Architecture - Caching & Storage\n\u2502 \u251c\u2500\u2500 compression_store.py # Phase 1: Store original content\n\u2502 \u251c\u2500\u2500 compression_feedback.py # Phase 4: Learn from retrievals\n\u2502 \u251c\u2500\u2500 anthropic.py # Anthropic cache optimizer\n\u2502 \u251c\u2500\u2500 openai.py # OpenAI cache optimizer\n\u2502 \u251c\u2500\u2500 google.py # Google cache optimizer\n\u2502 \u2514\u2500\u2500 dynamic_detector.py # Dynamic content detection\n\u2502\n\u251c\u2500\u2500 ccr/ # CCR Architecture - Tool Injection & Response Handling\n\u2502 \u251c\u2500\u2500 __init__.py # CCR module exports\n\u2502 \u251c\u2500\u2500 tool_injection.py # Phase 3: Inject retrieval tool\n\u2502 \u251c\u2500\u2500 response_handler.py # Phase 5: Handle CCR tool calls\n\u2502 \u251c\u2500\u2500 context_tracker.py # Phase 6: Multi-turn context tracking\n\u2502 \u2514\u2500\u2500 mcp_server.py # MCP server for retrieval tool\n\u2502\n\u251c\u2500\u2500 relevance/ # Relevance scoring for compression\n\u2502 \u251c\u2500\u2500 bm25.py # BM25 keyword scorer\n\u2502 \u251c\u2500\u2500 embedding.py # Semantic embedding scorer\n\u2502 \u2514\u2500\u2500 hybrid.py # Adaptive fusion scorer\n\u2502\n\u251c\u2500\u2500 storage/\n\u2502 \u251c\u2500\u2500 base.py # Storage protocol\n\u2502 \u251c\u2500\u2500 sqlite.py # SQLite implementation\n\u2502 \u2514\u2500\u2500 jsonl.py # JSON Lines implementation\n\u2502\n\u251c\u2500\u2500 proxy/\n\u2502 \u2514\u2500\u2500 server.py # Production HTTP proxy (CCR endpoints)\n\u2502\n\u2514\u2500\u2500 reporting/\n \u2514\u2500\u2500 generator.py # HTML report generation\n
"},{"location":"ARCHITECTURE/#key-design-decisions","title":"Key Design Decisions","text":""},{"location":"ARCHITECTURE/#1-provider-agnostic","title":"1. Provider-Agnostic","text":"Works with ANY OpenAI-compatible API: - OpenAI - Azure OpenAI - Anthropic - Groq - Together - Local models (Ollama)
"},{"location":"ARCHITECTURE/#2-deterministic-transforms","title":"2. Deterministic Transforms","text":"No LLM calls for compression. Everything is: - Statistical analysis - Pattern matching - Rule-based
This means: - Predictable results - Fast (<10ms overhead) - No added API costs
"},{"location":"ARCHITECTURE/#3-safety-first","title":"3. Safety First","text":" - Never modify user/assistant TEXT content
- Tool call + response are atomic (drop both or neither)
- Parse failures = no-op (return unchanged)
- Audit mode for testing before optimizing
"},{"location":"ARCHITECTURE/#4-smart-by-default","title":"4. Smart by Default","text":" - SmartCrusher enabled (statistical analysis)
- ToolCrusher disabled (naive rules)
- Conservative settings that preserve important data
"},{"location":"ARCHITECTURE/#what-makes-this-different","title":"What Makes This Different?","text":""},{"location":"ARCHITECTURE/#vs-summarization-llm-based-compression","title":"vs. Summarization (LLM-based compression)","text":"Headroom Summarization Deterministic Non-deterministic ~10ms overhead ~2-5 seconds overhead No extra API cost Costs money to summarize Preserves structure Loses structure Can't hallucinate Can hallucinate"},{"location":"ARCHITECTURE/#vs-simple-truncation","title":"vs. Simple Truncation","text":"Headroom Truncation Keeps important data Loses end of data Statistical analysis No analysis Detects spikes Misses spikes Factors out constants Keeps redundancy"},{"location":"ARCHITECTURE/#the-numbers-from-our-tests","title":"The Numbers (From Our Tests)","text":"Real-world SRE incident investigation: - 5 tool calls: Metrics, logs, status, deployments, runbook - Original: 22,048 tokens - After SmartCrusher: 2,190 tokens - Reduction: 90% - Quality Score: 5.0/5 (no information loss)
The model could still: - Identify the CPU spike (preserved by change point detection) - Reference specific error rates (kept in compressed data) - Provide correct remediation commands
"},{"location":"ARCHITECTURE/#summary","title":"Summary","text":"Headroom is a Context Budget Controller that:
- Wraps your existing LLM client
- Analyzes your messages to find waste
- Compresses tool outputs intelligently (not blindly)
- Preserves important information (spikes, anomalies, unique data)
- Logs everything for observability
- Saves 70-90% of tokens on tool-heavy workloads
The key insight: Most tool output redundancy is statistical (repeated values, constant fields, similar messages). By analyzing the data first, we can compress intelligently without losing the information that matters.
"},{"location":"LATENCY_BENCHMARKS/","title":"Headroom Latency Benchmarks","text":"Measured compression overhead across content types and sizes to answer: does the token savings outweigh the processing time?
Generated: 2026-02-24 01:11 UTC
"},{"location":"LATENCY_BENCHMARKS/#environment","title":"Environment","text":" - Platform: macOS-26.1-arm64-arm-64bit
- Processor: arm
- Python: 3.11.11
- Headroom: v0.3.7
Note: These benchmarks were captured on v0.3.7. Since then, v0.5.6 added parallel message compression, eliminated redundant token counting, and optimized hot-path hashing. Expect lower latency on current versions. Re-benchmarking is planned.
"},{"location":"LATENCY_BENCHMARKS/#tldr","title":"TL;DR","text":" - Average compression: 93% token reduction
- Maximum compression overhead: 12213ms (p50)
- Net latency win: 11/12 scenarios against Claude Sonnet 4.5
"},{"location":"LATENCY_BENCHMARKS/#compression-overhead-by-scenario","title":"Compression Overhead by Scenario","text":"Scenario Tokens In Tokens Out Saved Ratio p50 (ms) p95 (ms) Mean (ms) JSON: Search Results (100 items) 10.2K 1.5K 8.7K 86% 189 231 196 JSON: Search Results (500 items) 50.2K 1.5K 48.7K 97% 943 955 943 JSON: Search Results (1K items) 100.5K 1.5K 99.0K 99% 2012 2198 2032 JSON: Search Results (5K items) 502.6K 1.5K 501.2K 100% 12213 12804 12223 JSON: API Responses (500 items) 38.9K 1.1K 37.8K 97% 743 776 744 JSON: Database Rows (1K rows) 43.7K 605 43.1K 99% 961 1104 986 JSON: String Array (100 strings) 1.1K 231 820 78% 15.0 15.4 15.0 JSON: String Array (500 strings) 4.9K 233 4.6K 95% 71.9 80.3 72.7 JSON: String Array (1K strings) 9.6K 242 9.4K 97% 146 160 147 JSON: Number Array (200 numbers) 1.2K 192 1.1K 85% 30.9 61.9 33.8 JSON: Number Array (1K numbers) 6.1K 243 5.8K 96% 301 307 300 JSON: Mixed Array (250 items) 2.3K 368 1.9K 84% 38.4 39.8 38.4"},{"location":"LATENCY_BENCHMARKS/#per-transform-latency-breakdown","title":"Per-Transform Latency Breakdown","text":"Scenario Transform p50 (ms) % of Total JSON: Search Results (100 items) cache_aligner 2.2 1% JSON: Search Results (100 items) content_router 186 98% JSON: Search Results (100 items) rolling_window <0.01 0% JSON: Search Results (500 items) cache_aligner 10.7 1% JSON: Search Results (500 items) content_router 927 98% JSON: Search Results (500 items) rolling_window <0.01 0% JSON: Search Results (1K items) cache_aligner 21.0 1% JSON: Search Results (1K items) content_router 1980 98% JSON: Search Results (1K items) rolling_window <0.01 0% JSON: Search Results (5K items) cache_aligner 105 1% JSON: Search Results (5K items) content_router 11985 98% JSON: Search Results (5K items) rolling_window <0.01 0% JSON: API Responses (500 items) cache_aligner 8.8 1% JSON: API Responses (500 items) content_router 729 98% JSON: API Responses (500 items) rolling_window <0.01 0% JSON: Database Rows (1K rows) cache_aligner 9.3 1% JSON: Database Rows (1K rows) content_router 946 99% JSON: Database Rows (1K rows) rolling_window <0.01 0% JSON: String Array (100 strings) cache_aligner 0.27 2% JSON: String Array (100 strings) content_router 14.5 97% JSON: String Array (100 strings) rolling_window <0.01 0% JSON: String Array (500 strings) cache_aligner 0.95 1% JSON: String Array (500 strings) content_router 70.2 98% JSON: String Array (500 strings) rolling_window <0.01 0% JSON: String Array (1K strings) cache_aligner 1.9 1% JSON: String Array (1K strings) content_router 143 98% JSON: String Array (1K strings) rolling_window <0.01 0% JSON: Number Array (200 numbers) cache_aligner 0.66 2% JSON: Number Array (200 numbers) content_router 29.6 96% JSON: Number Array (200 numbers) rolling_window <0.01 0% JSON: Number Array (1K numbers) cache_aligner 2.5 1% JSON: Number Array (1K numbers) content_router 297 99% JSON: Number Array (1K numbers) rolling_window <0.01 0% JSON: Mixed Array (250 items) cache_aligner 0.58 1% JSON: Mixed Array (250 items) content_router 37.4 97% JSON: Mixed Array (250 items) rolling_window <0.01 0%"},{"location":"LATENCY_BENCHMARKS/#cost-benefit-analysis","title":"Cost-Benefit Analysis","text":"Net latency benefit = LLM time saved from fewer tokens - compression overhead.
Scenario Compress (ms) LLM Saved (ms)* Net Benefit $/1K Requests** JSON: Search Results (100 items) 189 261 +71.8ms $26.13 JSON: Search Results (500 items) 943 1461 +517.5ms $146.06 JSON: Search Results (1K items) 2012 2969 +956.9ms $296.91 JSON: Search Results (5K items) 12213 15035 +2822.2ms $1503.53 JSON: API Responses (500 items) 743 1134 +390.7ms $113.38 JSON: Database Rows (1K rows) 961 1292 +330.7ms $129.16 JSON: String Array (100 strings) 15.0 24.6 +9.6ms $2.46 JSON: String Array (500 strings) 71.9 139 +67.1ms $13.90 JSON: String Array (1K strings) 146 282 +135.9ms $28.16 JSON: Number Array (200 numbers) 30.9 31.6 +0.7ms $3.16 JSON: Number Array (1K numbers) 301 175 -126.3ms $17.45 JSON: Mixed Array (250 items) 38.4 56.6 +18.2ms $5.66 * LLM time saved based on Claude Sonnet 4.5 prefill rate (0.03ms/token) ** Cost savings at $3.0/MTok input pricing
"},{"location":"LATENCY_BENCHMARKS/#break-even-across-models","title":"Break-Even Across Models","text":"Compression overhead (p50) vs. LLM time saved for different model speed tiers:
Scenario Compress (ms) GPT-4o Mini GPT-4o Claude Sonnet 4.5 Claude Opus 4 JSON: Search Results (100 items) 189 -102ms +71.8ms +71.8ms +507ms JSON: Search Results (500 items) 943 -456ms +518ms +518ms +2952ms JSON: Search Results (1K items) 2012 -1022ms +957ms +957ms +5905ms JSON: Search Results (5K items) 12213 -7201ms +2822ms +2822ms +27881ms JSON: API Responses (500 items) 743 -365ms +391ms +391ms +2280ms JSON: Database Rows (1K rows) 961 -530ms +331ms +331ms +2483ms JSON: String Array (100 strings) 15.0 -6.8ms +9.6ms +9.6ms +50.6ms JSON: String Array (500 strings) 71.9 -25.6ms +67.1ms +67.1ms +299ms JSON: String Array (1K strings) 146 -51.9ms +136ms +136ms +605ms JSON: Number Array (200 numbers) 30.9 -20.4ms +0.68ms +0.68ms +53.3ms JSON: Number Array (1K numbers) 301 -243ms -126ms -126ms +165ms JSON: Mixed Array (250 items) 38.4 -19.5ms +18.2ms +18.2ms +113ms"},{"location":"LATENCY_BENCHMARKS/#key-takeaways","title":"Key Takeaways","text":" - Compression pays for itself in latency for 11/12 compressing scenarios (json). For these, the LLM prefill time saved exceeds compression overhead.
- ContentRouter is 98% of pipeline cost on average \u2014 it does the actual compression work. CacheAligner and context management are <2% of total time.
- Cost savings are substantial regardless of latency. The highest-compression scenario (JSON: Search Results (5K items)) saves $1504/1K requests at Claude Sonnet 4.5 pricing.
- Slower/pricier models benefit most. Claude Opus shows a net latency win in 12/12 scenarios vs 11 for Claude Sonnet 4.5, with 0.08ms/token prefill.
Benchmarks run with python benchmarks/bench_latency.py. Results vary based on hardware, Python version, and content characteristics.
"},{"location":"LIMITATIONS/","title":"Headroom Limitations & Known Behavior","text":"Honest documentation of when Headroom helps, when it doesn't, and what to watch out for.
"},{"location":"LIMITATIONS/#when-headroom-helps-and-when-it-doesnt","title":"When Headroom Helps (and When It Doesn't)","text":"Content Type Compression Latency Impact Best For JSON: Arrays of dicts (search results, API responses, DB rows) 86-100% Net latency win on Sonnet/Opus Primary use case \u2014 always use JSON: Arrays of strings (file paths, log lines, tags) 60-90% Net latency win New \u2014 works with all string arrays JSON: Arrays of numbers (metrics, time series) 70-85% Net latency win New \u2014 includes statistical summary JSON: Mixed-type arrays 50-70% Net latency win New \u2014 groups by type, compresses each Structured logs (as JSON) 82-95% Net latency win Log entries in tool outputs Agentic conversations (25-50 turns) 56-81% Break-even to net win Multi-tool agent sessions Plain text (documentation, articles) 43-46% Adds latency (cost savings only) Cost optimization, not speed Code Passthrough Minimal overhead See Code Compression RAG document contexts Passthrough Minimal overhead Not compressed (plain text in user messages) See LATENCY_BENCHMARKS.md for full data with per-scenario timing.
"},{"location":"LIMITATIONS/#code-compression","title":"Code Compression","text":"Headroom includes an AST-aware CodeCompressor (tree-sitter, 8 languages) but it's gated behind safety protections that prevent it from firing in most real-world scenarios. This is intentional.
Why code mostly passes through:
- Word count gate: Content under 50 words is silently skipped
- Recent code protection (
protect_recent_code=4): Code in the last 4 messages is never compressed. In typical tool-call patterns, the tool result is always \"recent\" - Analysis intent protection (
protect_analysis_context=True): If the most recent user message contains keywords like \"analyze\", \"review\", \"explain\", \"fix\", \"debug\", \"optimize\", \"error\", \"bug\" \u2014 ALL code in the conversation is protected
Why this is the right default: Code is almost always fetched because the user wants to work with it. Compressing function bodies would remove exactly what they need. LLMs like Claude are excellent at navigating large code files without compression.
Where code savings come from: The IntelligentContextManager drops old code messages that are no longer relevant (scoring-based), which is a better strategy than stripping function bodies from active code.
Override: Set protect_analysis_context=False in ContentRouterConfig for aggressive code compression. Requires headroom-ai[code] for tree-sitter.
"},{"location":"LIMITATIONS/#json-compression-constraints","title":"JSON Compression Constraints","text":""},{"location":"LIMITATIONS/#what-gets-compressed","title":"What gets compressed","text":" - Arrays of dicts: Full statistical analysis with adaptive K (Kneedle algorithm)
- Arrays of strings: Dedup + adaptive sampling + error preservation
- Arrays of numbers: Statistical summary + outlier/change-point preservation
- Mixed-type arrays: Grouped by type, each group compressed independently
- Nested objects: Recursed into, arrays within are compressed (up to depth 5)
"},{"location":"LIMITATIONS/#what-passes-through","title":"What passes through","text":" - Arrays below 5 items (
min_items_to_analyze) - Content below 200 tokens (
min_tokens_to_crush) - Bool-only arrays (not useful to compress)
- JSON objects without array values
- Malformed JSON (silently passes through, no error)
- Non-JSON content (handled by other pipeline stages)
"},{"location":"LIMITATIONS/#edge-cases","title":"Edge cases","text":" - NaN/Infinity in numeric fields: Filtered out before statistics are computed
- Nesting depth > 5: Inner arrays not examined for compression
- Mixed-type arrays with small groups: Groups below
min_items_to_analyze are kept as-is
"},{"location":"LIMITATIONS/#adaptive-k-how-item-retention-works","title":"Adaptive K: How Item Retention Works","text":"SmartCrusher doesn't use fixed K values. It uses information-theoretic sizing:
- Kneedle algorithm on bigram coverage curves finds the point where adding more items stops providing new information
- SimHash fingerprinting detects near-duplicate items
- zlib validation ensures the subset captures the full set's diversity
- The resulting K is split: 30% from array start, 15% from end, 55% for importance-scored items
Safety guarantees (additive, never dropped): - Error items (containing \"error\", \"exception\", \"failed\", \"critical\", etc.) \u2014 across ALL array types - Numeric anomalies (> 2\u03c3 from mean) - String length anomalies (> 2\u03c3 from mean length) - Change points (sudden shifts in running values)
These are kept even if they exceed the K budget.
"},{"location":"LIMITATIONS/#text-compression-llmlingua","title":"Text Compression (LLMLingua)","text":" - Requires:
headroom-ai[llmlingua] \u2014 downloads ~2GB model, needs ~1GB RAM - First call: 10-30s model load latency (cached globally after)
- Sequence length: Content chunked at 512 tokens (model limit)
- Content < 100 tokens: Skipped
- Latency: Adds overhead that doesn't break even on fast models (GPT-4o Mini, Sonnet). Use for cost savings, not speed
- Thread safety: Single global model instance with lock \u2014 sequential access under concurrency
"},{"location":"LIMITATIONS/#error-handling","title":"Error Handling","text":"All compressors follow the same principle: fail gracefully, return original content unchanged.
- Invalid JSON \u2192 passthrough (no error raised)
- AST parse failure in CodeCompressor \u2192 falls back to original or LLMLingua
- Compression makes output larger \u2192 original returned
- Missing optional dependencies (tree-sitter, LLMLingua) \u2192 passthrough with warning log
- One exception: LLMLingua out-of-memory during model loading raises
RuntimeError
Errors are logged at WARNING level and never propagated to callers.
"},{"location":"LIMITATIONS/#toin-cold-start","title":"TOIN Cold Start","text":"The Tool Output Intelligence Network (TOIN) learns compression patterns from usage. For new tool types:
- No learned patterns exist \u2192 falls back to statistical heuristics
- Confidence below
toin_confidence_threshold (default 0.3) \u2192 TOIN hints ignored - Patterns build up over time as tools are used repeatedly
- Cross-session learning requires persistence (
TelemetryConfig.storage_path)
"},{"location":"LIMITATIONS/#cachealigner-behavior","title":"CacheAligner Behavior","text":" - Only processes system messages for dynamic content extraction
- Dynamic content in user/assistant/tool messages is not extracted
- May add small markers (
[Dynamic Context] separator) that slightly increase token count - Whitespace normalization may affect content with significant indentation (code blocks, ASCII art)
"},{"location":"LIMITATIONS/#provider-interactions","title":"Provider Interactions","text":" - CacheAligner is designed to maximize Anthropic/OpenAI prefix cache hit rates
- Token counting uses model-specific tokenizers (tiktoken for OpenAI, calibrated estimation for Anthropic)
- Compression works with all providers \u2014 no provider-specific limitations
- Compressed content is valid JSON \u2014 downstream tools and parsers work unchanged
"},{"location":"LIMITATIONS/#performance-characteristics","title":"Performance Characteristics","text":" - ContentRouter accounts for 91-98% of pipeline cost \u2014 it does the actual compression work
- CacheAligner and RollingWindow are sub-millisecond
- Scaling is roughly linear with input size
- Full benchmark data: LATENCY_BENCHMARKS.md
"},{"location":"LIMITATIONS/#configuration-tuning","title":"Configuration Tuning","text":"Parameter Default Effect min_items_to_analyze 5 Arrays below this pass through min_tokens_to_crush 200 Content below this passes through max_items_after_crush 15 Upper bound on retained items variance_threshold 2.0 Std devs for anomaly detection (lower = more preserved) first_fraction 0.3 Fraction of K allocated to array start last_fraction 0.15 Fraction of K allocated to array end protect_analysis_context True Protect code when user asks about it protect_recent_code 4 Messages from end to protect code skip_user_messages True Never compress user messages toin_confidence_threshold 0.3 Minimum TOIN confidence to apply hints"},{"location":"agno/","title":"Agno Integration","text":"Headroom integrates with Agno (formerly Phidata) to provide automatic context optimization for AI agents. This guide covers model wrapping, observability hooks, and multi-provider support.
"},{"location":"agno/#installation","title":"Installation","text":"pip install \"headroom-ai[agno]\"\n
This installs Headroom with Agno support. You'll also need Agno itself:
pip install agno\n
"},{"location":"agno/#quick-start","title":"Quick Start","text":"from agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Wrap your model\nmodel = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n# Create agent as usual\nagent = Agent(model=model)\n\n# Use exactly like before\nresponse = agent.run(\"What's the capital of France?\")\n\n# Check savings\nprint(f\"Tokens saved: {model.total_tokens_saved}\")\nprint(model.get_savings_summary())\n# {'total_requests': 1, 'total_tokens_saved': 245, 'average_savings_percent': 12.3}\n
"},{"location":"agno/#integration-patterns","title":"Integration Patterns","text":""},{"location":"agno/#1-basic-model-wrapping","title":"1. Basic Model Wrapping","text":"The simplest integration - wrap any Agno model with HeadroomAgnoModel:
from agno.models.openai import OpenAIChat\nfrom agno.models.anthropic import Claude\nfrom agno.models.google import Gemini\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Works with any Agno model\nopenai_model = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\nclaude_model = HeadroomAgnoModel(Claude(id=\"claude-3-5-sonnet-20241022\"))\ngemini_model = HeadroomAgnoModel(Gemini(id=\"gemini-2.0-flash\"))\n\n# Each automatically uses the correct provider for accurate token counting\n
Why this matters: Headroom automatically detects the underlying provider and applies the correct tokenizer for accurate optimization metrics.
"},{"location":"agno/#2-agent-with-observability-hooks","title":"2. Agent with Observability Hooks","text":"Use hooks for detailed tracking without modifying your model:
from agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom headroom.integrations.agno import (\n HeadroomAgnoModel,\n HeadroomPreHook,\n HeadroomPostHook,\n)\n\n# Model wrapper for optimization\nmodel = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n# Hooks for observability\npre_hook = HeadroomPreHook()\npost_hook = HeadroomPostHook(token_alert_threshold=10000)\n\nagent = Agent(\n model=model,\n pre_hooks=[pre_hook],\n post_hooks=[post_hook],\n)\n\n# Run agent\nresponse = agent.run(\"Analyze this large dataset...\")\n\n# Check metrics from model\nprint(f\"Tokens saved: {model.total_tokens_saved}\")\n\n# Check observability from hooks\nprint(f\"Post-hook summary: {post_hook.get_summary()}\")\nprint(f\"Alerts triggered: {post_hook.alerts}\")\n
Why this matters: Hooks provide observability into agent behavior and can alert when token usage exceeds thresholds.
"},{"location":"agno/#3-convenience-hook-factory","title":"3. Convenience Hook Factory","text":"Use create_headroom_hooks() to create matched hook pairs:
from headroom.integrations.agno import create_headroom_hooks\n\npre_hook, post_hook = create_headroom_hooks(\n token_alert_threshold=5000,\n log_level=\"DEBUG\",\n)\n\nagent = Agent(\n model=model,\n pre_hooks=[pre_hook],\n post_hooks=[post_hook],\n)\n
"},{"location":"agno/#4-custom-configuration","title":"4. Custom Configuration","text":"Pass a HeadroomConfig for fine-grained control:
from headroom import HeadroomConfig, HeadroomMode\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\nconfig = HeadroomConfig(\n default_mode=HeadroomMode.OPTIMIZE,\n # Add other configuration options as needed\n)\n\nmodel = HeadroomAgnoModel(\n wrapped_model=OpenAIChat(id=\"gpt-4o\"),\n config=config,\n)\n
"},{"location":"agno/#5-standalone-message-optimization","title":"5. Standalone Message Optimization","text":"Optimize messages without wrapping a model:
from headroom.integrations.agno import optimize_messages\n\nmessages = [\n {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Analyze this large JSON: \" + large_json},\n]\n\noptimized_messages, metrics = optimize_messages(messages, model=\"gpt-4o\")\n\nprint(f\"Tokens saved: {metrics['tokens_saved']}\")\nprint(f\"Transforms applied: {metrics['transforms_applied']}\")\n
"},{"location":"agno/#6-async-operations","title":"6. Async Operations","text":"Full async support for high-throughput applications:
import asyncio\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\nasync def process_async():\n model = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n # Async response\n response = await model.aresponse(messages)\n\n # Async streaming\n async for chunk in model.aresponse_stream(messages):\n print(chunk, end=\"\", flush=True)\n\n print(f\"\\nTokens saved: {model.total_tokens_saved}\")\n\nasyncio.run(process_async())\n
"},{"location":"agno/#real-world-examples","title":"Real-World Examples","text":""},{"location":"agno/#example-1-tool-heavy-agent","title":"Example 1: Tool-Heavy Agent","text":"from agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom agno.tools.duckduckgo import DuckDuckGoTools\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Wrap model for optimization\nmodel = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n# Agent with search tools\nagent = Agent(\n model=model,\n tools=[DuckDuckGoTools()],\n show_tool_calls=True,\n)\n\n# Tool outputs get compressed automatically\nresponse = agent.run(\"Research the latest AI developments and summarize\")\n\n# Impact: Tool outputs (often 10K+ tokens) compressed by 70-90%\nprint(f\"Tokens saved: {model.total_tokens_saved}\")\nprint(model.get_savings_summary())\n
"},{"location":"agno/#example-2-multi-model-routing","title":"Example 2: Multi-Model Routing","text":"from agno.models.openai import OpenAIChat\nfrom agno.models.anthropic import Claude\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Different models for different tasks\nfast_model = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o-mini\"))\npowerful_model = HeadroomAgnoModel(Claude(id=\"claude-3-5-sonnet-20241022\"))\n\n# Use fast model for simple tasks\nsimple_agent = Agent(model=fast_model)\n\n# Use powerful model for complex reasoning\ncomplex_agent = Agent(model=powerful_model)\n\n# Each tracks its own metrics\nprint(f\"Fast model saved: {fast_model.total_tokens_saved}\")\nprint(f\"Powerful model saved: {powerful_model.total_tokens_saved}\")\n
"},{"location":"agno/#example-3-production-monitoring","title":"Example 3: Production Monitoring","text":"from agno.agent import Agent\nfrom headroom.integrations.agno import (\n HeadroomAgnoModel,\n create_headroom_hooks,\n)\n\nmodel = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\npre_hook, post_hook = create_headroom_hooks(\n token_alert_threshold=50000, # Alert on large requests\n log_level=\"WARNING\",\n)\n\nagent = Agent(\n model=model,\n pre_hooks=[pre_hook],\n post_hooks=[post_hook],\n)\n\n# Run multiple requests\nfor query in user_queries:\n response = agent.run(query)\n\n# Check for alerts\nif post_hook.alerts:\n print(f\"WARNING: {len(post_hook.alerts)} requests exceeded threshold\")\n for alert in post_hook.alerts:\n print(f\" - {alert}\")\n\n# Summary stats\nsummary = post_hook.get_summary()\nprint(f\"Total requests: {summary['total_requests']}\")\nprint(f\"Average tokens: {summary['average_tokens']}\")\n
"},{"location":"agno/#example-4-reset-for-new-sessions","title":"Example 4: Reset for New Sessions","text":"model = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n# Session 1\nagent.run(\"First conversation...\")\nprint(f\"Session 1 savings: {model.get_savings_summary()}\")\n\n# Reset for new session\nmodel.reset()\n\n# Session 2 - metrics start fresh\nagent.run(\"Second conversation...\")\nprint(f\"Session 2 savings: {model.get_savings_summary()}\")\n
"},{"location":"agno/#supported-providers","title":"Supported Providers","text":"HeadroomAgnoModel automatically detects the provider from the wrapped model:
Provider Agno Models Auto-Detected OpenAI OpenAIChat, OpenAILike Yes Anthropic Claude, AwsBedrock Yes Google Gemini, VertexAI Yes Cohere Cohere, CohereChat Yes Groq Groq Yes (OpenAI-compatible) Mistral Mistral Yes (OpenAI-compatible) Together Together Yes (OpenAI-compatible) Ollama Ollama Yes (OpenAI-compatible) To disable auto-detection:
model = HeadroomAgnoModel(\n wrapped_model=some_model,\n auto_detect_provider=False, # Falls back to OpenAI tokenizer\n)\n
"},{"location":"agno/#feature-coverage","title":"Feature Coverage","text":""},{"location":"agno/#whats-optimized","title":"What's Optimized","text":"HeadroomAgnoModel optimizes messages at the LLM call boundary. This covers:
Feature Optimized Notes User/Assistant Messages \u2705 Yes Full message history compressed Tool Calls \u2705 Yes Tool call arguments optimized Tool Results \u2705 Yes JSON responses compressed 70-90% via SmartCrusher System Prompts \u2705 Yes Included in message optimization Streaming Responses \u2705 Yes Both sync and async Multi-turn Conversations \u2705 Yes Full history available for optimization"},{"location":"agno/#known-limitations","title":"Known Limitations","text":"The integration operates at the model layer, not the agent layer. Some Agno features operate outside this boundary:
Agno Feature Status Explanation Agent Memory \u26a0\ufe0f Partial Memory content is optimized when it enters messages, but the persistent memory store itself is not compressed. If you're storing large amounts of data in agent memory, consider summarizing before storage. Knowledge Bases \u26a0\ufe0f Partial KB retrieval happens before messages reach the model. Retrieved context is optimized as part of the message, but we can't influence KB retrieval itself. Agent Teams \u274c Not supported Each agent's model is wrapped independently. No cross-agent optimization or team-level coordination. Tool Definitions \u26a0\ufe0f Not deduplicated Tool schemas are sent with every request. Future versions may deduplicate repeated tool definitions. Structured Outputs \u2705 Supported response_model works normally; optimization doesn't affect output parsing. Reasoning Models \u2705 Supported Extended thinking works; we don't compress reasoning traces."},{"location":"agno/#best-practices-for-maximum-savings","title":"Best Practices for Maximum Savings","text":" - Tool-heavy agents see the biggest wins \u2014 Tool results (JSON, logs, search results) compress 70-90%
- Long conversations benefit from RollingWindow \u2014 Configure context limits to avoid hitting provider maximums
- Wrap at the model level, not agent level \u2014 This ensures all LLM calls go through optimization
- Use hooks for observability \u2014 Track token usage patterns to identify optimization opportunities
"},{"location":"agno/#future-improvements","title":"Future Improvements","text":"We're tracking these potential enhancements:
- Memory optimization hooks \u2014 Compress data before it enters agent memory
- Knowledge base integration \u2014 Optimize retrieved context at the KB layer
- Tool schema deduplication \u2014 Cache and reference repeated tool definitions
- Team-level optimization \u2014 Shared context compression across agent teams
Contributions welcome! See CONTRIBUTING.md.
"},{"location":"agno/#configuration-reference","title":"Configuration Reference","text":""},{"location":"agno/#headroomagnomodel","title":"HeadroomAgnoModel","text":"Parameter Type Default Description wrapped_model Any Required The Agno model to wrap config HeadroomConfig None Custom configuration auto_detect_provider bool True Auto-detect provider for token counting Properties: - wrapped_model - Access the underlying Agno model - total_tokens_saved - Running total of tokens saved - metrics_history - List of last 100 OptimizationMetrics
Methods: - response(messages, **kwargs) - Sync response with optimization - response_stream(messages, **kwargs) - Sync streaming response - aresponse(messages, **kwargs) - Async response - aresponse_stream(messages, **kwargs) - Async streaming - get_savings_summary() - Returns dict with stats - reset() - Clear all metrics
"},{"location":"agno/#headroomprehook","title":"HeadroomPreHook","text":"Parameter Type Default Description config HeadroomConfig None Configuration (for future use) model str \"gpt-4o\" Model name for estimation"},{"location":"agno/#headroomposthook","title":"HeadroomPostHook","text":"Parameter Type Default Description log_level str \"INFO\" Logging level token_alert_threshold int None Alert if tokens exceed this Properties: - total_requests - Number of requests tracked - alerts - List of alert messages
Methods: - get_summary() - Returns dict with request stats - reset() - Clear history and alerts
"},{"location":"agno/#create_headroom_hooks","title":"create_headroom_hooks()","text":"Parameter Type Default Description config HeadroomConfig None Config for pre-hook model str \"gpt-4o\" Model for pre-hook log_level str \"INFO\" Log level for post-hook token_alert_threshold int None Alert threshold for post-hook Returns: tuple[HeadroomPreHook, HeadroomPostHook]
"},{"location":"agno/#import-reference","title":"Import Reference","text":"# Main integration\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Hooks\nfrom headroom.integrations.agno import HeadroomPreHook\nfrom headroom.integrations.agno import HeadroomPostHook\nfrom headroom.integrations.agno import create_headroom_hooks\n\n# Utilities\nfrom headroom.integrations.agno import optimize_messages\nfrom headroom.integrations.agno import agno_available\nfrom headroom.integrations.agno import get_headroom_provider\nfrom headroom.integrations.agno import get_model_name_from_agno\n\n# Or import everything from parent\nfrom headroom.integrations import (\n HeadroomAgnoModel,\n HeadroomPreHook,\n HeadroomPostHook,\n create_headroom_hooks,\n)\n
"},{"location":"agno/#troubleshooting","title":"Troubleshooting","text":""},{"location":"agno/#check-if-agno-is-available","title":"Check if Agno is Available","text":"from headroom.integrations.agno import agno_available\n\nif agno_available():\n from headroom.integrations.agno import HeadroomAgnoModel\nelse:\n print(\"Install agno: pip install agno\")\n
"},{"location":"agno/#provider-detection-issues","title":"Provider Detection Issues","text":"If auto-detection fails, check the detected provider:
from headroom.integrations.agno import get_headroom_provider, get_model_name_from_agno\n\nmodel = OpenAIChat(id=\"gpt-4o\")\nprovider = get_headroom_provider(model)\nmodel_name = get_model_name_from_agno(model)\n\nprint(f\"Detected provider: {type(provider).__name__}\")\nprint(f\"Model name: {model_name}\")\n
"},{"location":"agno/#metrics-not-updating","title":"Metrics Not Updating","text":"Ensure you're checking the correct object:
# Model metrics (optimization)\nprint(model.total_tokens_saved) # Actual savings\n\n# Hook metrics (observability)\nprint(post_hook.get_summary()) # Request tracking\n
Note: Hooks track request counts, not token savings. Use the model wrapper for optimization metrics.
"},{"location":"api/","title":"API Reference","text":""},{"location":"api/#headroomclient","title":"HeadroomClient","text":"The main entry point for Headroom SDK.
from headroom import HeadroomClient\nfrom openai import OpenAI\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n default_mode=\"optimize\",\n)\n
"},{"location":"api/#constructor-parameters","title":"Constructor Parameters","text":"Parameter Type Default Description original_client OpenAI \\| Anthropic Required The underlying LLM client provider Provider Auto-detected Token counting provider default_mode str \"audit\" Default mode: \"audit\", \"optimize\", \"off\" store_url str None Storage URL for metrics smart_crusher_config SmartCrusherConfig Default Compression settings cache_aligner_config CacheAlignerConfig Default Cache alignment settings rolling_window_config RollingWindowConfig Default Context window settings"},{"location":"api/#methods","title":"Methods","text":""},{"location":"api/#chatcompletionscreatekwargs","title":"chat.completions.create(**kwargs)","text":"Create a chat completion with optional optimization.
response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[...],\n headroom_mode=\"optimize\", # Override default mode\n)\n
Additional Parameters:
Parameter Type Description headroom_mode str Override mode for this request headroom_query str Query for relevance scoring"},{"location":"api/#chatcompletionssimulatekwargs","title":"chat.completions.simulate(**kwargs)","text":"Preview optimization without making an API call.
plan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=[...],\n)\n\nprint(f\"Tokens before: {plan.tokens_before}\")\nprint(f\"Tokens after: {plan.tokens_after}\")\nprint(f\"Savings: {plan.savings_percent:.1f}%\")\n
Returns: SimulationResult
"},{"location":"api/#configuration-classes","title":"Configuration Classes","text":""},{"location":"api/#smartcrusherconfig","title":"SmartCrusherConfig","text":"from headroom import SmartCrusherConfig\n\nconfig = SmartCrusherConfig(\n min_tokens_to_crush=200,\n max_items_after_crush=50,\n keep_first=3,\n keep_last=2,\n relevance_threshold=0.3,\n anomaly_std_threshold=2.0,\n preserve_errors=True,\n)\n
"},{"location":"api/#cachealignerconfig","title":"CacheAlignerConfig","text":"from headroom import CacheAlignerConfig\n\nconfig = CacheAlignerConfig(\n extract_dates=True,\n normalize_whitespace=True,\n stable_prefix_min_tokens=100,\n)\n
"},{"location":"api/#rollingwindowconfig","title":"RollingWindowConfig","text":"from headroom import RollingWindowConfig\n\nconfig = RollingWindowConfig(\n max_tokens=100000,\n preserve_system=True,\n preserve_recent_turns=5,\n drop_oldest_first=True,\n)\n
"},{"location":"api/#intelligentcontextconfig","title":"IntelligentContextConfig","text":"from headroom.config import IntelligentContextConfig, ScoringWeights\n\nweights = ScoringWeights(\n recency=0.20,\n semantic_similarity=0.20,\n toin_importance=0.25,\n error_indicator=0.15,\n forward_reference=0.15,\n token_density=0.05,\n)\n\nconfig = IntelligentContextConfig(\n enabled=True,\n keep_system=True,\n keep_last_turns=2,\n output_buffer_tokens=4000,\n use_importance_scoring=True,\n scoring_weights=weights,\n toin_integration=True,\n recency_decay_rate=0.1,\n compress_threshold=0.1,\n)\n
"},{"location":"api/#scoringweights","title":"ScoringWeights","text":"from headroom.config import ScoringWeights\n\nweights = ScoringWeights(\n recency=0.20, # Exponential decay from end\n semantic_similarity=0.20, # Embedding similarity to recent context\n toin_importance=0.25, # TOIN retrieval_rate\n error_indicator=0.15, # TOIN field_semantics error detection\n forward_reference=0.15, # Messages referenced by later messages\n token_density=0.05, # Unique/total token ratio\n)\n\n# Weights are auto-normalized to sum to 1.0\nnormalized = weights.normalized()\n
"},{"location":"api/#relevancescorerconfig","title":"RelevanceScorerConfig","text":"from headroom import RelevanceScorerConfig\n\nconfig = RelevanceScorerConfig(\n scorer_type=\"bm25\", # \"bm25\", \"embedding\", or \"hybrid\"\n embedding_model=None, # Model name for embedding scorer\n hybrid_alpha=0.5, # Weight for hybrid scoring\n)\n
"},{"location":"api/#data-models","title":"Data Models","text":""},{"location":"api/#simulationresult","title":"SimulationResult","text":"Returned by simulate().
@dataclass\nclass SimulationResult:\n tokens_before: int\n tokens_after: int\n tokens_saved: int\n savings_percent: float\n transforms_applied: list[str]\n waste_signals: WasteSignals\n
"},{"location":"api/#requestmetrics","title":"RequestMetrics","text":"Metrics for a single request.
@dataclass\nclass RequestMetrics:\n request_id: str\n timestamp: datetime\n model: str\n tokens_input_before: int\n tokens_input_after: int\n tokens_output: int\n cost_before: float\n cost_after: float\n transforms_applied: list[str]\n
"},{"location":"api/#wastesignals","title":"WasteSignals","text":"Detected waste in the request.
@dataclass\nclass WasteSignals:\n json_bloat_tokens: int\n html_noise_tokens: int\n whitespace_tokens: int\n dynamic_date_tokens: int\n repetition_tokens: int\n
"},{"location":"api/#providers","title":"Providers","text":""},{"location":"api/#openaiprovider","title":"OpenAIProvider","text":"from headroom import OpenAIProvider\n\nprovider = OpenAIProvider()\n\n# Get token counter\ncounter = provider.get_token_counter(\"gpt-4o\")\ntokens = counter.count_text(\"Hello, world!\")\n\n# Get context limit\nlimit = provider.get_context_limit(\"gpt-4o\") # 128000\n\n# Estimate cost\ncost = provider.estimate_cost(\n input_tokens=1000,\n output_tokens=500,\n model=\"gpt-4o\",\n)\n
"},{"location":"api/#anthropicprovider","title":"AnthropicProvider","text":"from headroom import AnthropicProvider\nfrom anthropic import Anthropic\n\nprovider = AnthropicProvider(client=Anthropic())\n\ncounter = provider.get_token_counter(\"claude-3-5-sonnet-latest\")\ntokens = counter.count_messages(messages) # Accurate count via API\n
"},{"location":"api/#relevance-scoring","title":"Relevance Scoring","text":""},{"location":"api/#bm25scorer","title":"BM25Scorer","text":"Fast keyword-based scoring (zero dependencies).
from headroom import BM25Scorer\n\nscorer = BM25Scorer()\nscores = scorer.score_items(\n items=[\"item 1\", \"item 2\", ...],\n query=\"search query\",\n)\n
"},{"location":"api/#embeddingscorer","title":"EmbeddingScorer","text":"Semantic similarity scoring (requires sentence-transformers).
from headroom import EmbeddingScorer, embedding_available\n\nif embedding_available():\n scorer = EmbeddingScorer(model=\"all-MiniLM-L6-v2\")\n scores = scorer.score_items(items, query)\n
"},{"location":"api/#hybridscorer","title":"HybridScorer","text":"Combines BM25 and embeddings.
from headroom import HybridScorer\n\nscorer = HybridScorer(alpha=0.5) # 50% BM25, 50% embedding\nscores = scorer.score_items(items, query)\n
"},{"location":"api/#create_scorer","title":"create_scorer()","text":"Factory function to create scorers.
from headroom import create_scorer\n\n# Auto-select best available scorer\nscorer = create_scorer()\n\n# Explicitly choose type\nscorer = create_scorer(scorer_type=\"hybrid\", alpha=0.7)\n
"},{"location":"api/#transforms-direct-use","title":"Transforms (Direct Use)","text":""},{"location":"api/#smartcrusher","title":"SmartCrusher","text":"from headroom import SmartCrusher\n\ncrusher = SmartCrusher()\nresult = crusher.crush(\n data={\"results\": [...]},\n query=\"user query\",\n)\n
"},{"location":"api/#cachealigner","title":"CacheAligner","text":"from headroom import CacheAligner\n\naligner = CacheAligner()\nresult = aligner.align(messages)\n
"},{"location":"api/#rollingwindow","title":"RollingWindow","text":"from headroom import RollingWindow\n\nwindow = RollingWindow(config)\nresult = window.apply(messages, max_tokens=100000)\n
"},{"location":"api/#intelligentcontextmanager","title":"IntelligentContextManager","text":"from headroom.transforms import IntelligentContextManager\nfrom headroom.config import IntelligentContextConfig\nfrom headroom.telemetry import get_toin\n\n# With TOIN integration for learned patterns\ntoin = get_toin()\nconfig = IntelligentContextConfig(\n keep_system=True,\n keep_last_turns=2,\n use_importance_scoring=True,\n)\n\nmanager = IntelligentContextManager(config, toin=toin)\nresult = manager.apply(messages, tokenizer, model_limit=128000)\n\n# Access scoring details\nprint(result.transforms_applied) # [\"intelligent_cap:3\"]\nprint(result.tokens_before, result.tokens_after)\n
"},{"location":"api/#messagescorer","title":"MessageScorer","text":"from headroom.transforms import MessageScorer, MessageScore\nfrom headroom.config import ScoringWeights\n\nscorer = MessageScorer(\n weights=ScoringWeights(),\n toin=None, # Optional TOIN for learned patterns\n embedding_provider=None, # Optional for semantic similarity\n recency_decay_rate=0.1,\n)\n\n# Score messages\nscores: list[MessageScore] = scorer.score_messages(\n messages=messages,\n protected_indices={0}, # System message\n tool_unit_indices={2, 3}, # Tool call + response\n)\n\nfor score in scores:\n print(f\"Message {score.message_index}: {score.total_score:.2f}\")\n print(f\" Recency: {score.recency_score:.2f}\")\n print(f\" TOIN: {score.toin_score:.2f}\")\n print(f\" Protected: {score.is_protected}\")\n
"},{"location":"api/#transformpipeline","title":"TransformPipeline","text":"from headroom import TransformPipeline\n\npipeline = TransformPipeline([\n SmartCrusher(),\n CacheAligner(),\n RollingWindow(),\n])\n\nresult = pipeline.transform(messages)\n
"},{"location":"api/#utilities","title":"Utilities","text":""},{"location":"api/#tokenizer","title":"Tokenizer","text":"from headroom import Tokenizer, count_tokens_text, count_tokens_messages\n\n# Quick counting\ntokens = count_tokens_text(\"Hello, world!\", model=\"gpt-4o\")\n\n# With tokenizer instance\ntokenizer = Tokenizer(model=\"gpt-4o\")\ntokens = tokenizer.count_text(\"Hello\")\ntokens = tokenizer.count_messages(messages)\n
"},{"location":"api/#generate_report","title":"generate_report()","text":"Generate HTML/Markdown reports from stored metrics.
from headroom import generate_report\n\nreport = generate_report(\n store_url=\"sqlite:///headroom.db\",\n format=\"html\",\n period=\"day\",\n)\n
"},{"location":"api/#typescript-sdk","title":"TypeScript SDK","text":"For the TypeScript SDK API reference, see TypeScript SDK.
The TypeScript SDK provides compress(), HeadroomClient, and framework adapters for Vercel AI SDK, OpenAI, and Anthropic.
"},{"location":"benchmarks/","title":"Benchmarks","text":"Headroom's core promise: compress context without losing accuracy. This page shows accuracy benchmarks, compression performance, and real-world production telemetry from 250+ active proxy instances.
Key Results
98.2% recall on article extraction with 94.9% compression. 52ms median overhead in production. 1.4 billion tokens saved across 249 instances.
"},{"location":"benchmarks/#compression-performance","title":"Compression Performance","text":"Tested on Apple M-series (CPU), headroom v0.5.18. Each test runs compress() on realistic tool outputs.
Content Type Original Compressed Saved Ratio Latency JSON array (100 items) 3,163 297 2,866 90.6% 1ms JSON array (500 items) 9,526 1,614 7,912 83.1% 2ms Shell output (200 lines) 3,238 469 2,769 85.5% 1ms Build log (200 lines) 2,412 148 2,264 93.9% 1ms grep results (150 hits) 2,624 2,624 0 0.0% <1ms Python source (~480 lines) 2,958 2,958 0 0.0% <1ms Total 23,921 8,110 15,811 66.1% 5ms Notes:
- grep results and Python source show 0% compression \u2014 these are already compact structured formats. SmartCrusher only compresses JSON arrays; code passes through to preserve correctness.
- Latency is for the
compress() SDK call, not the full proxy round-trip.
"},{"location":"benchmarks/#production-telemetry","title":"Production Telemetry","text":"Real-world data from 50,000+ proxy sessions across 250+ unique instances (March 30 \u2013 April 2, 2026). Collected via anonymous telemetry beacon (opt-out: HEADROOM_TELEMETRY=off).
"},{"location":"benchmarks/#proxy-overhead","title":"Proxy Overhead","text":"Percentile Latency Median (P50) 52ms P90 309ms P99 4,172ms Mean 161ms The median 52ms overhead is negligible compared to LLM inference time (typically 2-10 seconds).
"},{"location":"benchmarks/#compression-rate","title":"Compression Rate","text":"Percentile Compression P25 4.8% Median 4.8% P75 6.9% Mean 11.3% Median compression is modest because many requests are short conversational turns. Heavy tool-use sessions (file reads, shell output) see 40-80% compression.
"},{"location":"benchmarks/#pipeline-step-timing-production-median","title":"Pipeline Step Timing (Production Median)","text":"Step Median P90 Description pipeline_total 16.9ms 289ms Full compression pipeline content_router 11.7ms 259ms Content detection + routing compressor:smart_crusher 50.1ms 50ms JSON array compression compressor:text 32.0ms 576ms Text compression (Kompress ONNX) compressor:mixed 316ms 428ms Mixed content compression compressor:code_aware 815ms 886ms Tree-sitter AST compression _initial_token_count 2.9ms 16ms Token counting (tiktoken) _deep_copy 0.1ms 0.3ms Message copy overhead"},{"location":"benchmarks/#fleet-summary","title":"Fleet Summary","text":"Metric Value Clean instances 249 Total tokens saved 1.4 billion Total $ saved ~$4,000 OS distribution Linux 57%, macOS 38%, Windows 5% Top version 0.5.17 (77%) Models used Claude Opus 4.6, Sonnet 4.6, Haiku 4.5"},{"location":"benchmarks/#accuracy-benchmarks","title":"Accuracy Benchmarks","text":""},{"location":"benchmarks/#html-extraction","title":"HTML Extraction","text":"Dataset: Scrapinghub Article Extraction Benchmark Samples: 181 HTML pages with ground truth article bodies Baseline: trafilatura (0.958 F1)
Metric Value Description F1 Score 0.919 Token-level overlap with ground truth Precision 0.879 Proportion of extracted content that's relevant Recall 0.982 Proportion of ground truth content captured Compression 94.9% Average size reduction For LLM applications, recall is critical \u2014 98.2% means nearly all article content is preserved. The slight precision drop (some extra content) doesn't hurt LLM accuracy.
# Run it yourself\npip install \"headroom-ai[html]\" datasets\npytest tests/test_evals/test_html_oss_benchmarks.py::TestExtractionBenchmark -v -s\n
"},{"location":"benchmarks/#json-compression-smartcrusher","title":"JSON Compression (SmartCrusher)","text":"Test: 100 production log entries with critical error at position 67 Task: Find the error, error code, resolution, and affected count
Metric Baseline Headroom Input tokens 10,144 1,260 Correct answers 4/4 4/4 Compression \u2014 87.6% SmartCrusher preserves first N items (schema), last N items (recency), all anomalies (errors, warnings), and statistical distribution.
"},{"location":"benchmarks/#qa-accuracy-preservation","title":"QA Accuracy Preservation","text":"Metric Original HTML Extracted Delta F1 Score 0.85 0.87 +0.02 Exact Match 60% 62% +2% Extraction Can Improve Accuracy
Removing HTML noise sometimes helps LLMs focus on relevant content.
"},{"location":"benchmarks/#limitations","title":"Limitations","text":""},{"location":"benchmarks/#what-headroom-does-not-compress","title":"What Headroom Does NOT Compress","text":" - Short messages (< 300 tokens) \u2014 overhead exceeds savings
- Source code \u2014 passes through unchanged to preserve correctness (unless tree-sitter AST compression is enabled)
- grep/search results \u2014 compact structured format, already minimal
- Images \u2014 counted at fixed token cost (~1,600 tokens), not compressed as text
- System prompts \u2014 preserved for prefix cache compatibility
"},{"location":"benchmarks/#known-overhead-sources","title":"Known Overhead Sources","text":" - Token counting (P90: 16ms) \u2014 runs tiktoken twice (before + after compression)
- Tree-sitter AST parsing (P90: 886ms) \u2014 expensive for large code files
- Kompress ONNX (P90: 576ms) \u2014 ML inference on CPU for text compression
- Content detection (Magika) \u2014 ML classification of content type
"},{"location":"benchmarks/#when-headroom-adds-the-most-value","title":"When Headroom Adds the Most Value","text":" - Long agent sessions with accumulated tool outputs (40-80% compression)
- JSON-heavy workflows (API responses, database queries) \u2014 83-94% compression
- Build/test output \u2014 85-94% compression
- Multi-tool agents \u2014 60-76% compression across tool results
"},{"location":"benchmarks/#when-headroom-adds-little-value","title":"When Headroom Adds Little Value","text":" - Short conversational exchanges \u2014 median 4.8% compression
- Code-only sessions (reading/writing files) \u2014 code passes through
- Single-turn requests \u2014 no accumulated context to compress
"},{"location":"benchmarks/#methodology","title":"Methodology","text":""},{"location":"benchmarks/#token-level-f1","title":"Token-Level F1","text":"Precision = |predicted \u2229 ground_truth| / |predicted|\nRecall = |predicted \u2229 ground_truth| / |ground_truth|\nF1 = 2 * (Precision * Recall) / (Precision + Recall)\n
"},{"location":"benchmarks/#compression-ratio","title":"Compression Ratio","text":"Compression = 1 - (compressed_size / original_size)\n
A 94.9% compression means the output is 5.1% of the original size.
"},{"location":"benchmarks/#production-telemetry_1","title":"Production Telemetry","text":" - Collected via anonymous beacon (no prompts, no content, no PII)
- Image-inflated instances excluded (base64 counted as text tokens \u2014 fixed in v0.5.18)
- Multi-worker beacon spam excluded (per-instance MAX, not SUM)
- Opt-out:
HEADROOM_TELEMETRY=off
"},{"location":"benchmarks/#reproducing-results","title":"Reproducing Results","text":"# Clone the repo\ngit clone https://github.com/chopratejas/headroom.git\ncd headroom\n\n# Install with eval dependencies\npip install -e \".[evals,html]\"\n\n# Run all benchmarks\npytest tests/test_evals/ -v -s\n\n# Run compression benchmark\npython -c \"from headroom import compress; print(compress([{'role':'user','content':'test'}]))\"\n\n# Run local proxy mode benchmark (no API calls)\npython benchmarks/proxy_mode_benchmark.py --turns 12 --show-real-harness\n\n# Replay local Claude Code transcripts (no API calls)\npython benchmarks/claude_session_mode_benchmark.py --workers 1\n\n# Compare two refs on the same local Claude transcript corpus\npython benchmarks/claude_session_branch_compare.py --left-ref upstream/main --right-ref HEAD --recent-turns-per-session 200 --workers 1\n
This benchmark compares token vs cache proxy modes on the same synthetic conversation:
token should show higher compression. cache should preserve prior-turn stability and can win in long sessions with strong prefix-cache reuse.
--show-real-harness prints optional steps for running the same comparison with Claude Code, but does not call APIs by default.
claude_session_branch_compare.py runs the real local session replay benchmark twice, once per git ref, in isolated worktrees. It writes:
- per-ref replay outputs under
benchmark_results/branch_compare/<label>/ - a combined comparison report under
benchmark_results/branch_compare/
Use it when you want a clean PR-vs-main comparison on the same transcript slice.
For a deterministic cache-busting proof case, run:
python benchmarks/synthetic_token_cache_bust_report.py\n
That synthetic replay forces token mode to retroactively rewrite a prior tool result on the second turn while cache mode remains stable. Use it to verify the simulator can distinguish:
token: history rewrite + cache bust cache: no rewrite + no bust
For a reproducible local report bundle that combines:
- full real-session replay summaries
- local-only processed real input/output excerpts
- synthetic token-bust proof
- synthetic long-form stress tests
run:
python benchmarks/cache_validation_bundle.py --workers 1 --output-dir benchmark_results/cache_validation_bundle_full\n
Notes:
- By default the bundle is redaction-safe for sharing:
- real processed reports redact transcript-derived content excerpts
- manifest paths are redacted
- To include local processed content excerpts for private review on your own machine:
python benchmarks/cache_validation_bundle.py --workers 1 --include-content\n
- The bundle writes:
index.html / index.md: top-level summary and links bundle_manifest.json: runtime metadata + corpus fingerprint real/: full real-session replay reports real_processed/: processed before/after excerpts from real transcripts synthetic_token_bust/: minimal explicit cache-bust proof synthetic_long_suite/: long deterministic rewrite/TTL scenarios - Checkpoints are scoped under the bundle output directory and fingerprinted by the selected corpus so stale runs do not contaminate new results.
The Claude session benchmark replays local transcript data from ~/.claude/projects through baseline, token, and cache modes. It estimates raw tokens, cache read/write tokens, paid input/output costs, and prompt-window winners under two assumptions:
- cached tokens count against the model window
- cache reads do not count against the model window
Notes:
- It writes local output to
benchmark_results/, which is gitignored. - It is intentionally conservative on memory. Run with
--workers 1 for the most stable full-corpus replay. Higher worker counts increase memory use. - It uses transcript-visible messages only. Hidden Claude Code system/tool schemas are not available in the local
.jsonl files, so the numbers are comparative estimates rather than exact provider billing replicas.
"},{"location":"ccr/","title":"CCR: Compress-Cache-Retrieve","text":"Headroom's CCR architecture makes compression reversible. When content is compressed, the original data is cached. If the LLM needs more data, it can retrieve it instantly.
"},{"location":"ccr/#the-problem-with-traditional-compression","title":"The Problem with Traditional Compression","text":"Traditional compression is lossy \u2014 if you guess wrong about what's important, data is lost forever. This creates a difficult tradeoff:
- Aggressive compression: Risk losing data the LLM needs
- Conservative compression: Miss out on token savings
CCR eliminates this tradeoff.
"},{"location":"ccr/#ccr-enabled-components","title":"CCR-Enabled Components","text":"Component What it compresses CCR integration SmartCrusher JSON arrays (tool outputs) Stores original array, marker includes hash ContentRouter Code, logs, search results, text Stores original content by strategy IntelligentContextManager Messages (conversation turns) Stores dropped messages, marker includes hash"},{"location":"ccr/#how-ccr-works","title":"How CCR Works","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 TOOL OUTPUT (1000 items) \u2502\n\u2502 \u2514\u2500 SmartCrusher compresses to 20 items \u2502\n\u2502 \u2514\u2500 Original cached with hash=abc123 \u2502\n\u2502 \u2514\u2500 Retrieval tool injected into context \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LLM PROCESSING \u2502\n\u2502 Option A: LLM solves task with 20 items \u2192 Done (90% savings) \u2502\n\u2502 Option B: LLM calls headroom_retrieve(hash=abc123) \u2502\n\u2502 \u2192 Response Handler executes retrieval automatically \u2502\n\u2502 \u2192 LLM receives full data, responds accurately \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"ccr/#phase-1-compression-store","title":"Phase 1: Compression Store","text":"When SmartCrusher compresses tool output: 1. Original content is stored in an LRU cache 2. A hash key is generated for retrieval 3. A marker is added to the compressed output: [1000 items compressed to 20. Retrieve more: hash=abc123]
"},{"location":"ccr/#phase-2-tool-injection","title":"Phase 2: Tool Injection","text":"Headroom injects a headroom_retrieve tool into the LLM's available tools:
{\n \"name\": \"headroom_retrieve\",\n \"description\": \"Retrieve original uncompressed data from Headroom cache\",\n \"parameters\": {\n \"hash\": \"The hash key from the compression marker\",\n \"query\": \"Optional: search within the cached data\"\n }\n}\n
"},{"location":"ccr/#phase-3-response-handler","title":"Phase 3: Response Handler","text":"When the LLM calls headroom_retrieve: 1. Response Handler intercepts the tool call 2. Retrieves data from the local cache (~1ms) 3. Adds the result to the conversation 4. Continues the API call automatically
The client never sees CCR tool calls \u2014 they're handled transparently.
"},{"location":"ccr/#phase-4-context-tracker","title":"Phase 4: Context Tracker","text":"Across multiple turns, the Context Tracker: 1. Remembers what was compressed in earlier turns 2. Analyzes new queries for relevance to compressed content 3. Proactively expands relevant data before the LLM asks
Example:
Turn 1: User searches for files\n \u2192 Tool returns 500 files\n \u2192 SmartCrusher compresses to 15, caches original (hash=abc123)\n \u2192 LLM sees 15 files, answers question\n\nTurn 5: User asks \"What about the auth middleware?\"\n \u2192 Context Tracker detects \"auth\" might be in abc123\n \u2192 Proactively expands compressed content\n \u2192 LLM sees full file list, finds auth_middleware.py\n
"},{"location":"ccr/#message-level-ccr-intelligentcontext","title":"Message-Level CCR (IntelligentContext)","text":"IntelligentContextManager is a message-level compressor. When it drops low-importance messages to fit the context budget, those messages are stored in CCR:
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LONG CONVERSATION (100 messages, 50K tokens) \u2502\n\u2502 \u2514\u2500 IntelligentContext scores messages by importance \u2502\n\u2502 \u2514\u2500 Drops 60 low-scoring messages \u2502\n\u2502 \u2514\u2500 Dropped messages cached with hash=def456 \u2502\n\u2502 \u2514\u2500 Marker inserted: \"60 messages dropped, retrieve: def456\" \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LLM PROCESSING \u2502\n\u2502 Option A: LLM solves task with remaining messages \u2192 Done \u2502\n\u2502 Option B: LLM needs earlier context \u2502\n\u2502 \u2192 Calls headroom_retrieve(hash=def456) \u2502\n\u2502 \u2192 Full conversation restored \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
The marker includes the CCR reference:
[Earlier context compressed: 60 message(s) dropped by importance scoring.\nFull content available via ccr_retrieve tool with reference 'def456'.]\n
TOIN integration: When users retrieve dropped messages, TOIN learns to score those message patterns higher next time, improving future drop decisions across all users.
"},{"location":"ccr/#features","title":"Features","text":"Feature Description Automatic Response Handling When LLM calls headroom_retrieve, the proxy handles it automatically Multi-Turn Context Tracking Tracks compressed content across turns, proactively expands when relevant BM25 Search LLM can search within compressed data: headroom_retrieve(hash, query=\"errors\") Feedback Learning Learns from retrieval patterns to improve future compression"},{"location":"ccr/#configuration","title":"Configuration","text":"# Proxy with CCR enabled (default)\nheadroom proxy --port 8787\n\n# Disable CCR response handling\nheadroom proxy --no-ccr-responses\n\n# Disable proactive expansion\nheadroom proxy --no-ccr-expansion\n
"},{"location":"ccr/#why-this-matters","title":"Why This Matters","text":"Approach Risk Savings No compression None 0% Traditional compression Data loss 70-90% CCR compression None (reversible) 70-90% CCR gives you the savings of aggressive compression with zero risk \u2014 the LLM can always retrieve the original data if needed.
"},{"location":"ccr/#demo","title":"Demo","text":"Run the CCR demonstration to see it in action:
python examples/ccr_demo.py\n
Output:
1. COMPRESSION STORE\n Original: 100 items (7,059 chars)\n Compressed: 8 items (633 chars)\n Reduction: 91.0%\n\n3. RESPONSE HANDLER\n Detected CCR tool call: True\n Retrieved 100 items automatically\n\n4. CONTEXT TRACKER\n Turn 5: User asks \"show authentication middleware\"\n Tracker found 1 relevant context\n \u2192 relevance=0.73\n Proactively expanded: 100 items\n
"},{"location":"ccr/#architecture","title":"Architecture","text":"For implementation details, see ARCHITECTURE.md.
"},{"location":"compression/","title":"Universal Compression","text":"Headroom's Universal Compression module provides intelligent, automatic compression with ML-based content detection and structure preservation.
"},{"location":"compression/#overview","title":"Overview","text":"Universal Compression combines several techniques:
- ML-based Detection - Automatically detects content type (JSON, code, logs, text) using Magika
- Structure Preservation - Keeps keys, signatures, and templates intact via structure masks
- Intelligent Compression - Compresses content while preserving meaning with LLMLingua
- Reversible via CCR - Stores originals for retrieval when LLM needs full context
"},{"location":"compression/#quick-start","title":"Quick Start","text":""},{"location":"compression/#one-liner","title":"One-Liner","text":"from headroom.compression import compress\n\nresult = compress(content)\nprint(result.compressed)\nprint(f\"Saved {result.savings_percentage:.0f}% tokens\")\n
"},{"location":"compression/#with-configuration","title":"With Configuration","text":"from headroom.compression import UniversalCompressor, UniversalCompressorConfig\n\nconfig = UniversalCompressorConfig(\n compression_ratio_target=0.5, # Keep 50% of content\n use_entropy_preservation=True, # Preserve UUIDs, hashes\n)\n\ncompressor = UniversalCompressor(config=config)\nresult = compressor.compress(content)\n
"},{"location":"compression/#how-it-works","title":"How It Works","text":""},{"location":"compression/#detection-flow","title":"Detection Flow","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Content \u2502\u2500\u2500\u2500>\u2502 Detect \u2502\u2500\u2500\u2500>\u2502 Extract \u2502\u2500\u2500\u2500>\u2502 Compress \u2502\n\u2502 Input \u2502 \u2502 Type \u2502 \u2502 Structure \u2502 \u2502 Content \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502 \u2502\n \u25bc \u25bc \u25bc\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Magika \u2502 \u2502 Handler \u2502 \u2502 LLMLingua \u2502\n \u2502 (ML) \u2502 \u2502 (JSON, \u2502 \u2502 (optional) \u2502\n \u2502 \u2502 \u2502 Code...) \u2502 \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"compression/#structure-masks","title":"Structure Masks","text":"Structure masks identify what to preserve:
Content Type What's Preserved What's Compressed JSON Keys, brackets, booleans, nulls, short values, UUIDs Long string values, whitespace Code Imports, function signatures, class definitions, types Function bodies, comments Logs Timestamps, log levels, error messages Repeated patterns, verbose details Text High-entropy tokens (IDs, hashes) Low-information content"},{"location":"compression/#configuration","title":"Configuration","text":""},{"location":"compression/#universalcompressorconfig","title":"UniversalCompressorConfig","text":"from headroom.compression import UniversalCompressorConfig\n\nconfig = UniversalCompressorConfig(\n # Detection\n use_magika=True, # Use ML-based detection (requires magika)\n\n # Compression\n use_llmlingua=True, # Use LLMLingua for compression\n compression_ratio_target=0.3, # Keep 30% of content (70% reduction)\n min_content_length=100, # Skip content shorter than this\n\n # Structure preservation\n use_entropy_preservation=True, # Preserve high-entropy tokens\n entropy_threshold=0.85, # Entropy threshold for preservation\n\n # CCR\n ccr_enabled=True, # Store originals for retrieval\n)\n
"},{"location":"compression/#configuration-options","title":"Configuration Options","text":"Option Default Description use_magika True Use ML-based content detection use_llmlingua True Use LLMLingua for compression compression_ratio_target 0.3 Target ratio (0.3 = keep 30%) min_content_length 100 Minimum chars to compress use_entropy_preservation True Preserve high-entropy tokens entropy_threshold 0.85 Entropy threshold (0.0-1.0) ccr_enabled True Enable CCR storage"},{"location":"compression/#content-handlers","title":"Content Handlers","text":""},{"location":"compression/#json-handler","title":"JSON Handler","text":"Preserves JSON structure while compressing values:
from headroom.compression.handlers.json_handler import JSONStructureHandler\n\nhandler = JSONStructureHandler(\n preserve_short_values=True, # Keep values < 20 chars\n short_value_threshold=20, # Threshold for \"short\"\n preserve_high_entropy=True, # Keep UUIDs, hashes\n entropy_threshold=0.85, # Entropy threshold\n max_array_items_full=3, # Keep first N array items full\n max_number_digits=10, # Preserve numbers up to N digits\n)\n
What's Preserved: - All keys (navigational - LLM sees schema) - Structural syntax ({, }, [, ], :, ,) - Booleans and nulls (semantically important) - High-entropy strings (UUIDs, hashes - identifiers) - Short numbers (often IDs)
Example:
# Before\n{\n \"id\": \"usr_abc123\",\n \"name\": \"Alice Johnson\",\n \"bio\": \"A long description that goes on and on...\"\n}\n\n# After (structure preserved, long values compressed)\n{\n \"id\": \"usr_abc123\",\n \"name\": \"Alice Johnson\",\n \"bio\": \"A long...[compressed]...\"\n}\n
"},{"location":"compression/#code-handler","title":"Code Handler","text":"Preserves code structure using AST parsing (tree-sitter) or regex fallback:
from headroom.compression.handlers.code_handler import CodeStructureHandler\n\nhandler = CodeStructureHandler(\n preserve_comments=False, # Preserve comments as structural\n use_tree_sitter=True, # Use tree-sitter for parsing\n default_language=\"python\", # Default when detection fails\n)\n
What's Preserved: - Import statements - Function/method signatures - Class definitions - Type annotations - Decorators
What's Compressed: - Function bodies (implementations) - Comments (unless preserve_comments=True)
Example:
# Before\ndef process_data(items: List[str]) -> Dict[str, int]:\n \"\"\"Process items and count occurrences.\"\"\"\n result = {}\n for item in items:\n item = item.strip().lower()\n if item in result:\n result[item] += 1\n else:\n result[item] = 1\n return result\n\n# After (signature preserved, body compressed)\ndef process_data(items: List[str]) -> Dict[str, int]:\n \"\"\"Process items and count occurrences.\"\"\"\n result = {}\n for item in items:\n ...[compressed]...\n
"},{"location":"compression/#supported-languages","title":"Supported Languages","text":"Language Parser Support Level Python tree-sitter Full AST JavaScript tree-sitter Full AST TypeScript tree-sitter Full AST Go tree-sitter Full AST Rust tree-sitter Full AST Java tree-sitter Full AST C tree-sitter Full AST C++ tree-sitter Full AST"},{"location":"compression/#compression-result","title":"Compression Result","text":"from headroom.compression import compress\n\nresult = compress(content)\n\n# Access result fields\nprint(result.compressed) # Compressed content\nprint(result.original) # Original content\nprint(result.compression_ratio) # e.g., 0.35 (35% of original size)\nprint(result.tokens_before) # Estimated tokens before\nprint(result.tokens_after) # Estimated tokens after\nprint(result.tokens_saved) # tokens_before - tokens_after\nprint(result.savings_percentage) # e.g., 65.0 (65% savings)\n\n# Detection info\nprint(result.content_type) # ContentType.JSON, CODE, etc.\nprint(result.detection_confidence) # 0.0-1.0\n\n# Structure info\nprint(result.handler_used) # \"json\", \"code\", etc.\nprint(result.preservation_ratio) # Fraction preserved as structure\n\n# CCR info\nprint(result.ccr_key) # Key for retrieval (if CCR enabled)\n
"},{"location":"compression/#batch-compression","title":"Batch Compression","text":"For multiple contents, batch compression is more efficient:
from headroom.compression import UniversalCompressor\n\ncompressor = UniversalCompressor()\n\ncontents = [\n '{\"users\": [...]}',\n 'def hello(): pass',\n 'Plain text content',\n]\n\nresults = compressor.compress_batch(contents)\n\nfor result in results:\n print(f\"{result.content_type}: {result.savings_percentage:.0f}% saved\")\n
"},{"location":"compression/#custom-handlers","title":"Custom Handlers","text":"Register custom handlers for specific content types:
from headroom.compression import UniversalCompressor\nfrom headroom.compression.detector import ContentType\nfrom headroom.compression.handlers.base import BaseStructureHandler, HandlerResult\nfrom headroom.compression.masks import StructureMask\n\n\nclass LogStructureHandler(BaseStructureHandler):\n \"\"\"Custom handler for log content.\"\"\"\n\n def __init__(self):\n super().__init__(name=\"log\")\n\n def can_handle(self, content: str) -> bool:\n return \"[INFO]\" in content or \"[ERROR]\" in content\n\n def _extract_mask(self, content, tokens, **kwargs):\n # Mark timestamps and log levels as structural\n mask = [False] * len(content)\n # ... (custom logic)\n return HandlerResult(\n mask=StructureMask(tokens=tokens, mask=mask),\n handler_name=self.name,\n confidence=0.9,\n )\n\n\n# Register the custom handler\ncompressor = UniversalCompressor()\ncompressor.register_handler(ContentType.TEXT, LogStructureHandler())\n
"},{"location":"compression/#ccr-integration","title":"CCR Integration","text":"Universal Compression integrates with CCR (Compress-Cache-Retrieve) for reversible compression:
from headroom.compression import UniversalCompressor, UniversalCompressorConfig\n\nconfig = UniversalCompressorConfig(ccr_enabled=True)\ncompressor = UniversalCompressor(config=config)\n\nresult = compressor.compress(large_content)\n\n# CCR key for retrieval\nif result.ccr_key:\n print(f\"Original stored with key: {result.ccr_key}\")\n # LLM can request original via CCR when needed\n
See CCR Guide for full CCR documentation.
"},{"location":"compression/#performance","title":"Performance","text":"Content Type Compression Speed Accuracy JSON (large arrays) 70-90% ~1ms Keys preserved Code (Python) 50-70% ~10ms Signatures preserved Plain text 60-80% ~5ms High-entropy preserved Overhead: ~1-10ms per compression depending on content size and type.
"},{"location":"compression/#installation","title":"Installation","text":"# Basic compression (fallback to simple compression)\npip install headroom-ai\n\n# With ML detection (recommended)\npip install \"headroom-ai[magika]\"\n\n# With LLMLingua compression\npip install \"headroom-ai[llmlingua]\"\n\n# With AST-based code handling\npip install \"headroom-ai[code]\"\n\n# Everything\npip install \"headroom-ai[all]\"\n
"},{"location":"compression/#example-full-pipeline","title":"Example: Full Pipeline","text":"from headroom.compression import UniversalCompressor, UniversalCompressorConfig\n\n# Configure for aggressive compression\nconfig = UniversalCompressorConfig(\n compression_ratio_target=0.25, # Keep 25%\n use_magika=True,\n use_llmlingua=True,\n ccr_enabled=True,\n)\n\ncompressor = UniversalCompressor(config=config)\n\n# Compress JSON API response\njson_content = \"\"\"\n{\n \"users\": [\n {\"id\": \"usr_123\", \"name\": \"Alice\", \"bio\": \"Software engineer...\"},\n {\"id\": \"usr_456\", \"name\": \"Bob\", \"bio\": \"Product manager...\"}\n ],\n \"total\": 2,\n \"page\": 1\n}\n\"\"\"\n\nresult = compressor.compress(json_content)\n\nprint(f\"Type: {result.content_type}\") # ContentType.JSON\nprint(f\"Handler: {result.handler_used}\") # json\nprint(f\"Saved: {result.savings_percentage:.0f}%\") # ~60%\nprint(f\"Structure: {result.preservation_ratio:.0%} preserved\") # ~40%\nprint(f\"CCR Key: {result.ccr_key}\") # For retrieval\n
"},{"location":"compression/#see-also","title":"See Also","text":" - Transforms Reference - Other compression transforms
- CCR Guide - Reversible compression architecture
- Text Compression - Opt-in utilities for search/logs
"},{"location":"configuration/","title":"Configuration","text":"Headroom can be configured via the SDK, proxy command line, or per-request overrides.
"},{"location":"configuration/#sdk-configuration","title":"SDK Configuration","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n\n # Mode: \"audit\" (observe only) or \"optimize\" (apply transforms)\n default_mode=\"optimize\",\n\n # Enable provider-specific cache optimization\n enable_cache_optimizer=True,\n\n # Enable query-level semantic caching\n enable_semantic_cache=False,\n\n # Override default context limits per model\n model_context_limits={\n \"gpt-4o\": 128000,\n \"gpt-4o-mini\": 128000,\n },\n\n # Database location (defaults to temp directory)\n # store_url=\"sqlite:////absolute/path/to/headroom.db\",\n)\n
"},{"location":"configuration/#proxy-configuration","title":"Proxy Configuration","text":""},{"location":"configuration/#command-line-options","title":"Command Line Options","text":"headroom proxy \\\n --port 8787 \\ # Port to listen on\n --host 0.0.0.0 \\ # Host to bind to\n --budget 10.00 \\ # Daily budget limit in USD\n --log-file headroom.jsonl # Log file path\n
"},{"location":"configuration/#feature-flags","title":"Feature Flags","text":"# Disable optimization (passthrough mode)\nheadroom proxy --no-optimize\n\n# Disable semantic caching\nheadroom proxy --no-cache\n\n# Disable CCR response handling\nheadroom proxy --no-ccr-responses\n\n# Disable proactive expansion\nheadroom proxy --no-ccr-expansion\n\n# Enable LLMLingua ML compression\nheadroom proxy --llmlingua\nheadroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.4\n
"},{"location":"configuration/#all-options","title":"All Options","text":"headroom proxy --help\n
"},{"location":"configuration/#per-request-overrides","title":"Per-Request Overrides","text":"Override configuration for specific requests:
response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[...],\n\n # Override mode for this request\n headroom_mode=\"audit\",\n\n # Reserve more tokens for output\n headroom_output_buffer_tokens=8000,\n\n # Keep last N turns (don't compress)\n headroom_keep_turns=5,\n\n # Skip compression for specific tools\n headroom_tool_profiles={\n \"important_tool\": {\"skip_compression\": True}\n }\n)\n
"},{"location":"configuration/#modes","title":"Modes","text":"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"},{"location":"configuration/#simulate-mode","title":"Simulate Mode","text":"Preview what would happen without making an API call:
plan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=large_conversation,\n)\n\nprint(f\"Would save {plan.tokens_saved} tokens\")\nprint(f\"Transforms: {plan.transforms}\")\nprint(f\"Estimated savings: {plan.estimated_savings}\")\n
"},{"location":"configuration/#smartcrusher-configuration","title":"SmartCrusher Configuration","text":"Fine-tune JSON compression behavior:
from headroom.transforms import SmartCrusherConfig\n\nconfig = SmartCrusherConfig(\n # Maximum items to keep after compression\n max_items_after_crush=15,\n\n # Minimum tokens before applying compression\n min_tokens_to_crush=200,\n\n # Relevance scoring tier: \"bm25\" (fast) or \"embedding\" (accurate)\n relevance_tier=\"bm25\",\n\n # Always keep items with these field values\n preserve_fields=[\"error\", \"warning\", \"failure\"],\n)\n
"},{"location":"configuration/#cache-aligner-configuration","title":"Cache Aligner Configuration","text":"Control prefix stabilization:
from headroom.transforms import CacheAlignerConfig\n\nconfig = CacheAlignerConfig(\n # Enable/disable cache alignment\n enabled=True,\n\n # Patterns to extract from system prompt\n dynamic_patterns=[\n r\"Today is \\w+ \\d+, \\d{4}\",\n r\"Current time: .*\",\n ],\n)\n
"},{"location":"configuration/#rolling-window-configuration","title":"Rolling Window Configuration","text":"Control context window management:
from headroom.transforms import RollingWindowConfig\n\nconfig = RollingWindowConfig(\n # Minimum turns to always keep\n min_keep_turns=3,\n\n # Reserve tokens for output\n output_buffer_tokens=4000,\n\n # Drop oldest tool outputs first\n prefer_drop_tool_outputs=True,\n)\n
"},{"location":"configuration/#intelligent-context-manager-configuration","title":"Intelligent Context Manager Configuration","text":"For semantic-aware context management with importance scoring:
from headroom.config import IntelligentContextConfig, ScoringWeights\n\n# Customize scoring weights (must sum to 1.0, or will be normalized)\nweights = ScoringWeights(\n recency=0.20, # Newer messages score higher\n semantic_similarity=0.20, # Similarity to recent context\n toin_importance=0.25, # TOIN-learned retrieval patterns\n error_indicator=0.15, # TOIN-learned error field types\n forward_reference=0.15, # Messages referenced by later messages\n token_density=0.05, # Information density\n)\n\nconfig = IntelligentContextConfig(\n # Enable/disable the manager\n enabled=True,\n\n # Protection settings\n keep_system=True, # Never drop system messages\n keep_last_turns=2, # Protect last N user turns\n\n # Token budget\n output_buffer_tokens=4000, # Reserve for model output\n\n # Scoring settings\n use_importance_scoring=True, # Use semantic scoring (vs position-only)\n scoring_weights=weights, # Custom weights\n toin_integration=True, # Use TOIN patterns if available\n recency_decay_rate=0.1, # Exponential decay lambda\n\n # Strategy thresholds\n compress_threshold=0.1, # Try compression first if <10% over budget\n)\n
"},{"location":"configuration/#ccr-integration","title":"CCR Integration","text":"When IntelligentContext drops messages, they're stored in CCR for potential retrieval:
from headroom.telemetry import get_toin\n\n# Pass TOIN for bidirectional integration\ntoin = get_toin()\nmanager = IntelligentContextManager(config=config, toin=toin)\n\n# Dropped messages are:\n# 1. Stored in CCR (so LLM can retrieve if needed)\n# 2. Recorded to TOIN (so it learns which patterns matter)\n# 3. Marked with CCR reference in the inserted message\n
The marker inserted when messages are dropped includes the CCR reference:
[Earlier context compressed: 14 message(s) dropped by importance scoring.\nFull content available via ccr_retrieve tool with reference 'abc123def456'.]\n
"},{"location":"configuration/#scoring-weights","title":"Scoring Weights","text":"The ScoringWeights class controls how messages are scored:
Weight Default Description recency 0.20 Exponential decay from conversation end semantic_similarity 0.20 Embedding cosine similarity to recent context toin_importance 0.25 TOIN retrieval_rate (high retrieval = important) error_indicator 0.15 TOIN field_semantics error detection forward_reference 0.15 Count of later messages referencing this one token_density 0.05 Unique tokens / total tokens Weights are automatically normalized to sum to 1.0:
weights = ScoringWeights(recency=1.0, toin_importance=1.0)\nnormalized = weights.normalized()\n# recency=0.5, toin_importance=0.5, others=0.0\n
"},{"location":"configuration/#environment-variables","title":"Environment Variables","text":"Some settings can be configured via environment variables:
Variable Description Default HEADROOM_LOG_LEVEL Logging level INFO HEADROOM_STORE_URL Database URL temp directory HEADROOM_DEFAULT_MODE Default mode optimize HEADROOM_MODEL_LIMITS Custom model config (JSON string or file path) -"},{"location":"configuration/#custom-model-configuration","title":"Custom Model Configuration","text":"Configure context limits and pricing for new or custom models. Useful when: - A new model is released before Headroom is updated - You're using fine-tuned or custom models - You want to override built-in limits
"},{"location":"configuration/#configuration-methods","title":"Configuration Methods","text":"Settings are resolved in this order (later overrides earlier): 1. Built-in defaults 2. ~/.headroom/models.json config file 3. HEADROOM_MODEL_LIMITS environment variable 4. SDK constructor arguments
"},{"location":"configuration/#config-file-format","title":"Config File Format","text":"Create ~/.headroom/models.json:
{\n \"anthropic\": {\n \"context_limits\": {\n \"claude-4-opus-20250301\": 200000,\n \"claude-custom-finetune\": 128000\n },\n \"pricing\": {\n \"claude-4-opus-20250301\": {\n \"input\": 15.00,\n \"output\": 75.00,\n \"cached_input\": 1.50\n }\n }\n },\n \"openai\": {\n \"context_limits\": {\n \"gpt-5\": 256000,\n \"ft:gpt-4o:my-org\": 128000\n },\n \"pricing\": {\n \"gpt-5\": [5.00, 15.00]\n }\n }\n}\n
"},{"location":"configuration/#environment-variable","title":"Environment Variable","text":"Set HEADROOM_MODEL_LIMITS as a JSON string or file path:
# JSON string\nexport HEADROOM_MODEL_LIMITS='{\"anthropic\":{\"context_limits\":{\"claude-new\":200000}}}'\n\n# File path\nexport HEADROOM_MODEL_LIMITS=/path/to/models.json\n
"},{"location":"configuration/#pattern-based-inference","title":"Pattern-Based Inference","text":"Unknown models are automatically 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 o1*, o3* 200K context, reasoning model pricing This means new models like claude-4-sonnet-20251201 will work automatically with Sonnet-tier defaults.
"},{"location":"configuration/#sdk-override","title":"SDK Override","text":"Override in code for specific models:
from headroom import HeadroomClient, AnthropicProvider\n\nclient = HeadroomClient(\n original_client=Anthropic(),\n provider=AnthropicProvider(\n context_limits={\n \"claude-new-model\": 300000,\n }\n ),\n)\n
"},{"location":"configuration/#provider-specific-settings","title":"Provider-Specific Settings","text":""},{"location":"configuration/#openai","title":"OpenAI","text":"from headroom import OpenAIProvider\n\nprovider = OpenAIProvider(\n # Enable automatic prefix caching\n enable_prefix_caching=True,\n)\n
"},{"location":"configuration/#anthropic","title":"Anthropic","text":"from headroom import AnthropicProvider\n\nprovider = AnthropicProvider(\n # Enable cache_control blocks\n enable_cache_control=True,\n)\n
"},{"location":"configuration/#google","title":"Google","text":"from headroom import GoogleProvider\n\nprovider = GoogleProvider(\n # Enable context caching\n enable_context_caching=True,\n)\n
"},{"location":"configuration/#configuration-precedence","title":"Configuration Precedence","text":"Settings are applied in this order (later overrides earlier):
- Default values
- Environment variables
- SDK constructor arguments
- Per-request overrides
"},{"location":"configuration/#validation","title":"Validation","text":"Validate your configuration:
result = client.validate_setup()\n\nif not result[\"valid\"]:\n print(\"Configuration issues:\")\n for issue in result[\"issues\"]:\n print(f\" - {issue}\")\n
"},{"location":"configuration/#typescript-sdk-configuration","title":"TypeScript SDK Configuration","text":"The TypeScript SDK is configured via environment variables or constructor options.
"},{"location":"configuration/#environment-variables_1","title":"Environment Variables","text":"Variable Description Default HEADROOM_BASE_URL Base URL of the Headroom proxy or cloud API http://localhost:8787 HEADROOM_API_KEY API key for Headroom Cloud authentication -"},{"location":"configuration/#usage","title":"Usage","text":"export HEADROOM_BASE_URL=http://localhost:8787\nexport HEADROOM_API_KEY=your-api-key\n
import { HeadroomClient } from 'headroom-ai';\n\n// Reads from HEADROOM_BASE_URL and HEADROOM_API_KEY automatically\nconst client = new HeadroomClient();\n\n// Or configure explicitly\nconst client = new HeadroomClient({\n baseUrl: 'http://localhost:8787',\n apiKey: 'your-api-key',\n});\n
See the TypeScript SDK Guide for full configuration options.
"},{"location":"errors/","title":"Error Handling","text":"Headroom provides explicit exceptions for debugging, with a safety guarantee that compression failures never break your LLM calls.
"},{"location":"errors/#exception-hierarchy","title":"Exception Hierarchy","text":"from headroom import (\n HeadroomError, # Base class - catch all Headroom errors\n ConfigurationError, # Invalid configuration\n ProviderError, # Provider issues (unknown model, etc.)\n StorageError, # Database/storage failures\n CompressionError, # Compression failures (rare)\n ValidationError, # Setup validation failures\n)\n
"},{"location":"errors/#usage","title":"Usage","text":"from headroom import (\n HeadroomClient,\n HeadroomError,\n ConfigurationError,\n StorageError,\n)\n\ntry:\n client = HeadroomClient(...)\n response = client.chat.completions.create(...)\n\nexcept ConfigurationError as e:\n print(f\"Config issue: {e}\")\n print(f\"Details: {e.details}\") # Additional context\n\nexcept StorageError as e:\n print(f\"Storage issue: {e}\")\n # Headroom continues to work, just without metrics persistence\n\nexcept HeadroomError as e:\n print(f\"Headroom error: {e}\")\n
"},{"location":"errors/#exception-types","title":"Exception Types","text":""},{"location":"errors/#configurationerror","title":"ConfigurationError","text":"Raised when configuration is invalid.
# Examples:\n# - Invalid mode value\n# - Missing required provider\n# - Invalid model context limit\n\ntry:\n client = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"invalid_mode\", # Will raise ConfigurationError\n )\nexcept ConfigurationError as e:\n print(f\"Config error: {e}\")\n print(f\"Field: {e.details.get('field')}\")\n
"},{"location":"errors/#providererror","title":"ProviderError","text":"Raised for provider-specific issues.
# Examples:\n# - Unknown model name\n# - Provider API error\n# - Token counting failure\n\ntry:\n response = client.chat.completions.create(\n model=\"unknown-model-xyz\",\n messages=[...]\n )\nexcept ProviderError as e:\n print(f\"Provider error: {e}\")\n print(f\"Provider: {e.details.get('provider')}\")\n
"},{"location":"errors/#storageerror","title":"StorageError","text":"Raised when database operations fail.
# Examples:\n# - Database connection failure\n# - Write permission denied\n# - Disk full\n\ntry:\n metrics = client.get_metrics()\nexcept StorageError as e:\n print(f\"Storage error: {e}\")\n # Application can continue - just won't have metrics\n
"},{"location":"errors/#compressionerror","title":"CompressionError","text":"Raised when compression fails (rare).
# Examples:\n# - Malformed JSON in tool output\n# - Unexpected data structure\n\n# Note: In practice, compression errors are caught internally\n# and the original content passes through unchanged.\n# This exception is only raised if you explicitly enable strict mode.\n
"},{"location":"errors/#validationerror","title":"ValidationError","text":"Raised when setup validation fails.
result = client.validate_setup()\nif not result[\"valid\"]:\n raise ValidationError(\n \"Setup validation failed\",\n details={\"issues\": result[\"issues\"]}\n )\n
"},{"location":"errors/#safety-guarantee","title":"Safety Guarantee","text":"If compression fails, the original content passes through unchanged.
This is a core design principle. Your LLM calls never fail due to Headroom:
# Even if SmartCrusher encounters unexpected data:\nmessages = [\n {\"role\": \"tool\", \"content\": \"malformed json {{{\"}\n]\n\n# This will NOT raise an exception\n# Instead, the malformed content passes through unchanged\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages\n)\n
"},{"location":"errors/#logging-errors","title":"Logging Errors","text":"Enable logging to see error details:
import logging\nlogging.basicConfig(level=logging.WARNING)\n\n# Now you'll see warnings when compression is skipped:\n# WARNING:headroom.transforms.smart_crusher:Skipping compression: invalid JSON\n
"},{"location":"errors/#error-details","title":"Error Details","text":"All Headroom exceptions include a details dict with context:
try:\n client = HeadroomClient(...)\nexcept HeadroomError as e:\n print(f\"Error: {e}\")\n print(f\"Type: {type(e).__name__}\")\n print(f\"Details: {e.details}\")\n\n # Details might include:\n # - field: which config field caused the error\n # - provider: which provider was involved\n # - model: which model was requested\n # - original_error: underlying exception\n
"},{"location":"errors/#best-practices","title":"Best Practices","text":""},{"location":"errors/#1-catch-specific-exceptions","title":"1. Catch Specific Exceptions","text":"# Good: catch specific exceptions\ntry:\n response = client.chat.completions.create(...)\nexcept ConfigurationError:\n # Handle config issues\n pass\nexcept ProviderError:\n # Handle provider issues\n pass\n\n# Avoid: catching all exceptions\ntry:\n response = client.chat.completions.create(...)\nexcept Exception:\n # Too broad - might hide real bugs\n pass\n
"},{"location":"errors/#2-let-storageerror-pass","title":"2. Let StorageError Pass","text":"# Storage errors don't affect core functionality\ntry:\n metrics = client.get_metrics()\nexcept StorageError:\n metrics = [] # Continue without historical metrics\n
"},{"location":"errors/#3-validate-on-startup","title":"3. Validate on Startup","text":"client = HeadroomClient(...)\n\n# Validate once at startup\nresult = client.validate_setup()\nif not result[\"valid\"]:\n raise SystemExit(f\"Headroom setup invalid: {result['issues']}\")\n\n# Then use client normally\nresponse = client.chat.completions.create(...)\n
"},{"location":"errors/#debugging","title":"Debugging","text":""},{"location":"errors/#enable-debug-logging","title":"Enable Debug Logging","text":"import logging\nlogging.basicConfig(level=logging.DEBUG)\n\n# Shows detailed transform decisions\n# DEBUG:headroom.transforms.smart_crusher:Analyzing 1000 items...\n# DEBUG:headroom.transforms.smart_crusher:Kept 15 items (errors: 2, anomalies: 3)\n
"},{"location":"errors/#check-stats-after-error","title":"Check Stats After Error","text":"try:\n response = client.chat.completions.create(...)\nexcept HeadroomError:\n # Check what happened\n stats = client.get_stats()\n print(f\"Last request stats: {stats}\")\n
"},{"location":"getting-started/","title":"Getting Started with Headroom","text":"This guide will help you get up and running with Headroom in under 5 minutes.
"},{"location":"getting-started/#installation","title":"Installation","text":"Python:
# Core package (minimal dependencies)\npip install headroom\n\n# With proxy server\npip install headroom[proxy]\n\n# With semantic relevance (for smarter compression)\npip install headroom[relevance]\n\n# Everything\npip install headroom[all]\n
TypeScript / Node.js:
npm install headroom-ai\n
"},{"location":"getting-started/#quick-start-proxy-mode-recommended","title":"Quick Start: Proxy Mode (Recommended)","text":"The easiest way to use Headroom is as a proxy server:
# Start the proxy\nheadroom proxy --port 8787\n
Then point your LLM client at it:
# Claude Code\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n\n# OpenAI-compatible clients\nOPENAI_BASE_URL=http://localhost:8787/v1 your-app\n
That's it! All your requests now go through Headroom and get optimized automatically.
"},{"location":"getting-started/#quick-start-python-sdk","title":"Quick Start: Python SDK","text":"If you want programmatic control:
from headroom import HeadroomClient\nfrom openai import OpenAI\n\n# Create a wrapped client\nclient = HeadroomClient(\n original_client=OpenAI(),\n default_mode=\"optimize\",\n)\n\n# Use exactly like the original\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[\n {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Hello!\"},\n ],\n)\n
"},{"location":"getting-started/#modes","title":"Modes","text":""},{"location":"getting-started/#audit-mode","title":"Audit Mode","text":"Observe without modifying:
client = HeadroomClient(\n original_client=OpenAI(),\n default_mode=\"audit\",\n)\n# Logs metrics but doesn't change requests\n
"},{"location":"getting-started/#optimize-mode","title":"Optimize Mode","text":"Apply transforms to reduce tokens:
client = HeadroomClient(\n original_client=OpenAI(),\n default_mode=\"optimize\",\n)\n# Compresses tool outputs, aligns cache prefixes, etc.\n
"},{"location":"getting-started/#simulate-mode","title":"Simulate Mode","text":"Preview what optimizations would do:
plan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=[...],\n)\nprint(f\"Would save {plan.tokens_saved} tokens\")\nprint(f\"Transforms: {plan.transforms_applied}\")\n
"},{"location":"getting-started/#next-steps","title":"Next Steps","text":" - Proxy Server Documentation - Configure the proxy
- Transforms Reference - Understand each transform
- API Reference - Full API documentation
"},{"location":"image-compression/","title":"Image Compression","text":"Headroom automatically compresses images in your LLM requests, reducing token usage by 40-90% while maintaining answer accuracy.
"},{"location":"image-compression/#overview","title":"Overview","text":"Vision models charge by the token, and images are expensive: - A 1024x1024 image costs ~765 tokens (OpenAI) - A 2048x2048 image costs ~2,900 tokens
Headroom's image compression uses a trained ML router to analyze your query and automatically select the optimal compression technique:
Technique Savings When Used full_low ~87% General questions (\"What is this?\") preserve 0% Fine details needed (\"Count the whiskers\") crop 50-90% Region-specific (\"What's in the corner?\") transcode ~99% Text extraction (\"Read the sign\")"},{"location":"image-compression/#how-it-works","title":"How It Works","text":"User uploads image + asks question\n \u2193\n [Query Analysis]\n TrainedRouter (MiniLM from HuggingFace)\n Classifies: \"What animal is this?\" \u2192 full_low\n \u2193\n [Image Analysis]\n SigLIP analyzes image properties\n (has text? complex? fine details?)\n \u2193\n [Apply Compression]\n OpenAI: detail=\"low\"\n Anthropic: Resize to 512px\n Google: Resize to 768px\n \u2193\n Compressed request to LLM\n
"},{"location":"image-compression/#quick-start","title":"Quick Start","text":""},{"location":"image-compression/#with-headroom-proxy-zero-code-changes","title":"With Headroom Proxy (Zero Code Changes)","text":"# Start the proxy\nheadroom proxy --port 8787\n\n# Connect your client\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n
Images are automatically compressed based on your queries.
"},{"location":"image-compression/#with-headroomclient","title":"With HeadroomClient","text":"from headroom import HeadroomClient\n\nclient = HeadroomClient(provider=\"openai\")\n\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": \"What animal is this?\"},\n {\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/jpeg;base64,...\"}}\n ]\n }]\n)\n# Image automatically compressed with detail=\"low\" (87% savings)\n
"},{"location":"image-compression/#direct-api","title":"Direct API","text":"from headroom.image import ImageCompressor\n\ncompressor = ImageCompressor()\n\n# Compress images in messages\ncompressed_messages = compressor.compress(messages, provider=\"openai\")\n\n# Check savings\nprint(f\"Saved {compressor.last_savings:.0f}% tokens\")\nprint(f\"Technique: {compressor.last_result.technique.value}\")\n
"},{"location":"image-compression/#configuration","title":"Configuration","text":""},{"location":"image-compression/#proxy-configuration","title":"Proxy Configuration","text":"# Enable image compression (default: true)\nheadroom proxy --image-optimize\n\n# Disable image compression\nheadroom proxy --no-image-optimize\n
"},{"location":"image-compression/#programmatic-configuration","title":"Programmatic Configuration","text":"from headroom.image import ImageCompressor\n\ncompressor = ImageCompressor(\n model_id=\"chopratejas/technique-router\", # HuggingFace model\n use_siglip=True, # Enable image analysis\n device=\"cuda\", # Use GPU if available\n)\n
"},{"location":"image-compression/#provider-support","title":"Provider Support","text":"Provider Detection Compression Method OpenAI image_url Sets detail=\"low\" Anthropic image with source Resizes to 512px Google inlineData Resizes to 768px (tile-optimized)"},{"location":"image-compression/#openai","title":"OpenAI","text":"Uses the native detail parameter:
# Before\n{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:...\"}}\n\n# After (full_low technique)\n{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:...\", \"detail\": \"low\"}}\n
"},{"location":"image-compression/#anthropic","title":"Anthropic","text":"Resizes the image using PIL:
# Before: 1024x1024 image (~1,398 tokens)\n# After: 512x512 image (~349 tokens) - 75% savings\n
"},{"location":"image-compression/#google-gemini","title":"Google Gemini","text":"Resizes to 768px (optimal for Gemini's 768x768 tile system):
# Before: 1536x1536 image (4 tiles \u00d7 258 = 1,032 tokens)\n# After: 768x768 image (1 tile \u00d7 258 = 258 tokens) - 75% savings\n
"},{"location":"image-compression/#techniques-explained","title":"Techniques Explained","text":""},{"location":"image-compression/#full_low-87-savings","title":"full_low (87% savings)","text":"Best for general understanding questions: - \"What is this?\" - \"Describe the scene\" - \"Is this indoors or outdoors?\"
The model doesn't need fine details to answer these questions.
"},{"location":"image-compression/#preserve-0-savings","title":"preserve (0% savings)","text":"Required when fine details matter: - \"Count the whiskers\" - \"What brand is shown?\" - \"Read the serial number\" - \"What time does the clock show?\"
"},{"location":"image-compression/#crop-50-90-savings","title":"crop (50-90% savings)","text":"For region-specific queries: - \"What's in the top-right corner?\" - \"Focus on the background\" - \"Zoom into the left side\"
Note: Currently implemented as resize. True cropping coming soon.
"},{"location":"image-compression/#transcode-99-savings","title":"transcode (99% savings)","text":"For text extraction (converts image to text): - \"Read the sign\" - \"What does it say?\" - \"Transcribe the document\"
Note: Requires vision model call. Currently falls back to preserve.
"},{"location":"image-compression/#the-trained-router","title":"The Trained Router","text":"The routing decision is made by a fine-tuned MiniLM classifier:
- Model:
chopratejas/technique-router on HuggingFace - Size: ~128MB
- Accuracy: 93.7% on validation set
- Training data: 1,157 examples across 4 techniques
The model is downloaded automatically on first use and cached locally.
"},{"location":"image-compression/#training-data-examples","title":"Training Data Examples","text":"Query Technique \"What animal is this?\" full_low \"Count the spots\" preserve \"Read the text on the sign\" transcode \"What's in the corner?\" crop"},{"location":"image-compression/#performance","title":"Performance","text":""},{"location":"image-compression/#token-savings-by-query-type","title":"Token Savings by Query Type","text":"Query Type Before After Savings General (\"What is this?\") 765 85 89% Detail (\"Count items\") 765 765 0% Region (\"Top corner?\") 765 85 89% Text (\"Read the sign\") 765 85 89%"},{"location":"image-compression/#latency","title":"Latency","text":" - Router inference: ~10ms (CPU), ~2ms (GPU)
- Image resize: ~5-20ms depending on size
- First request: +2-3s (model download, cached after)
"},{"location":"image-compression/#troubleshooting","title":"Troubleshooting","text":""},{"location":"image-compression/#model-download-issues","title":"Model Download Issues","text":"The HuggingFace model downloads on first use:
# Force a specific cache directory\nimport os\nos.environ[\"HF_HOME\"] = \"/path/to/cache\"\n\nfrom headroom.image import ImageCompressor\ncompressor = ImageCompressor()\n
"},{"location":"image-compression/#gpu-memory","title":"GPU Memory","text":"SigLIP requires ~400MB GPU memory. To use CPU only:
compressor = ImageCompressor(device=\"cpu\")\n
"},{"location":"image-compression/#disable-image-compression","title":"Disable Image Compression","text":"# Proxy\nheadroom proxy --no-image-optimize\n\n# Direct\n# Simply don't call compress()\n
"},{"location":"image-compression/#api-reference","title":"API Reference","text":""},{"location":"image-compression/#imagecompressor","title":"ImageCompressor","text":"class ImageCompressor:\n def __init__(\n self,\n model_id: str = \"chopratejas/technique-router\",\n use_siglip: bool = True,\n device: str | None = None,\n ): ...\n\n def has_images(self, messages: list[dict]) -> bool:\n \"\"\"Check if messages contain images.\"\"\"\n\n def compress(\n self,\n messages: list[dict],\n provider: str = \"openai\",\n ) -> list[dict]:\n \"\"\"Compress images in messages.\"\"\"\n\n @property\n def last_result(self) -> CompressionResult | None:\n \"\"\"Result of last compression.\"\"\"\n\n @property\n def last_savings(self) -> float:\n \"\"\"Savings percentage from last compression.\"\"\"\n
"},{"location":"image-compression/#compressionresult","title":"CompressionResult","text":"@dataclass\nclass CompressionResult:\n technique: Technique # full_low, preserve, crop, transcode\n original_tokens: int # Estimated tokens before\n compressed_tokens: int # Estimated tokens after\n confidence: float # Router confidence (0-1)\n\n @property\n def savings_percent(self) -> float:\n \"\"\"Percentage of tokens saved.\"\"\"\n
"},{"location":"image-compression/#technique","title":"Technique","text":"class Technique(Enum):\n FULL_LOW = \"full_low\" # 87% savings\n PRESERVE = \"preserve\" # 0% savings\n CROP = \"crop\" # 50-90% savings\n TRANSCODE = \"transcode\" # 99% savings\n
"},{"location":"image-compression/#see-also","title":"See Also","text":" - Compression Guide - Text compression techniques
- CCR Guide - Reversible compression with retrieval
- Proxy Guide - Zero-code deployment
- Architecture - System design
"},{"location":"integration-guide/","title":"Integration Guide","text":"You don't need to run the Headroom proxy. Headroom is a compression library that works with any LLM client, proxy, or framework.
"},{"location":"integration-guide/#pick-your-path","title":"Pick Your Path","text":"You have... Use this Setup Any Python app compress() 2 lines LiteLLM LiteLLM callback 1 line A Python proxy (FastAPI, custom) ASGI middleware 1 line Claude Code / Cursor Headroom proxy 1 env var Agno agents Agno integration Wrap model LangChain LangChain integration Wrap model Non-Python app Headroom proxy HTTP TypeScript SDK compress() npm install headroom-ai Vercel AI SDK headroomMiddleware() Middleware adapter OpenAI Node SDK withHeadroom() Client wrapper Anthropic TS SDK withHeadroom() Client wrapper"},{"location":"integration-guide/#compress-function","title":"compress() Function","text":"The simplest integration. Works with any LLM client.
from headroom import compress\n\n# Before sending to your LLM:\nresult = compress(messages, model=\"claude-sonnet-4-5-20250929\")\nresponse = your_client.create(messages=result.messages) # Fewer tokens, same answer\n\nprint(f\"Saved {result.tokens_saved} tokens ({result.compression_ratio:.0%})\")\n
"},{"location":"integration-guide/#with-anthropic-sdk","title":"With Anthropic SDK","text":"from anthropic import Anthropic\nfrom headroom import compress\n\nclient = Anthropic()\nmessages = [\n {\"role\": \"user\", \"content\": \"What went wrong?\"},\n {\"role\": \"assistant\", \"content\": \"Let me check.\", \"tool_use\": [...]},\n {\"role\": \"user\", \"content\": [{\"type\": \"tool_result\", \"content\": huge_json}]},\n]\n\ncompressed = compress(messages, model=\"claude-sonnet-4-5-20250929\")\nresponse = client.messages.create(\n model=\"claude-sonnet-4-5-20250929\",\n messages=compressed.messages,\n max_tokens=1000,\n)\n
"},{"location":"integration-guide/#with-openai-sdk","title":"With OpenAI SDK","text":"from openai import OpenAI\nfrom headroom import compress\n\nclient = OpenAI()\nmessages = [\n {\"role\": \"user\", \"content\": \"Analyze these results\"},\n {\"role\": \"tool\", \"content\": big_json_output, \"tool_call_id\": \"call_1\"},\n]\n\ncompressed = compress(messages, model=\"gpt-4o\")\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=compressed.messages,\n)\n
"},{"location":"integration-guide/#with-litellm-direct","title":"With LiteLLM (direct)","text":"import litellm\nfrom headroom import compress\n\nmessages = [...]\ncompressed = compress(messages, model=\"bedrock/claude-sonnet\")\nresponse = litellm.completion(model=\"bedrock/claude-sonnet\", messages=compressed.messages)\n
"},{"location":"integration-guide/#with-any-http-client","title":"With any HTTP client","text":"import httpx\nfrom headroom import compress\n\ncompressed = compress(messages, model=\"claude-sonnet-4-5-20250929\")\nhttpx.post(\"https://api.anthropic.com/v1/messages\", json={\n \"model\": \"claude-sonnet-4-5-20250929\",\n \"messages\": compressed.messages,\n}, headers={\"X-Api-Key\": api_key, \"anthropic-version\": \"2023-06-01\"})\n
"},{"location":"integration-guide/#what-compress-returns","title":"What compress() returns","text":"result = compress(messages, model=\"gpt-4o\")\nresult.messages # list[dict] \u2014 compressed messages, same format as input\nresult.tokens_before # int \u2014 original token count\nresult.tokens_after # int \u2014 compressed token count\nresult.tokens_saved # int \u2014 tokens removed\nresult.compression_ratio # float \u2014 0.0 (no savings) to 1.0 (100% removed)\nresult.transforms_applied # list[str] \u2014 what ran (e.g., [\"router:smart_crusher:0.35\"])\n
"},{"location":"integration-guide/#litellm","title":"LiteLLM","text":"If you're already using LiteLLM as your LLM gateway, add Headroom as a callback:
import litellm\nfrom headroom.integrations.litellm_callback import HeadroomCallback\n\nlitellm.callbacks = [HeadroomCallback()]\n\n# All calls now compressed automatically\nresponse = litellm.completion(model=\"gpt-4o\", messages=[...])\nresponse = litellm.completion(model=\"bedrock/claude-sonnet\", messages=[...])\nresponse = litellm.completion(model=\"azure/gpt-4o\", messages=[...])\n
The callback compresses messages in LiteLLM's pre_call_hook before they're sent to the provider. Works with all 100+ LiteLLM-supported providers.
"},{"location":"integration-guide/#with-litellm-proxy","title":"With LiteLLM Proxy","text":"If you run LiteLLM as a proxy server, use the ASGI middleware instead:
# In your LiteLLM proxy startup\nfrom litellm.proxy.proxy_server import app\nfrom headroom.integrations.asgi import CompressionMiddleware\n\napp.add_middleware(CompressionMiddleware)\n
Or use the callback in your LiteLLM config:
# litellm_config.yaml\nlitellm_settings:\n callbacks: [\"headroom.integrations.litellm_callback.HeadroomCallback\"]\n
"},{"location":"integration-guide/#asgi-middleware","title":"ASGI Middleware","text":"Drop-in middleware for any ASGI application (FastAPI, Starlette, LiteLLM proxy, custom proxies).
from headroom.integrations.asgi import CompressionMiddleware\n\n# FastAPI\napp = FastAPI()\napp.add_middleware(CompressionMiddleware)\n\n# Starlette\napp = Starlette(routes=[...])\napp.add_middleware(CompressionMiddleware)\n\n# LiteLLM proxy\nfrom litellm.proxy.proxy_server import app\napp.add_middleware(CompressionMiddleware)\n
The middleware intercepts POST requests to /v1/messages, /v1/chat/completions, /v1/responses, and /chat/completions. All other requests pass through untouched.
Response headers include: - x-headroom-compressed: true \u2014 compression was applied - x-headroom-tokens-saved: 1234 \u2014 tokens removed
"},{"location":"integration-guide/#proxy","title":"Proxy","text":"The Headroom proxy is a standalone HTTP server. Best for non-Python apps or tools that only support base URL configuration (Claude Code, Cursor).
pip install \"headroom-ai[all]\"\nheadroom proxy --port 8787\n
# Claude Code\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n\n# Cursor / Any OpenAI client\nOPENAI_BASE_URL=http://localhost:8787/v1 cursor\n
"},{"location":"integration-guide/#with-cloud-providers","title":"With Cloud Providers","text":"# AWS Bedrock\nheadroom proxy --backend bedrock --region us-east-1\n\n# Google Vertex AI\nheadroom proxy --backend vertex_ai --region us-central1\n\n# Azure OpenAI\nheadroom proxy --backend azure\n\n# OpenRouter (400+ models)\nOPENROUTER_API_KEY=sk-or-... headroom proxy --backend openrouter\n
See Proxy Documentation for all options.
"},{"location":"integration-guide/#agno","title":"Agno","text":"Full integration with the Agno agent framework.
from agno.agent import Agent\nfrom agno.models.anthropic import Claude\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\nmodel = HeadroomAgnoModel(Claude(id=\"claude-sonnet-4-20250514\"))\nagent = Agent(model=model, tools=[your_tools])\nresponse = agent.run(\"Investigate the issue\")\n\nprint(f\"Tokens saved: {model.total_tokens_saved}\")\n
See Agno Guide for hooks, multi-provider, and streaming.
"},{"location":"integration-guide/#langchain","title":"LangChain","text":"Full integration with LangChain \u2014 chat models, memory, retrievers, tool wrappers, and streaming.
from langchain_openai import ChatOpenAI\nfrom headroom.integrations import HeadroomChatModel\n\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\nresponse = llm.invoke(\"Hello!\")\n
See LangChain Guide for details and known limitations.
"},{"location":"integration-guide/#typescript-sdk","title":"TypeScript SDK","text":"For Node.js, Next.js, and any TypeScript/JavaScript application.
npm install headroom-ai\n
See the TypeScript SDK Guide for full documentation including Vercel AI SDK middleware, OpenAI SDK wrapper, and Anthropic SDK wrapper.
"},{"location":"integration-guide/#openclaw","title":"OpenClaw","text":"Context compression plugin for OpenClaw agents.
pip install \"headroom-ai[proxy]\"\nopenclaw plugins install headroom-openclaw\n
Configure as context engine:
{ \"plugins\": { \"slots\": { \"contextEngine\": \"headroom\" } } }\n
The plugin auto-detects a running Headroom proxy or starts one. Compression happens in assemble() \u2014 zero changes to the agent's behavior.
See the OpenClaw plugin documentation for full setup.
"},{"location":"integration-guide/#compression-hooks-advanced","title":"Compression Hooks (Advanced)","text":"Customize compression behavior without modifying Headroom's code:
from headroom import compress, CompressionHooks, CompressContext\n\nclass MyHooks(CompressionHooks):\n def pre_compress(self, messages, ctx):\n # Modify messages before compression (dedup, filter, inject)\n return messages\n\n def compute_biases(self, messages, ctx):\n # Per-message compression aggressiveness\n # >1.0 = keep more, <1.0 = compress more\n return {5: 1.5, 6: 0.5} # Keep message 5, compress message 6\n\n def post_compress(self, event):\n # Observe results (logging, analytics, learning)\n print(f\"Saved {event.tokens_saved} tokens\")\n\nresult = compress(messages, model=\"gpt-4o\", hooks=MyHooks())\n
See Architecture for how hooks integrate with the pipeline.
"},{"location":"integration-guide/#faq","title":"FAQ","text":"Q: Does Headroom change the response format? No. Your LLM returns the same response format. Headroom only modifies the input messages.
Q: What if compression removes something the LLM needs? Headroom stores originals in CCR (Compress-Cache-Retrieve). The LLM can call headroom_retrieve to get full uncompressed content. Compression summaries tell the LLM what's available.
Q: Does it work with streaming? Yes. Compression happens before the request is sent. Streaming responses are unaffected.
Q: How much latency does it add? 15-200ms depending on content size and type. Small JSON arrays take ~15ms, large tool outputs take 100-200ms. The token savings typically save far more time on the LLM side than compression adds \u2014 a 50% token reduction on a Sonnet call saves seconds of generation time. See Latency Benchmarks for real numbers.
"},{"location":"langchain/","title":"LangChain Integration","text":"Headroom provides seamless integration with LangChain, enabling automatic context optimization across all LangChain patterns: chat models, memory, retrievers, agents, and observability.
"},{"location":"langchain/#installation","title":"Installation","text":"pip install \"headroom-ai[langchain]\"\n
This installs Headroom with LangChain dependencies (langchain-core).
"},{"location":"langchain/#quick-start","title":"Quick Start","text":""},{"location":"langchain/#wrap-any-chat-model-1-line","title":"Wrap Any Chat Model (1 Line)","text":"from langchain_openai import ChatOpenAI\nfrom headroom.integrations import HeadroomChatModel\n\n# Wrap your model - that's it!\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Use exactly like before\nresponse = llm.invoke(\"Hello!\")\n
Headroom automatically: - Detects the provider (OpenAI, Anthropic, Google) - Compresses tool outputs in conversation history - Optimizes for provider caching - Tracks token savings
"},{"location":"langchain/#check-your-savings","title":"Check Your Savings","text":"# After some usage\nprint(llm.get_metrics())\n# {'tokens_saved': 12500, 'savings_percent': 45.2, 'requests': 50}\n
"},{"location":"langchain/#integration-patterns","title":"Integration Patterns","text":""},{"location":"langchain/#1-chat-model-wrapper","title":"1. Chat Model Wrapper","text":"The HeadroomChatModel wraps any LangChain BaseChatModel:
from langchain_openai import ChatOpenAI\nfrom langchain_anthropic import ChatAnthropic\nfrom headroom.integrations import HeadroomChatModel\n\n# OpenAI\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Anthropic (auto-detected)\nllm = HeadroomChatModel(ChatAnthropic(model=\"claude-3-5-sonnet-20241022\"))\n\n# Custom configuration\nfrom headroom import HeadroomConfig, HeadroomMode\n\nconfig = HeadroomConfig(\n default_mode=HeadroomMode.OPTIMIZE,\n smart_crusher_target_ratio=0.3, # Target 70% compression\n)\nllm = HeadroomChatModel(\n ChatOpenAI(model=\"gpt-4o\"),\n headroom_config=config,\n)\n
"},{"location":"langchain/#async-support","title":"Async Support","text":"Full async support for ainvoke and astream:
# Async invoke\nresponse = await llm.ainvoke(\"Hello!\")\n\n# Async streaming\nasync for chunk in llm.astream(\"Tell me a story\"):\n print(chunk.content, end=\"\", flush=True)\n
"},{"location":"langchain/#tool-calling","title":"Tool Calling","text":"Works seamlessly with LangChain tool calling:
from langchain_core.tools import tool\n\n@tool\ndef search(query: str) -> str:\n \"\"\"Search the web.\"\"\"\n return {\"results\": [...]} # Large JSON response\n\nllm_with_tools = llm.bind_tools([search])\nresponse = llm_with_tools.invoke(\"Search for Python tutorials\")\n# Tool outputs are automatically compressed in subsequent turns\n
"},{"location":"langchain/#2-memory-integration","title":"2. Memory Integration","text":"HeadroomChatMessageHistory wraps any chat history with automatic compression:
from langchain.memory import ConversationBufferMemory\nfrom langchain_community.chat_message_histories import ChatMessageHistory\nfrom headroom.integrations import HeadroomChatMessageHistory\n\n# Wrap any history\nbase_history = ChatMessageHistory()\ncompressed_history = HeadroomChatMessageHistory(\n base_history,\n compress_threshold_tokens=4000, # Compress when over 4K tokens\n keep_recent_turns=5, # Always keep last 5 turns\n)\n\n# Use with any memory class\nmemory = ConversationBufferMemory(chat_memory=compressed_history)\n\n# Zero changes to your chain!\nchain = ConversationChain(llm=llm, memory=memory)\n
Why this matters: Long conversations can blow up to 50K+ tokens. HeadroomChatMessageHistory automatically compresses older turns while preserving recent context.
# Check compression stats\nprint(compressed_history.get_compression_stats())\n# {'compression_count': 12, 'total_tokens_saved': 28000}\n
"},{"location":"langchain/#3-retriever-integration","title":"3. Retriever Integration","text":"HeadroomDocumentCompressor filters retrieved documents by relevance:
from langchain.retrievers import ContextualCompressionRetriever\nfrom langchain_community.vectorstores import FAISS\nfrom headroom.integrations import HeadroomDocumentCompressor\n\n# Create vector store retriever (retrieve many for recall)\nvectorstore = FAISS.from_documents(documents, embeddings)\nbase_retriever = vectorstore.as_retriever(search_kwargs={\"k\": 50})\n\n# Wrap with Headroom compression (keep best for precision)\ncompressor = HeadroomDocumentCompressor(\n max_documents=10, # Keep top 10\n min_relevance=0.3, # Minimum relevance score\n prefer_diverse=True, # MMR-style diversity\n)\n\nretriever = ContextualCompressionRetriever(\n base_compressor=compressor,\n base_retriever=base_retriever,\n)\n\n# Retrieves 50 docs, returns best 10\ndocs = retriever.invoke(\"What is Python?\")\n
Why this matters: Vector search often returns many marginally-relevant documents. HeadroomDocumentCompressor uses BM25-style scoring to keep only the most relevant ones, reducing context size while improving answer quality.
"},{"location":"langchain/#4-agent-tool-wrapping","title":"4. Agent Tool Wrapping","text":"wrap_tools_with_headroom compresses tool outputs for agents:
from langchain.agents import create_openai_tools_agent, AgentExecutor\nfrom langchain_core.tools import tool\nfrom headroom.integrations import wrap_tools_with_headroom\n\n@tool\ndef search_database(query: str) -> str:\n \"\"\"Search the database.\"\"\"\n # Returns 1000 results as JSON\n return json.dumps({\"results\": [...], \"total\": 1000})\n\n@tool\ndef fetch_logs(service: str) -> str:\n \"\"\"Fetch service logs.\"\"\"\n # Returns 500 log entries\n return json.dumps({\"logs\": [...]})\n\n# Wrap tools with compression\ntools = [search_database, fetch_logs]\nwrapped_tools = wrap_tools_with_headroom(\n tools,\n min_chars_to_compress=1000, # Only compress large outputs\n)\n\n# Create agent with wrapped tools\nagent = create_openai_tools_agent(llm, wrapped_tools, prompt)\nexecutor = AgentExecutor(agent=agent, tools=wrapped_tools)\n\n# Tool outputs are automatically compressed\nresult = executor.invoke({\"input\": \"Find users who logged in yesterday\"})\n
Per-tool metrics:
from headroom.integrations import get_tool_metrics\n\nmetrics = get_tool_metrics()\nprint(metrics.get_summary())\n# {\n# 'total_invocations': 25,\n# 'total_compressions': 18,\n# 'total_chars_saved': 450000,\n# 'by_tool': {\n# 'search_database': {'invocations': 15, 'chars_saved': 320000},\n# 'fetch_logs': {'invocations': 10, 'chars_saved': 130000},\n# }\n# }\n
"},{"location":"langchain/#5-streaming-metrics","title":"5. Streaming Metrics","text":"Track output tokens during streaming:
from headroom.integrations import StreamingMetricsTracker\n\ntracker = StreamingMetricsTracker(model=\"gpt-4o\")\n\nfor chunk in llm.stream(\"Write a poem about coding\"):\n tracker.add_chunk(chunk)\n print(chunk.content, end=\"\", flush=True)\n\nmetrics = tracker.finish()\nprint(f\"\\nOutput tokens: {metrics.output_tokens}\")\nprint(f\"Duration: {metrics.duration_ms:.0f}ms\")\n
Context manager style:
from headroom.integrations import StreamingMetricsCallback\n\nwith StreamingMetricsCallback(model=\"gpt-4o\") as tracker:\n for chunk in llm.stream(messages):\n tracker.add_chunk(chunk)\n print(chunk.content, end=\"\")\n\nprint(f\"Metrics: {tracker.metrics}\")\n
"},{"location":"langchain/#6-langsmith-integration","title":"6. LangSmith Integration","text":"Add Headroom metrics to LangSmith traces:
from headroom.integrations import HeadroomLangSmithCallbackHandler\n\n# Create callback handler\nlangsmith_handler = HeadroomLangSmithCallbackHandler()\n\n# Use with your LLM\nllm = HeadroomChatModel(\n ChatOpenAI(model=\"gpt-4o\"),\n callbacks=[langsmith_handler],\n)\n\n# After calls, metrics appear in LangSmith traces:\n# - headroom.tokens_before\n# - headroom.tokens_after\n# - headroom.tokens_saved\n# - headroom.compression_ratio\n
"},{"location":"langchain/#real-world-examples","title":"Real-World Examples","text":""},{"location":"langchain/#example-1-langgraph-react-agent","title":"Example 1: LangGraph ReAct Agent","text":"The ReAct pattern is the most common agent architecture. Here's how to optimize it:
from langchain_openai import ChatOpenAI\nfrom langchain_core.tools import tool\nfrom langgraph.prebuilt import create_react_agent\nfrom headroom.integrations import HeadroomChatModel, wrap_tools_with_headroom\n\n# Define tools that return large outputs\n@tool\ndef search_web(query: str) -> str:\n \"\"\"Search the web for information.\"\"\"\n # Simulating large search results\n return json.dumps({\n \"results\": [\n {\"title\": f\"Result {i}\", \"snippet\": \"...\" * 100, \"url\": f\"https://...\"}\n for i in range(100)\n ],\n \"total\": 1000,\n })\n\n@tool\ndef query_database(sql: str) -> str:\n \"\"\"Execute SQL query.\"\"\"\n return json.dumps({\n \"rows\": [{\"id\": i, \"data\": \"...\" * 50} for i in range(500)],\n \"total\": 500,\n })\n\n# Wrap model with Headroom\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Wrap tools with compression\ntools = wrap_tools_with_headroom([search_web, query_database])\n\n# Create ReAct agent\nagent = create_react_agent(llm, tools)\n\n# Run - tool outputs are automatically compressed between iterations\nresult = agent.invoke({\n \"messages\": [(\"user\", \"Find all users who signed up last week and their activity\")]\n})\n\n# Check savings\nprint(f\"Tokens saved: {llm.get_metrics()['tokens_saved']}\")\n
Without Headroom: Each tool call adds 10-50K tokens to context. With Headroom: Tool outputs compressed to 1-2K tokens, agent runs faster and cheaper.
"},{"location":"langchain/#example-1b-langgraph-custom-graph-with-compress_tool_messages-node","title":"Example 1b: LangGraph Custom Graph with compress_tool_messages Node","text":"If you're building a custom LangGraph StateGraph (instead of using create_react_agent), you can insert a compression node between tools and the agent. This compresses all ToolMessage content in the graph state before the LLM sees it.
from langchain_openai import ChatOpenAI\nfrom langchain_core.messages import HumanMessage\nfrom langgraph.graph import StateGraph, MessagesState, START, END\nfrom headroom.integrations.langchain import create_compress_tool_messages_node\n\n# Define your agent and tools nodes\ndef agent_node(state: MessagesState):\n llm = ChatOpenAI(model=\"gpt-4o\")\n response = llm.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\ndef tools_node(state: MessagesState):\n # Your tool execution logic here\n ...\n\n# Build the graph with a compression step\ngraph = StateGraph(MessagesState)\ngraph.add_node(\"agent\", agent_node)\ngraph.add_node(\"tools\", tools_node)\ngraph.add_node(\"compress\", create_compress_tool_messages_node(\n min_tokens_to_compress=100, # Only compress outputs > ~100 tokens\n))\n\n# Wire: tools -> compress -> agent (instead of tools -> agent directly)\ngraph.add_edge(START, \"agent\")\ngraph.add_edge(\"tools\", \"compress\")\ngraph.add_edge(\"compress\", \"agent\")\n# ... add conditional edges from agent to tools/END as needed\n\napp = graph.compile()\nresult = app.invoke({\"messages\": [HumanMessage(content=\"Find sales data\")]})\n
You can also use compress_tool_messages directly as a standalone function:
from headroom.integrations.langchain import compress_tool_messages\n\n# Compress ToolMessages in any list of LangChain messages\nresult = compress_tool_messages(messages, min_tokens_to_compress=100)\ncompressed_messages = result.messages\nprint(f\"Saved {result.total_tokens_saved} tokens across {result.messages_compressed} messages\")\n
"},{"location":"langchain/#example-2-rag-pipeline-with-document-filtering","title":"Example 2: RAG Pipeline with Document Filtering","text":"from langchain_openai import ChatOpenAI, OpenAIEmbeddings\nfrom langchain_community.vectorstores import Chroma\nfrom langchain.chains import RetrievalQA\nfrom langchain.retrievers import ContextualCompressionRetriever\nfrom headroom.integrations import HeadroomChatModel, HeadroomDocumentCompressor\n\n# Setup vector store\nembeddings = OpenAIEmbeddings()\nvectorstore = Chroma.from_documents(documents, embeddings)\n\n# High-recall retriever (get many candidates)\nbase_retriever = vectorstore.as_retriever(search_kwargs={\"k\": 50})\n\n# Headroom compressor for precision\ncompressor = HeadroomDocumentCompressor(\n max_documents=5, # Keep only top 5\n min_relevance=0.4, # Must be 40%+ relevant\n prefer_diverse=True, # Avoid redundant docs\n)\n\n# Combine into compression retriever\nretriever = ContextualCompressionRetriever(\n base_compressor=compressor,\n base_retriever=base_retriever,\n)\n\n# Wrap LLM\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Create QA chain\nqa_chain = RetrievalQA.from_chain_type(\n llm=llm,\n retriever=retriever,\n return_source_documents=True,\n)\n\n# Query - retrieves 50 docs, uses best 5\nresult = qa_chain.invoke({\"query\": \"How do I configure authentication?\"})\nprint(f\"Answer: {result['result']}\")\nprint(f\"Sources: {len(result['source_documents'])} docs\")\n
Impact: - Without filtering: 50 docs \u00d7 ~500 tokens = 25K context tokens - With Headroom: 5 docs \u00d7 ~500 tokens = 2.5K context tokens (90% reduction)
"},{"location":"langchain/#example-3-conversational-agent-with-memory","title":"Example 3: Conversational Agent with Memory","text":"from langchain_openai import ChatOpenAI\nfrom langchain.memory import ConversationBufferMemory\nfrom langchain_community.chat_message_histories import ChatMessageHistory\nfrom langchain.chains import ConversationChain\nfrom headroom.integrations import HeadroomChatModel, HeadroomChatMessageHistory\n\n# Wrap LLM\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Wrap memory with auto-compression\nbase_history = ChatMessageHistory()\ncompressed_history = HeadroomChatMessageHistory(\n base_history,\n compress_threshold_tokens=8000, # Compress when over 8K\n keep_recent_turns=10, # Always keep last 10 turns\n)\n\nmemory = ConversationBufferMemory(\n chat_memory=compressed_history,\n return_messages=True,\n)\n\n# Create conversation chain\nchain = ConversationChain(llm=llm, memory=memory)\n\n# Long conversation - memory auto-compresses\nfor i in range(100):\n response = chain.invoke({\"input\": f\"Tell me about topic {i}\"})\n print(f\"Turn {i}: {len(response['response'])} chars\")\n\n# Check memory stats\nprint(compressed_history.get_compression_stats())\n# {'compression_count': 8, 'total_tokens_saved': 45000}\n
Impact: Without compression, 100-turn conversation = 100K+ tokens. With HeadroomChatMessageHistory, it stays under 8K tokens while preserving recent context.
"},{"location":"langchain/#example-4-multi-tool-research-agent","title":"Example 4: Multi-Tool Research Agent","text":"from langchain_openai import ChatOpenAI\nfrom langchain.agents import AgentExecutor, create_openai_tools_agent\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.tools import tool\nfrom headroom.integrations import (\n HeadroomChatModel,\n wrap_tools_with_headroom,\n get_tool_metrics,\n reset_tool_metrics,\n)\n\n@tool\ndef search_arxiv(query: str) -> str:\n \"\"\"Search arXiv for papers.\"\"\"\n return json.dumps({\"papers\": [{\"title\": f\"Paper {i}\", \"abstract\": \"...\" * 200} for i in range(50)]})\n\n@tool\ndef search_github(query: str) -> str:\n \"\"\"Search GitHub repositories.\"\"\"\n return json.dumps({\"repos\": [{\"name\": f\"repo-{i}\", \"description\": \"...\" * 100, \"stars\": i * 100} for i in range(100)]})\n\n@tool\ndef fetch_documentation(url: str) -> str:\n \"\"\"Fetch documentation from URL.\"\"\"\n return \"...\" * 5000 # Large doc content\n\n# Wrap everything\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\ntools = wrap_tools_with_headroom([search_arxiv, search_github, fetch_documentation])\n\nprompt = ChatPromptTemplate.from_messages([\n (\"system\", \"You are a research assistant. Use tools to gather information.\"),\n (\"human\", \"{input}\"),\n (\"placeholder\", \"{agent_scratchpad}\"),\n])\n\nagent = create_openai_tools_agent(llm, tools, prompt)\nexecutor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n\n# Reset metrics for this session\nreset_tool_metrics()\n\n# Run complex research task\nresult = executor.invoke({\n \"input\": \"Research the latest advances in LLM context compression and find relevant GitHub projects\"\n})\n\n# Check per-tool metrics\nmetrics = get_tool_metrics().get_summary()\nprint(f\"Total chars saved: {metrics['total_chars_saved']:,}\")\nprint(f\"Per-tool breakdown: {metrics['by_tool']}\")\n
"},{"location":"langchain/#configuration-options","title":"Configuration Options","text":""},{"location":"langchain/#headroomchatmodel","title":"HeadroomChatModel","text":"HeadroomChatModel(\n wrapped_model, # Any LangChain BaseChatModel\n headroom_config=HeadroomConfig(), # Headroom configuration\n auto_detect_provider=True, # Auto-detect from wrapped model\n)\n
"},{"location":"langchain/#headroomchatmessagehistory","title":"HeadroomChatMessageHistory","text":"HeadroomChatMessageHistory(\n base_history, # Any BaseChatMessageHistory\n compress_threshold_tokens=4000, # Token threshold for compression\n keep_recent_turns=5, # Minimum turns to preserve\n model=\"gpt-4o\", # Model for token counting\n)\n
"},{"location":"langchain/#headroomdocumentcompressor","title":"HeadroomDocumentCompressor","text":"HeadroomDocumentCompressor(\n max_documents=10, # Maximum docs to return\n min_relevance=0.0, # Minimum relevance score (0-1)\n prefer_diverse=False, # Use MMR for diversity\n)\n
"},{"location":"langchain/#wrap_tools_with_headroom","title":"wrap_tools_with_headroom","text":"wrap_tools_with_headroom(\n tools, # List of LangChain tools\n min_chars_to_compress=1000, # Minimum output size\n smart_crusher_config=None, # SmartCrusher configuration\n)\n
"},{"location":"langchain/#import-reference","title":"Import Reference","text":"from headroom.integrations import (\n # Chat Model\n HeadroomChatModel,\n\n # Memory\n HeadroomChatMessageHistory,\n\n # Retrievers\n HeadroomDocumentCompressor,\n\n # Agents\n HeadroomToolWrapper,\n wrap_tools_with_headroom,\n get_tool_metrics,\n reset_tool_metrics,\n\n # Streaming\n StreamingMetricsTracker,\n StreamingMetricsCallback,\n track_streaming_response,\n\n # LangSmith\n HeadroomLangSmithCallbackHandler,\n\n # Provider Detection\n detect_provider,\n get_headroom_provider,\n)\n\n# Or import from subpackage directly\nfrom headroom.integrations.langchain import HeadroomChatModel\nfrom headroom.integrations.langchain.memory import HeadroomChatMessageHistory\n
"},{"location":"langchain/#troubleshooting","title":"Troubleshooting","text":""},{"location":"langchain/#langchain-not-detected","title":"LangChain not detected","text":"from headroom.integrations import langchain_available\n\nif not langchain_available():\n print(\"Install with: pip install headroom-ai[langchain]\")\n
"},{"location":"langchain/#provider-detection-failing","title":"Provider detection failing","text":"# Force a specific provider\nfrom headroom.providers import AnthropicProvider\n\nllm = HeadroomChatModel(\n ChatAnthropic(model=\"claude-3-5-sonnet-20241022\"),\n auto_detect_provider=False,\n)\nllm._provider = AnthropicProvider()\n
"},{"location":"langchain/#memory-not-compressing","title":"Memory not compressing","text":"Check that your message count exceeds the threshold:
history = HeadroomChatMessageHistory(\n base_history,\n compress_threshold_tokens=1000, # Lower threshold\n keep_recent_turns=2, # Fewer preserved turns\n)\n
"},{"location":"langchain/#performance-tips","title":"Performance Tips","text":" - Use tool wrapping for agents - Agents with tools benefit most from compression
- Set appropriate thresholds - Don't compress small conversations
- Enable diversity for RAG -
prefer_diverse=True improves answer quality - Monitor with LangSmith - Use the callback handler to track savings over time
- Batch similar requests - Provider caching works better with stable prefixes
"},{"location":"learn/","title":"Headroom Learn","text":"Offline failure learning for coding agents. Analyzes past conversations, finds what went wrong, correlates it with what eventually worked, and writes specific project-level learnings that prevent the same mistakes next session.
"},{"location":"learn/#quick-start","title":"Quick Start","text":"# See recommendations for current project (dry-run, no changes)\nheadroom learn\n\n# Write recommendations to CLAUDE.md and MEMORY.md\nheadroom learn --apply\n\n# Analyze a specific project\nheadroom learn --project ~/my-project --apply\n\n# Analyze all projects\nheadroom learn --all --apply\n
"},{"location":"learn/#how-it-works","title":"How It Works","text":"Past Sessions \u2192 Plugin \u2192 Analyzer \u2192 Writer \u2192 Agent-native context file\n \u2502 \u2502 \u2502\n \u2502 \u2502 \u2514\u2500 Writes marker-delimited sections\n \u2502 \u2502 (replaced on re-run, not duplicated)\n \u2502 \u2502\n \u2502 \u2514\u2500 LLM-based analysis: finds failure patterns,\n \u2502 success correlations, and actionable rules\n \u2502\n \u2514\u2500 Plugin reads agent-specific logs:\n \u2022 Claude Code: ~/.claude/projects/*.jsonl\n \u2022 Codex: ~/.codex/sessions/*.json\n \u2022 Gemini CLI: ~/.gemini/tmp/*/chats/session-*.json\n
"},{"location":"learn/#success-correlation","title":"Success Correlation","text":"The core innovation. Instead of cataloging failures (\"Read failed 5 times\"), Headroom finds what the model did to fix each failure:
- Failed:
Read axion-formats/src/main/java/.../FirstClassEntity.java - Then succeeded:
Read axion-scala-common/src/main/scala/.../FirstClassEntity.scala - Learning: \"
FirstClassEntity is at axion-scala-common/, not axion-formats/\"
This produces specific, actionable corrections \u2014 not generic advice.
"},{"location":"learn/#what-it-learns","title":"What It Learns","text":""},{"location":"learn/#1-environment-facts-claudemd","title":"1. Environment Facts \u2192 CLAUDE.md","text":"Which runtime commands work vs fail.
### Environment\n- **Python**: use `uv run python` (not `python3` \u2014 modules not available outside venv)\n
"},{"location":"learn/#2-file-path-corrections-claudemd","title":"2. File Path Corrections \u2192 CLAUDE.md","text":"Wrong paths the model keeps guessing, with the correct locations.
### File Path Corrections\n- `axion-common/src/.../AxionSparkConstants.scala`\n \u2192 actually at `axion-spark-common/src/.../AxionSparkConstants.scala`\n
"},{"location":"learn/#3-search-scope-claudemd","title":"3. Search Scope \u2192 CLAUDE.md","text":"Which directories to search in (narrow paths fail, broader ones work).
### Search Scope\n- Don't search `axion-model/` \u2192 use `axion/` (the repo root)\n
"},{"location":"learn/#4-command-patterns-claudemd","title":"4. Command Patterns \u2192 CLAUDE.md","text":"How commands should (and shouldn't) be run.
### Command Patterns\n- **user_prefers_manual**: User rejected gradle 18 times \u2014 show the command, don't execute\n- **python_runtime**: Use `uv run python` not `python3` (ModuleNotFoundError)\n
"},{"location":"learn/#5-known-large-files-claudemd","title":"5. Known Large Files \u2192 CLAUDE.md","text":"Files that need offset/limit with Read.
### Known Large Files\n- `proxy/server.py` (~8000 lines) \u2014 always use offset/limit\n
"},{"location":"learn/#6-retry-prevention-memorymd","title":"6. Retry Prevention \u2192 MEMORY.md","text":"Specific suggestions derived from actual corrections.
"},{"location":"learn/#7-permission-notes-memorymd","title":"7. Permission Notes \u2192 MEMORY.md","text":"Commands repeatedly rejected \u2014 model should suggest them to the user instead.
"},{"location":"learn/#where-learnings-go","title":"Where Learnings Go","text":"Pattern Claude Code Codex Gemini CLI Environment, paths, commands CLAUDE.md AGENTS.md GEMINI.md Retry patterns, permissions MEMORY.md instructions.md GEMINI.md Output files are agent-native: Claude Code uses CLAUDE.md/MEMORY.md, Codex uses AGENTS.md, Gemini uses GEMINI.md. The same learnings, written to the format each agent reads.
"},{"location":"learn/#marker-based-updates","title":"Marker-Based Updates","text":"Headroom manages a clearly-delimited section in each file:
<!-- headroom:learn:start -->\n## Headroom Learned Patterns\n*Auto-generated by `headroom learn` \u2014 do not edit manually*\n...\n<!-- headroom:learn:end -->\n
On re-run, only the content between markers is replaced. Your existing file content is preserved.
"},{"location":"learn/#architecture-plugin-system","title":"Architecture (Plugin System)","text":"Headroom Learn uses a plugin architecture where each agent is a self-contained plugin:
Plugin Registry (auto-discovered)\n\u251c\u2500\u2500 ClaudeCodePlugin \u2192 Analyzer (LLM) \u2192 ClaudeCodeWriter \u2192 CLAUDE.md / MEMORY.md\n\u251c\u2500\u2500 CodexPlugin \u2192 Analyzer (LLM) \u2192 CodexWriter \u2192 AGENTS.md / instructions.md\n\u251c\u2500\u2500 GeminiPlugin \u2192 Analyzer (LLM) \u2192 GeminiWriter \u2192 GEMINI.md\n\u2514\u2500\u2500 (your plugin) \u2192 Analyzer (LLM) \u2192 (your writer) \u2192 (your file)\n
Plugins bundle scanning, detection, and writing for one agent. Built-in plugins are auto-discovered from headroom.learn.plugins.*. External plugins register via the headroom.learn_plugin entry point.
The Analyzer is shared \u2014 it uses an LLM (Sonnet, GPT-4o, or Gemini Flash) to find patterns. Same analysis for any agent.
"},{"location":"learn/#adding-support-for-a-new-agent","title":"Adding Support for a New Agent","text":" - Create
headroom/learn/plugins/myagent.py - Implement
LearnPlugin + ConversationScanner (scanner + writer + detection) - Add
plugin = MyAgentPlugin() at module scope - Done \u2014
headroom learn --agent myagent works automatically
Or install an external plugin: pip install headroom-learn-cursor (registers via entry point).
"},{"location":"learn/#cli-reference","title":"CLI Reference","text":"headroom learn [OPTIONS]\n\nOptions:\n --project PATH Project directory (default: current directory)\n --all Analyze all discovered projects\n --apply Write recommendations (default: dry-run)\n --agent [auto|claude|codex|gemini]\n Which agent to analyze (default: auto-detect)\n --model TEXT LLM for analysis (default: auto from API keys)\n
"},{"location":"learn/#supported-agents","title":"Supported Agents","text":"Agent Scanner Writer Output Files Claude Code Reads ~/.claude/projects/*.jsonl ClaudeCodeWriter CLAUDE.md, MEMORY.md OpenAI Codex Reads ~/.codex/sessions/*.json CodexWriter AGENTS.md, instructions.md Gemini CLI Reads ~/.gemini/tmp/*/chats/session-*.json GeminiWriter GEMINI.md"},{"location":"learn/#real-world-results","title":"Real-World Results","text":"Tested on 67,583 tool calls across 23 projects:
Metric Value Failure rate 7.5% (5,066 failures) Corrections extracted 164 per project (avg) Specific path corrections 22 (axion project) Search scope corrections 24 (axion project) Command patterns learned 5 (axion project) Estimated preventable waste ~27 MB across corpus"},{"location":"llmlingua/","title":"LLMLingua-2 Integration","text":"For maximum compression, Headroom integrates with LLMLingua-2, Microsoft's BERT-based token classifier trained via GPT-4 distillation. It achieves up to 20x compression while preserving semantic meaning.
"},{"location":"llmlingua/#when-to-use-llmlingua-2","title":"When to Use LLMLingua-2","text":"Approach Best For Compression Speed SmartCrusher JSON tool outputs 70-90% ~1ms Text Utilities Search/logs 50-90% ~1ms LLMLingua-2 Any text, max compression 80-95% ~50-200ms LLMLingua-2 is ideal when you need maximum compression and can tolerate slightly higher latency (e.g., compressing large tool outputs before storage, offline processing).
"},{"location":"llmlingua/#installation","title":"Installation","text":"# Adds ~2GB of model weights\npip install \"headroom-ai[llmlingua]\"\n
"},{"location":"llmlingua/#basic-usage","title":"Basic Usage","text":"from headroom.transforms import LLMLinguaCompressor\n\n# Create compressor (model loaded lazily on first use)\ncompressor = LLMLinguaCompressor()\n\n# Compress any text\nlong_output = \"The function processUserData takes a user object and validates...\"\nresult = compressor.compress(long_output)\n\nprint(f\"Before: {result.original_tokens} tokens\")\nprint(f\"After: {result.compressed_tokens} tokens\")\nprint(f\"Saved: {result.savings_percentage:.1f}%\")\nprint(result.compressed)\n
"},{"location":"llmlingua/#content-aware-compression","title":"Content-Aware Compression","text":"LLMLingua-2 automatically adjusts compression based on content type:
from headroom.transforms import LLMLinguaCompressor, LLMLinguaConfig\n\n# Conservative for code (keep 40% of tokens)\nconfig = LLMLinguaConfig(\n code_compression_rate=0.4, # More conservative\n json_compression_rate=0.35, # Moderate\n text_compression_rate=0.25, # Aggressive\n)\n\ncompressor = LLMLinguaCompressor(config)\n\n# Auto-detects content type\ncode_result = compressor.compress(\"def calculate(x): return x * 2\")\ntext_result = compressor.compress(\"This is a verbose explanation...\")\n
"},{"location":"llmlingua/#memory-management","title":"Memory Management","text":"The model uses ~1GB RAM. Unload it when done:
from headroom.transforms import (\n LLMLinguaCompressor,\n unload_llmlingua_model,\n is_llmlingua_model_loaded,\n)\n\ncompressor = LLMLinguaCompressor()\nresult = compressor.compress(content) # Model loaded here\n\n# Check if loaded\nprint(is_llmlingua_model_loaded()) # True\n\n# Free memory when done\nunload_llmlingua_model() # Frees ~1GB\nprint(is_llmlingua_model_loaded()) # False\n\n# Next compression will reload automatically\n
"},{"location":"llmlingua/#device-configuration","title":"Device Configuration","text":"from headroom.transforms import LLMLinguaConfig, LLMLinguaCompressor\n\n# Force CPU (slower but works everywhere)\nconfig = LLMLinguaConfig(device=\"cpu\")\n\n# Force GPU (faster but needs CUDA)\nconfig = LLMLinguaConfig(device=\"cuda\")\n\n# Auto-detect (default): uses CUDA > MPS > CPU\nconfig = LLMLinguaConfig(device=\"auto\")\n\ncompressor = LLMLinguaCompressor(config)\n
"},{"location":"llmlingua/#use-in-pipeline","title":"Use in Pipeline","text":"from headroom.transforms import TransformPipeline, LLMLinguaCompressor, SmartCrusher\n\n# Combine with other transforms\npipeline = TransformPipeline([\n SmartCrusher(), # First: compress JSON\n LLMLinguaCompressor(), # Then: ML compression on remaining text\n])\n\nresult = pipeline.apply(messages, tokenizer)\n
"},{"location":"llmlingua/#proxy-integration","title":"Proxy Integration","text":"Enable LLMLingua in the proxy server for automatic ML compression:
# Enable LLMLingua in proxy (requires: pip install headroom-ai[llmlingua,proxy])\nheadroom proxy --llmlingua\n\n# With custom settings\nheadroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.4\n\n# The proxy shows LLMLingua status at startup:\n# LLMLingua: ENABLED (device=cuda, rate=0.4)\n#\n# If llmlingua is installed but not enabled, you'll see a helpful hint:\n# LLMLingua: available (enable with --llmlingua for ML compression)\n
"},{"location":"llmlingua/#configuration-reference","title":"Configuration Reference","text":"Option Default Description device \"auto\" Device to run model on: auto, cpu, cuda, mps code_compression_rate 0.4 Keep 40% of tokens for code json_compression_rate 0.35 Keep 35% of tokens for JSON text_compression_rate 0.25 Keep 25% of tokens for text force_tokens [] Tokens to always preserve drop_consecutive True Drop consecutive whitespace"},{"location":"llmlingua/#performance-characteristics","title":"Performance Characteristics","text":"Metric Value Model size ~500MB Memory usage ~1GB RAM Cold start 10-30s (first load) Inference 50-200ms per request Compression 80-95%"},{"location":"llmlingua/#why-opt-in","title":"Why Opt-In?","text":"LLMLingua adds significant dependencies and overhead:
Aspect Default Proxy With LLMLingua Dependencies ~50MB ~2GB Cold start <1s 10-30s Per-request ~1-5ms ~50-200ms Compression 70-90% 80-95% The default proxy is lightweight and fast. Enable LLMLingua when you need maximum compression and can accept the tradeoffs.
"},{"location":"llmlingua/#troubleshooting","title":"Troubleshooting","text":""},{"location":"llmlingua/#model-not-found","title":"\"Model not found\"","text":"# Ensure llmlingua extra is installed\npip install \"headroom-ai[llmlingua]\"\n
"},{"location":"llmlingua/#cuda-out-of-memory","title":"\"CUDA out of memory\"","text":"# Force CPU mode\nconfig = LLMLinguaConfig(device=\"cpu\")\n
"},{"location":"llmlingua/#slow-compression","title":"\"Slow compression\"","text":" - Use GPU if available:
device=\"cuda\" - Batch multiple compressions
- Consider using SmartCrusher for JSON (faster, similar results)
"},{"location":"macos-deployment/","title":"macOS Deployment Guide","text":"This guide covers deploying the headroom proxy server as a background service on macOS using LaunchAgent. The service will start automatically on login and restart on crash.
"},{"location":"macos-deployment/#overview","title":"Overview","text":"macOS LaunchAgent provides a native way to run background services with:
- Automatic startup on user login
- Crash recovery with automatic restart
- Standard logging to
~/Library/Logs/ - Native lifecycle management via
launchctl
This is ideal for local development environments where you want \"set and forget\" proxy configuration.
"},{"location":"macos-deployment/#prerequisites","title":"Prerequisites","text":" - macOS 10.13+ (High Sierra or later)
- headroom-ai installed with proxy support
- Anthropic API key configured
"},{"location":"macos-deployment/#installing-headroom-with-proxy-support","title":"Installing Headroom with Proxy Support","text":"# Install with proxy support\npip install headroom-ai[proxy]\n\n# Verify installation\nheadroom proxy --help\n
"},{"location":"macos-deployment/#api-key-configuration","title":"API Key Configuration","text":"Your Anthropic API key can be configured in several ways:
Option 1: Shell environment (recommended)
# Add to ~/.bashrc or ~/.zshrc\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n
Option 2: LaunchAgent plist
<key>EnvironmentVariables</key>\n<dict>\n <key>ANTHROPIC_API_KEY</key>\n <string>sk-ant-...</string>\n</dict>\n
Option 3: System environment
# Add to /etc/launchd.conf (requires admin)\nsetenv ANTHROPIC_API_KEY sk-ant-...\n
"},{"location":"macos-deployment/#quick-install","title":"Quick Install","text":"The automated installer handles all setup:
# Clone or navigate to headroom repository\ncd examples/deployment/macos-launchagent\n\n# Run installer\n./install.sh\n
The installer will:
- Detect your headroom installation
- Prompt for port configuration (default: 8787)
- Create log directory
- Generate LaunchAgent plist
- Load and start the service
- Verify service is running
"},{"location":"macos-deployment/#installation-options","title":"Installation Options","text":"Custom port:
./install.sh --port 9000\n
Unattended install (no prompts):
./install.sh --port 8787 --unattended\n
Reinstall over existing:
# Installer will prompt to reinstall if service exists\n./install.sh\n
"},{"location":"macos-deployment/#manual-installation","title":"Manual Installation","text":"If you prefer full control over the installation:
"},{"location":"macos-deployment/#step-1-create-log-directory","title":"Step 1: Create Log Directory","text":"mkdir -p ~/Library/Logs/headroom\n
"},{"location":"macos-deployment/#step-2-generate-launchagent-plist","title":"Step 2: Generate LaunchAgent Plist","text":"Copy and customize the template:
cd examples/deployment/macos-launchagent\ncp com.headroom.proxy.plist.template ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Edit ~/Library/LaunchAgents/com.headroom.proxy.plist:
- Replace
__HEADROOM_PATH__ with your headroom path:
command -v headroom\n# Example output: /usr/local/bin/headroom\n
-
Replace __PORT__ with your desired port (e.g., 8787)
-
Replace __HOME__ with your home directory:
echo $HOME\n# Example output: /Users/yourusername\n
"},{"location":"macos-deployment/#step-3-load-the-launchagent","title":"Step 3: Load the LaunchAgent","text":"launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.headroom.proxy.plist\n
"},{"location":"macos-deployment/#step-4-verify-service","title":"Step 4: Verify Service","text":"# Check if service is running\nlaunchctl print gui/$(id -u)/com.headroom.proxy\n\n# Check if port is listening\nlsof -iTCP:8787 -sTCP:LISTEN\n\n# Test health endpoint\ncurl http://localhost:8787/health\n
"},{"location":"macos-deployment/#configuration","title":"Configuration","text":""},{"location":"macos-deployment/#port-customization","title":"Port Customization","text":"The default port is 8787. To use a custom port:
During installation:
./install.sh --port 9000\n
After installation:
- Uninstall:
./uninstall.sh - Reinstall with new port:
./install.sh --port 9000 - Update shell integration:
export HEADROOM_PROXY_PORT=9000
"},{"location":"macos-deployment/#log-location","title":"Log Location","text":"Logs are written to standard macOS locations:
- Standard output:
~/Library/Logs/headroom/proxy.log - Error output:
~/Library/Logs/headroom/proxy-error.log
To change log locations, edit the plist:
<key>StandardOutPath</key>\n<string>/custom/path/proxy.log</string>\n
"},{"location":"macos-deployment/#environment-variables","title":"Environment Variables","text":"Configure additional options in the plist EnvironmentVariables section:
<key>EnvironmentVariables</key>\n<dict>\n <!-- Required: Proxy port -->\n <key>HEADROOM_PROXY_PORT</key>\n <string>8787</string>\n\n <!-- Optional: API key (or set in shell) -->\n <key>ANTHROPIC_API_KEY</key>\n <string>sk-ant-...</string>\n\n <!-- Optional: Enable LLMLingua compression -->\n <key>HEADROOM_COMPRESSION_PROVIDER</key>\n <string>llmlingua</string>\n\n <!-- Optional: LLMLingua device (auto, cuda, cpu, mps) -->\n <key>HEADROOM_LLMLINGUA_DEVICE</key>\n <string>mps</string>\n</dict>\n
Note: LLMLingua requires additional installation:
pip install headroom-ai[llmlingua]\n
"},{"location":"macos-deployment/#crash-recovery","title":"Crash Recovery","text":"The LaunchAgent is configured with:
- KeepAlive: Automatically restarts on crash
- ThrottleInterval: 10 seconds between restart attempts
To disable automatic restart, edit the plist:
<key>KeepAlive</key>\n<false/>\n
"},{"location":"macos-deployment/#shell-integration","title":"Shell Integration","text":"Automatically configure your shell to use the proxy when available.
"},{"location":"macos-deployment/#setup","title":"Setup","text":"Add to ~/.bashrc (bash) or ~/.zshrc (zsh):
# Configure port (optional, defaults to 8787)\nexport HEADROOM_PROXY_PORT=8787\n\n# Source shell integration\nsource /path/to/headroom/examples/deployment/macos-launchagent/shell-integration.sh\n
"},{"location":"macos-deployment/#what-it-does","title":"What It Does","text":"The shell integration script:
- Checks if proxy is running on configured port
- If running, sets
ANTHROPIC_BASE_URL=http://localhost:8787 - If not running, attempts to start the LaunchAgent
- Provides status messages on first load
This makes Claude clients automatically use the proxy without manual configuration.
"},{"location":"macos-deployment/#manual-configuration","title":"Manual Configuration","text":"If you prefer not to use shell integration:
# Add to ~/.bashrc or ~/.zshrc\nexport ANTHROPIC_BASE_URL=http://localhost:8787\n
"},{"location":"macos-deployment/#service-management","title":"Service Management","text":""},{"location":"macos-deployment/#check-status","title":"Check Status","text":"# View service status\nlaunchctl print gui/$(id -u)/com.headroom.proxy\n\n# Check if port is listening\nlsof -iTCP:8787 -sTCP:LISTEN\n\n# Test health endpoint\ncurl http://localhost:8787/health\n
"},{"location":"macos-deployment/#view-logs","title":"View Logs","text":"# Tail standard output\ntail -f ~/Library/Logs/headroom/proxy.log\n\n# Tail error output\ntail -f ~/Library/Logs/headroom/proxy-error.log\n\n# View last 50 lines\ntail -n 50 ~/Library/Logs/headroom/proxy-error.log\n
"},{"location":"macos-deployment/#restart-service","title":"Restart Service","text":"# Graceful restart (stop and let KeepAlive restart it)\nlaunchctl kickstart -k gui/$(id -u)/com.headroom.proxy\n\n# Manual stop/start\nlaunchctl bootout gui/$(id -u)/com.headroom.proxy\nlaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.headroom.proxy.plist\n
"},{"location":"macos-deployment/#stop-service-temporarily","title":"Stop Service Temporarily","text":"# Disable without uninstalling\nlaunchctl disable gui/$(id -u)/com.headroom.proxy\n\n# Re-enable\nlaunchctl enable gui/$(id -u)/com.headroom.proxy\n
"},{"location":"macos-deployment/#verification","title":"Verification","text":"After installation, verify everything is working:
"},{"location":"macos-deployment/#1-check-service-status","title":"1. Check Service Status","text":"launchctl print gui/$(id -u)/com.headroom.proxy\n
Expected output includes:
state = running\n
"},{"location":"macos-deployment/#2-check-port","title":"2. Check Port","text":"lsof -iTCP:8787 -sTCP:LISTEN\n
Should show headroom listening on port 8787.
"},{"location":"macos-deployment/#3-test-health-endpoint","title":"3. Test Health Endpoint","text":"curl http://localhost:8787/health\n
Expected response:
{\"status\": \"healthy\"}\n
"},{"location":"macos-deployment/#4-test-proxy-functionality","title":"4. Test Proxy Functionality","text":"# Set base URL\nexport ANTHROPIC_BASE_URL=http://localhost:8787\n\n# Test with Python\npython -c \"\nimport anthropic\nclient = anthropic.Anthropic()\nresponse = client.messages.create(\n model='claude-3-5-sonnet-20241022',\n max_tokens=50,\n messages=[{'role': 'user', 'content': 'Hi'}]\n)\nprint(response.content[0].text)\n\"\n
"},{"location":"macos-deployment/#5-check-logs-for-errors","title":"5. Check Logs for Errors","text":"tail -n 20 ~/Library/Logs/headroom/proxy-error.log\n
Should show no errors. Common startup errors are listed in Troubleshooting.
"},{"location":"macos-deployment/#troubleshooting","title":"Troubleshooting","text":""},{"location":"macos-deployment/#service-wont-start","title":"Service Won't Start","text":"Symptom: launchctl print shows service not loaded or failed state
Check logs:
tail -n 50 ~/Library/Logs/headroom/proxy-error.log\n
Common causes:
Error Solution ANTHROPIC_API_KEY not set Set API key in environment or plist ModuleNotFoundError: No module named 'headroom' Install: pip install headroom-ai[proxy] command not found: headroom Update plist with correct path: command -v headroom Address already in use Change port or stop conflicting service"},{"location":"macos-deployment/#port-already-in-use","title":"Port Already in Use","text":"Symptom: Service starts but port not listening, logs show \"Address already in use\"
Find what's using the port:
lsof -iTCP:8787 -sTCP:LISTEN\n
Solutions:
- Stop conflicting service
- Use different port:
./uninstall.sh && ./install.sh --port 9000
"},{"location":"macos-deployment/#service-crashes-immediately","title":"Service Crashes Immediately","text":"Symptom: Service starts but immediately exits
Check for Python errors:
tail -f ~/Library/Logs/headroom/proxy-error.log\n
Common causes:
- Missing dependencies:
pip install headroom-ai[proxy] - Invalid API key: Verify
ANTHROPIC_API_KEY - Python version incompatible: Requires Python 3.9+
"},{"location":"macos-deployment/#anthropic_base_url-not-set","title":"ANTHROPIC_BASE_URL Not Set","text":"Symptom: Shell integration not setting environment variable
Verify proxy is running:
curl http://localhost:8787/health\n
Reload shell configuration:
source ~/.bashrc # or ~/.zshrc\n
Check shell integration is sourced:
# Should be set to 1\necho $HEADROOM_SHELL_INTEGRATION_LOADED\n
"},{"location":"macos-deployment/#service-not-auto-starting-on-login","title":"Service Not Auto-Starting on Login","text":"Symptom: Service doesn't start after reboot
Verify LaunchAgent is loaded:
launchctl list | grep headroom\n
If not listed:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Check RunAtLoad is enabled:
grep -A1 RunAtLoad ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Should show:
<key>RunAtLoad</key>\n<true/>\n
"},{"location":"macos-deployment/#permission-issues","title":"Permission Issues","text":"Symptom: \"Operation not permitted\" errors
Ensure plist has correct permissions:
chmod 644 ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Verify ownership:
ls -l ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Should be owned by your user, not root.
"},{"location":"macos-deployment/#uninstallation","title":"Uninstallation","text":""},{"location":"macos-deployment/#quick-uninstall","title":"Quick Uninstall","text":"cd examples/deployment/macos-launchagent\n./uninstall.sh\n
This will:
- Stop the service
- Remove LaunchAgent plist
- Optionally remove log directory (prompts)
"},{"location":"macos-deployment/#remove-everything","title":"Remove Everything","text":"# Uninstall service and remove logs\n./uninstall.sh --remove-logs\n\n# Remove shell integration from ~/.bashrc or ~/.zshrc\n# Delete or comment out:\n# export HEADROOM_PROXY_PORT=8787\n# source .../shell-integration.sh\n
"},{"location":"macos-deployment/#manual-uninstall","title":"Manual Uninstall","text":"# Stop service\nlaunchctl bootout gui/$(id -u)/com.headroom.proxy\n\n# Remove plist\nrm ~/Library/LaunchAgents/com.headroom.proxy.plist\n\n# Remove logs (optional)\nrm -rf ~/Library/Logs/headroom\n
"},{"location":"macos-deployment/#production-deployment","title":"Production Deployment","text":"For production environments, consider:
- System-wide LaunchDaemon instead of per-user LaunchAgent
- Resource limits in plist (CPU, memory)
- Log rotation for long-running deployments
- Monitoring via external tools
- Multiple instances on different ports for redundancy
LaunchAgent is designed for single-user development. For production, evaluate:
- Docker deployment for containerized environments
- systemd on Linux servers
- Cloud-native solutions (ECS, Cloud Run, etc.)
"},{"location":"macos-deployment/#related-documentation","title":"Related Documentation","text":" - Proxy Server Documentation - Core proxy configuration and features
- Configuration Guide - Detailed configuration options
- Architecture - How Headroom works internally
- Troubleshooting - General troubleshooting guide
"},{"location":"macos-deployment/#platform-alternatives","title":"Platform Alternatives","text":" - Linux: Use systemd instead of LaunchAgent
- Windows: Use Task Scheduler or NSSM (Non-Sucking Service Manager)
- Docker: See proxy.md for containerized deployment
"},{"location":"macos-deployment/#security-considerations","title":"Security Considerations","text":""},{"location":"macos-deployment/#launchagent-vs-launchdaemon","title":"LaunchAgent vs LaunchDaemon","text":"LaunchAgent (used here):
- Runs in user context
- No root privileges required
- Starts on user login
- Per-user isolation
LaunchDaemon (not covered):
- Runs as root or specific user
- System-wide service
- Starts on boot
- Requires admin privileges
For single-user development, LaunchAgent is recommended for security.
"},{"location":"macos-deployment/#api-key-security","title":"API Key Security","text":"Store API keys securely:
- \u2705 Use environment variables in shell config
- \u2705 Use macOS Keychain (advanced)
- \u2705 Restrict plist file permissions:
chmod 600 - \u274c Don't commit API keys to version control
- \u274c Don't store in world-readable files
"},{"location":"macos-deployment/#network-security","title":"Network Security","text":"The proxy binds to 127.0.0.1 (localhost only) by default:
- \u2705 Only accessible from local machine
- \u2705 No external network exposure
- \u274c Don't bind to
0.0.0.0 without firewall rules
"},{"location":"macos-deployment/#advanced-configuration","title":"Advanced Configuration","text":""},{"location":"macos-deployment/#multiple-proxy-instances","title":"Multiple Proxy Instances","text":"Run multiple proxies on different ports:
# Install first instance\n./install.sh --port 8787\n\n# For second instance, manually create plist with different label\ncp com.headroom.proxy.plist.template ~/Library/LaunchAgents/com.headroom.proxy-2.plist\n# Edit: Change Label to com.headroom.proxy-2, port to 8788\nlaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.headroom.proxy-2.plist\n
"},{"location":"macos-deployment/#custom-launchagent-schedule","title":"Custom LaunchAgent Schedule","text":"Run proxy only during business hours:
<!-- Add to plist -->\n<key>StartCalendarInterval</key>\n<dict>\n <key>Hour</key>\n <integer>9</integer>\n <key>Minute</key>\n <integer>0</integer>\n</dict>\n
"},{"location":"macos-deployment/#resource-limits","title":"Resource Limits","text":"Limit CPU and memory usage:
<!-- Add to plist -->\n<key>HardResourceLimits</key>\n<dict>\n <key>NumberOfProcesses</key>\n <integer>1</integer>\n <key>MemoryMax</key>\n <integer>536870912</integer> <!-- 512 MB -->\n</dict>\n
"},{"location":"macos-deployment/#faq","title":"FAQ","text":"Q: Why LaunchAgent instead of running headroom proxy manually?
A: LaunchAgent provides automatic startup, crash recovery, and proper lifecycle management. You don't have to remember to start the proxy or keep a terminal window open.
Q: Can I use this in production?
A: LaunchAgent is designed for development. For production, use Docker, systemd, or cloud-native deployment.
Q: How much does the proxy impact performance?
A: Minimal. The proxy adds ~10-50ms latency while reducing token costs by 50-90%. The cost savings far outweigh the latency.
Q: Do I need to restart the proxy when configuration changes?
A: Yes. After changing the plist, reload the service:
launchctl kickstart -k gui/$(id -u)/com.headroom.proxy\n
Q: Can I use this with multiple API providers?
A: The LaunchAgent setup is Anthropic-specific. For other providers, see proxy.md for configuration options.
Q: Does this work with Apple Silicon (M1/M2/M3)?
A: Yes, fully compatible. For LLMLingua compression, use --llmlingua-device mps for Apple Silicon acceleration.
"},{"location":"mcp/","title":"MCP Server \u2014 Context Engineering Toolkit","text":"Headroom's MCP server exposes compression, retrieval, and observability as tools that any MCP-compatible AI coding tool can use \u2014 Claude Code, Cursor, Codex, and more.
"},{"location":"mcp/#quick-start","title":"Quick Start","text":"# Install (MCP is included with proxy, or standalone)\npip install \"headroom-ai[proxy]\" # Proxy + MCP tools\npip install \"headroom-ai[mcp]\" # MCP tools only (lightweight)\n\n# Register with Claude Code (one-time)\nheadroom mcp install\n\n# Start Claude Code \u2014 it now has headroom tools!\nclaude\n
That's it. Claude Code can now compress content on demand, retrieve originals, and check session stats \u2014 no proxy required.
For automatic compression of ALL traffic, also run the proxy:
# Terminal 1\nheadroom proxy\n\n# Terminal 2\nANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude\n
"},{"location":"mcp/#tools","title":"Tools","text":"The MCP server provides three tools:
"},{"location":"mcp/#headroom_compress","title":"headroom_compress","text":"Compress content on demand. The LLM calls this when it wants to shrink large content before reasoning over it.
Tool: headroom_compress\n\nParameters:\n - content (required): Text to compress (files, JSON, logs, search results, etc.)\n\nReturns:\n - compressed: Compressed text\n - hash: Key for retrieving the original later\n - original_tokens / compressed_tokens / savings_percent\n - transforms: Which compression algorithms were applied\n
Example \u2014 Claude reads a large file, then compresses it:
Claude: Let me compress this large output to save context space.\n\n\u2192 headroom_compress(content=\"[5000 lines of grep results...]\")\n\n\u2190 {\n \"compressed\": \"[key matches with context...]\",\n \"hash\": \"a1b2c3d4e5f6...\",\n \"original_tokens\": 12000,\n \"compressed_tokens\": 3200,\n \"savings_percent\": 73.3,\n \"transforms\": [\"router:search:0.27\"]\n }\n
The original is stored locally for the session (1-hour TTL). If Claude needs the full content later, it calls headroom_retrieve.
"},{"location":"mcp/#headroom_retrieve","title":"headroom_retrieve","text":"Retrieve original uncompressed content by hash.
Tool: headroom_retrieve\n\nParameters:\n - hash (required): Hash key from compression\n - query (optional): Search within the original to return only matching items\n\nReturns:\n - original_content (full retrieval) or results (search)\n - source: \"local\" or \"proxy\"\n
Retrieval checks the local store first (content compressed via headroom_compress), then falls back to the proxy's store (content compressed automatically by the proxy). Hashes from either source work transparently.
"},{"location":"mcp/#headroom_stats","title":"headroom_stats","text":"Session compression statistics \u2014 including sub-agent stats and proxy cache info.
Tool: headroom_stats\n\nReturns:\n - compressions, retrievals, tokens_saved, savings_percent\n - estimated_cost_saved_usd\n - recent_events (last 10 compression/retrieval events)\n - sub_agents (stats from sub-agent MCP instances, if any)\n - combined (main + sub-agent totals)\n - proxy (request count, cache hits, cost saved \u2014 if proxy is running)\n
Sub-agent stats are aggregated via a shared stats file (~/.headroom/session_stats.jsonl). Each MCP server instance (main session and sub-agents) writes events there, and headroom_stats reads across all of them.
"},{"location":"mcp/#architecture","title":"Architecture","text":""},{"location":"mcp/#mcp-only-no-proxy","title":"MCP Only (no proxy)","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Claude Code / Cursor / Codex \u2502\n\u2502 \u2502\n\u2502 LLM calls headroom_compress on demand \u2502\n\u2502 \u2193 \u2502\n\u2502 Compression happens locally in MCP process \u2502\n\u2502 Original stored in local CompressionStore \u2502\n\u2502 \u2193 \u2502\n\u2502 LLM calls headroom_retrieve when needed \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"mcp/#mcp-proxy-full-setup","title":"MCP + Proxy (full setup)","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Claude Code \u2502\n\u2502 \u2502\n\u2502 1. Sends request \u2500\u2500\u2192 Proxy (auto-compress) \u2502\n\u2502 2. Gets response with compressed outputs \u2502\n\u2502 3. Can call headroom_compress for more \u2502\n\u2502 4. headroom_retrieve checks: \u2502\n\u2502 local store \u2192 proxy store \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 MCP (stdio)\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Headroom MCP Server \u2502\n\u2502 \u251c\u2500\u2500 headroom_compress (local compression) \u2502\n\u2502 \u251c\u2500\u2500 headroom_retrieve (local + proxy) \u2502\n\u2502 \u2514\u2500\u2500 headroom_stats (aggregated stats) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
No double-compression: the proxy compresses at the HTTP level (before the LLM sees content). MCP tools operate after the LLM receives content. They don't touch the same data.
"},{"location":"mcp/#cli-commands","title":"CLI Commands","text":""},{"location":"mcp/#install","title":"Install","text":"headroom mcp install # Default setup\nheadroom mcp install --proxy-url http://host:9000 # Custom proxy URL\nheadroom mcp install --force # Overwrite existing\n
"},{"location":"mcp/#status","title":"Status","text":"headroom mcp status\n
Headroom MCP Status\n========================================\nMCP SDK: \u2713 Installed\nClaude Config: \u2713 Configured\n /Users/you/.claude/mcp.json\nProxy URL: http://127.0.0.1:8787\nProxy Status: \u2713 Running at http://127.0.0.1:8787\n
"},{"location":"mcp/#uninstall","title":"Uninstall","text":"headroom mcp uninstall\n
"},{"location":"mcp/#debug","title":"Debug","text":"headroom mcp serve --debug\n
"},{"location":"mcp/#cross-tool-compatibility","title":"Cross-Tool Compatibility","text":"The MCP server works with any MCP-compatible host:
Tool MCP Support Setup Claude Code Native headroom mcp install Cursor Supported Add to Cursor MCP settings Codex If supported Configure MCP server Any MCP host Yes Point to headroom mcp serve"},{"location":"mcp/#troubleshooting","title":"Troubleshooting","text":""},{"location":"mcp/#mcp-sdk-not-installed","title":"\"MCP SDK not installed\"","text":"pip install \"headroom-ai[mcp]\"\n
"},{"location":"mcp/#proxy-not-running-when-using-proxy-features","title":"\"Proxy not running\" (when using proxy features)","text":"headroom proxy # In another terminal\n
"},{"location":"mcp/#entry-not-found-or-expired","title":"\"Entry not found or expired\"","text":" - Content compressed via
headroom_compress: stored for 1 hour (session TTL) - Content compressed by the proxy: stored for 5 minutes (proxy TTL)
- The proxy must be running for proxy-compressed content
"},{"location":"mcp/#claude-doesnt-see-headroom-tools","title":"Claude doesn't see headroom tools","text":" - Check:
headroom mcp status - Restart Claude Code after installing MCP
- Verify with
/mcp in Claude Code \u2014 should show 3 headroom tools
"},{"location":"mcp/#sub-agent-stats-not-showing","title":"Sub-agent stats not showing","text":"Sub-agent stats appear in headroom_stats only after sub-agents have run compressions. The shared stats file is at ~/.headroom/session_stats.jsonl.
"},{"location":"memory/","title":"Memory","text":"Hierarchical, temporal memory for LLM applications. Enable your AI to remember across conversations with intelligent scoping and versioning.
"},{"location":"memory/#why-memory","title":"Why Memory?","text":"LLMs have two fundamental limitations: 1. Context windows overflow - Too much history, need to truncate 2. No persistence - Every conversation starts from zero
Memory solves both: extract key facts, persist them, inject when relevant.
This is temporal compression - instead of carrying 10,000 tokens of conversation history, carry 100 tokens of extracted memories.
"},{"location":"memory/#what-makes-headroom-memory-different","title":"What Makes Headroom Memory Different?","text":"Feature Headroom Letta (MemGPT) Mem0 Cross-Agent Memory Any agent shares one DB via proxy Per-agent only Per-user, no cross-agent Agent Provenance Tracks which agent saved/updated each memory No No LLM-Mediated Dedup Piggybacks on user's own LLM for merge decisions No Separate LLM call ($) Transparent Proxy Zero code changes \u2014 just route through proxy Requires agent framework Requires SDK integration Hierarchical Scoping User \u2192 Session \u2192 Agent \u2192 Turn Flat (per-agent) Flat (per-user) Temporal Versioning Full supersession chains No No Zero-Latency Extraction Inline (Letta-style) Inline Separate call One-Liner Integration with_memory(client) Requires agent setup Requires separate client Pluggable Backends SQLite, HNSW, FTS5, any embedder PostgreSQL Qdrant/Chroma Semantic + Full-Text Search Both Semantic only Semantic only Memory Bubbling Auto-promote important memories No No Protocol-Based Architecture Yes (dependency injection) No No"},{"location":"memory/#cross-agent-memory-proxy","title":"Cross-Agent Memory (Proxy)","text":"The most powerful way to use memory: any agent that routes through the proxy shares the same memory store. Claude saves a fact, Codex reads it back. Zero configuration needed.
# Start the proxy with memory enabled\nheadroom proxy --memory\n\n# Or use wrap (auto-starts proxy)\nheadroom wrap claude --memory # Claude Code with persistent memory\nheadroom wrap codex --memory # Codex with the SAME memory store\nheadroom wrap aider --memory # Aider shares it too\n
"},{"location":"memory/#how-it-works","title":"How It Works","text":"Claude Code Codex CLI Gemini CLI\n \u2502 \u2502 \u2502\n \u2514\u2500\u2500 /v1/messages \u2500\u2500\u2510 \u2514\u2500\u2500 /v1/chat/completions \u2500\u2500\u2524 \u2514\u2500\u2500 /generateContent \u2500\u2500\u2510\n \u2502 \u2502 \u2502\n \u25bc \u25bc \u25bc\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Headroom Proxy (--memory) \u2502\n \u2502 \u2502\n \u2502 1. Search memory DB for relevant context \u2502\n \u2502 2. Inject memories as system context (provider-native format) \u2502\n \u2502 3. Add memory_save/search/update/delete tools \u2502\n \u2502 4. Forward to upstream LLM \u2502\n \u2502 5. Handle memory tool calls in response \u2502\n \u2502 6. Async background dedup (>92% cosine \u2192 auto-remove) \u2502\n \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n .headroom/memory.db\n (project-scoped SQLite)\n
"},{"location":"memory/#project-scoped-database","title":"Project-Scoped Database","text":"Memory is stored per-project at {cwd}/.headroom/memory.db. Each project has its own memory \u2014 no cross-project contamination. Override with --memory-db-path for a custom location.
"},{"location":"memory/#user-identity","title":"User Identity","text":"User ID is auto-detected from $USER (your OS username). Override per-request with the x-headroom-user-id header. All memories are scoped to the user \u2014 multiple developers on the same project have separate memory stores.
"},{"location":"memory/#agent-provenance","title":"Agent Provenance","text":"Every memory tracks which agent created or updated it:
{\n \"content\": \"Project uses alembic for migrations\",\n \"metadata\": {\n \"source_agent\": \"claude\",\n \"source_provider\": \"anthropic\",\n \"created_via\": \"tool_call\",\n \"created_at_utc\": \"2026-04-10T17:30:00Z\"\n }\n}\n
When an agent updates a memory, the update is tracked:
{\n \"reason\": \"Updated by codex via openai: Added version info\"\n}\n
"},{"location":"memory/#intelligent-deduplication","title":"Intelligent Deduplication","text":"When the LLM calls memory_save, headroom:
- Saves immediately (zero latency)
- Searches for similar existing memories (cosine similarity)
- Returns an enriched hint if duplicates found:
{\n \"status\": \"saved\",\n \"memory_id\": \"abc123\",\n \"note\": \"Similar memory exists (id: def456, 89% match, saved by codex):\n 'DB migration tool is alembic'. Call memory_update('def456',\n '<merged content>') to consolidate.\"\n}\n
The LLM then decides whether to merge \u2014 using the user's own LLM, not a separate model. No extra cost to headroom.
- Background auto-dedup: If similarity >92%, the older duplicate is automatically removed (async, non-blocking).
"},{"location":"memory/#supported-providers","title":"Supported Providers","text":"Memory works with ALL providers routing through the proxy:
Provider Context Injection Memory Tools Format Anthropic (Claude) System parameter Anthropic tool_use Native OpenAI (Codex, GPT) System message OpenAI function calling Native Gemini systemInstruction functionDeclarations Native Any OpenAI-compatible System message Function calling OpenAI format"},{"location":"memory/#quick-start","title":"Quick Start","text":"from openai import OpenAI\nfrom headroom import with_memory\n\n# One line - that's it\nclient = with_memory(OpenAI(), user_id=\"alice\")\n\n# Use exactly like normal\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": \"I prefer Python for backend work\"}]\n)\n# Memory extracted INLINE - zero extra latency\n\n# Later, in a new conversation...\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": \"What language should I use?\"}]\n)\n# \u2192 Response uses the Python preference from memory\n
"},{"location":"memory/#how-it-works_1","title":"How It Works","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 with_memory() \u2502\n\u2502 \u2502\n\u2502 1. INJECT: Semantic search \u2192 prepend to user message \u2502\n\u2502 2. INSTRUCT: Add memory extraction instruction \u2502\n\u2502 3. CALL: Forward to LLM \u2502\n\u2502 4. PARSE: Extract <memory> block from response \u2502\n\u2502 5. STORE: Save with embeddings + vector index + FTS \u2502\n\u2502 6. RETURN: Clean response (without memory block) \u2502\n\u2502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Key insight: Memory extraction happens inline as part of the LLM response (Letta-style). No extra API calls, no extra latency.
"},{"location":"memory/#hierarchical-scoping","title":"Hierarchical Scoping","text":"Memories exist at different scope levels, enabling fine-grained control:
USER (broadest)\n \u2514\u2500\u2500 SESSION\n \u2514\u2500\u2500 AGENT\n \u2514\u2500\u2500 TURN (narrowest)\n
"},{"location":"memory/#scope-levels","title":"Scope Levels","text":"Scope Persists Across Use Case USER All sessions, all time Long-term preferences, identity SESSION Current session only Current task context AGENT Current agent in session Agent-specific context TURN Single turn only Ephemeral working memory"},{"location":"memory/#example-multi-session-memory","title":"Example: Multi-Session Memory","text":"from openai import OpenAI\nfrom headroom import with_memory\n\n# Session 1: Morning\nclient1 = with_memory(\n OpenAI(),\n user_id=\"bob\",\n session_id=\"morning-session\",\n)\nresponse = client1.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": \"I prefer Go for performance-critical code\"}]\n)\n# Memory stored at USER level (persists across sessions)\n\n# Session 2: Afternoon (different session, same user)\nclient2 = with_memory(\n OpenAI(),\n user_id=\"bob\", # Same user\n session_id=\"afternoon-session\", # Different session\n)\nresponse = client2.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": \"What language for my new microservice?\"}]\n)\n# \u2192 Recalls Go preference from morning session!\n
"},{"location":"memory/#temporal-versioning-supersession","title":"Temporal Versioning (Supersession)","text":"Memories evolve over time. When facts change, Headroom creates a supersession chain preserving history:
from headroom.memory import HierarchicalMemory, MemoryConfig\n\nmemory = await HierarchicalMemory.create()\n\n# Original fact\norig = await memory.add(\n content=\"User works at Google\",\n user_id=\"alice\",\n category=MemoryCategory.FACT,\n)\n\n# User changes jobs - supersede the old memory\nnew = await memory.supersede(\n old_memory_id=orig.id,\n new_content=\"User now works at Anthropic\",\n)\n\n# Query current state (excludes superseded)\ncurrent = await memory.query(MemoryFilter(\n user_id=\"alice\",\n include_superseded=False, # Default\n))\n# \u2192 Returns only \"User now works at Anthropic\"\n\n# Query full history (includes superseded)\nhistory = await memory.query(MemoryFilter(\n user_id=\"alice\",\n include_superseded=True,\n))\n# \u2192 Returns both memories with validity timestamps\n\n# Get the chain\nchain = await memory.get_history(new.id)\n# \u2192 [\n# Memory(content=\"User works at Google\", valid_until=..., is_current=False),\n# Memory(content=\"User now works at Anthropic\", valid_until=None, is_current=True),\n# ]\n
"},{"location":"memory/#why-temporal-versioning-matters","title":"Why Temporal Versioning Matters","text":" - Audit trail - Know what was true at any point in time
- Debugging - Understand why the LLM made certain decisions
- Rollback - Restore previous state if needed
- Analytics - Track how user preferences evolve
"},{"location":"memory/#memory-categories","title":"Memory Categories","text":"Memories are categorized for better organization and retrieval:
Category Description Examples PREFERENCE Likes, dislikes, preferred approaches \"Prefers Python\", \"Likes dark mode\" FACT Identity, role, constraints \"Works at fintech startup\", \"Senior engineer\" CONTEXT Current goals, ongoing tasks \"Migrating to microservices\", \"Working on auth\" ENTITY Information about entities \"Project Apollo uses React\", \"Team lead is Sarah\" DECISION Decisions made \"Chose PostgreSQL over MySQL\", \"Using REST not GraphQL\" INSIGHT Derived insights \"User tends to prefer typed languages\""},{"location":"memory/#memory-api","title":"Memory API","text":"The with_memory() wrapper provides a .memory API for direct access:
client = with_memory(OpenAI(), user_id=\"alice\")\n\n# Search memories (semantic)\nresults = client.memory.search(\"python preferences\", top_k=5)\nfor memory in results:\n print(f\"{memory.content}\")\n\n# Add manual memory\nclient.memory.add(\n \"User is a senior engineer\",\n category=\"fact\",\n importance=0.9,\n)\n\n# Get all memories\nall_memories = client.memory.get_all()\n\n# Clear memories\nclient.memory.clear()\n\n# Get stats\nstats = client.memory.stats()\nprint(f\"Total memories: {stats['total']}\")\nprint(f\"By category: {stats['categories']}\")\n
"},{"location":"memory/#advanced-usage-direct-hierarchicalmemory-api","title":"Advanced Usage: Direct HierarchicalMemory API","text":"For full control, use the HierarchicalMemory class directly:
import asyncio\nfrom headroom.memory import (\n HierarchicalMemory,\n MemoryConfig,\n MemoryCategory,\n EmbedderBackend,\n)\nfrom headroom.memory.ports import MemoryFilter, VectorFilter\n\nasync def main():\n # Create with custom configuration\n config = MemoryConfig(\n db_path=\"my_memory.db\",\n embedder_backend=EmbedderBackend.LOCAL, # or OPENAI, OLLAMA\n vector_dimension=384,\n cache_max_size=2000,\n )\n memory = await HierarchicalMemory.create(config)\n\n # Add memory with full control\n mem = await memory.add(\n content=\"User prefers functional programming\",\n user_id=\"alice\",\n session_id=\"sess-123\",\n agent_id=\"code-assistant\",\n category=MemoryCategory.PREFERENCE,\n importance=0.9,\n entity_refs=[\"functional-programming\", \"coding-style\"],\n metadata={\"source\": \"conversation\", \"confidence\": 0.95},\n )\n\n # Semantic search\n results = await memory.search(\n query=\"programming paradigm preferences\",\n user_id=\"alice\",\n top_k=5,\n min_similarity=0.5,\n categories=[MemoryCategory.PREFERENCE],\n )\n for r in results:\n print(f\"[{r.similarity:.3f}] {r.memory.content}\")\n\n # Full-text search\n text_results = await memory.text_search(\n query=\"functional\",\n user_id=\"alice\",\n )\n\n # Query with filters\n memories = await memory.query(MemoryFilter(\n user_id=\"alice\",\n categories=[MemoryCategory.PREFERENCE, MemoryCategory.FACT],\n min_importance=0.7,\n limit=10,\n ))\n\n # Convenience methods\n await memory.remember(\"Likes coffee\", user_id=\"alice\", importance=0.6)\n relevant = await memory.recall(\"beverage preferences\", user_id=\"alice\")\n\nasyncio.run(main())\n
"},{"location":"memory/#configuration","title":"Configuration","text":""},{"location":"memory/#embedder-backends","title":"Embedder Backends","text":"from headroom.memory import MemoryConfig, EmbedderBackend\n\n# Local embeddings (recommended - fast, free, private)\nconfig = MemoryConfig(\n embedder_backend=EmbedderBackend.LOCAL,\n embedder_model=\"all-MiniLM-L6-v2\", # 384 dimensions, fast\n)\n\n# OpenAI embeddings (higher quality, costs money)\nconfig = MemoryConfig(\n embedder_backend=EmbedderBackend.OPENAI,\n openai_api_key=\"sk-...\",\n embedder_model=\"text-embedding-3-small\",\n)\n\n# Ollama embeddings (local server, many models)\nconfig = MemoryConfig(\n embedder_backend=EmbedderBackend.OLLAMA,\n ollama_base_url=\"http://localhost:11434\",\n embedder_model=\"nomic-embed-text\",\n)\n
"},{"location":"memory/#storage-configuration","title":"Storage Configuration","text":"config = MemoryConfig(\n db_path=\"memory.db\", # SQLite database path\n vector_dimension=384, # Must match embedder output\n hnsw_ef_construction=200, # HNSW index quality (higher = better, slower)\n hnsw_m=16, # HNSW connections per node\n hnsw_ef_search=50, # HNSW search quality\n cache_enabled=True, # Enable LRU cache\n cache_max_size=1000, # Max cached memories\n)\n
"},{"location":"memory/#wrapper-configuration","title":"Wrapper Configuration","text":"client = with_memory(\n OpenAI(),\n user_id=\"alice\",\n db_path=\"memory.db\",\n top_k=5, # Memories to inject per request\n session_id=\"optional-session\",\n agent_id=\"optional-agent\",\n embedder_backend=EmbedderBackend.LOCAL,\n)\n
"},{"location":"memory/#architecture","title":"Architecture","text":""},{"location":"memory/#protocol-based-design","title":"Protocol-Based Design","text":"Headroom Memory uses Protocol interfaces (ports) for all components, enabling easy swapping:
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 HierarchicalMemory \u2502\n\u2502 (Orchestrator) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 MemoryStore \u2502 \u2502 VectorIndex \u2502 \u2502 TextIndex \u2502 \u2502\n\u2502 \u2502 Protocol \u2502 \u2502 Protocol \u2502 \u2502 Protocol \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 SQLite \u2502 \u2502 HNSW \u2502 \u2502 FTS5 \u2502 \u2502\n\u2502 \u2502 Adapter \u2502 \u2502 Adapter \u2502 \u2502 Adapter \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 Embedder \u2502 \u2502 MemoryCache \u2502 \u2502\n\u2502 \u2502 Protocol \u2502 \u2502 Protocol \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502Local/OpenAI/\u2502 \u2502 LRU Cache \u2502 \u2502\n\u2502 \u2502 Ollama \u2502 \u2502 \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"memory/#components","title":"Components","text":"Component Protocol Default Adapter Purpose MemoryStore MemoryStore SQLiteMemoryStore CRUD + filtering + supersession VectorIndex VectorIndex HNSWVectorIndex Semantic similarity search TextIndex TextIndex FTS5TextIndex Full-text keyword search Embedder Embedder LocalEmbedder Text \u2192 vector conversion Cache MemoryCache LRUMemoryCache Hot memory caching"},{"location":"memory/#comparison-with-state-of-the-art","title":"Comparison with State of the Art","text":""},{"location":"memory/#vs-letta-memgpt","title":"vs Letta (MemGPT)","text":"Letta pioneered inline memory extraction. Headroom builds on this with:
Aspect Headroom Letta Scoping 4-level hierarchy (user/session/agent/turn) Flat per-agent Temporal Full supersession chains with history No versioning Integration One-liner wrapper for any client Requires Letta agent framework Search Semantic + full-text Semantic only Storage SQLite + HNSW (embedded) PostgreSQL (external) Extensibility Protocol-based adapters Monolithic When to use Letta: You want a full agent framework with built-in memory. When to use Headroom: You want memory as a layer on your existing stack.
"},{"location":"memory/#vs-mem0","title":"vs Mem0","text":"Mem0 provides a managed memory service. Headroom differs:
Aspect Headroom Mem0 Deployment Embedded (no server) Managed service or self-hosted Scoping 4-level hierarchy Flat per-user Temporal Supersession chains No versioning Extraction Inline (zero latency) Separate API call Search Semantic + full-text Semantic only Cost Free (local embeddings) API costs or infra costs Privacy All local Data leaves your infra When to use Mem0: You want a managed service and don't mind external dependencies. When to use Headroom: You want embedded memory with no external services.
"},{"location":"memory/#feature-matrix","title":"Feature Matrix","text":"Feature Headroom Letta Mem0 Cross-agent sharing (proxy) \u2705 \u274c \u274c Agent provenance tracking \u2705 \u274c \u274c LLM-mediated dedup (no extra cost) \u2705 \u274c \u274c (uses separate LLM) Transparent proxy (zero code) \u2705 \u274c \u274c Hierarchical scoping \u2705 \u274c \u274c Temporal versioning \u2705 \u274c \u274c Zero-latency extraction \u2705 \u2705 \u274c Full-text search \u2705 \u274c \u274c Embedded (no server) \u2705 \u274c \u274c One-liner integration \u2705 \u274c \u274c Protocol-based extensibility \u2705 \u274c \u274c Memory bubbling \u2705 \u274c \u274c Local embeddings \u2705 \u274c \u2705 Managed service option \u274c \u274c \u2705"},{"location":"memory/#multi-user-isolation","title":"Multi-User Isolation","text":"Memories are isolated by user_id:
# Alice's memories\nalice_client = with_memory(OpenAI(), user_id=\"alice\")\n\n# Bob's memories (completely separate)\nbob_client = with_memory(OpenAI(), user_id=\"bob\")\n\n# Bob cannot see Alice's memories, even with the same database\n
"},{"location":"memory/#performance","title":"Performance","text":"Operation Latency Notes Memory injection <50ms Local embeddings + HNSW search Memory extraction +50-100 tokens Part of LLM response (inline) Memory storage <10ms SQLite + HNSW + FTS5 indexing Cache hit <1ms LRU cache lookup Overhead: ~100 extra output tokens per response for the <memory> block.
"},{"location":"memory/#providers","title":"Providers","text":"Memory works with any OpenAI-compatible client:
from openai import OpenAI\nfrom headroom import with_memory\n\n# OpenAI\nclient = with_memory(OpenAI(), user_id=\"alice\")\n\n# Azure OpenAI\nclient = with_memory(\n OpenAI(base_url=\"https://your-resource.openai.azure.com/...\"),\n user_id=\"alice\",\n)\n\n# Groq\nfrom groq import Groq\nclient = with_memory(Groq(), user_id=\"alice\")\n\n# Any OpenAI-compatible client\nclient = with_memory(YourClient(), user_id=\"alice\")\n
"},{"location":"memory/#example-full-conversation-flow","title":"Example: Full Conversation Flow","text":"from openai import OpenAI\nfrom headroom import with_memory\n\nclient = with_memory(OpenAI(), user_id=\"developer_jane\")\n\n# Conversation 1: User shares context\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\n \"role\": \"user\",\n \"content\": \"I'm a Python developer at a fintech startup. We use PostgreSQL and FastAPI.\"\n }]\n)\n# Memories extracted:\n# - [FACT] Python developer at fintech startup\n# - [PREFERENCE] Uses PostgreSQL for databases\n# - [PREFERENCE] Uses FastAPI for web APIs\n\n# Conversation 2 (new session): User asks question\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\n \"role\": \"user\",\n \"content\": \"What database should I use for my new project?\"\n }]\n)\n# Response references PostgreSQL preference from memory:\n# \u2192 \"Given your experience with PostgreSQL at your fintech company,\n# I'd recommend sticking with it for consistency...\"\n\n# Check stored memories\nprint(\"Stored memories:\")\nfor m in client.memory.get_all():\n print(f\" [{m.category.value}] {m.content}\")\n
"},{"location":"memory/#troubleshooting","title":"Troubleshooting","text":""},{"location":"memory/#memories-not-being-extracted","title":"Memories not being extracted","text":" - Check if the conversation has memory-worthy content (not just greetings)
- Verify the LLM is following the memory instruction
- Enable logging:
import logging; logging.basicConfig(level=logging.DEBUG)
"},{"location":"memory/#memories-not-being-retrieved","title":"Memories not being retrieved","text":" - Verify
user_id matches between sessions - Check if memories exist:
client.memory.get_all() - Try a more specific search query
- Check similarity threshold
"},{"location":"memory/#high-latency","title":"High latency","text":" - Use local embeddings:
embedder_backend=EmbedderBackend.LOCAL - Reduce
top_k for fewer memories to retrieve - Enable caching (enabled by default)
"},{"location":"memory/#memory-not-persisting","title":"Memory not persisting","text":" - Check
db_path is the same across sessions - Ensure the database file is writable
- Check for exceptions in logs
"},{"location":"memory/#best-practices","title":"Best Practices","text":" - Use consistent
user_id - Same ID across sessions for continuity - Use session scoping - Set
session_id for session-specific context - Start with local embeddings - Faster, free, good enough for most cases
- Monitor memory growth - Use
client.memory.stats() to track - Use importance scores - Higher importance = more likely to be retrieved
- Leverage categories - Helps with debugging and selective retrieval
- Consider supersession - Use
supersede() when facts change, not add()
"},{"location":"metrics/","title":"Metrics & Monitoring","text":"Headroom provides comprehensive metrics for monitoring compression performance, cost savings, and system health.
"},{"location":"metrics/#proxy-metrics","title":"Proxy Metrics","text":""},{"location":"metrics/#stats-endpoint","title":"Stats Endpoint","text":"curl http://localhost:8787/stats\n
{\n \"persistent_savings\": {\n \"lifetime\": {\n \"tokens_saved\": 12500,\n \"compression_savings_usd\": 0.04\n },\n \"recent_history\": [\n {\n \"timestamp\": \"2026-03-27T09:00:00Z\",\n \"total_tokens_saved\": 12500,\n \"compression_savings_usd\": 0.04\n }\n ]\n },\n \"requests\": {\n \"total\": 42,\n \"cached\": 5,\n \"rate_limited\": 0,\n \"failed\": 0\n },\n \"tokens\": {\n \"input\": 50000,\n \"output\": 8000,\n \"saved\": 12500,\n \"savings_percent\": 25.0\n },\n \"cost\": {\n \"total_cost_usd\": 0.15,\n \"total_savings_usd\": 0.04\n },\n \"cache\": {\n \"entries\": 10,\n \"total_hits\": 5\n }\n}\n
/stats keeps the existing live/session fields, including savings_history, for backward compatibility. The new persistent_savings block is durable local proxy compression history stored by default at ~/.headroom/proxy_savings.json. Use HEADROOM_SAVINGS_PATH to override the file location.
For Anthropic-style providers that return cache-write TTL buckets, /stats also surfaces observed cache TTL usage under prefix_cache:
{\n \"prefix_cache\": {\n \"by_provider\": {\n \"anthropic\": {\n \"observed_ttl_buckets\": {\n \"5m\": {\"tokens\": 20000, \"requests\": 8},\n \"1h\": {\"tokens\": 50000, \"requests\": 12}\n },\n \"observed_ttl_mix\": {\n \"5m_pct\": 28.6,\n \"1h_pct\": 71.4,\n \"active_buckets\": [\"5m\", \"1h\"]\n }\n }\n },\n \"totals\": {\n \"observed_ttl_buckets\": {\n \"5m\": {\"tokens\": 20000, \"requests\": 8},\n \"1h\": {\"tokens\": 50000, \"requests\": 12}\n }\n }\n }\n}\n
These fields are observational only:
- they reflect provider-reported cache write buckets
- they do not configure TTL
- they do not represent remaining expiration time
"},{"location":"metrics/#historical-savings-endpoint","title":"Historical Savings Endpoint","text":"curl http://localhost:8787/stats-history\n
{\n \"schema_version\": 1,\n \"generated_at\": \"2026-03-27T09:10:00Z\",\n \"lifetime\": {\n \"tokens_saved\": 12500,\n \"compression_savings_usd\": 0.04\n },\n \"history\": [\n {\n \"timestamp\": \"2026-03-27T09:00:00Z\",\n \"total_tokens_saved\": 12000,\n \"compression_savings_usd\": 0.038\n }\n ],\n \"series\": {\n \"hourly\": [],\n \"daily\": [],\n \"weekly\": [],\n \"monthly\": []\n },\n \"exports\": {\n \"default_format\": \"json\",\n \"available_formats\": [\"json\", \"csv\"],\n \"available_series\": [\"history\", \"hourly\", \"daily\", \"weekly\", \"monthly\"]\n }\n}\n
/stats-history is the stable frontend-facing API for durable proxy compression history. It survives proxy restarts, tolerates missing or malformed state files, and powers the historical view in /dashboard. It now includes hourly, daily, weekly, and monthly chart-ready rollups.
For export-friendly downloads:
curl \"http://localhost:8787/stats-history?format=csv&series=daily\"\ncurl \"http://localhost:8787/stats-history?format=csv&series=monthly\"\n
CSV exports are available for history, hourly, daily, weekly, and monthly. Plain JSON remains the default response format.
"},{"location":"metrics/#prometheus-metrics","title":"Prometheus Metrics","text":"curl http://localhost:8787/metrics\n
# HELP headroom_requests_total Total number of requests\nheadroom_requests_total 1234\n\n# HELP headroom_latency_ms_count Count of observed request latencies\nheadroom_latency_ms_count 1234\n\n# HELP headroom_tokens_saved_total Tokens saved by optimization\nheadroom_tokens_saved_total 5678900\n\n# HELP headroom_requests_by_provider Requests by provider\nheadroom_requests_by_provider{provider=\"anthropic\"} 800\nheadroom_requests_by_provider{provider=\"openai\"} 434\n\n# HELP headroom_transform_timing_ms_sum Sum of transform timing in milliseconds\nheadroom_transform_timing_ms_sum{transform=\"router\"} 5123.7\n\n# HELP headroom_cache_write_ttl_tokens_total Provider cache write tokens by observed TTL bucket\nheadroom_cache_write_ttl_tokens_total{provider=\"anthropic\",ttl=\"5m\"} 20000\nheadroom_cache_write_ttl_tokens_total{provider=\"anthropic\",ttl=\"1h\"} 50000\n
The built-in Prometheus endpoint exposes the proxy's in-memory operational state, including:
- request counters
- token totals and savings
- latency / overhead / TTFB summaries
- per-provider and per-model request counts
- per-stage pipeline timing
- waste signal token totals
- provider cache read/write and TTL-bucket counters
- cache bust counters
"},{"location":"metrics/#otel-metrics","title":"OTEL Metrics","text":"Headroom now emits the same operational events through a shared OTEL metrics facade.
There are two integration modes:
- Ambient OTEL app setup - if your application already configures a global OTEL meter provider, Headroom records into that provider automatically.
- Headroom-managed export - if you want the proxy to configure its own OTEL metrics exporter, install:
pip install \"headroom-ai[proxy,otel]\"\n
Then set:
HEADROOM_OTEL_METRICS_ENABLED=1\nHEADROOM_OTEL_METRICS_EXPORTER=otlp_http\nHEADROOM_OTEL_METRICS_ENDPOINT=http://127.0.0.1:4318/v1/metrics\nHEADROOM_OTEL_SERVICE_NAME=headroom-proxy\nHEADROOM_OTEL_RESOURCE_ATTRIBUTES=deployment.environment=dev,service.namespace=headroom\n
For local validation without a collector:
HEADROOM_OTEL_METRICS_ENABLED=1\nHEADROOM_OTEL_METRICS_EXPORTER=console\nheadroom proxy\n
The proxy's /stats response now includes an otel block that reports whether Headroom is managing an OTEL exporter for the current process.
Headroom's managed OTEL exporters are intentionally scoped to Headroom's own instrumentation. If you already manage global OTEL providers in your app, keep using those and let Headroom record into the ambient providers instead of enabling HEADROOM_OTEL_*.
"},{"location":"metrics/#otel-environment-variables","title":"OTEL Environment Variables","text":"Variable Default Description HEADROOM_OTEL_METRICS_ENABLED 0 Enables Headroom-managed OTEL metric export HEADROOM_OTEL_METRICS_EXPORTER otlp_http Exporter type: otlp_http or console HEADROOM_OTEL_METRICS_ENDPOINT unset OTLP HTTP metrics endpoint HEADROOM_OTEL_METRICS_HEADERS unset Comma-separated key=value headers for OTLP export HEADROOM_OTEL_METRICS_EXPORT_INTERVAL_MS 10000 Periodic export interval in milliseconds HEADROOM_OTEL_SERVICE_NAME headroom-proxy in proxy mode OTEL service.name HEADROOM_OTEL_RESOURCE_ATTRIBUTES unset Comma-separated resource attributes"},{"location":"metrics/#anonymous-telemetry-vs-otel","title":"Anonymous Telemetry vs OTEL","text":"Headroom has two separate systems:
HEADROOM_TELEMETRY / --no-telemetry controls the privacy-preserving anonymous data-flywheel beacon and TOIN-related aggregate reporting. HEADROOM_OTEL_* controls operational OTEL metric export.
They are independent by design so you can disable the anonymous beacon while keeping OTEL metrics enabled, or vice versa.
"},{"location":"metrics/#langfuse","title":"Langfuse","text":"Langfuse fits next to this implementation as a trace backend, not as a metrics backend.
- Headroom metrics continue to go to
/metrics and/or your OTEL metrics exporter. - Langfuse receives OTLP traces for Headroom's compression pipeline.
- Headroom's
/stats response includes a langfuse block when Headroom is managing Langfuse trace export for the process.
Enable it with:
HEADROOM_LANGFUSE_ENABLED=1\nLANGFUSE_PUBLIC_KEY=pk-lf-...\nLANGFUSE_SECRET_KEY=sk-lf-...\nLANGFUSE_BASE_URL=https://cloud.langfuse.com\n
For self-hosted Langfuse, set LANGFUSE_BASE_URL to your instance URL.
"},{"location":"metrics/#health-check","title":"Health Check","text":"curl http://localhost:8787/health\n
{\n \"status\": \"healthy\",\n \"version\": \"0.1.0\",\n \"uptime_seconds\": 3600,\n \"llmlingua_enabled\": false\n}\n
"},{"location":"metrics/#sdk-metrics","title":"SDK Metrics","text":""},{"location":"metrics/#session-stats","title":"Session Stats","text":"Quick stats for the current session (no database query):
stats = client.get_stats()\nprint(stats)\n
{\n \"session\": {\n \"requests_total\": 10,\n \"tokens_input_before\": 50000,\n \"tokens_input_after\": 35000,\n \"tokens_saved_total\": 15000,\n \"tokens_output_total\": 8000,\n \"cache_hits\": 3,\n \"compression_ratio_avg\": 0.70\n },\n \"config\": {\n \"mode\": \"optimize\",\n \"provider\": \"openai\",\n \"cache_optimizer_enabled\": True,\n \"semantic_cache_enabled\": False\n },\n \"transforms\": {\n \"smart_crusher_enabled\": True,\n \"cache_aligner_enabled\": True,\n \"rolling_window_enabled\": True\n }\n}\n
"},{"location":"metrics/#historical-metrics","title":"Historical Metrics","text":"Query stored metrics from the database:
from datetime import datetime, timedelta\n\n# Get recent metrics\nmetrics = client.get_metrics(\n start_time=datetime.utcnow() - timedelta(hours=1),\n limit=100,\n)\n\nfor m in metrics:\n print(f\"{m.timestamp}: {m.tokens_input_before} -> {m.tokens_input_after}\")\n
"},{"location":"metrics/#summary-statistics","title":"Summary Statistics","text":"Aggregate statistics across all stored metrics:
summary = client.get_summary()\nprint(f\"Total requests: {summary['total_requests']}\")\nprint(f\"Total tokens saved: {summary['total_tokens_saved']}\")\nprint(f\"Average compression: {summary['avg_compression_ratio']:.1%}\")\nprint(f\"Total cost savings: ${summary['total_cost_saved_usd']:.2f}\")\n
"},{"location":"metrics/#logging","title":"Logging","text":""},{"location":"metrics/#enable-logging","title":"Enable Logging","text":"import logging\n\n# INFO level shows compression summaries\nlogging.basicConfig(level=logging.INFO)\n\n# DEBUG level shows detailed transform decisions\nlogging.basicConfig(level=logging.DEBUG)\n
"},{"location":"metrics/#log-output-examples","title":"Log Output Examples","text":"INFO:headroom.transforms.pipeline:Pipeline complete: 45000 -> 4500 tokens (saved 40500, 90.0% reduction)\nINFO:headroom.transforms.smart_crusher:SmartCrusher applied top_n strategy: kept 15 of 1000 items\nINFO:headroom.cache.compression_store:CCR cache hit: hash=abc123, retrieved 1000 items\nDEBUG:headroom.transforms.smart_crusher:Kept items: [0,1,2,42,77,97,98,99] (errors at 42, warnings at 77)\n
"},{"location":"metrics/#proxy-logging","title":"Proxy Logging","text":"# Log to file\nheadroom proxy --log-file headroom.jsonl\n\n# Increase verbosity\nheadroom proxy --log-level debug\n
"},{"location":"metrics/#grafana-dashboard","title":"Grafana Dashboard","text":"Example Grafana dashboard configuration for Prometheus metrics:
{\n \"panels\": [\n {\n \"title\": \"Tokens Saved\",\n \"type\": \"stat\",\n \"targets\": [{\"expr\": \"headroom_tokens_saved_total\"}]\n },\n {\n \"title\": \"Average Request Latency (ms)\",\n \"type\": \"gauge\",\n \"targets\": [{\"expr\": \"headroom_latency_ms_sum / clamp_min(headroom_latency_ms_count, 1)\"}]\n },\n {\n \"title\": \"Max Request Latency (ms)\",\n \"type\": \"graph\",\n \"targets\": [{\"expr\": \"headroom_latency_ms_max\"}]\n },\n {\n \"title\": \"Provider Cache Hit Rate\",\n \"type\": \"gauge\",\n \"targets\": [{\"expr\": \"headroom_provider_cache_hit_requests_total / clamp_min(headroom_provider_cache_requests_total, 1)\"}]\n }\n ]\n}\n
"},{"location":"metrics/#cost-tracking","title":"Cost Tracking","text":""},{"location":"metrics/#per-request-cost","title":"Per-Request Cost","text":"Each request includes cost metadata in the response:
response = client.chat.completions.create(...)\n\n# Access via response metadata (if available)\n# Cost is calculated based on model pricing and token counts\n
"},{"location":"metrics/#budget-alerts","title":"Budget Alerts","text":"Set a budget limit in the proxy:
headroom proxy --budget 10.00\n
When the budget is exceeded: - Requests return a budget exceeded error - The /stats endpoint shows budget status - Logs indicate budget state
"},{"location":"metrics/#validation","title":"Validation","text":"Validate your setup is correct:
result = client.validate_setup()\n\nif result[\"valid\"]:\n print(\"Setup is correct!\")\nelse:\n print(\"Issues found:\")\n for issue in result[\"issues\"]:\n print(f\" - {issue}\")\n
"},{"location":"metrics/#key-metrics-to-monitor","title":"Key Metrics to Monitor","text":"Metric What It Tells You Target tokens_saved_total Total cost savings Higher is better compression_ratio_avg Efficiency 0.7-0.9 typical cache_hit_rate Cache effectiveness >20% is good latency_p99 Performance impact <10ms failed_requests Reliability 0"},{"location":"proxy/","title":"Proxy Server Documentation","text":"The Headroom proxy server is a production-ready HTTP server that applies context optimization to all requests passing through it.
New: The proxy now supports the TypeScript SDK via the POST /v1/compress endpoint, enabling compression-as-a-service for any HTTP client without calling an LLM.
"},{"location":"proxy/#starting-the-proxy","title":"Starting the Proxy","text":"# Basic usage\nheadroom proxy\n\n# Custom port\nheadroom proxy --port 8080\n\n# With all options\nheadroom proxy \\\n --host 0.0.0.0 \\\n --port 8787 \\\n --log-file /var/log/headroom.jsonl \\\n --budget 100.0\n
Anonymous aggregate telemetry is enabled by default. Opt out with HEADROOM_TELEMETRY=off or headroom proxy --no-telemetry. Downstream apps can set HEADROOM_SDK=headroom-app to override the anonymous telemetry sdk label; the default remains proxy.
Operational OTEL metrics are configured separately and are off by default. Install headroom-ai[proxy,otel] and set:
HEADROOM_OTEL_METRICS_ENABLED=1\nHEADROOM_OTEL_METRICS_EXPORTER=otlp_http\nHEADROOM_OTEL_METRICS_ENDPOINT=http://127.0.0.1:4318/v1/metrics\nHEADROOM_OTEL_SERVICE_NAME=headroom-proxy\n
Use HEADROOM_OTEL_METRICS_EXPORTER=console for local smoke testing. HEADROOM_TELEMETRY controls the anonymous data-flywheel beacon only; it does not disable or enable OTEL export.
Langfuse can be enabled alongside this OTEL path for trace ingestion. Langfuse does not ingest OTEL metrics, so Headroom keeps metrics and Langfuse traces as complementary signals:
HEADROOM_LANGFUSE_ENABLED=1\nLANGFUSE_PUBLIC_KEY=pk-lf-...\nLANGFUSE_SECRET_KEY=sk-lf-...\nLANGFUSE_BASE_URL=https://cloud.langfuse.com\n
When configured, Headroom emits OTLP traces for the shared compression pipeline to Langfuse while continuing to expose metrics through /metrics and OTEL metric exporters.
"},{"location":"proxy/#command-line-options","title":"Command Line Options","text":""},{"location":"proxy/#core-options","title":"Core Options","text":"Option Default Description --host 127.0.0.1 Host to bind to --port 8787 Port to bind to --mode token Run mode: token (maximize compression) or cache (freeze prior turns) --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 --budget None Daily budget limit in USD --openai-api-url https://api.openai.com Custom OpenAI API URL endpoint"},{"location":"proxy/#run-modes","title":"Run Modes","text":"Headroom proxy has two explicit run modes:
token mode: prioritize token reduction. Prior history may be rewritten when that improves compression. cache mode: prioritize provider prefix cache stability. Prior turns are frozen; only the newest turn is mutable.
Set via CLI or env:
headroom proxy --mode token\nHEADROOM_MODE=cache headroom proxy\n
When to pick each:
token: best for maximizing immediate compression savings. cache: best for long conversations where preserving prior-turn bytes improves prefix-cache reuse.
Legacy values (token_headroom, cost_savings) are still accepted as aliases.
"},{"location":"proxy/#context-management-options","title":"Context Management Options","text":"Option Default Description --no-intelligent-context false Disable IntelligentContextManager (fall back to RollingWindow) --no-intelligent-scoring false Disable multi-factor importance scoring (use position-based) --no-compress-first false Disable trying deeper compression before dropping messages By default, the proxy uses IntelligentContextManager which scores messages by multiple factors (recency, semantic similarity, TOIN-learned patterns, error indicators, forward references) and drops lowest-scored messages first. This is smarter than simple age-based truncation.
CCR Integration: When messages are dropped, they're stored in CCR so the LLM can retrieve them if needed. The inserted marker includes the CCR reference. Drops are also recorded to TOIN, so the system learns which message patterns are important across all users.
# Use legacy RollingWindow (drops oldest first)\nheadroom proxy --no-intelligent-context\n\n# Disable semantic scoring (faster, but less intelligent)\nheadroom proxy --no-intelligent-scoring\n
"},{"location":"proxy/#llmlingua-options-ml-compression","title":"LLMLingua Options (ML Compression)","text":"Option Default Description --llmlingua false Enable LLMLingua-2 ML-based compression --llmlingua-device auto Device for model: auto, cuda, cpu, mps --llmlingua-rate 0.3 Target compression rate (0.3 = keep 30% of tokens) Note: LLMLingua requires additional dependencies: pip install headroom-ai[llmlingua]
# Enable LLMLingua with GPU acceleration\nheadroom proxy --llmlingua --llmlingua-device cuda\n\n# More aggressive compression (keep only 20%)\nheadroom proxy --llmlingua --llmlingua-rate 0.2\n\n# Conservative compression for code (keep 50%)\nheadroom proxy --llmlingua --llmlingua-rate 0.5\n
"},{"location":"proxy/#api-endpoints","title":"API Endpoints","text":""},{"location":"proxy/#liveness","title":"Liveness","text":"curl http://localhost:8787/livez\n
Response:
{\n \"service\": \"headroom-proxy\",\n \"status\": \"healthy\",\n \"alive\": true,\n \"version\": \"0.5.21\",\n \"timestamp\": \"2026-04-10T16:36:25Z\",\n \"uptime_seconds\": 12.483\n}\n
"},{"location":"proxy/#readiness","title":"Readiness","text":"curl http://localhost:8787/readyz\n
Response:
{\n \"service\": \"headroom-proxy\",\n \"status\": \"healthy\",\n \"ready\": true,\n \"version\": \"0.5.21\",\n \"timestamp\": \"2026-04-10T16:36:25Z\",\n \"uptime_seconds\": 12.483,\n \"checks\": {\n \"startup\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"http_client\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"cache\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"rate_limiter\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"memory\": {\"enabled\": false, \"ready\": true, \"status\": \"disabled\"}\n }\n}\n
/readyz returns HTTP 503 when Headroom has not completed startup or a required enabled subsystem is unavailable. This is the endpoint used by the container health checks.
"},{"location":"proxy/#aggregate-health","title":"Aggregate Health","text":"curl http://localhost:8787/health\n
Response:
{\n \"status\": \"healthy\",\n \"ready\": true,\n \"version\": \"0.5.21\",\n \"config\": {\n \"optimize\": true,\n \"cache\": true,\n \"rate_limit\": true\n },\n \"checks\": {\n \"startup\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"http_client\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"}\n }\n}\n
"},{"location":"proxy/#detailed-statistics","title":"Detailed Statistics","text":"curl http://localhost:8787/stats\n
/stats remains the live/session-oriented endpoint and now also includes a persistent_savings block with durable proxy compression lifetime totals plus a small recent preview. The existing savings_history field is still present and remains session-scoped for backward compatibility.
For providers that return cache-write TTL bucket usage, /stats also includes observed TTL breakdowns under prefix_cache:
observed_ttl_buckets.5m.tokens observed_ttl_buckets.1h.tokens observed_ttl_mix
These are provider-reported observations, not configured TTL and not remaining expiration time.
"},{"location":"proxy/#historical-savings","title":"Historical Savings","text":"curl http://localhost:8787/stats-history\n
/stats-history exposes durable proxy compression history for dashboards and other Headroom frontends. It returns:
- lifetime proxy compression totals
- bounded persisted checkpoint history
- derived hourly, daily, weekly, and monthly rollups for charts
- UTC timestamps throughout
By default the proxy stores this history at ~/.headroom/proxy_savings.json. Set HEADROOM_SAVINGS_PATH to override the location.
/dashboard uses this endpoint directly for its historical view, including the daily/weekly/monthly rollups and built-in JSON / CSV export buttons.
curl \"http://localhost:8787/stats-history?format=csv&series=weekly\"\ncurl \"http://localhost:8787/stats-history?format=csv&series=monthly\"\n
"},{"location":"proxy/#prometheus-metrics","title":"Prometheus Metrics","text":"curl http://localhost:8787/metrics\n
/metrics remains the built-in Prometheus-formatted operational view. The proxy now also emits the same operational events through the OTEL facade when OTEL metrics are configured.
"},{"location":"proxy/#llm-apis","title":"LLM APIs","text":"The proxy supports both Anthropic and OpenAI API formats:
# Anthropic format\nPOST /v1/messages\n\n# OpenAI format\nPOST /v1/chat/completions\n
"},{"location":"proxy/#post-v1compress","title":"POST /v1/compress","text":"Compression-only endpoint. Compresses messages without calling any LLM. Used by the TypeScript SDK and any HTTP client that wants compression as a service.
Request:
{\n \"messages\": [...], // OpenAI chat format\n \"model\": \"gpt-4o\" // model name (for token counting)\n}\n
Response:
{\n \"messages\": [...], // compressed messages\n \"tokens_before\": 15000,\n \"tokens_after\": 3500,\n \"tokens_saved\": 11500,\n \"compression_ratio\": 0.23,\n \"transforms_applied\": [\"router:smart_crusher:0.35\"],\n \"ccr_hashes\": [\"a1b2c3\"]\n}\n
Headers: - x-headroom-bypass: true \u2014 skip compression, return messages as-is
Error responses: 400 (missing fields), 401 (bad API key), 503 (compression failed)
"},{"location":"proxy/#using-with-claude-code","title":"Using with Claude Code","text":"# Start proxy\nheadroom proxy --port 8787\n\n# In another terminal\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n
"},{"location":"proxy/#using-with-cursor","title":"Using with Cursor","text":" - Start the proxy:
headroom proxy - In Cursor settings, set the base URL to
http://localhost:8787
"},{"location":"proxy/#using-with-openai-sdk","title":"Using with OpenAI SDK","text":"from openai import OpenAI\n\nclient = OpenAI(\n base_url=\"http://localhost:8787/v1\",\n api_key=\"your-api-key\", # Still needed for upstream\n)\n
"},{"location":"proxy/#features","title":"Features","text":""},{"location":"proxy/#llmlingua-ml-compression-opt-in","title":"LLMLingua ML Compression (Opt-In)","text":"When enabled, the proxy uses Microsoft's LLMLingua-2 model for ML-based token compression:
headroom proxy --llmlingua\n
How it works: - LLMLinguaCompressor is added to the transform pipeline (before RollingWindow) - Automatically detects content type (JSON, code, text) and adjusts compression - Stores original content in CCR for retrieval if needed
Startup feedback:
# When enabled and available:\nLLMLingua: ENABLED (device=cuda, rate=0.3)\n\n# When installed but not enabled (helpful hint):\nLLMLingua: available (enable with --llmlingua for ML compression)\n\n# When enabled but not installed:\nWARNING: LLMLingua requested but not installed. Install with: pip install headroom-ai[llmlingua]\n
Why opt-in? | Concern | Default Proxy | With LLMLingua | |---------|---------------|----------------| | Dependencies | ~50MB | +2GB (torch, transformers) | | Cold start | <1s | 10-30s (model load) | | Memory | ~100MB | +1GB (model in RAM) | | Overhead | <5ms | 50-200ms per request |
Enable LLMLingua when maximum compression justifies the resource cost.
"},{"location":"proxy/#semantic-caching","title":"Semantic Caching","text":"The proxy caches responses for repeated queries:
- LRU eviction with configurable max entries
- TTL-based expiration
- Cache key based on message content hash
"},{"location":"proxy/#rate-limiting","title":"Rate Limiting","text":"Token bucket rate limiting protects against runaway costs:
- Configurable requests per minute
- Configurable tokens per minute
- Per-API-key tracking
"},{"location":"proxy/#cost-tracking","title":"Cost Tracking","text":"Track spending and enforce budgets:
- Real-time cost estimation
- Budget periods: hourly, daily, monthly
- Automatic request rejection when over budget
"},{"location":"proxy/#prometheus-metrics_1","title":"Prometheus Metrics","text":"Export metrics for monitoring:
headroom_requests_total\nheadroom_tokens_saved_total\nheadroom_cost_usd_total\nheadroom_latency_ms_sum\n
"},{"location":"proxy/#configuration-via-environment","title":"Configuration via Environment","text":"export HEADROOM_HOST=0.0.0.0\nexport HEADROOM_PORT=8787\nexport HEADROOM_BUDGET=100.0\nexport OPENAI_TARGET_API_URL=https://custom.openai.endpoint.com\nheadroom proxy\n
"},{"location":"proxy/#running-in-production","title":"Running in Production","text":"For production deployments:
# Use a process manager\npip install gunicorn\n\n# Run with gunicorn\ngunicorn headroom.proxy.server:app \\\n --workers 4 \\\n --bind 0.0.0.0:8787 \\\n --worker-class uvicorn.workers.UvicornWorker\n
Or with Docker:
FROM python:3.11-slim\nRUN apt-get update && apt-get install -y --no-install-recommends build-essential \\\n && pip install \"headroom-ai[proxy]\" \\\n && apt-get purge -y build-essential && apt-get autoremove -y \\\n && rm -rf /var/lib/apt/lists/*\nEXPOSE 8787\nCMD [\"headroom\", \"proxy\", \"--host\", \"0.0.0.0\"]\n
Note: build-essential is required at install time because headroom-ai includes hnswlib, a C++ extension that must be compiled from source. It is removed after installation to keep the image slim.
"},{"location":"quickstart/","title":"Quickstart Guide","text":"Get Headroom running in 5 minutes with these copy-paste examples.
"},{"location":"quickstart/#installation","title":"Installation","text":"Python:
# Core only (minimal dependencies)\npip install headroom-ai\n\n# With proxy server\npip install \"headroom-ai[proxy]\"\n\n# Everything\npip install \"headroom-ai[all]\"\n
TypeScript / Node.js:
npm install headroom-ai\n
"},{"location":"quickstart/#option-1-proxy-server-zero-code-changes","title":"Option 1: Proxy Server (Zero Code Changes)","text":"The fastest way to start saving tokens. Works with any OpenAI-compatible client.
"},{"location":"quickstart/#step-1-start-the-proxy","title":"Step 1: Start the Proxy","text":"headroom proxy --port 8787\n
"},{"location":"quickstart/#step-2-verify-its-running","title":"Step 2: Verify It's Running","text":"curl http://localhost:8787/health\n# Expected: {\"status\": \"healthy\", \"mode\": \"optimize\", ...}\n
"},{"location":"quickstart/#step-3-point-your-client","title":"Step 3: Point Your Client","text":"# Claude Code\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n\n# Cursor / Continue / any OpenAI client\nOPENAI_BASE_URL=http://localhost:8787/v1 your-app\n\n# Python\nexport OPENAI_BASE_URL=http://localhost:8787/v1\npython your_script.py\n
"},{"location":"quickstart/#step-4-check-savings","title":"Step 4: Check Savings","text":"curl http://localhost:8787/stats\n# {\"requests_total\": 42, \"tokens_saved_total\": 125000, ...}\n
"},{"location":"quickstart/#option-2-python-sdk","title":"Option 2: Python SDK","text":"Wrap your existing client for fine-grained control.
"},{"location":"quickstart/#basic-example","title":"Basic Example","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\n\n# Create wrapped client\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n)\n\n# Use exactly like OpenAI client\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[\n {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Hello!\"},\n ],\n)\n\nprint(response.choices[0].message.content)\n\n# Check what happened\nstats = client.get_stats()\nprint(f\"Tokens saved: {stats['session']['tokens_saved_total']}\")\n
"},{"location":"quickstart/#with-tool-outputs-where-savings-happen","title":"With Tool Outputs (Where Savings Happen)","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\nimport json\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n)\n\n# Simulate a conversation with large tool outputs\nmessages = [\n {\"role\": \"system\", \"content\": \"You analyze search results.\"},\n {\"role\": \"user\", \"content\": \"Search for Python tutorials.\"},\n {\n \"role\": \"assistant\",\n \"content\": None,\n \"tool_calls\": [{\n \"id\": \"call_1\",\n \"type\": \"function\",\n \"function\": {\"name\": \"search\", \"arguments\": '{\"q\": \"python\"}'},\n }],\n },\n {\n \"role\": \"tool\",\n \"tool_call_id\": \"call_1\",\n # This is where Headroom shines - compressing large outputs\n \"content\": json.dumps({\n \"results\": [{\"title\": f\"Result {i}\", \"score\": 100-i} for i in range(500)]\n }),\n },\n {\"role\": \"user\", \"content\": \"What are the top 3 results?\"},\n]\n\n# Headroom compresses the 500 results to ~20, keeping the most relevant\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n)\n\nprint(response.choices[0].message.content)\n
"},{"location":"quickstart/#simulate-before-sending","title":"Simulate Before Sending","text":"Preview optimizations without making an API call:
# See what would happen without calling the API\nplan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=messages,\n)\n\nprint(f\"Tokens before: {plan.tokens_before}\")\nprint(f\"Tokens after: {plan.tokens_after}\")\nprint(f\"Would save: {plan.tokens_saved} tokens ({plan.tokens_saved/plan.tokens_before*100:.0f}%)\")\nprint(f\"Transforms: {plan.transforms}\")\nprint(f\"Estimated savings: {plan.estimated_savings}\")\n
"},{"location":"quickstart/#option-3-anthropic-sdk","title":"Option 3: Anthropic SDK","text":"from headroom import HeadroomClient, AnthropicProvider\nfrom anthropic import Anthropic\n\nclient = HeadroomClient(\n original_client=Anthropic(),\n provider=AnthropicProvider(),\n default_mode=\"optimize\",\n)\n\n# Use Anthropic-style API\nresponse = client.messages.create(\n model=\"claude-sonnet-4-20250514\",\n max_tokens=1024,\n messages=[\n {\"role\": \"user\", \"content\": \"Hello, Claude!\"},\n ],\n)\n\nprint(response.content[0].text)\n
"},{"location":"quickstart/#verify-its-working","title":"Verify It's Working","text":""},{"location":"quickstart/#method-1-enable-logging","title":"Method 1: Enable Logging","text":"import logging\nlogging.basicConfig(level=logging.INFO)\n\n# Now you'll see:\n# INFO:headroom.transforms.pipeline:Pipeline complete: 45000 -> 4500 tokens (saved 40500, 90.0% reduction)\n# INFO:headroom.transforms.smart_crusher:SmartCrusher: keeping 15 of 500 items\n
"},{"location":"quickstart/#method-2-check-session-stats","title":"Method 2: Check Session Stats","text":"stats = client.get_stats()\nprint(stats)\n# {\n# \"session\": {\"requests_total\": 10, \"tokens_saved_total\": 5000, ...},\n# \"config\": {\"mode\": \"optimize\", \"provider\": \"openai\", ...},\n# \"transforms\": {\"smart_crusher_enabled\": True, ...}\n# }\n
"},{"location":"quickstart/#method-3-validate-setup","title":"Method 3: Validate Setup","text":"result = client.validate_setup()\nif not result[\"valid\"]:\n print(\"Setup issues:\", result)\nelse:\n print(\"Setup OK!\")\n print(f\"Provider: {result['provider']['name']}\")\n print(f\"Storage: {result['storage']['url']}\")\n
"},{"location":"quickstart/#common-configuration","title":"Common Configuration","text":""},{"location":"quickstart/#adjust-compression","title":"Adjust Compression","text":"from headroom import HeadroomClient, OpenAIProvider, HeadroomConfig\n\nconfig = HeadroomConfig()\n\n# Keep more items after compression (default: 15)\nconfig.smart_crusher.max_items_after_crush = 30\n\n# Only compress if tool output has > 500 tokens (default: 200)\nconfig.smart_crusher.min_tokens_to_crush = 500\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n config=config, # Pass custom config\n default_mode=\"optimize\",\n)\n
"},{"location":"quickstart/#skip-compression-for-specific-tools","title":"Skip Compression for Specific Tools","text":"response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n headroom_tool_profiles={\n \"database_query\": {\"skip_compression\": True}, # Never compress\n \"search\": {\"max_items\": 50}, # Keep more items\n },\n)\n
"},{"location":"quickstart/#audit-mode-observe-only","title":"Audit Mode (Observe Only)","text":"# Start in audit mode - see what WOULD be optimized\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"audit\", # No modifications, just logging\n)\n\n# Override per-request\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n headroom_mode=\"optimize\", # Enable for this request only\n)\n
"},{"location":"quickstart/#what-gets-optimized","title":"What Gets Optimized?","text":"Content Type What Headroom Does Typical Savings Tool outputs with lists Keeps errors, anomalies, high-score items 70-90% Repeated search results Deduplicates and samples 60-80% Long conversations Drops old turns, keeps recent 40-60% System prompts with dates Stabilizes for cache hits Cache savings"},{"location":"quickstart/#next-steps","title":"Next Steps","text":" - Configuration Reference - All configuration options
- Transform Reference - How each transform works
- Troubleshooting - Common issues and solutions
- Examples - More complete examples
"},{"location":"quickstart/#quick-troubleshooting","title":"Quick Troubleshooting","text":""},{"location":"quickstart/#no-token-savings","title":"\"No token savings\"","text":"# 1. Check mode\nstats = client.get_stats()\nprint(stats[\"config\"][\"mode\"]) # Should be \"optimize\"\n\n# 2. Enable logging to see what's happening\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\n
"},{"location":"quickstart/#high-latency","title":"\"High latency\"","text":"# Use BM25 instead of embeddings for faster relevance scoring\nconfig.smart_crusher.relevance.tier = \"bm25\"\n
"},{"location":"quickstart/#compression-too-aggressive","title":"\"Compression too aggressive\"","text":"# Keep more items\nconfig.smart_crusher.max_items_after_crush = 50\n
See Troubleshooting Guide for more solutions.
"},{"location":"sdk/","title":"SDK Guide","text":"The Headroom SDK wraps your existing LLM client to add compression and optimization transparently.
"},{"location":"sdk/#installation","title":"Installation","text":"pip install headroom-ai openai\n
"},{"location":"sdk/#quick-start","title":"Quick Start","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\n\n# Create wrapped client\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n)\n\n# Use exactly like the original client\nresponse = client.chat.completions.create(\n model=\"gpt-4o-mini\",\n messages=[\n {\"role\": \"user\", \"content\": \"Hello!\"},\n ],\n)\n\nprint(response.choices[0].message.content)\n
"},{"location":"sdk/#tool-output-compression","title":"Tool Output Compression","text":"Real savings happen with tool outputs. Here's where Headroom shines:
import json\n\n# Conversation with large tool output\nmessages = [\n {\"role\": \"user\", \"content\": \"Search for Python tutorials\"},\n {\n \"role\": \"assistant\",\n \"content\": None,\n \"tool_calls\": [{\n \"id\": \"call_123\",\n \"type\": \"function\",\n \"function\": {\"name\": \"search\", \"arguments\": '{\"q\": \"python\"}'},\n }],\n },\n {\n \"role\": \"tool\",\n \"tool_call_id\": \"call_123\",\n \"content\": json.dumps({\n \"results\": [\n {\"title\": f\"Tutorial {i}\", \"score\": 100-i}\n for i in range(500)\n ]\n }),\n },\n {\"role\": \"user\", \"content\": \"What are the top 3?\"},\n]\n\n# Headroom compresses 500 results to ~15, keeping highest-scoring items\nresponse = client.chat.completions.create(\n model=\"gpt-4o-mini\",\n messages=messages\n)\n\n# Check savings\nstats = client.get_stats()\nprint(f\"Tokens saved: {stats['session']['tokens_saved_total']}\")\n# Typical output: \"Tokens saved: 3500\"\n
"},{"location":"sdk/#supported-providers","title":"Supported Providers","text":""},{"location":"sdk/#openai","title":"OpenAI","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n)\n
"},{"location":"sdk/#anthropic","title":"Anthropic","text":"from headroom import HeadroomClient, AnthropicProvider\nfrom anthropic import Anthropic\n\nclient = HeadroomClient(\n original_client=Anthropic(),\n provider=AnthropicProvider(),\n)\n\nresponse = client.messages.create(\n model=\"claude-3-5-sonnet-20241022\",\n max_tokens=1024,\n messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n)\n
"},{"location":"sdk/#google","title":"Google","text":"from headroom import HeadroomClient, GoogleProvider\nimport google.generativeai as genai\n\nclient = HeadroomClient(\n original_client=genai,\n provider=GoogleProvider(),\n)\n
"},{"location":"sdk/#check-stats","title":"Check Stats","text":"# Session stats (no database query)\nstats = client.get_stats()\nprint(stats)\n# {\n# \"session\": {\"requests_total\": 10, \"tokens_saved_total\": 5000, ...},\n# \"config\": {\"mode\": \"optimize\", \"provider\": \"openai\", ...},\n# \"transforms\": {\"smart_crusher_enabled\": True, ...}\n# }\n
"},{"location":"sdk/#validate-setup","title":"Validate Setup","text":"result = client.validate_setup()\nif not result[\"valid\"]:\n print(\"Setup issues:\", result[\"issues\"])\n
"},{"location":"sdk/#modes","title":"Modes","text":""},{"location":"sdk/#optimize-default","title":"Optimize (Default)","text":"Applies all safe transforms:
client = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n)\n
"},{"location":"sdk/#audit","title":"Audit","text":"Observes and logs without modifying:
client = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"audit\",\n)\n
"},{"location":"sdk/#simulate","title":"Simulate","text":"Returns a plan without making the API call:
plan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=large_conversation,\n)\n\nprint(f\"Would save {plan.tokens_saved} tokens\")\nprint(f\"Transforms: {plan.transforms}\")\n
"},{"location":"sdk/#per-request-overrides","title":"Per-Request Overrides","text":"response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[...],\n\n # Override mode for this request\n headroom_mode=\"audit\",\n\n # Reserve more tokens for output\n headroom_output_buffer_tokens=8000,\n\n # Keep last N turns\n headroom_keep_turns=5,\n)\n
"},{"location":"sdk/#enable-logging","title":"Enable Logging","text":"import logging\nlogging.basicConfig(level=logging.INFO)\n\n# Now you'll see:\n# INFO:headroom.transforms.pipeline:Pipeline complete: 45000 -> 4500 tokens\n# INFO:headroom.transforms.smart_crusher:SmartCrusher: kept 15 of 1000 items\n
"},{"location":"sdk/#streaming","title":"Streaming","text":"Streaming works transparently:
stream = client.chat.completions.create(\n model=\"gpt-4o-mini\",\n messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n stream=True,\n)\n\nfor chunk in stream:\n if chunk.choices[0].delta.content:\n print(chunk.choices[0].delta.content, end=\"\")\n
"},{"location":"sdk/#error-handling","title":"Error Handling","text":"from headroom import (\n HeadroomClient,\n HeadroomError,\n ConfigurationError,\n ProviderError,\n)\n\ntry:\n response = client.chat.completions.create(...)\nexcept ConfigurationError as e:\n print(f\"Config issue: {e}\")\nexcept ProviderError as e:\n print(f\"Provider issue: {e}\")\nexcept HeadroomError as e:\n print(f\"Headroom error: {e}\")\n
"},{"location":"sdk/#historical-metrics","title":"Historical Metrics","text":"Query stored metrics:
from datetime import datetime, timedelta\n\nmetrics = client.get_metrics(\n start_time=datetime.utcnow() - timedelta(hours=1),\n limit=100,\n)\n\nfor m in metrics:\n print(f\"{m.timestamp}: {m.tokens_input_before} -> {m.tokens_input_after}\")\n
"},{"location":"sdk/#advanced-configuration","title":"Advanced Configuration","text":"See Configuration for full options:
client = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n enable_cache_optimizer=True,\n enable_semantic_cache=False,\n model_context_limits={\n \"gpt-4o\": 128000,\n \"gpt-4o-mini\": 128000,\n },\n)\n
"},{"location":"sdk/#comparison-with-proxy","title":"Comparison with Proxy","text":"Aspect SDK Proxy Setup Wrap client Point URL Control Fine-grained Global Metrics In-process Centralized Best for Custom apps Existing tools Use the SDK when you need fine-grained control. Use the proxy for existing tools like Claude Code, Cursor, etc.
"},{"location":"shared-context/","title":"SharedContext \u2014 Compressed Inter-Agent Context Sharing","text":"When agents hand off to each other, context gets replayed in full. SharedContext compresses what moves between agents using Headroom's compression pipeline.
"},{"location":"shared-context/#quick-start","title":"Quick Start","text":"from headroom import SharedContext\n\nctx = SharedContext()\n\n# Agent A stores large output\nctx.put(\"research\", big_research_output, agent=\"researcher\")\n\n# Agent B gets compressed version (~80% smaller)\nsummary = ctx.get(\"research\")\n\n# Agent B needs full details\nfull = ctx.get(\"research\", full=True)\n
"},{"location":"shared-context/#api","title":"API","text":""},{"location":"shared-context/#putkey-content-agentnone","title":"put(key, content, *, agent=None)","text":"Store content under a key. Compresses automatically using Headroom's full pipeline (SmartCrusher for JSON, CodeCompressor for code, Kompress for text).
entry = ctx.put(\"findings\", big_json_output, agent=\"researcher\")\n\nentry.original_tokens # 20,000\nentry.compressed_tokens # 4,000\nentry.savings_percent # 80.0\nentry.transforms # [\"router:json:0.20\"]\n
"},{"location":"shared-context/#getkey-fullfalse","title":"get(key, *, full=False)","text":"Retrieve content. Returns compressed version by default, original with full=True.
compressed = ctx.get(\"findings\") # 4K tokens\noriginal = ctx.get(\"findings\", full=True) # 20K tokens\nmissing = ctx.get(\"nonexistent\") # None\n
"},{"location":"shared-context/#get_entrykey","title":"get_entry(key)","text":"Get the full ContextEntry with metadata.
entry = ctx.get_entry(\"findings\")\nentry.key # \"findings\"\nentry.agent # \"researcher\"\nentry.original_tokens # 20000\nentry.compressed_tokens # 4000\nentry.savings_percent # 80.0\nentry.timestamp # 1710000000.0\nentry.transforms # [\"router:json:0.20\"]\n
"},{"location":"shared-context/#keys","title":"keys()","text":"List all non-expired keys.
"},{"location":"shared-context/#stats","title":"stats()","text":"Aggregated stats across all entries.
stats = ctx.stats()\nstats.entries # 3\nstats.total_original_tokens # 60000\nstats.total_compressed_tokens # 12000\nstats.total_tokens_saved # 48000\nstats.savings_percent # 80.0\n
"},{"location":"shared-context/#clear","title":"clear()","text":"Remove all entries.
"},{"location":"shared-context/#configuration","title":"Configuration","text":"ctx = SharedContext(\n model=\"claude-sonnet-4-5-20250929\", # For token counting\n ttl=3600, # 1 hour (default)\n max_entries=100, # Evicts oldest when full\n)\n
"},{"location":"shared-context/#framework-examples","title":"Framework Examples","text":""},{"location":"shared-context/#crewai","title":"CrewAI","text":"from headroom import SharedContext\n\nctx = SharedContext()\n\n# After researcher task\nctx.put(\"findings\", researcher_task.output.raw)\n\n# Coder task gets compressed context\ncoder_context = ctx.get(\"findings\")\n
"},{"location":"shared-context/#langgraph","title":"LangGraph","text":"from headroom import SharedContext\n\nctx = SharedContext()\n\ndef researcher_node(state):\n result = do_research()\n ctx.put(\"research\", result)\n return {\"research_summary\": ctx.get(\"research\")}\n\ndef coder_node(state):\n # Compressed summary in state, full details on demand\n full = ctx.get(\"research\", full=True)\n return {\"code\": write_code(full)}\n
"},{"location":"shared-context/#openai-agents-sdk","title":"OpenAI Agents SDK","text":"from headroom import SharedContext\n\nctx = SharedContext()\n\ndef compress_handoff(messages):\n for msg in messages:\n if len(msg.content) > 1000:\n ctx.put(msg.id, msg.content)\n msg.content = ctx.get(msg.id)\n return messages\n\nhandoff(agent=coder, input_filter=compress_handoff)\n
"},{"location":"shared-context/#any-framework","title":"Any Framework","text":"SharedContext is framework-agnostic. It's just put() and get(). Use it wherever context moves between agents.
"},{"location":"shared-context/#how-it-works","title":"How It Works","text":"Under the hood, put() calls headroom.compress() (the same pipeline used by the proxy) and stores the original in memory. get() returns the compressed version. get(full=True) returns the original.
- JSON arrays \u2192 SmartCrusher (70-95% compression)
- Code \u2192 CodeCompressor (AST-aware, with
[code] extra) - Text \u2192 Kompress (ModernBERT, with
[ml] extra) or passthrough - Entries expire after TTL (default 1 hour)
- Oldest entries evicted when max_entries reached
"},{"location":"strands/","title":"Strands Integration","text":"Headroom integrates with Strands Agents to provide automatic context optimization. Two integration patterns: wrap the model, or hook into tool calls.
"},{"location":"strands/#installation","title":"Installation","text":"pip install headroom-ai strands-agents\n
"},{"location":"strands/#quick-start","title":"Quick Start","text":"from strands import Agent\nfrom strands.models.bedrock import BedrockModel\nfrom headroom.integrations.strands import HeadroomStrandsModel\n\n# Wrap your model\nmodel = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-20250514-v1:0\")\noptimized = HeadroomStrandsModel(wrapped_model=model)\n\n# Create agent as usual\nagent = Agent(model=optimized)\nresponse = agent(\"Investigate the production incident\")\n\n# Check savings\nprint(f\"Tokens saved: {optimized.total_tokens_saved}\")\n
Every API call the agent makes \u2014 including tool result round-trips \u2014 gets compressed automatically.
"},{"location":"strands/#integration-patterns","title":"Integration Patterns","text":""},{"location":"strands/#1-model-wrapping","title":"1. Model Wrapping","text":"Wraps the Strands Model interface. Every call to stream() compresses the messages before they hit the provider.
from strands.models.bedrock import BedrockModel\nfrom headroom.integrations.strands import HeadroomStrandsModel\n\nmodel = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-20250514-v1:0\")\noptimized = HeadroomStrandsModel(wrapped_model=model)\n\n# Streaming works identically\nagent = Agent(model=optimized)\nresponse = agent(\"Analyze these logs\")\n
With custom config:
from headroom import HeadroomConfig\n\nconfig = HeadroomConfig()\noptimized = HeadroomStrandsModel(wrapped_model=model, config=config)\n
"},{"location":"strands/#2-hook-provider-tool-output-compression","title":"2. Hook Provider (Tool Output Compression)","text":"Compresses tool call results via Strands' hook system. Uses SmartCrusher on JSON arrays returned by tools.
from strands import Agent\nfrom strands.models.bedrock import BedrockModel\nfrom headroom.integrations.strands import HeadroomHookProvider\n\nmodel = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-20250514-v1:0\")\nhooks = HeadroomHookProvider(\n compress_tool_outputs=True,\n min_tokens_to_compress=200,\n preserve_errors=True,\n)\n\nagent = Agent(model=model, hooks=[hooks])\nresponse = agent(\"Search the database for recent failures\")\n\n# Check tool compression savings\nprint(f\"Tokens saved by hooks: {hooks.total_tokens_saved}\")\n
The hook preserves:
- Error items (error indicators, exceptions)
- Anomalous values (statistical outliers)
- Items matching the user's query context
- First/last items for boundary context
"},{"location":"strands/#3-both-together","title":"3. Both Together","text":"Model wrapping compresses conversation history. Hooks compress individual tool results. Use both for maximum savings.
from headroom.integrations.strands import HeadroomStrandsModel, HeadroomHookProvider\n\noptimized = HeadroomStrandsModel(wrapped_model=model)\nhooks = HeadroomHookProvider(compress_tool_outputs=True)\n\nagent = Agent(model=optimized, hooks=[hooks])\n
"},{"location":"strands/#structured-output","title":"Structured Output","text":"HeadroomStrandsModel supports Strands' structured output feature:
from pydantic import BaseModel\n\nclass Analysis(BaseModel):\n severity: str\n root_cause: str\n recommendation: str\n\nresult = optimized.structured_output(Analysis, messages)\n
"},{"location":"strands/#metrics","title":"Metrics","text":"# Per-request metrics\nfor m in optimized.metrics_history:\n print(f\" {m.tokens_before} \u2192 {m.tokens_after} ({m.tokens_saved} saved)\")\n\n# Running total\nprint(f\"Total saved: {optimized.total_tokens_saved}\")\n
"},{"location":"strands/#how-it-works","title":"How It Works","text":"Agent decides to call tool\n \u2502\n \u25bc\nTool executes, returns result\n \u2502\n \u25bc\nHeadroomHookProvider (optional)\n compresses tool result JSON\n \u2502\n \u25bc\nAgent builds next API request\n \u2502\n \u25bc\nHeadroomStrandsModel.stream()\n compresses full message list\n \u2502\n \u25bc\nProvider API (Bedrock, etc.)\n
The model wrapper uses Headroom's full pipeline (CacheAligner \u2192 ContentRouter \u2192 IntelligentContext). The hook provider uses SmartCrusher directly for fast JSON compression of individual tool results.
"},{"location":"strands/#supported-providers","title":"Supported Providers","text":"HeadroomStrandsModel auto-detects the provider from the wrapped model:
Strands Model Provider Detected BedrockModel Anthropic (via Bedrock) OllamaModel OpenAI-compatible Custom Model Falls back to estimation"},{"location":"text-compression/","title":"Text Compression Utilities","text":"For coding tasks, Headroom provides standalone text compression utilities that applications can use explicitly. These are opt-in \u2014 they're not applied automatically, giving you full control over when and how to compress text content.
Design Philosophy: SmartCrusher compresses JSON automatically because it's structure-preserving and safe. Text compression is lossy and context-dependent, so applications should decide when to use it.
"},{"location":"text-compression/#available-utilities","title":"Available Utilities","text":"Utility Input Type Use Case SearchCompressor grep/ripgrep output Search results with file:line:content format LogCompressor Build/test logs pytest, npm, cargo, make output TextCompressor Generic text Any plain text with anchor preservation detect_content_type Any content Detect content type for routing decisions"},{"location":"text-compression/#searchcompressor","title":"SearchCompressor","text":"Compresses search results (grep, ripgrep, ag) while preserving relevant matches.
from headroom.transforms import SearchCompressor\n\n# Your grep/ripgrep output (could be 1000s of lines)\nsearch_results = \"\"\"\nsrc/utils.py:42:def process_data(items):\nsrc/utils.py:43: \\\"\\\"\\\"Process items.\\\"\\\"\\\"\nsrc/models.py:15:class DataProcessor:\nsrc/models.py:89: def process(self, items):\n... hundreds more matches ...\n\"\"\"\n\n# Explicitly compress when you decide it's appropriate\ncompressor = SearchCompressor()\nresult = compressor.compress(search_results, context=\"find process\")\n\nprint(f\"Compressed {result.original_match_count} matches to {result.compressed_match_count}\")\nprint(result.compressed)\n
"},{"location":"text-compression/#what-gets-preserved","title":"What Gets Preserved","text":" - Exact query matches: Lines containing the search term
- High-relevance matches: Scored by BM25 similarity to context
- File diversity: Ensures results from different files are kept
- First/last matches: Context from start and end of results
"},{"location":"text-compression/#logcompressor","title":"LogCompressor","text":"Compresses build and test output while preserving errors, warnings, and summaries.
from headroom.transforms import LogCompressor\n\n# pytest output with 1000s of lines\nbuild_output = \"\"\"\n===== test session starts =====\ncollected 500 items\ntests/test_foo.py::test_1 PASSED\n... hundreds of passed tests ...\ntests/test_bar.py::test_fail FAILED\nAssertionError: expected 5, got 3\n===== 1 failed, 499 passed =====\n\"\"\"\n\n# Compress logs, preserving errors and stack traces\ncompressor = LogCompressor()\nresult = compressor.compress(build_output)\n\n# Errors, stack traces, and summary are preserved\nprint(result.compressed)\nprint(f\"Compression ratio: {result.compression_ratio:.1%}\")\n
"},{"location":"text-compression/#what-gets-preserved_1","title":"What Gets Preserved","text":" - Errors and failures: Any line with ERROR, FAILED, Exception, etc.
- Warnings: Warning messages that might be important
- Stack traces: Full tracebacks for debugging
- Summaries: Test/build summary lines
- Section headers: Structural markers like
=====
"},{"location":"text-compression/#textcompressor","title":"TextCompressor","text":"General-purpose text compression with anchor preservation.
from headroom.transforms import TextCompressor\n\nlong_text = \"\"\"\n... thousands of lines of documentation ...\n\"\"\"\n\ncompressor = TextCompressor()\nresult = compressor.compress(long_text, context=\"authentication\")\n\nprint(result.compressed)\n
"},{"location":"text-compression/#what-gets-preserved_2","title":"What Gets Preserved","text":" - Relevant paragraphs: Scored by similarity to context
- Anchors: Headers, section markers, important keywords
- Structure: Document organization is maintained
"},{"location":"text-compression/#content-type-detection","title":"Content Type Detection","text":"Automatically detect content type to route to the right compressor.
from headroom.transforms import detect_content_type, ContentType\n\ncontent = \"src/main.py:42:def process():\"\n\ndetection = detect_content_type(content)\nif detection.content_type == ContentType.SEARCH_RESULTS:\n # Route to SearchCompressor\n pass\nelif detection.content_type == ContentType.BUILD_OUTPUT:\n # Route to LogCompressor\n pass\nelif detection.content_type == ContentType.PLAIN_TEXT:\n # Route to TextCompressor\n pass\n
"},{"location":"text-compression/#content-types","title":"Content Types","text":"Type Detection Pattern SEARCH_RESULTS file:line:content format BUILD_OUTPUT pytest, npm, cargo markers JSON Valid JSON structure PLAIN_TEXT Default fallback"},{"location":"text-compression/#integration-pattern","title":"Integration Pattern","text":"from headroom.transforms import (\n detect_content_type, ContentType,\n SearchCompressor, LogCompressor, TextCompressor\n)\n\ndef compress_tool_output(content: str, context: str = \"\") -> str:\n \"\"\"Application-level compression with explicit control.\"\"\"\n detection = detect_content_type(content)\n\n if detection.content_type == ContentType.SEARCH_RESULTS:\n result = SearchCompressor().compress(content, context)\n return result.compressed\n elif detection.content_type == ContentType.BUILD_OUTPUT:\n result = LogCompressor().compress(content)\n return result.compressed\n elif detection.content_type == ContentType.PLAIN_TEXT:\n result = TextCompressor().compress(content, context)\n return result.compressed\n else:\n # JSON or other - let SmartCrusher handle it automatically\n return content\n
"},{"location":"text-compression/#configuration","title":"Configuration","text":"Each compressor accepts configuration options:
from headroom.transforms import SearchCompressor, SearchCompressorConfig\n\nconfig = SearchCompressorConfig(\n max_results=50, # Keep up to 50 matches\n preserve_file_diversity=True, # Ensure different files represented\n relevance_threshold=0.3, # Minimum relevance score to keep\n)\n\ncompressor = SearchCompressor(config)\n
"},{"location":"text-compression/#performance","title":"Performance","text":"Compressor Typical Input Output Speed SearchCompressor 1000 matches 30-50 matches ~2ms LogCompressor 5000 lines 100-200 lines ~3ms TextCompressor 10000 chars 2000 chars ~2ms"},{"location":"text-compression/#when-to-use","title":"When to Use","text":"Scenario Recommendation JSON tool output Let SmartCrusher handle automatically grep/ripgrep results Use SearchCompressor pytest/npm/cargo output Use LogCompressor Documentation/README Use TextCompressor Unknown content Use detect_content_type to route"},{"location":"transforms/","title":"Transform Reference","text":"Headroom provides several transforms that work together to optimize LLM context.
"},{"location":"transforms/#smartcrusher","title":"SmartCrusher","text":"Statistical compression for JSON tool outputs.
"},{"location":"transforms/#how-it-works","title":"How It Works","text":"SmartCrusher analyzes JSON arrays and selectively keeps important items:
- First/Last items - Context for pagination and recency
- Error items - 100% preservation of error states
- Anomalies - Statistical outliers (> 2 std dev from mean)
- Relevant items - Matches to user's query via BM25/embeddings
- Change points - Significant transitions in data
"},{"location":"transforms/#configuration","title":"Configuration","text":"from headroom import SmartCrusherConfig\n\nconfig = SmartCrusherConfig(\n min_tokens_to_crush=200, # Only compress if > 200 tokens\n max_items_after_crush=50, # Keep at most 50 items\n keep_first=3, # Always keep first 3 items\n keep_last=2, # Always keep last 2 items\n relevance_threshold=0.3, # Keep items with relevance > 0.3\n anomaly_std_threshold=2.0, # Keep items > 2 std dev from mean\n preserve_errors=True, # Always keep error items\n)\n
"},{"location":"transforms/#example","title":"Example","text":"from headroom import SmartCrusher\n\ncrusher = SmartCrusher(config)\n\n# Before: 1000 search results (45,000 tokens)\ntool_output = {\"results\": [...1000 items...]}\n\n# After: ~50 important items (4,500 tokens) - 90% reduction\ncompressed = crusher.crush(tool_output, query=\"user's question\")\n
"},{"location":"transforms/#what-gets-preserved","title":"What Gets Preserved","text":"Category Preserved Why Errors 100% Critical for debugging First N 100% Context/pagination Last N 100% Recency Anomalies All Unusual values matter Relevant Top K Match user's query Others Sampled Statistical representation"},{"location":"transforms/#cachealigner","title":"CacheAligner","text":"Prefix stabilization for improved cache hit rates.
"},{"location":"transforms/#the-problem","title":"The Problem","text":"LLM providers cache request prefixes. But dynamic content breaks caching:
\"You are helpful. Today is January 7, 2025.\" # Changes daily = no cache\n
"},{"location":"transforms/#the-solution","title":"The Solution","text":"CacheAligner extracts dynamic content to stabilize the prefix:
from headroom import CacheAligner\n\naligner = CacheAligner()\nresult = aligner.align(messages)\n\n# Static prefix (cacheable):\n# \"You are helpful.\"\n\n# Dynamic content moved to end:\n# [Current date context]\n
"},{"location":"transforms/#configuration_1","title":"Configuration","text":"from headroom import CacheAlignerConfig\n\nconfig = CacheAlignerConfig(\n extract_dates=True, # Move dates to dynamic section\n normalize_whitespace=True, # Consistent spacing\n stable_prefix_min_tokens=100, # Min prefix size for alignment\n)\n
"},{"location":"transforms/#cache-hit-improvement","title":"Cache Hit Improvement","text":"Scenario Before After Daily date in prompt 0% hits ~95% hits Dynamic user context ~10% hits ~80% hits Consistent prompts ~90% hits ~95% hits"},{"location":"transforms/#rollingwindow","title":"RollingWindow","text":"Context management within token limits.
"},{"location":"transforms/#the-problem_1","title":"The Problem","text":"Long conversations exceed context limits. Naive truncation breaks tool calls:
[tool_call: search] # Kept\n[tool_result: ...] # Dropped = orphaned call!\n
"},{"location":"transforms/#the-solution_1","title":"The Solution","text":"RollingWindow drops complete tool units, preserving pairs:
from headroom import RollingWindow\n\nwindow = RollingWindow(config)\nresult = window.apply(messages, max_tokens=100000)\n\n# Guarantees:\n# 1. Tool calls paired with results\n# 2. System prompt preserved\n# 3. Recent turns kept\n# 4. Oldest tool outputs dropped first\n
"},{"location":"transforms/#configuration_2","title":"Configuration","text":"from headroom import RollingWindowConfig\n\nconfig = RollingWindowConfig(\n max_tokens=100000, # Target token limit\n preserve_system=True, # Always keep system prompt\n preserve_recent_turns=5, # Keep last 5 user/assistant turns\n drop_oldest_first=True, # Remove oldest tool outputs\n)\n
"},{"location":"transforms/#drop-priority","title":"Drop Priority","text":" - Oldest tool outputs - First to go
- Old assistant messages - Summary preserved
- Old user messages - Only if necessary
- Never dropped: System prompt, recent turns, active tool pairs
Note: For more intelligent context management based on semantic importance rather than just position, see IntelligentContextManager below.
"},{"location":"transforms/#intelligentcontextmanager","title":"IntelligentContextManager","text":"Semantic-aware context management with TOIN-learned importance scoring.
"},{"location":"transforms/#the-problem_2","title":"The Problem","text":"RollingWindow drops messages by position (oldest first), but position doesn't equal importance:
- An error message from turn 3 might be critical
- A verbose success response from turn 10 might be expendable
- Messages referenced by later turns should be preserved
"},{"location":"transforms/#the-solution_2","title":"The Solution","text":"IntelligentContextManager uses multi-factor importance scoring:
from headroom.transforms import IntelligentContextManager, IntelligentContextConfig\n\nmanager = IntelligentContextManager(config)\nresult = manager.apply(messages, tokenizer, model_limit=128000)\n\n# Guarantees:\n# 1. System messages never dropped (configurable)\n# 2. Last N turns always protected\n# 3. Tool calls/responses dropped atomically\n# 4. Drops by importance score, not just position\n
"},{"location":"transforms/#how-scoring-works","title":"How Scoring Works","text":"Messages are scored on multiple factors (all learned, no hardcodes):
Factor Weight Description Recency 20% Exponential decay from conversation end Semantic Similarity 20% Embedding similarity to recent context TOIN Importance 25% Learned from retrieval patterns Error Indicators 15% TOIN-learned error field detection Forward References 15% Messages referenced by later messages Token Density 5% Information density (unique/total tokens) Key principle: No hardcoded patterns. Error detection uses TOIN's field_semantics.inferred_type == \"error_indicator\", not keyword matching.
"},{"location":"transforms/#configuration_3","title":"Configuration","text":"from headroom.transforms import IntelligentContextManager\nfrom headroom.config import IntelligentContextConfig, ScoringWeights\n\n# Custom scoring weights\nweights = ScoringWeights(\n recency=0.20,\n semantic_similarity=0.20,\n toin_importance=0.25,\n error_indicator=0.15,\n forward_reference=0.15,\n token_density=0.05,\n)\n\nconfig = IntelligentContextConfig(\n enabled=True,\n keep_system=True, # Never drop system messages\n keep_last_turns=2, # Protect last N user turns\n output_buffer_tokens=4000, # Reserve for model output\n use_importance_scoring=True, # Enable semantic scoring\n scoring_weights=weights, # Custom weights\n toin_integration=True, # Use TOIN patterns\n recency_decay_rate=0.1, # Exponential decay lambda\n compress_threshold=0.1, # Try compression first if <10% over\n)\n\nmanager = IntelligentContextManager(config)\n
"},{"location":"transforms/#strategy-selection","title":"Strategy Selection","text":"Based on how much over budget you are:
Overage Strategy Action Under budget NONE No action needed < 10% over COMPRESS_FIRST Try deeper compression >= 10% over DROP_BY_SCORE Drop lowest-scored messages"},{"location":"transforms/#toin-ccr-integration","title":"TOIN + CCR Integration","text":"IntelligentContextManager is a message-level compressor. Just like SmartCrusher compresses items in a JSON array, IntelligentContext \"compresses\" messages in a conversation by dropping low-value ones.
Bidirectional TOIN integration:
- Scoring uses TOIN patterns: Learned retrieval rates and field semantics inform importance scores
- Drops are recorded to TOIN: When messages are dropped, TOIN learns the pattern
- CCR stores originals: Dropped messages are stored in CCR for potential retrieval
- Retrievals feed back to TOIN: If users retrieve dropped messages, TOIN learns to score those patterns higher
from headroom.telemetry import get_toin\n\ntoin = get_toin()\nmanager = IntelligentContextManager(config, toin=toin)\n\n# TOIN provides (for scoring):\n# - retrieval_rate: How often this message pattern is retrieved (high = important)\n# - field_semantics: Learned field types (error_indicator, identifier, etc.)\n# - commonly_retrieved_fields: Fields that users frequently need\n\n# TOIN receives (from drops):\n# - Message pattern signatures (role counts, has_tools, has_errors)\n# - Token counts (original vs marker size)\n# - Retrieval feedback when users access CCR\n
What this means: - When you drop a message pattern and users frequently retrieve it, TOIN learns to score it higher next time - When you drop a pattern and no one retrieves it, that confirms it was safe to drop - The feedback loop improves drop decisions across all users, not just in one session
"},{"location":"transforms/#example-before-vs-after","title":"Example: Before vs After","text":"RollingWindow (position-based):
Messages: [sys, user1, asst1, user2, asst2_error, user3, asst3, user4, asst4]\nOver budget by 3 messages.\nDrops: user1, asst1, user2 (oldest first)\nResult: Loses context, keeps verbose asst3\n
IntelligentContextManager (score-based):
Messages scored:\n - asst2_error: 0.85 (TOIN learned error indicator)\n - asst1: 0.45 (old, low density)\n - asst3: 0.40 (verbose, low unique tokens)\n\nDrops: asst1, asst3, user1 (lowest scores)\nResult: Preserves critical error message\n
"},{"location":"transforms/#backwards-compatibility","title":"Backwards Compatibility","text":"Convert from RollingWindowConfig:
from headroom.config import IntelligentContextConfig, RollingWindowConfig\n\nrolling_config = RollingWindowConfig(\n max_tokens=100000,\n preserve_system=True,\n preserve_recent_turns=3,\n)\n\n# Convert to intelligent context config\nintelligent_config = IntelligentContextConfig(\n keep_system=rolling_config.preserve_system,\n keep_last_turns=rolling_config.preserve_recent_turns,\n)\n
"},{"location":"transforms/#llmlinguacompressor-optional","title":"LLMLinguaCompressor (Optional)","text":"ML-based compression using Microsoft's LLMLingua-2 model.
"},{"location":"transforms/#when-to-use","title":"When to Use","text":"Transform Best For Speed Compression SmartCrusher JSON arrays ~1ms 70-90% Text Utilities Search/logs ~1ms 50-90% LLMLinguaCompressor Any text, max compression 50-200ms 80-95%"},{"location":"transforms/#installation","title":"Installation","text":"pip install \"headroom-ai[llmlingua]\" # Adds ~2GB\n
"},{"location":"transforms/#configuration_4","title":"Configuration","text":"from headroom.transforms import LLMLinguaCompressor, LLMLinguaConfig\n\nconfig = LLMLinguaConfig(\n device=\"auto\", # auto, cuda, cpu, mps\n target_compression_rate=0.3, # Keep 30% of tokens\n min_tokens_for_compression=100, # Skip small content\n code_compression_rate=0.4, # Conservative for code\n json_compression_rate=0.35, # Moderate for JSON\n text_compression_rate=0.25, # Aggressive for text\n enable_ccr=True, # Store original for retrieval\n)\n\ncompressor = LLMLinguaCompressor(config)\n
"},{"location":"transforms/#content-aware-rates","title":"Content-Aware Rates","text":"LLMLinguaCompressor auto-detects content type:
Content Type Default Rate Behavior Code 0.4 Conservative - preserves syntax JSON 0.35 Moderate - keeps structure Text 0.3 Aggressive - maximum compression"},{"location":"transforms/#memory-management","title":"Memory Management","text":"from headroom.transforms import (\n is_llmlingua_model_loaded,\n unload_llmlingua_model,\n)\n\n# Check if model is loaded\nprint(is_llmlingua_model_loaded()) # True/False\n\n# Free ~1GB RAM when done\nunload_llmlingua_model()\n
"},{"location":"transforms/#proxy-integration","title":"Proxy Integration","text":"# Enable in proxy\nheadroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.3\n
"},{"location":"transforms/#codeawarecompressor-optional","title":"CodeAwareCompressor (Optional)","text":"AST-based compression for source code using tree-sitter.
"},{"location":"transforms/#when-to-use_1","title":"When to Use","text":"Transform Best For Speed Compression SmartCrusher JSON arrays ~1ms 70-90% CodeAwareCompressor Source code ~10-50ms 40-70% LLMLinguaCompressor Any text 50-200ms 80-95%"},{"location":"transforms/#key-benefits","title":"Key Benefits","text":" - Syntax validity guaranteed \u2014 Output always parses correctly
- Preserves critical structure \u2014 Imports, signatures, types, error handlers
- Multi-language support \u2014 Python, JavaScript, TypeScript, Go, Rust, Java, C, C++
- Lightweight \u2014 ~50MB vs ~1GB for LLMLingua
"},{"location":"transforms/#installation_1","title":"Installation","text":"pip install \"headroom-ai[code]\" # Adds tree-sitter-language-pack\n
"},{"location":"transforms/#configuration_5","title":"Configuration","text":"from headroom.transforms import CodeAwareCompressor, CodeCompressorConfig, DocstringMode\n\nconfig = CodeCompressorConfig(\n preserve_imports=True, # Always keep imports\n preserve_signatures=True, # Always keep function signatures\n preserve_type_annotations=True, # Keep type hints\n preserve_error_handlers=True, # Keep try/except blocks\n preserve_decorators=True, # Keep decorators\n docstring_mode=DocstringMode.FIRST_LINE, # FULL, FIRST_LINE, REMOVE\n target_compression_rate=0.2, # Keep 20% of tokens\n max_body_lines=5, # Lines to keep per function body\n min_tokens_for_compression=100, # Skip small content\n language_hint=None, # Auto-detect if None\n fallback_to_llmlingua=True, # Use LLMLingua for unknown langs\n)\n\ncompressor = CodeAwareCompressor(config)\n
"},{"location":"transforms/#example_1","title":"Example","text":"from headroom.transforms import CodeAwareCompressor\n\ncompressor = CodeAwareCompressor()\n\ncode = '''\nimport os\nfrom typing import List\n\ndef process_items(items: List[str]) -> List[str]:\n \"\"\"Process a list of items.\"\"\"\n results = []\n for item in items:\n if not item:\n continue\n processed = item.strip().lower()\n results.append(processed)\n return results\n'''\n\nresult = compressor.compress(code, language=\"python\")\nprint(result.compressed)\n# import os\n# from typing import List\n#\n# def process_items(items: List[str]) -> List[str]:\n# \"\"\"Process a list of items.\"\"\"\n# results = []\n# for item in items:\n# # ... (5 lines compressed)\n# pass\n\nprint(f\"Compression: {result.compression_ratio:.0%}\") # ~55%\nprint(f\"Syntax valid: {result.syntax_valid}\") # True\n
"},{"location":"transforms/#supported-languages","title":"Supported Languages","text":"Tier Languages Support Level 1 Python, JavaScript, TypeScript Full AST analysis 2 Go, Rust, Java, C, C++ Function body compression"},{"location":"transforms/#memory-management_1","title":"Memory Management","text":"from headroom.transforms import is_tree_sitter_available, unload_tree_sitter\n\n# Check if tree-sitter is installed\nprint(is_tree_sitter_available()) # True/False\n\n# Free memory when done (parsers are lazy-loaded)\nunload_tree_sitter()\n
"},{"location":"transforms/#contentrouter","title":"ContentRouter","text":"Intelligent compression orchestrator that routes content to the optimal compressor.
"},{"location":"transforms/#how-it-works_1","title":"How It Works","text":"ContentRouter analyzes content and selects the best compression strategy:
- Detect content type \u2014 JSON, code, logs, search results, plain text
- Consider source hints \u2014 File paths, tool names for high-confidence routing
- Route to compressor \u2014 SmartCrusher, CodeAwareCompressor, SearchCompressor, etc.
- Log decisions \u2014 Transparent routing for debugging
"},{"location":"transforms/#configuration_6","title":"Configuration","text":"from headroom.transforms import ContentRouter, ContentRouterConfig, CompressionStrategy\n\nconfig = ContentRouterConfig(\n min_section_tokens=100, # Minimum tokens to compress\n enable_code_aware=True, # Use CodeAwareCompressor for code\n enable_search_compression=True, # Use SearchCompressor for grep output\n enable_log_compression=True, # Use LogCompressor for logs\n default_strategy=CompressionStrategy.TEXT, # Fallback strategy\n)\n\nrouter = ContentRouter(config)\n
"},{"location":"transforms/#example_2","title":"Example","text":"from headroom.transforms import ContentRouter\n\nrouter = ContentRouter()\n\n# Router auto-detects content type and routes to optimal compressor\nresult = router.compress(content)\n\nprint(result.strategy_used) # CompressionStrategy.CODE_AWARE, SMART_CRUSHER, etc.\nprint(result.routing_log) # List of routing decisions\n
"},{"location":"transforms/#compression-strategies","title":"Compression Strategies","text":"Strategy Used For Compressor CODE_AWARE Source code CodeAwareCompressor SMART_CRUSHER JSON arrays SmartCrusher SEARCH Grep/find output SearchCompressor LOG Log files LogCompressor TEXT Plain text TextCompressor LLMLINGUA Any (max compression) LLMLinguaCompressor PASSTHROUGH Small content None"},{"location":"transforms/#content-detection","title":"Content Detection","text":"The router automatically detects content types by analyzing the content itself:
- Source code: Detected by syntax patterns, indentation, keywords
- JSON arrays: Detected by JSON structure with array elements
- Search results: Detected by
file:line: patterns - Log output: Detected by timestamp and log level patterns
- Plain text: Fallback for prose content
No manual hints required - the router inspects content directly.
"},{"location":"transforms/#toin-integration","title":"TOIN Integration","text":"ContentRouter records all compressions to TOIN (Tool Output Intelligence Network) for cross-user learning:
- All strategies tracked: Code, search, logs, text, and LLMLingua compressions are recorded
- Retrieval feedback: When users retrieve original content via CCR, TOIN learns which compressions need expansion
- Pattern learning: TOIN builds signatures for each content type to improve future compressions
This enables the feedback loop where compression decisions improve based on actual user behavior across all content types, not just JSON arrays.
"},{"location":"transforms/#transformpipeline","title":"TransformPipeline","text":"Combine transforms for optimal results.
from headroom import TransformPipeline, SmartCrusher, CacheAligner, RollingWindow\n\npipeline = TransformPipeline([\n SmartCrusher(), # First: compress tool outputs\n CacheAligner(), # Then: stabilize prefix\n RollingWindow(), # Finally: fit in context\n])\n\nresult = pipeline.transform(messages)\nprint(f\"Saved {result.tokens_saved} tokens\")\n
"},{"location":"transforms/#with-llmlingua-optional","title":"With LLMLingua (Optional)","text":"from headroom.transforms import (\n TransformPipeline, SmartCrusher, CacheAligner,\n RollingWindow, LLMLinguaCompressor\n)\n\npipeline = TransformPipeline([\n CacheAligner(), # 1. Stabilize prefix\n SmartCrusher(), # 2. Compress JSON arrays\n LLMLinguaCompressor(), # 3. ML compression on remaining text\n RollingWindow(), # 4. Final size constraint (always last)\n])\n
"},{"location":"transforms/#recommended-order","title":"Recommended Order","text":"Order Transform Purpose 1 CacheAligner Stabilize prefix for caching 2 SmartCrusher Compress JSON tool outputs 3 LLMLinguaCompressor ML compression (optional) 4 RollingWindow Enforce token limits (always last) Why this order? - CacheAligner first to maximize prefix stability - SmartCrusher handles JSON arrays efficiently - LLMLingua compresses remaining long text - RollingWindow truncates only if still over limit
"},{"location":"transforms/#safety-guarantees","title":"Safety Guarantees","text":"All transforms follow strict safety rules:
- Never remove human content - User/assistant text is sacred
- Never break tool ordering - Calls and results stay paired
- Parse failures are no-ops - Malformed content passes through
- Preserves recency - Last N turns always kept
- 100% error preservation - Error items never dropped
"},{"location":"troubleshooting/","title":"Troubleshooting Guide","text":"Solutions for common Headroom issues.
"},{"location":"troubleshooting/#proxy-server-issues","title":"Proxy Server Issues","text":""},{"location":"troubleshooting/#proxy-wont-start","title":"\"Proxy won't start\"","text":"Symptom: headroom proxy fails or hangs.
Solutions:
# 1. Check if port is already in use\nlsof -i :8787\n# If something is using the port, either kill it or use a different port\n\n# 2. Try a different port\nheadroom proxy --port 8788\n\n# 3. Check for missing dependencies\npip install \"headroom-ai[proxy]\"\n\n# 4. Run with debug logging\nheadroom proxy --log-level debug\n
"},{"location":"troubleshooting/#connection-refused-when-calling-proxy","title":"\"Connection refused\" when calling proxy","text":"Symptom: curl: (7) Failed to connect to localhost port 8787
Solutions:
# 1. Verify proxy is running\ncurl http://localhost:8787/health\n\n# 2. Check if proxy started on a different port\nps aux | grep headroom\n\n# 3. Check firewall settings (macOS)\nsudo pfctl -s rules | grep 8787\n
"},{"location":"troubleshooting/#proxy-returns-errors-for-some-requests","title":"\"Proxy returns errors for some requests\"","text":"Symptom: Some requests work, others fail with 502/503.
Solutions:
# 1. Check proxy logs for the actual error\nheadroom proxy --log-level debug\n\n# 2. Verify API key is set\necho $OPENAI_API_KEY # or ANTHROPIC_API_KEY\n\n# 3. Test the underlying API directly\ncurl https://api.openai.com/v1/models -H \"Authorization: Bearer $OPENAI_API_KEY\"\n
"},{"location":"troubleshooting/#sdk-issues","title":"SDK Issues","text":""},{"location":"troubleshooting/#no-token-savings","title":"\"No token savings\"","text":"Symptom: stats['session']['tokens_saved_total'] is 0.
Diagnosis:
# 1. Check mode\nstats = client.get_stats()\nprint(f\"Mode: {stats['config']['mode']}\") # Should be \"optimize\"\n\n# 2. Check transforms are enabled\nprint(f\"SmartCrusher: {stats['transforms']['smart_crusher_enabled']}\")\n\n# 3. Check if content meets threshold\n# SmartCrusher only compresses tool outputs > 200 tokens by default\n
Solutions:
# 1. Ensure mode is \"optimize\"\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\", # NOT \"audit\"\n)\n\n# 2. Or override per-request\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n headroom_mode=\"optimize\",\n)\n\n# 3. Lower the compression threshold\nconfig = HeadroomConfig()\nconfig.smart_crusher.min_tokens_to_crush = 100 # Default is 200\n
Why It Might Be 0: - Mode is \"audit\" (observation only) - Messages don't contain tool outputs - Tool outputs are below the token threshold - Data isn't compressible (high uniqueness)
"},{"location":"troubleshooting/#compression-too-aggressive","title":"\"Compression too aggressive\"","text":"Symptom: LLM responses are missing information that was in tool outputs.
Solutions:
# 1. Keep more items\nconfig = HeadroomConfig()\nconfig.smart_crusher.max_items_after_crush = 50 # Default: 15\n\n# 2. Skip compression for specific tools\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n headroom_tool_profiles={\n \"important_tool\": {\"skip_compression\": True},\n },\n)\n\n# 3. Disable SmartCrusher entirely\nconfig.smart_crusher.enabled = False\n
"},{"location":"troubleshooting/#high-latency","title":"\"High latency\"","text":"Symptom: Requests take longer than expected.
Diagnosis:
import time\nimport logging\n\nlogging.basicConfig(level=logging.DEBUG)\n\nstart = time.time()\nresponse = client.chat.completions.create(...)\nprint(f\"Total time: {time.time() - start:.2f}s\")\n\n# Check logs for:\n# - \"SmartCrusher\" timing\n# - \"EmbeddingScorer\" timing (slow if using embeddings)\n
Solutions:
# 1. Use BM25 instead of embeddings (faster)\nconfig = HeadroomConfig()\nconfig.smart_crusher.relevance.tier = \"bm25\" # Default may use embeddings\n\n# 2. Increase threshold to skip small payloads\nconfig.smart_crusher.min_tokens_to_crush = 500\n\n# 3. Disable transforms you don't need\nconfig.cache_aligner.enabled = False\nconfig.rolling_window.enabled = False\n
"},{"location":"troubleshooting/#validationerror-on-setup","title":"\"ValidationError on setup\"","text":"Symptom: validate_setup() returns errors.
Common Issues:
result = client.validate_setup()\nprint(result)\n\n# Provider error:\n# {\"provider\": {\"ok\": False, \"error\": \"No API key\"}}\n# \u2192 Set OPENAI_API_KEY or pass api_key to OpenAI()\n\n# Storage error:\n# {\"storage\": {\"ok\": False, \"error\": \"unable to open database\"}}\n# \u2192 Check path permissions, use :memory: for testing\n\n# Config error:\n# {\"config\": {\"ok\": False, \"error\": \"Invalid mode\"}}\n# \u2192 Use \"audit\" or \"optimize\" only\n
Solutions:
# 1. For testing, use in-memory storage\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n store_url=\"sqlite:///:memory:\", # No file created\n)\n\n# 2. For temp directory storage\nimport tempfile\nimport os\ndb_path = os.path.join(tempfile.gettempdir(), \"headroom.db\")\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n store_url=f\"sqlite:///{db_path}\",\n)\n
"},{"location":"troubleshooting/#importinstallation-issues","title":"Import/Installation Issues","text":""},{"location":"troubleshooting/#pip-install-fails-with-c-compilation-error","title":"\"pip install fails with C++ compilation error\"","text":"Symptom: Installation fails with an error like:
RuntimeError: Unsupported compiler -- at least C++11 support is needed!\nERROR: Failed building wheel for hnswlib\n
Cause: headroom-ai depends on hnswlib, a C++ extension that must be compiled from source. Slim environments (Docker slim images, minimal CI runners) lack the required build tools.
Solutions:
# Linux / Debian-based (including Docker)\napt-get install -y build-essential && pip install headroom-ai\n\n# macOS (Xcode command line tools)\nxcode-select --install && pip install headroom-ai\n
In a Dockerfile, install and remove build tools in one layer to keep the image slim:
FROM python:3.11-slim\nRUN apt-get update && apt-get install -y --no-install-recommends build-essential \\\n && pip install \"headroom-ai[proxy]\" \\\n && apt-get purge -y build-essential && apt-get autoremove -y \\\n && rm -rf /var/lib/apt/lists/*\n
"},{"location":"troubleshooting/#modulenotfounderror-no-module-named-headroom","title":"\"ModuleNotFoundError: No module named 'headroom'\"","text":"# 1. Check it's installed in the right environment\npip show headroom-ai\n\n# 2. If using virtual environment, ensure it's activated\nsource venv/bin/activate # or equivalent\n\n# 3. Reinstall\npip install --upgrade headroom-ai\n
"},{"location":"troubleshooting/#importerror-cannot-import-name-x-from-headroom","title":"\"ImportError: cannot import name 'X' from 'headroom'\"","text":"# Check available imports\nimport headroom\nprint(dir(headroom))\n\n# Common imports:\nfrom headroom import (\n HeadroomClient,\n OpenAIProvider,\n AnthropicProvider,\n HeadroomConfig,\n # Exceptions\n HeadroomError,\n ConfigurationError,\n ProviderError,\n)\n
"},{"location":"troubleshooting/#missing-optional-dependency","title":"\"Missing optional dependency\"","text":"# For proxy server\npip install \"headroom-ai[proxy]\"\n\n# For embedding-based relevance scoring\npip install \"headroom-ai[relevance]\"\n\n# For everything\npip install \"headroom-ai[all]\"\n
"},{"location":"troubleshooting/#provider-specific-issues","title":"Provider-Specific Issues","text":""},{"location":"troubleshooting/#openai-invalid-api-key","title":"OpenAI: \"Invalid API key\"","text":"from openai import OpenAI\nimport os\n\n# Ensure key is set\napi_key = os.environ.get(\"OPENAI_API_KEY\")\nif not api_key:\n raise ValueError(\"OPENAI_API_KEY not set\")\n\nclient = HeadroomClient(\n original_client=OpenAI(api_key=api_key),\n provider=OpenAIProvider(),\n)\n
"},{"location":"troubleshooting/#anthropic-authentication-error","title":"Anthropic: \"Authentication error\"","text":"from anthropic import Anthropic\nimport os\n\napi_key = os.environ.get(\"ANTHROPIC_API_KEY\")\nclient = HeadroomClient(\n original_client=Anthropic(api_key=api_key),\n provider=AnthropicProvider(),\n)\n
"},{"location":"troubleshooting/#unknown-model-warnings","title":"\"Unknown model\" warnings","text":"# For custom/fine-tuned models, specify context limit\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n model_context_limits={\n \"ft:gpt-4o-2024-08-06:my-org::abc123\": 128000,\n \"my-custom-model\": 32000,\n },\n)\n
"},{"location":"troubleshooting/#debugging-techniques","title":"Debugging Techniques","text":""},{"location":"troubleshooting/#enable-full-logging","title":"Enable Full Logging","text":"import logging\n\n# See everything\nlogging.basicConfig(\n level=logging.DEBUG,\n format=\"%(asctime)s %(name)s %(levelname)s %(message)s\",\n)\n\n# Or just Headroom logs\nlogging.getLogger(\"headroom\").setLevel(logging.DEBUG)\n
"},{"location":"troubleshooting/#inspect-transform-results","title":"Inspect Transform Results","text":"# Use simulate to see what would happen\nplan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=messages,\n)\n\nprint(f\"Tokens: {plan.tokens_before} -> {plan.tokens_after}\")\nprint(f\"Transforms: {plan.transforms}\")\nprint(f\"Waste signals: {plan.waste_signals}\")\n\n# See the actual optimized messages\nimport json\nprint(json.dumps(plan.messages_optimized, indent=2))\n
"},{"location":"troubleshooting/#check-storage-contents","title":"Check Storage Contents","text":"from datetime import datetime, timedelta\n\n# Get recent metrics\nmetrics = client.get_metrics(\n start_time=datetime.utcnow() - timedelta(hours=1),\n limit=10,\n)\n\nfor m in metrics:\n print(f\"{m.timestamp}: {m.tokens_input_before} -> {m.tokens_input_after}\")\n print(f\" Transforms: {m.transforms_applied}\")\n if m.error:\n print(f\" ERROR: {m.error}\")\n
"},{"location":"troubleshooting/#manual-transform-testing","title":"Manual Transform Testing","text":"from headroom import SmartCrusher, Tokenizer\nfrom headroom.config import SmartCrusherConfig\nimport json\n\n# Test compression directly\nconfig = SmartCrusherConfig()\ncrusher = SmartCrusher(config)\ntokenizer = Tokenizer()\n\nmessages = [\n {\"role\": \"tool\", \"content\": json.dumps({\"items\": list(range(100))}), \"tool_call_id\": \"1\"}\n]\n\nresult = crusher.apply(messages, tokenizer)\nprint(f\"Tokens: {result.tokens_before} -> {result.tokens_after}\")\nprint(f\"Compressed content: {result.messages[0]['content'][:200]}...\")\n
"},{"location":"troubleshooting/#error-reference","title":"Error Reference","text":"Exception Meaning Solution ConfigurationError Invalid config values Check config parameters ProviderError Provider issue (unknown model, etc.) Set model_context_limits StorageError Database issue Check path/permissions CompressionError Compression failed Rare - check data format TokenizationError Token counting failed Check model name ValidationError Setup validation failed Run validate_setup()"},{"location":"troubleshooting/#handling-errors","title":"Handling Errors","text":"from headroom import (\n HeadroomClient,\n HeadroomError,\n ConfigurationError,\n StorageError,\n)\n\ntry:\n client = HeadroomClient(...)\n response = client.chat.completions.create(...)\nexcept ConfigurationError as e:\n print(f\"Config issue: {e}\")\n print(f\"Details: {e.details}\")\nexcept StorageError as e:\n print(f\"Storage issue: {e}\")\n # Headroom continues to work, just without metrics persistence\nexcept HeadroomError as e:\n print(f\"Headroom error: {e}\")\n
"},{"location":"troubleshooting/#getting-help","title":"Getting Help","text":" - Enable debug logging and check the output
- Use simulate() to see what transforms would apply
- Check validate_setup() for configuration issues
- File an issue at https://github.com/headroom-sdk/headroom/issues
When filing an issue, include: - Headroom version (pip show headroom) - Python version - Provider (OpenAI/Anthropic) - Debug log output - Minimal reproduction code
"},{"location":"typescript-sdk/","title":"TypeScript SDK","text":"The Headroom TypeScript SDK lets any JavaScript or TypeScript application compress LLM messages before sending them to a model. It saves tokens, reduces costs, and fits more context into every request.
"},{"location":"typescript-sdk/#install","title":"Install","text":"npm install headroom-ai\n
Requires a running Headroom proxy or Headroom Cloud API key.
"},{"location":"typescript-sdk/#quick-start","title":"Quick Start","text":"import { compress } from 'headroom-ai';\n\nconst result = await compress(messages, { model: 'gpt-4o' });\nconsole.log(`Saved ${result.tokensSaved} tokens`);\n\nconst response = await openai.chat.completions.create({\n model: 'gpt-4o',\n messages: result.messages,\n});\n
"},{"location":"typescript-sdk/#how-it-works","title":"How It Works","text":"The TypeScript SDK is an HTTP client. When you call compress(), it sends your messages to the Headroom proxy's POST /v1/compress endpoint. The proxy runs the full compression pipeline (SmartCrusher, ContentRouter, CacheAligner, etc.) and returns compressed messages. No compression logic runs in Node.js \u2014 all the heavy lifting happens in the proxy.
Your TypeScript App\n \u2502\n \u2502 compress(messages)\n \u25bc\nheadroom-ai (npm) \u2190 HTTP client\n \u2502\n \u2502 POST /v1/compress\n \u25bc\nHeadroom Proxy / Cloud \u2190 compression pipeline (Python)\n \u2502\n \u2502 compressed messages\n \u25bc\nYour TypeScript App\n \u2502\n \u2502 openai.chat.completions.create(compressed)\n \u25bc\nLLM Provider\n
"},{"location":"typescript-sdk/#core-api-compress","title":"Core API: compress()","text":"import { compress } from 'headroom-ai';\n\nconst result = await compress(messages, {\n model: 'gpt-4o', // model name (for token counting)\n baseUrl: 'http://localhost:8787', // proxy URL (default)\n apiKey: 'hr_...', // Headroom Cloud key\n timeout: 30000, // ms (default)\n fallback: true, // return uncompressed if proxy down (default)\n retries: 1, // retry on transient errors (default)\n});\n\nresult.messages // compressed messages (same format as input)\nresult.tokensBefore // original token count\nresult.tokensAfter // compressed token count\nresult.tokensSaved // tokens removed\nresult.compressionRatio // tokensAfter / tokensBefore\nresult.transformsApplied // e.g. ['router:smart_crusher:0.35']\nresult.compressed // false if fallback kicked in\n
Messages use standard OpenAI chat format: { role, content, tool_calls?, tool_call_id? }.
"},{"location":"typescript-sdk/#environment-variables","title":"Environment Variables","text":"Instead of passing options, set environment variables:
HEADROOM_BASE_URL \u2014 proxy or cloud URL (default: http://localhost:8787) HEADROOM_API_KEY \u2014 Headroom Cloud API key
"},{"location":"typescript-sdk/#reusable-client","title":"Reusable Client","text":"For apps making many calls, create a client once and reuse it:
import { HeadroomClient } from 'headroom-ai';\n\nconst client = new HeadroomClient({\n baseUrl: 'http://localhost:8787',\n apiKey: 'hr_...',\n});\n\nconst r1 = await client.compress(messages1, { model: 'gpt-4o' });\nconst r2 = await client.compress(messages2, { model: 'gpt-4o' });\n
"},{"location":"typescript-sdk/#framework-adapters","title":"Framework Adapters","text":""},{"location":"typescript-sdk/#vercel-ai-sdk","title":"Vercel AI SDK","text":"The Headroom middleware plugs directly into Vercel AI SDK's wrapLanguageModel():
import { headroomMiddleware } from 'headroom-ai/vercel-ai';\nimport { wrapLanguageModel, generateText } from 'ai';\nimport { openai } from '@ai-sdk/openai';\n\nconst model = wrapLanguageModel({\n model: openai('gpt-4o'),\n middleware: headroomMiddleware(),\n});\n\n// All calls through this model are automatically compressed\nconst { text } = await generateText({ model, messages });\n
The middleware intercepts messages in the transformParams hook, converts Vercel's internal format to OpenAI format, compresses via the proxy, and converts back. Your app code doesn't change.
You can also compress Vercel messages directly:
import { compressVercelMessages } from 'headroom-ai/vercel-ai';\n\nconst result = await compressVercelMessages(modelMessages, { model: 'gpt-4o' });\n// result.messages is in Vercel ModelMessage[] format\n
"},{"location":"typescript-sdk/#openai-sdk","title":"OpenAI SDK","text":"Wrap your OpenAI client to auto-compress messages on every chat.completions.create() call:
import { withHeadroom } from 'headroom-ai/openai';\nimport OpenAI from 'openai';\n\nconst client = withHeadroom(new OpenAI());\n\n// Messages are compressed before sending \u2014 transparent to your code\nconst response = await client.chat.completions.create({\n model: 'gpt-4o',\n messages: longConversation,\n});\n
Only chat.completions.create() is intercepted. All other methods (embeddings, images, audio) pass through unchanged.
"},{"location":"typescript-sdk/#anthropic-sdk","title":"Anthropic SDK","text":"Same pattern for the Anthropic client:
import { withHeadroom } from 'headroom-ai/anthropic';\nimport Anthropic from '@anthropic-ai/sdk';\n\nconst client = withHeadroom(new Anthropic());\n\nconst response = await client.messages.create({\n model: 'claude-sonnet-4-5-20250929',\n messages: longConversation,\n max_tokens: 1024,\n});\n
Only messages.create() is intercepted. The adapter converts between Anthropic's content block format and OpenAI format automatically.
"},{"location":"typescript-sdk/#error-handling","title":"Error Handling","text":"import { compress, HeadroomConnectionError, HeadroomAuthError } from 'headroom-ai';\n\ntry {\n const result = await compress(messages, { model: 'gpt-4o', fallback: false });\n} catch (error) {\n if (error instanceof HeadroomAuthError) {\n // Invalid API key (401)\n } else if (error instanceof HeadroomConnectionError) {\n // Proxy unreachable\n }\n}\n
With fallback: true (the default), connection errors and 5xx responses return the original messages uncompressed instead of throwing. Auth errors (401) and client errors (400) always throw.
"},{"location":"typescript-sdk/#fallback-behavior","title":"Fallback Behavior","text":"By default, compress() never blocks your app. If the proxy is unreachable:
Scenario fallback: true (default) fallback: false Proxy unreachable Returns uncompressed, compressed: false Throws HeadroomConnectionError Proxy 503 error Returns uncompressed after retries Throws HeadroomCompressError Invalid API key (401) Throws HeadroomAuthError Throws HeadroomAuthError Bad request (400) Throws HeadroomCompressError Throws HeadroomCompressError"},{"location":"typescript-sdk/#zero-dependencies","title":"Zero Dependencies","text":"The headroom-ai package has no runtime dependencies. Framework SDKs (Vercel AI, OpenAI, Anthropic) are optional peer dependencies \u2014 only install what you use.
"},{"location":"typescript-sdk/#openclaw-plugin","title":"OpenClaw Plugin","text":"The TypeScript SDK powers the headroom-openclaw plugin for OpenClaw agents. The plugin uses HeadroomClient internally to compress context during the assemble() lifecycle hook. Install it with openclaw plugins install headroom-openclaw. See the plugin source for details.
"},{"location":"typescript-sdk/#comparison-with-python-sdk","title":"Comparison with Python SDK","text":"Feature Python SDK TypeScript SDK compress() Native (runs locally) HTTP client (calls proxy) Proxy Built-in server Connects to proxy Vercel AI SDK N/A Middleware adapter OpenAI SDK HeadroomClient wrapper withHeadroom() wrapper Anthropic SDK HeadroomClient wrapper withHeadroom() wrapper LangChain HeadroomChatModel Use compress() directly Memory system Full (SQLite + HNSW) Not yet (use proxy) MCP server Built-in Not yet CLI tools headroom proxy, headroom wrap, etc. N/A (use Python CLI)"},{"location":"plans/dynamic-smart-crusher/","title":"Dynamic SmartCrusher Preservation Plan","text":""},{"location":"plans/dynamic-smart-crusher/#problem-statement","title":"Problem Statement","text":"Current SmartCrusher uses static \"First 3 + Last 2\" preservation regardless of: - Array size (100 items vs 10,000 items get same treatment) - Data pattern (time series vs search results vs logs) - Query context (user asking about \"latest\" vs \"oldest\") - Position importance (first items might all be identical/wasteful) - Learned retrieval patterns (which positions do users actually need?)
This is too simplistic for a production-grade compression system.
"},{"location":"plans/dynamic-smart-crusher/#current-implementation-analysis","title":"Current Implementation Analysis","text":"Location: headroom/transforms/smart_crusher.py
Current Logic (lines 2273-2279, 2353-2359, 2561-2567):
# Always keep first 3\nfor i in range(min(3, n)):\n keep_indices.add(i)\n\n# Always keep last 2\nfor i in range(max(0, n - 2), n):\n keep_indices.add(i)\n
Problems: 1. Fixed slots waste budget - If first 3 items are identical, we've wasted 3 slots 2. No size adaptation - 20-item array loses 25% to anchors; 1000-item array loses 0.5% 3. Pattern-agnostic - Search results don't need \"last 2\"; time series might need more recency 4. No learning - Doesn't adapt based on what users actually retrieve
"},{"location":"plans/dynamic-smart-crusher/#proposed-solution-adaptive-slot-allocation","title":"Proposed Solution: Adaptive Slot Allocation","text":""},{"location":"plans/dynamic-smart-crusher/#idea-1-size-proportional-anchor-budget","title":"Idea 1: Size-Proportional Anchor Budget","text":"Instead of fixed counts, allocate a percentage budget for position-based anchors:
def calculate_anchor_budget(array_size: int, max_items: int) -> AnchorBudget:\n \"\"\"Allocate slots proportionally, with floors and ceilings.\"\"\"\n\n # Base: 20% of output budget for position anchors\n total_anchor_slots = max(3, min(10, int(max_items * 0.20)))\n\n # Distribution: 60% front, 40% back (front-weighted for context)\n front_slots = max(1, int(total_anchor_slots * 0.6))\n back_slots = max(1, total_anchor_slots - front_slots)\n\n return AnchorBudget(front=front_slots, back=back_slots)\n
Example: | Array Size | Max Items | Anchor Budget | Front | Back | |------------|-----------|---------------|-------|------| | 50 | 10 | 3 | 2 | 1 | | 200 | 15 | 3 | 2 | 1 | | 1000 | 20 | 4 | 3 | 1 | | 5000 | 30 | 6 | 4 | 2 |
"},{"location":"plans/dynamic-smart-crusher/#idea-2-pattern-aware-anchor-weighting","title":"Idea 2: Pattern-Aware Anchor Weighting","text":"Different data patterns need different position importance:
class AnchorStrategy(Enum):\n FRONT_HEAVY = \"front_heavy\" # Search results: top items matter most\n BACK_HEAVY = \"back_heavy\" # Logs: recent items matter most\n BALANCED = \"balanced\" # Time series: both ends matter\n MIDDLE_AWARE = \"middle_aware\" # Database: order might be arbitrary\n\ndef get_anchor_strategy(pattern: DataPattern) -> AnchorStrategy:\n return {\n DataPattern.SEARCH_RESULTS: AnchorStrategy.FRONT_HEAVY, # Top N by score\n DataPattern.LOGS: AnchorStrategy.BACK_HEAVY, # Recency matters\n DataPattern.TIME_SERIES: AnchorStrategy.BALANCED, # Both ends for trend\n DataPattern.GENERIC: AnchorStrategy.MIDDLE_AWARE, # Don't assume order\n }.get(pattern, AnchorStrategy.BALANCED)\n
FRONT_HEAVY (Search Results): - Front: 80% of anchor budget - Back: 20% of anchor budget - Rationale: Top search results are ranked by relevance
BACK_HEAVY (Logs): - Front: 20% of anchor budget - Back: 80% of anchor budget - Rationale: Most recent logs are usually most relevant
BALANCED (Time Series): - Front: 50% of anchor budget - Back: 50% of anchor budget - Rationale: Need both start and end for trend analysis
MIDDLE_AWARE (Generic/Database): - Front: 30% of anchor budget - Back: 30% of anchor budget - Middle sample: 40% of anchor budget (stratified) - Rationale: Order might be arbitrary; sample across positions
"},{"location":"plans/dynamic-smart-crusher/#idea-3-query-aware-dynamic-weighting","title":"Idea 3: Query-Aware Dynamic Weighting","text":"Adjust anchor strategy based on user's query:
def adjust_for_query(base_strategy: AnchorStrategy, query: str) -> AnchorWeights:\n \"\"\"Shift anchor weights based on query intent.\"\"\"\n\n weights = base_strategy.default_weights()\n\n # Recency signals\n recency_keywords = [\"latest\", \"recent\", \"last\", \"newest\", \"current\"]\n if any(kw in query.lower() for kw in recency_keywords):\n weights.back_weight *= 1.5\n weights.front_weight *= 0.7\n\n # Historical signals\n historical_keywords = [\"first\", \"oldest\", \"earliest\", \"original\", \"initial\"]\n if any(kw in query.lower() for kw in historical_keywords):\n weights.front_weight *= 1.5\n weights.back_weight *= 0.7\n\n # Range signals\n range_keywords = [\"all\", \"every\", \"complete\", \"full\"]\n if any(kw in query.lower() for kw in range_keywords):\n weights.middle_weight *= 1.3 # Better coverage\n\n return weights.normalize()\n
"},{"location":"plans/dynamic-smart-crusher/#idea-4-information-density-anchor-selection","title":"Idea 4: Information-Density Anchor Selection","text":"Don't blindly take first N - select most informative items from anchor regions:
def select_informative_anchors(\n items: list[dict],\n region: str, # \"front\", \"back\", \"middle\"\n slots: int,\n all_items_hash: set[str]\n) -> list[int]:\n \"\"\"Select most informative items from a region.\"\"\"\n\n if region == \"front\":\n candidates = list(range(min(slots * 3, len(items)))) # Consider 3x candidates\n elif region == \"back\":\n start = max(0, len(items) - slots * 3)\n candidates = list(range(start, len(items)))\n else: # middle\n step = len(items) // (slots * 3 + 1)\n candidates = [i * step for i in range(1, slots * 3 + 1)]\n\n # Score each candidate by information content\n scored = []\n for idx in candidates:\n item = items[idx]\n item_hash = hash_item(item)\n\n # Skip if we've seen identical item\n if item_hash in all_items_hash:\n continue\n\n score = calculate_information_score(item, items)\n scored.append((idx, score, item_hash))\n\n # Select top N by information score\n scored.sort(key=lambda x: x[1], reverse=True)\n selected = []\n for idx, _, item_hash in scored[:slots]:\n selected.append(idx)\n all_items_hash.add(item_hash)\n\n return sorted(selected)\n\n\ndef calculate_information_score(item: dict, all_items: list[dict]) -> float:\n \"\"\"Score item by how much unique information it contributes.\"\"\"\n\n score = 0.0\n\n # 1. Field uniqueness - rare field values score higher\n for field, value in item.items():\n field_values = [i.get(field) for i in all_items if field in i]\n value_frequency = field_values.count(value) / len(field_values)\n score += (1 - value_frequency) # Rare values score higher\n\n # 2. Structural uniqueness - different fields than typical\n typical_fields = get_typical_fields(all_items)\n unique_fields = set(item.keys()) - typical_fields\n score += len(unique_fields) * 0.5\n\n # 3. Content length - longer items often more informative\n content_length = len(json.dumps(item))\n avg_length = sum(len(json.dumps(i)) for i in all_items) / len(all_items)\n if content_length > avg_length:\n score += 0.3\n\n return score\n
"},{"location":"plans/dynamic-smart-crusher/#idea-5-toin-learned-position-importance","title":"Idea 5: TOIN-Learned Position Importance","text":"Track which positions users actually retrieve and learn from it:
@dataclass\nclass PositionRetrievalPattern:\n \"\"\"Learned position importance from retrieval data.\"\"\"\n tool_name: str\n total_compressions: int\n position_retrievals: dict[str, int] # \"front_10%\", \"middle\", \"back_10%\"\n\n def get_position_weights(self) -> dict[str, float]:\n \"\"\"Convert retrieval counts to weights.\"\"\"\n total = sum(self.position_retrievals.values())\n if total == 0:\n return {\"front\": 0.5, \"middle\": 0.0, \"back\": 0.5}\n\n return {\n position: count / total\n for position, count in self.position_retrievals.items()\n }\n\n\nclass TOINPositionLearning:\n \"\"\"Learn position importance from retrieval patterns.\"\"\"\n\n def record_retrieval(\n self,\n tool_name: str,\n original_size: int,\n retrieved_indices: list[int]\n ):\n \"\"\"Record which positions were retrieved.\"\"\"\n for idx in retrieved_indices:\n position = self._classify_position(idx, original_size)\n self._increment_position_count(tool_name, position)\n\n def _classify_position(self, idx: int, size: int) -> str:\n \"\"\"Classify index into position bucket.\"\"\"\n relative_pos = idx / size\n if relative_pos < 0.1:\n return \"front_10%\"\n elif relative_pos < 0.3:\n return \"front_30%\"\n elif relative_pos > 0.9:\n return \"back_10%\"\n elif relative_pos > 0.7:\n return \"back_30%\"\n else:\n return \"middle\"\n\n def get_anchor_recommendation(self, tool_name: str) -> AnchorWeights:\n \"\"\"Get learned anchor weights for a tool.\"\"\"\n pattern = self._get_pattern(tool_name)\n if pattern.total_compressions < 10:\n return AnchorWeights.default() # Not enough data\n\n weights = pattern.get_position_weights()\n return AnchorWeights(\n front=weights.get(\"front_10%\", 0.3) + weights.get(\"front_30%\", 0.1),\n middle=weights.get(\"middle\", 0.2),\n back=weights.get(\"back_10%\", 0.3) + weights.get(\"back_30%\", 0.1),\n )\n
"},{"location":"plans/dynamic-smart-crusher/#idea-6-stratified-sampling-for-middle-positions","title":"Idea 6: Stratified Sampling for Middle Positions","text":"For large arrays, sample strategically from middle:
def stratified_middle_sample(\n items: list[dict],\n num_samples: int,\n analysis: ArrayAnalysis\n) -> list[int]:\n \"\"\"Sample middle positions using stratified approach.\"\"\"\n\n n = len(items)\n front_boundary = int(n * 0.1)\n back_boundary = int(n * 0.9)\n middle_items = list(range(front_boundary, back_boundary))\n\n if not middle_items or num_samples <= 0:\n return []\n\n # Strategy 1: Cluster-based sampling\n if analysis.has_clusterable_field:\n clusters = cluster_by_field(items, analysis.cluster_field)\n return sample_from_clusters(clusters, num_samples, middle_items)\n\n # Strategy 2: Variance-based sampling (pick high-variance points)\n if analysis.numeric_fields:\n variance_scores = calculate_position_variance(items, analysis.numeric_fields)\n sorted_by_variance = sorted(\n middle_items,\n key=lambda i: variance_scores.get(i, 0),\n reverse=True\n )\n return sorted(sorted_by_variance[:num_samples])\n\n # Strategy 3: Even spacing (fallback)\n step = len(middle_items) // (num_samples + 1)\n return [middle_items[i * step] for i in range(1, num_samples + 1)]\n
"},{"location":"plans/dynamic-smart-crusher/#testing-strategy","title":"Testing Strategy","text":""},{"location":"plans/dynamic-smart-crusher/#test-category-1-adversarial-position-tests","title":"Test Category 1: Adversarial Position Tests","text":"Test cases where important data is NOT at expected positions:
class TestAdversarialPositions:\n \"\"\"Test scenarios that break 'first 3 + last 2' assumption.\"\"\"\n\n def test_important_data_in_middle(self):\n \"\"\"Critical error at position 50 of 100-item array.\"\"\"\n items = [{\"status\": \"ok\", \"value\": i} for i in range(100)]\n items[50] = {\"status\": \"error\", \"error_code\": \"CRITICAL\", \"value\": 50}\n\n result = smart_crusher.crush(items, max_items=10)\n\n # Error item MUST be preserved regardless of position\n assert any(item.get(\"error_code\") == \"CRITICAL\" for item in result)\n\n def test_spike_not_at_boundaries(self):\n \"\"\"Numeric spike at position 75 of 100-item array.\"\"\"\n items = [{\"metric\": 10.0 + random.random()} for _ in range(100)]\n items[75][\"metric\"] = 1000.0 # Spike\n\n result = smart_crusher.crush(items, max_items=10)\n\n # Spike MUST be preserved as anomaly\n assert any(item[\"metric\"] > 500 for item in result)\n\n def test_first_items_identical(self):\n \"\"\"First 10 items are identical - shouldn't waste slots.\"\"\"\n items = [{\"id\": \"same\", \"value\": 0}] * 10 + [\n {\"id\": f\"unique_{i}\", \"value\": i} for i in range(90)\n ]\n\n result = smart_crusher.crush(items, max_items=10)\n\n # Should NOT have multiple identical items\n ids = [item[\"id\"] for item in result]\n # At most 1-2 of the identical items, not 3\n assert ids.count(\"same\") <= 2\n\n def test_last_items_identical(self):\n \"\"\"Last 10 items are identical - shouldn't waste slots.\"\"\"\n items = [{\"id\": f\"unique_{i}\", \"value\": i} for i in range(90)] + [\n {\"id\": \"same\", \"value\": 100}\n ] * 10\n\n result = smart_crusher.crush(items, max_items=10)\n\n ids = [item[\"id\"] for item in result]\n assert ids.count(\"same\") <= 2\n\n def test_relevant_item_in_middle(self):\n \"\"\"Item matching user query is in middle of array.\"\"\"\n items = [{\"name\": f\"item_{i}\", \"status\": \"active\"} for i in range(100)]\n items[42][\"name\"] = \"target_item\"\n items[42][\"description\"] = \"This is what user asked about\"\n\n result = smart_crusher.crush(\n items,\n max_items=10,\n query=\"find target_item\"\n )\n\n # Query-matched item MUST be preserved\n assert any(\"target_item\" in item.get(\"name\", \"\") for item in result)\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-2-size-adaptation-tests","title":"Test Category 2: Size Adaptation Tests","text":"class TestSizeAdaptation:\n \"\"\"Test that anchor allocation scales with array size.\"\"\"\n\n @pytest.mark.parametrize(\"size,expected_min_anchors\", [\n (20, 3), # Small array: at least 3 anchors\n (100, 4), # Medium array: at least 4 anchors\n (500, 5), # Large array: at least 5 anchors\n (2000, 6), # Very large: at least 6 anchors\n ])\n def test_anchor_count_scales(self, size, expected_min_anchors):\n \"\"\"Anchor count should increase with array size.\"\"\"\n items = [{\"id\": i, \"value\": i * 10} for i in range(size)]\n\n result = smart_crusher.crush(items, max_items=20)\n\n # Count items from first 10% and last 10%\n anchor_count = sum(\n 1 for item in result\n if item[\"id\"] < size * 0.1 or item[\"id\"] > size * 0.9\n )\n\n assert anchor_count >= expected_min_anchors\n\n def test_small_array_high_preservation(self):\n \"\"\"Small arrays should preserve higher percentage.\"\"\"\n items = [{\"id\": i} for i in range(15)]\n\n result = smart_crusher.crush(items, max_items=20)\n\n # Should preserve most/all of small array\n assert len(result) >= 10 # At least 66%\n\n def test_large_array_efficient_sampling(self):\n \"\"\"Large arrays should sample efficiently.\"\"\"\n items = [{\"id\": i, \"value\": i} for i in range(5000)]\n\n result = smart_crusher.crush(items, max_items=20)\n\n # Should have good distribution across positions\n positions = [item[\"id\"] for item in result]\n\n has_front = any(p < 500 for p in positions)\n has_middle = any(500 < p < 4500 for p in positions)\n has_back = any(p > 4500 for p in positions)\n\n assert has_front and has_back\n # Middle should be represented if array is large enough\n assert has_middle\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-3-pattern-specific-tests","title":"Test Category 3: Pattern-Specific Tests","text":"class TestPatternAwareAnchoring:\n \"\"\"Test pattern-specific anchor strategies.\"\"\"\n\n def test_search_results_front_heavy(self):\n \"\"\"Search results should preserve more from front.\"\"\"\n items = [\n {\"title\": f\"Result {i}\", \"score\": 1.0 - (i * 0.01)}\n for i in range(100)\n ]\n\n result = smart_crusher.crush(items, max_items=10)\n\n # More items should be from front (high scores)\n front_count = sum(1 for item in result if item[\"score\"] > 0.9)\n back_count = sum(1 for item in result if item[\"score\"] < 0.1)\n\n assert front_count > back_count\n\n def test_logs_back_heavy(self):\n \"\"\"Logs should preserve more from back (recent).\"\"\"\n items = [\n {\"timestamp\": f\"2024-01-{i:02d}\", \"level\": \"INFO\", \"message\": f\"Log {i}\"}\n for i in range(1, 31)\n ]\n\n result = smart_crusher.crush(items, max_items=10)\n\n # More items should be from back (recent logs)\n timestamps = [item[\"timestamp\"] for item in result]\n recent_count = sum(1 for ts in timestamps if int(ts[-2:]) > 20)\n old_count = sum(1 for ts in timestamps if int(ts[-2:]) < 10)\n\n assert recent_count >= old_count\n\n def test_time_series_balanced(self):\n \"\"\"Time series should have balanced front/back.\"\"\"\n items = [\n {\"timestamp\": f\"2024-01-01T{i:02d}:00:00\", \"value\": 100 + i}\n for i in range(24)\n ]\n\n result = smart_crusher.crush(items, max_items=8)\n\n hours = [int(item[\"timestamp\"][11:13]) for item in result]\n front_count = sum(1 for h in hours if h < 8)\n back_count = sum(1 for h in hours if h > 16)\n\n # Should be roughly balanced\n assert abs(front_count - back_count) <= 2\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-4-query-aware-tests","title":"Test Category 4: Query-Aware Tests","text":"class TestQueryAwareAnchoring:\n \"\"\"Test query-based anchor adjustment.\"\"\"\n\n def test_latest_query_shifts_to_back(self):\n \"\"\"'Latest' in query should preserve more recent items.\"\"\"\n items = [{\"id\": i, \"created\": f\"2024-01-{i:02d}\"} for i in range(1, 31)]\n\n result = smart_crusher.crush(\n items,\n max_items=8,\n query=\"Show me the latest entries\"\n )\n\n ids = [item[\"id\"] for item in result]\n recent_count = sum(1 for id in ids if id > 20)\n\n assert recent_count >= 3 # At least 3 recent items\n\n def test_first_query_shifts_to_front(self):\n \"\"\"'First' in query should preserve earlier items.\"\"\"\n items = [{\"id\": i, \"created\": f\"2024-01-{i:02d}\"} for i in range(1, 31)]\n\n result = smart_crusher.crush(\n items,\n max_items=8,\n query=\"Show me the first entries\"\n )\n\n ids = [item[\"id\"] for item in result]\n early_count = sum(1 for id in ids if id < 10)\n\n assert early_count >= 3\n\n def test_specific_id_query_finds_item(self):\n \"\"\"Query for specific ID should find it regardless of position.\"\"\"\n items = [{\"id\": f\"item_{i:04d}\", \"value\": i} for i in range(1000)]\n\n result = smart_crusher.crush(\n items,\n max_items=10,\n query=\"Find item_0567\"\n )\n\n assert any(item[\"id\"] == \"item_0567\" for item in result)\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-5-coverage-metrics-tests","title":"Test Category 5: Coverage Metrics Tests","text":"class TestCoverageMetrics:\n \"\"\"Test that preserved items represent the full distribution.\"\"\"\n\n def test_value_range_coverage(self):\n \"\"\"Preserved items should cover the value range.\"\"\"\n items = [{\"value\": i} for i in range(100)]\n\n result = smart_crusher.crush(items, max_items=10)\n\n values = [item[\"value\"] for item in result]\n\n # Should cover most of the range\n assert min(values) < 10 # Has low values\n assert max(values) > 90 # Has high values\n\n # Should have some middle values too\n middle_count = sum(1 for v in values if 30 < v < 70)\n assert middle_count >= 1\n\n def test_category_coverage(self):\n \"\"\"Preserved items should represent all categories.\"\"\"\n items = [\n {\"category\": cat, \"id\": i}\n for i, cat in enumerate([\"A\"] * 30 + [\"B\"] * 30 + [\"C\"] * 40)\n ]\n\n result = smart_crusher.crush(items, max_items=10)\n\n categories = set(item[\"category\"] for item in result)\n\n # Should have at least 2 of 3 categories\n assert len(categories) >= 2\n\n def test_temporal_coverage(self):\n \"\"\"Preserved items should span the time range.\"\"\"\n items = [\n {\"timestamp\": f\"2024-{m:02d}-15\", \"event\": f\"event_{i}\"}\n for i, m in enumerate(range(1, 13))\n ]\n\n result = smart_crusher.crush(items, max_items=5)\n\n months = [int(item[\"timestamp\"][5:7]) for item in result]\n\n # Should span at least 6 months of the year\n assert max(months) - min(months) >= 6\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-6-retrieval-simulation-tests","title":"Test Category 6: Retrieval Simulation Tests","text":"class TestRetrievalSimulation:\n \"\"\"Simulate user retrieval patterns to measure effectiveness.\"\"\"\n\n def test_retrieval_hit_rate_random_queries(self):\n \"\"\"Measure how often preserved items satisfy random queries.\"\"\"\n items = [\n {\"id\": i, \"name\": f\"Item {i}\", \"category\": f\"cat_{i % 5}\"}\n for i in range(100)\n ]\n\n compressed = smart_crusher.crush(items, max_items=15)\n compressed_ids = {item[\"id\"] for item in compressed}\n\n # Simulate 100 random \"queries\" (random item lookups)\n hits = 0\n for _ in range(100):\n target_id = random.randint(0, 99)\n if target_id in compressed_ids:\n hits += 1\n\n # Should hit at least 15% (we keep 15 of 100)\n assert hits >= 15\n\n def test_retrieval_hit_rate_weighted_queries(self):\n \"\"\"Measure hits for queries weighted toward common patterns.\"\"\"\n items = [{\"id\": i, \"value\": i * 10} for i in range(100)]\n\n compressed = smart_crusher.crush(items, max_items=15)\n compressed_ids = {item[\"id\"] for item in compressed}\n\n # Weight queries toward front (30%), back (30%), anomalies (40%)\n hits = 0\n queries = (\n list(range(10)) * 3 + # Front queries\n list(range(90, 100)) * 3 + # Back queries\n [50] * 4 # Middle anomaly queries\n )\n\n for target_id in queries:\n if target_id in compressed_ids:\n hits += 1\n\n # Should hit more often than random due to anchor strategy\n assert hits >= 20 # At least 20% hit rate\n
"},{"location":"plans/dynamic-smart-crusher/#implementation-phases","title":"Implementation Phases","text":""},{"location":"plans/dynamic-smart-crusher/#phase-1-refactor-anchor-logic-foundation","title":"Phase 1: Refactor Anchor Logic (Foundation)","text":" - Extract anchor selection into
AnchorSelector class - Make slot counts configurable via
AnchorConfig - Add size-proportional allocation
- Maintain backward compatibility with current defaults
"},{"location":"plans/dynamic-smart-crusher/#phase-2-pattern-aware-anchoring","title":"Phase 2: Pattern-Aware Anchoring","text":" - Map
DataPattern to AnchorStrategy - Implement front-heavy, back-heavy, balanced, middle-aware strategies
- Add pattern-specific anchor weight configs
"},{"location":"plans/dynamic-smart-crusher/#phase-3-information-density-selection","title":"Phase 3: Information-Density Selection","text":" - Add
calculate_information_score() for items - Select from candidate region instead of fixed positions
- Deduplicate identical items across regions
"},{"location":"plans/dynamic-smart-crusher/#phase-4-query-aware-adjustment","title":"Phase 4: Query-Aware Adjustment","text":" - Parse query for position intent keywords
- Adjust anchor weights dynamically
- Add query-position relevance scoring
"},{"location":"plans/dynamic-smart-crusher/#phase-5-toin-position-learning","title":"Phase 5: TOIN Position Learning","text":" - Track retrieval positions in TOIN
- Learn per-tool position importance
- Use learned weights to adjust anchor strategy
"},{"location":"plans/dynamic-smart-crusher/#phase-6-comprehensive-testing","title":"Phase 6: Comprehensive Testing","text":" - Implement all adversarial tests
- Add coverage metric tests
- Add retrieval simulation tests
- Performance benchmarks
"},{"location":"plans/dynamic-smart-crusher/#configuration-schema","title":"Configuration Schema","text":"@dataclass\nclass AnchorConfig:\n \"\"\"Configuration for dynamic anchor allocation.\"\"\"\n\n # Base anchor budget as percentage of max_items\n anchor_budget_pct: float = 0.20 # 20% of slots for position anchors\n\n # Minimum and maximum anchor slots\n min_anchor_slots: int = 3\n max_anchor_slots: int = 10\n\n # Default distribution (overridden by pattern)\n default_front_weight: float = 0.5\n default_back_weight: float = 0.5\n default_middle_weight: float = 0.0\n\n # Pattern-specific overrides\n search_front_weight: float = 0.8\n logs_back_weight: float = 0.8\n time_series_balance: float = 0.5\n\n # Query keyword detection\n recency_keywords: list[str] = field(default_factory=lambda: [\n \"latest\", \"recent\", \"last\", \"newest\", \"current\"\n ])\n historical_keywords: list[str] = field(default_factory=lambda: [\n \"first\", \"oldest\", \"earliest\", \"original\", \"initial\"\n ])\n\n # Information density selection\n use_information_density: bool = True\n candidate_multiplier: int = 3 # Consider 3x candidates per slot\n\n # TOIN learning\n use_learned_positions: bool = True\n min_samples_for_learning: int = 10\n
"},{"location":"plans/dynamic-smart-crusher/#success-metrics","title":"Success Metrics","text":" - Retrieval Coverage: % of user retrievals that hit preserved items (target: >80%)
- Information Density: Unique information per preserved slot (target: no duplicate items)
- Distribution Coverage: Preserved items span full value/time/category ranges
- Adversarial Robustness: All adversarial tests pass
- Backward Compatibility: Existing tests still pass
- Performance: <5ms additional latency for anchor selection
"},{"location":"plans/dynamic-smart-crusher/#risks-and-mitigations","title":"Risks and Mitigations","text":"Risk Mitigation Information density calculation is expensive Cache scores, limit candidate pool Query keyword detection is brittle Use as soft signal, not hard rule TOIN learning needs cold start Fall back to pattern-based defaults Breaking existing behavior Feature flag, A/B testing Middle sampling misses important items Always include anomalies/errors regardless"},{"location":"plans/dynamic-smart-crusher/#next-steps","title":"Next Steps","text":" - Review and approve this plan
- Write failing tests first (TDD approach)
- Implement Phase 1 (refactor foundation)
- Iterate through phases with test validation
- Benchmark against current implementation
- A/B test in production with telemetry
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"],"fields":{"title":{"boost":1000.0},"text":{"boost":1.0},"tags":{"boost":1000000.0}}},"docs":[{"location":"","title":"Headroom","text":"The Context Optimization Layer for LLM Applications
Compress everything your AI agent reads. Same answers, fraction of the tokens.
87% Avg Token Reduction 100% Answer Accuracy 6 Compression Algorithms 100+ LLM Providers"},{"location":"#what-it-does","title":"What It Does","text":"Every tool call, DB query, file read, and RAG retrieval your agent makes is 70-95% boilerplate. Headroom compresses it away before it hits the model. The LLM sees less noise, responds faster, and costs less.
Your Agent / App\n \u2502\n \u2502 tool outputs, logs, DB reads, RAG results, file reads, API responses\n \u25bc\n Headroom \u2190 proxy, Python library, or framework integration\n \u2502\n \u25bc\n LLM Provider (OpenAI, Anthropic, Google, Bedrock, 100+ via LiteLLM)\n
Headroom works as a transparent proxy (zero code changes), a Python function (compress()), or a framework integration (LangChain, Agno, Strands, LiteLLM, MCP).
"},{"location":"#quick-start","title":"Quick Start","text":"Proxy (Zero Code Changes)Python SDKCoding AgentsTypeScript SDKLiteLLM Callback pip install \"headroom-ai[all]\"\nheadroom proxy\n
# Point any tool at the proxy\nANTHROPIC_BASE_URL=http://localhost:8787 claude\nOPENAI_BASE_URL=http://localhost:8787/v1 your-app\n
That's it. Your existing code works unchanged, with 40-90% fewer tokens.
from headroom import compress\n\nresult = compress(messages, model=\"claude-sonnet-4-5-20250929\")\nresponse = client.messages.create(\n model=\"claude-sonnet-4-5-20250929\",\n messages=result.messages,\n)\nprint(f\"Saved {result.tokens_saved} tokens ({result.compression_ratio:.0%})\")\n
Works with any Python LLM client. Full SDK guide \u2192
headroom wrap claude # Claude Code\nheadroom wrap copilot -- --model claude-sonnet-4-20250514\nheadroom wrap codex # OpenAI Codex CLI\nheadroom wrap aider # Aider\nheadroom wrap cursor # Cursor\nheadroom wrap openclaw # OpenClaw plugin bootstrap\n
Starts the proxy, points your tool at it, compresses everything automatically.
import { compress } from 'headroom-ai';\n\nconst result = await compress(messages, { model: 'claude-sonnet-4-5-20250929' });\n// Use result.messages with any LLM client\nconsole.log(`Saved ${result.tokensSaved} tokens`);\n
Works with Vercel AI SDK, OpenAI Node SDK, and Anthropic TS SDK. Full TS guide \u2192
import litellm\nfrom headroom.integrations.litellm_callback import HeadroomCallback\n\nlitellm.callbacks = [HeadroomCallback()]\n# All 100+ providers now compressed automatically\n
"},{"location":"#framework-integrations","title":"Framework Integrations","text":"All integration patterns \u2192
"},{"location":"#langchain","title":"LangChain","text":"Wrap any chat model. Supports memory, retrievers, tools, streaming, async.
from headroom.integrations import HeadroomChatModel\n\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n
LangChain Guide \u2192
"},{"location":"#agno","title":"Agno","text":"Full agent framework integration with observability hooks.
from headroom.integrations.agno import HeadroomAgnoModel\n\nmodel = HeadroomAgnoModel(Claude(id=\"claude-sonnet-4-20250514\"))\nagent = Agent(model=model)\n
Agno Guide \u2192
"},{"location":"#strands","title":"Strands","text":"Model wrapping + tool output hook provider for Strands Agents.
from headroom.integrations.strands import HeadroomStrandsModel\n\nmodel = HeadroomStrandsModel(wrapped_model=bedrock_model)\nagent = Agent(model=model)\n
Strands Guide \u2192
"},{"location":"#mcp-tools","title":"MCP Tools","text":"Three tools for Claude Code, Cursor, or any MCP client: headroom_compress, headroom_retrieve, headroom_stats.
headroom mcp install && claude\n
MCP Guide \u2192
"},{"location":"#typescript-sdk","title":"TypeScript SDK","text":"compress(), Vercel AI SDK middleware, OpenAI and Anthropic client wrappers.
npm install headroom-ai\n
TypeScript SDK Guide \u2192
"},{"location":"#openclaw","title":"OpenClaw","text":"ContextEngine plugin for OpenClaw agents. Auto-compresses context in assemble().
headroom wrap openclaw\n
OpenClaw Plugin \u2192
"},{"location":"#how-it-works","title":"How It Works","text":"Headroom runs a three-stage pipeline on every request:
graph LR\n A[Your Prompt] --> B[CacheAligner]\n B --> C[ContentRouter]\n C --> D[IntelligentContext]\n D --> E[LLM Provider]\n\n C -->|JSON| F[SmartCrusher]\n C -->|Code| G[CodeCompressor]\n C -->|Text| H[Kompress]\n C -->|Logs| I[LogCompressor]\n\n F --> D\n G --> D\n H --> D\n I --> D
Stage 1: CacheAligner \u2014 Stabilizes message prefixes so the provider's KV cache actually hits. Claude offers a 90% read discount on cached prefixes; CacheAligner makes that work.
Stage 2: ContentRouter \u2014 Auto-detects content type (JSON, code, logs, search results, diffs, HTML, plain text) and routes each to the optimal compressor:
Content Type Compressor How It Works JSON arrays SmartCrusher Statistical analysis: keeps errors, anomalies, boundaries. No hardcoded rules. Source code CodeCompressor AST-aware (tree-sitter). Preserves function signatures, collapses bodies. Plain text Kompress ModernBERT token classification. Removes redundant tokens while preserving meaning. Build/test logs LogCompressor Keeps failures, errors, warnings. Drops passing noise. Search results SearchCompressor Ranks by relevance to user query, keeps top matches. Git diffs DiffCompressor Preserves change hunks, drops unchanged context. HTML HTMLExtractor Strips markup, extracts readable content. Stage 3: IntelligentContext \u2014 If the conversation still exceeds the model's context limit, scores each message by importance (recency, references, density) and drops the lowest-value ones.
Nothing is lost. Compressed content goes into the CCR store (Compress-Cache-Retrieve). The LLM gets a headroom_retrieve tool and can fetch full originals when it needs more detail.
Full architecture deep dive \u2192
"},{"location":"#results","title":"Results","text":"100 production log entries. One critical error buried at position 67.
Metric Baseline Headroom Input tokens 10,144 1,260 Correct answers 4/4 4/4 87.6% fewer tokens. Same answer. The FATAL error was automatically preserved \u2014 not by keyword matching, but by statistical analysis of field variance.
"},{"location":"#real-workloads","title":"Real Workloads","text":"Scenario Before After Savings Code search (100 results) 17,765 1,408 92% SRE incident debugging 65,694 5,118 92% Codebase exploration 78,502 41,254 47% GitHub issue triage 54,174 14,761 73%"},{"location":"#accuracy-benchmarks","title":"Accuracy Benchmarks","text":"Benchmark Category N Accuracy Compression GSM8K Math 100 0.870 0.000 delta TruthfulQA Factual 100 0.560 +0.030 delta SQuAD v2 QA 100 97% 19% reduction BFCL Tool/Function 100 97% 32% reduction CCR Needle Lossless 50 100% 77% reduction Full benchmark methodology \u2192 | Known limitations \u2192
"},{"location":"#key-features","title":"Key Features","text":""},{"location":"#lossless-compression-ccr","title":"Lossless Compression (CCR)","text":"Compresses aggressively, stores originals, gives the LLM a tool to retrieve full details. Nothing is thrown away. Learn more \u2192
"},{"location":"#smart-content-detection","title":"Smart Content Detection","text":"Auto-detects JSON, code, logs, text, diffs, HTML. Routes each to the best compressor. Zero configuration needed. Learn more \u2192
"},{"location":"#cache-optimization","title":"Cache Optimization","text":"Stabilizes prefixes so provider KV caches hit. Tracks frozen messages to preserve the 90% read discount. Learn more \u2192
"},{"location":"#image-compression","title":"Image Compression","text":"40-90% token reduction via trained ML router. Automatically selects resize/quality tradeoff per image. Learn more \u2192
"},{"location":"#persistent-memory","title":"Persistent Memory","text":"Hierarchical memory (user/session/agent/turn) with SQLite + HNSW backends. Survives across conversations. Learn more \u2192
"},{"location":"#failure-learning","title":"Failure Learning","text":"Reads past sessions, finds failed tool calls, correlates with what succeeded, writes learnings to CLAUDE.md. Learn more \u2192
"},{"location":"#multi-agent-context","title":"Multi-Agent Context","text":"Compress what moves between agents. Any framework.
ctx = SharedContext()\nctx.put(\"research\", big_output)\nsummary = ctx.get(\"research\") # ~80% smaller\n
Learn more \u2192"},{"location":"#metrics-observability","title":"Metrics & Observability","text":"Prometheus endpoint, per-request logging, cost tracking, budget limits, pipeline timing breakdowns. Learn more \u2192
"},{"location":"#cloud-providers","title":"Cloud Providers","text":"Works with any LLM provider out of the box:
headroom proxy # Direct Anthropic/OpenAI\nheadroom proxy --backend bedrock --region us-east-1 # AWS Bedrock\nheadroom proxy --backend vertex_ai --region us-central1 # Google Vertex AI\nheadroom proxy --backend azure # Azure OpenAI\nheadroom proxy --backend openrouter # OpenRouter (400+ models)\n
Or via LiteLLM for 100+ providers (Together, Groq, Fireworks, Ollama, vLLM, etc.).
"},{"location":"#installation","title":"Installation","text":"pip install headroom-ai # Core library (Python)\npip install \"headroom-ai[all]\" # Everything (recommended)\nnpm install headroom-ai # TypeScript / Node.js\npip install \"headroom-ai[proxy]\" # Proxy server + MCP tools\npip install \"headroom-ai[ml]\" # ML compression (Kompress, requires torch)\npip install \"headroom-ai[langchain]\" # LangChain integration\npip install \"headroom-ai[agno]\" # Agno integration\npip install \"headroom-ai[evals]\" # Evaluation framework\n
Requires Python 3.10+.
"},{"location":"#next-steps","title":"Next Steps","text":" - Quickstart \u2014 Running in 5 minutes
- Integration Guide \u2014 Every way to add Headroom to your stack
- Architecture \u2014 How the pipeline works under the hood
- Benchmarks \u2014 Accuracy and latency data
- Limitations \u2014 When compression helps and when it doesn't
Apache 2.0 \u2014 Free for commercial use. GitHub | PyPI | Discord
"},{"location":"ARCHITECTURE/","title":"Headroom SDK: A Complete Explanation","text":""},{"location":"ARCHITECTURE/#architecture-overview","title":"Architecture Overview","text":"flowchart TB\n subgraph Entry[\"Entry Points\"]\n Proxy[\"Proxy Mode<br/><i>Zero code changes</i>\"]\n SDK[\"SDK Mode<br/><i>HeadroomClient</i>\"]\n Integrations[\"Integrations<br/><i>LangChain / Agno</i>\"]\n end\n\n subgraph Pipeline[\"Transform Pipeline\"]\n direction TB\n\n CA[\"Cache Aligner<br/>\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501<br/>Extracts dynamic content<br/>(dates, UUIDs, tokens)<br/>Stable prefix for caching\"]\n\n SC[\"Smart Crusher<br/>\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501<br/>Analyzes tool outputs<br/>Keeps: first, last, errors, outliers<br/>70-95% reduction\"]\n\n CM[\"Context Manager<br/>\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501<br/>Enforces token limits<br/>Scores by recency and relevance<br/>Fits context window\"]\n\n CA --> SC --> CM\n end\n\n subgraph Cache[\"Provider Cache Optimization\"]\n direction LR\n Anthropic[\"Anthropic<br/><i>cache_control blocks</i><br/>90% savings\"]\n OpenAI[\"OpenAI<br/><i>Prefix alignment</i><br/>50% savings\"]\n Google[\"Google<br/><i>CachedContent API</i><br/>75% savings\"]\n end\n\n subgraph CCR[\"CCR: Compress-Cache-Retrieve\"]\n Store[(\"Compression<br/>Store\")]\n Tool[\"Retrieve Tool<br/><i>LLM requests original</i>\"]\n Store <--> Tool\n end\n\n LLM[\"LLM API<br/><i>OpenAI / Anthropic / Google</i>\"]\n\n Entry --> Pipeline\n Pipeline --> Cache\n Cache --> LLM\n SC -.->|\"Stores original\"| Store\n LLM -.->|\"If needed\"| Tool
"},{"location":"ARCHITECTURE/#what-problem-does-headroom-solve","title":"What Problem Does Headroom Solve?","text":"When you use AI models like GPT-4 or Claude, you pay for tokens - the pieces of text you send (input) and receive (output). The problem is:
- Tool outputs are HUGE: When an AI agent calls tools (search, database queries, APIs), the responses are often massive JSON blobs with thousands of tokens
- Most of that data is REDUNDANT: 60 metric data points showing
cpu: 45% repeated, or 50 log entries with the same error message - You're paying for waste: Every token costs money and adds latency
- Context windows fill up: Models have limits (128K tokens), and bloated tool outputs eat into your available space
Headroom creates \"headroom\" - it intelligently compresses your input tokens so you have more room (and budget) for what matters.
"},{"location":"ARCHITECTURE/#how-headroom-works-the-big-picture","title":"How Headroom Works: The Big Picture","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 YOUR APPLICATION \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 HEADROOM CLIENT \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 ANALYZE \u2502\u2192 \u2502 TRANSFORM \u2502\u2192 \u2502 CALL \u2502 \u2502\n\u2502 \u2502 (Parser) \u2502 \u2502 (Pipeline) \u2502 \u2502 (API) \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502\n\u2502 \u25bc \u25bc \u25bc \u2502\n\u2502 Count tokens Apply compressions Send to OpenAI/Claude \u2502\n\u2502 Detect waste Preserve meaning Log metrics \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 OPENAI / ANTHROPIC API \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"ARCHITECTURE/#the-core-components-in-simple-terms","title":"The Core Components (In Simple Terms)","text":""},{"location":"ARCHITECTURE/#1-headroomclient-clientpy-the-wrapper","title":"1. HeadroomClient (client.py) - The Wrapper","text":"This is what you interact with. It wraps your existing OpenAI or Anthropic client:
# Before (normal OpenAI)\nclient = OpenAI(api_key=\"...\")\nresponse = client.chat.completions.create(model=\"gpt-4o\", messages=[...])\n\n# After (with Headroom)\nbase = OpenAI(api_key=\"...\")\nclient = HeadroomClient(original_client=base, provider=OpenAIProvider())\nresponse = client.chat.completions.create(model=\"gpt-4o\", messages=[...])\n
What it does: - Intercepts your API calls - Runs messages through the transform pipeline - Calls the real API with optimized messages - Logs metrics to a database - Returns the response unchanged
Two modes: - audit: Just observe and log (no changes) - optimize: Apply transforms to reduce tokens
"},{"location":"ARCHITECTURE/#2-providers-providers-model-specific-knowledge","title":"2. Providers (providers/) - Model-Specific Knowledge","text":"Different AI providers have different rules:
class OpenAIProvider:\n # Knows GPT-4o has 128K context\n # Knows how to count tokens (tiktoken)\n # Knows pricing ($2.50 per million input tokens)\n\nclass AnthropicProvider:\n # Knows Claude has 200K context\n # Uses different tokenization (~4 chars per token)\n # Different pricing structure\n
Why this matters: Token counting is model-specific. GPT-4 uses different tokenization than Claude. Headroom needs accurate counts to know how much to compress.
"},{"location":"ARCHITECTURE/#3-parser-parserpy-understanding-your-messages","title":"3. Parser (parser.py) - Understanding Your Messages","text":"Before optimizing, Headroom needs to understand what's in your messages:
messages = [\n {\"role\": \"system\", \"content\": \"You are helpful...\"},\n {\"role\": \"user\", \"content\": \"Search for X\"},\n {\"role\": \"assistant\", \"tool_calls\": [...]},\n {\"role\": \"tool\", \"content\": \"{huge JSON}\"},\n]\n\n# Parser breaks this into \"blocks\":\nblocks = [\n Block(kind=\"system\", tokens=50, ...),\n Block(kind=\"user\", tokens=10, ...),\n Block(kind=\"tool_call\", tokens=20, ...),\n Block(kind=\"tool_result\", tokens=5000, ...), # \u2190 This is the problem!\n]\n
It also detects waste signals: - Large JSON blobs (>500 tokens) - HTML tags and comments - Base64 encoded data - Excessive whitespace
"},{"location":"ARCHITECTURE/#4-transforms-transforms-the-compression-magic","title":"4. Transforms (transforms/) - The Compression Magic","text":"This is where the real work happens. Headroom has 4 transforms that run in sequence:
"},{"location":"ARCHITECTURE/#transform-1-cache-aligner","title":"Transform 1: Cache Aligner","text":"Problem: LLM providers cache your prompts, but only if they're byte-identical. If your system prompt has today's date, every day is a cache miss.
# Before:\n\"You are helpful. Current Date: 2024-12-15\" # Changes daily = no cache\n\n# After:\n\"You are helpful.\" # Static = cacheable\n\"[Context: Current Date: 2024-12-15]\" # Dynamic part moved to end\n
How it works: 1. Find date patterns in system prompt 2. Extract them 3. Move to end of message 4. Now the PREFIX is stable \u2192 cache hits!
"},{"location":"ARCHITECTURE/#transform-2-tool-crusher-naive-disabled-by-default","title":"Transform 2: Tool Crusher (Naive) - DISABLED BY DEFAULT","text":"This was our first approach - simple but limited:
# Before: 60 items\n[{\"ts\": 1, \"cpu\": 45}, {\"ts\": 2, \"cpu\": 45}, ..., {\"ts\": 60, \"cpu\": 95}]\n\n# After: First 10 items only\n[{\"ts\": 1, \"cpu\": 45}, ..., {\"ts\": 10, \"cpu\": 45}, {\"__truncated\": 50}]\n
Problem: If the important data (CPU spike) is at position 45, it gets thrown away!
"},{"location":"ARCHITECTURE/#transform-3-smart-crusher-new-default","title":"Transform 3: Smart Crusher (NEW DEFAULT)","text":"This is the intelligent approach using statistical analysis:
# Analyzes the data first:\nanalysis = {\n \"ts\": {\"type\": \"sequential\", \"unique_ratio\": 1.0},\n \"host\": {\"type\": \"constant\", \"value\": \"prod-1\"}, # \u2190 Same everywhere!\n \"cpu\": {\"variance\": 892, \"change_points\": [45]}, # \u2190 Spike detected!\n}\n\n# Smart compression:\n{\n \"__headroom_constants\": {\"host\": \"prod-1\"}, # Factor out\n \"__headroom_summary\": \"items 0-44: cpu stable at ~45\", # Summarize boring part\n \"data\": [\n {\"ts\": 45, \"cpu\": 92}, # Keep the spike!\n {\"ts\": 46, \"cpu\": 95},\n ...\n ]\n}\n
Strategies it uses: 1. TIME_SERIES: Detect variance spikes, keep change points 2. CLUSTER: Group similar log messages, keep 1-2 per cluster 3. TOP_N: For search results, keep highest scored 4. SMART_SAMPLE: Statistical sampling with constant extraction
"},{"location":"ARCHITECTURE/#transform-4-llmlingua-compressor-optional","title":"Transform 4: LLMLingua Compressor (Optional)","text":"When to use: Maximum compression needed and latency is acceptable.
# Opt-in ML-based compression using Microsoft's LLMLingua-2\n# BERT-based token classifier trained via GPT-4 distillation\n\n# Before: Long tool output text\n\"The function processUserData takes a user object and validates all fields...\"\n\n# After: Compressed while preserving semantic meaning\n\"function processUserData validates user fields...\"\n
Key characteristics: - Uses microsoft/llmlingua-2-xlm-roberta-large-meetingbank model - Auto-detects content type (code, JSON, text) for optimal compression rates - Stores original in CCR for retrieval if needed - Adds 50-200ms latency per request - Requires ~1GB RAM when loaded
Proxy integration (opt-in):
headroom proxy --llmlingua --llmlingua-device cuda\n
"},{"location":"ARCHITECTURE/#transform-5-rolling-window","title":"Transform 5: Rolling Window","text":"Problem: Even after compression, you might exceed the model's context limit.
# Model limit: 128K tokens\n# Your messages: 150K tokens\n# Need to drop 22K tokens\n\n# Rolling Window drops OLDEST messages first:\n# - Keeps system prompt (always)\n# - Keeps last 2 turns (always)\n# - Drops old tool calls + their responses as atomic units\n
Safety rule: If we drop a tool CALL, we MUST drop its RESPONSE too (or vice versa). Otherwise the model sees orphaned data.
"},{"location":"ARCHITECTURE/#transform-6-intelligent-context-manager-advanced","title":"Transform 6: Intelligent Context Manager (Advanced)","text":"Problem: Rolling Window drops by position (oldest first), but position doesn't equal importance.
# Scenario: Error at turn 3, verbose success at turn 10\n# Rolling Window: Drops turn 3 error (oldest first)\n# Intelligent Context: Keeps turn 3 error (high TOIN error score)\n
The Solution: Multi-factor importance scoring using TOIN-learned patterns:
# Message scores (all learned, no hardcodes):\nscores = {\n \"recency\": 0.20, # Exponential decay from end\n \"semantic_similarity\": 0.20, # Embedding similarity to recent context\n \"toin_importance\": 0.25, # TOIN retrieval_rate (high = important)\n \"error_indicator\": 0.15, # TOIN field_semantics.inferred_type\n \"forward_reference\": 0.15, # Referenced by later messages\n \"token_density\": 0.05, # Unique tokens / total tokens\n}\n\n# Drops lowest-scored messages first\n# Preserves critical errors even if old\n
Key principle: No hardcoded patterns. Error detection uses TOIN's learned field_semantics.inferred_type == \"error_indicator\", not keyword matching like \"error\" or \"fail\".
TOIN + CCR Integration:
IntelligentContext is a message-level compressor \u2014 just like SmartCrusher compresses items in an array, IntelligentContext \"compresses\" messages in a conversation. This means full CCR integration:
# When messages are dropped:\n# 1. Store dropped messages in CCR for potential retrieval\nccr_ref = store.store(\n original=json.dumps(dropped_messages),\n compressed=\"[60 messages dropped]\",\n tool_name=\"intelligent_context_drop\",\n)\n\n# 2. Record drop to TOIN for cross-user learning\ntoin.record_compression(\n tool_signature=message_signature, # Pattern of roles, tools, errors\n original_count=len(dropped_messages),\n compressed_count=1, # The marker\n strategy=\"intelligent_context_drop\",\n)\n\n# 3. Insert marker with CCR reference\nmarker = f\"[Earlier context compressed: 60 messages dropped. Retrieve: {ccr_ref}]\"\n
The feedback loop: - If users retrieve dropped messages via CCR, TOIN learns those patterns are important - Future drops of similar message patterns get higher importance scores - The system gets smarter across all users, not just within one session
"},{"location":"ARCHITECTURE/#5-storage-storage-metrics-database","title":"5. Storage (storage/) - Metrics Database","text":"Every request is logged:
CREATE TABLE requests (\n id TEXT PRIMARY KEY,\n timestamp TEXT,\n model TEXT,\n mode TEXT, -- audit or optimize\n tokens_input_before INTEGER, -- Before Headroom\n tokens_input_after INTEGER, -- After Headroom\n tokens_saved INTEGER, -- The win!\n transforms_applied TEXT, -- What we did\n ...\n);\n
This lets you: - See how much you're saving - Generate reports - Track trends over time
"},{"location":"ARCHITECTURE/#the-data-flow-step-by-step","title":"The Data Flow (Step by Step)","text":"Let's trace a real request:
"},{"location":"ARCHITECTURE/#step-1-you-call-the-api","title":"Step 1: You call the API","text":"response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[\n {\"role\": \"system\", \"content\": \"You are an SRE. Date: 2024-12-15\"},\n {\"role\": \"user\", \"content\": \"Check the metrics\"},\n {\"role\": \"assistant\", \"tool_calls\": [...]},\n {\"role\": \"tool\", \"content\": \"{60 metric points...}\"}, # 5000 tokens!\n {\"role\": \"user\", \"content\": \"What's wrong?\"},\n ],\n headroom_mode=\"optimize\",\n)\n
"},{"location":"ARCHITECTURE/#step-2-headroomclient-intercepts","title":"Step 2: HeadroomClient intercepts","text":"# In client.py:\ndef _create(self, messages, ...):\n # 1. Parse messages into blocks\n blocks, breakdown, waste = parse_messages(messages, tokenizer)\n # breakdown = {\"system\": 50, \"user\": 20, \"tool_result\": 5000, ...}\n\n # 2. Count original tokens\n tokens_before = 5100\n
"},{"location":"ARCHITECTURE/#step-3-transform-pipeline-runs","title":"Step 3: Transform Pipeline runs","text":"# In pipeline.py:\ndef apply(self, messages, ...):\n # Transform 1: Cache Aligner\n # - Extracts \"Date: 2024-12-15\" from system prompt\n # - Moves to end\n\n # Transform 2: Smart Crusher\n # - Analyzes 60 metric points\n # - Detects CPU spike at point 45\n # - Compresses to 17 points (preserving spike)\n # - Factors out constant \"host\" field\n\n # Transform 3: LLMLingua (if enabled via --llmlingua)\n # - ML-based compression on remaining long text\n # - Auto-detects content type for optimal rate\n # - Stores original in CCR for retrieval\n\n # Transform 4: Rolling Window\n # - Checks if we're under limit (we are)\n # - No drops needed\n\n return TransformResult(\n messages=optimized,\n tokens_before=5100,\n tokens_after=1200, # 76% reduction!\n transforms=[\"cache_align\", \"smart_crush:1\"]\n )\n
"},{"location":"ARCHITECTURE/#step-4-call-real-api","title":"Step 4: Call real API","text":"# In client.py:\nresponse = self._original.chat.completions.create(\n model=\"gpt-4o\",\n messages=optimized_messages, # Only 1200 tokens now!\n)\n
"},{"location":"ARCHITECTURE/#step-5-log-metrics-and-return","title":"Step 5: Log metrics and return","text":"# Save to database\nmetrics = RequestMetrics(\n tokens_input_before=5100,\n tokens_input_after=1200,\n tokens_saved=3900, # 76%!\n ...\n)\nstorage.save(metrics)\n\nreturn response # Unchanged from API\n
"},{"location":"ARCHITECTURE/#the-smart-crusher-deep-dive","title":"The Smart Crusher Deep Dive","text":"This is the most sophisticated part. Here's how it analyzes data:
"},{"location":"ARCHITECTURE/#field-analysis","title":"Field Analysis","text":"def analyze_field(key, items):\n values = [item[key] for item in items]\n\n return {\n \"unique_ratio\": len(set(values)) / len(values),\n # 0.0 = all same (constant)\n # 1.0 = all different (unique IDs)\n\n \"variance\": statistics.variance(values), # For numbers\n # Low = stable\n # High = changing\n\n \"change_points\": detect_spikes(values),\n # Indices where value jumps significantly\n }\n
"},{"location":"ARCHITECTURE/#pattern-detection","title":"Pattern Detection","text":"def detect_pattern(field_stats):\n # Has timestamp + numeric variance? \u2192 TIME_SERIES\n if has_timestamp and has_numeric_variance:\n return \"time_series\"\n\n # Has message field + level field? \u2192 LOGS\n if has_message_field and has_level_field:\n return \"logs\"\n\n # Has score/rank field? \u2192 SEARCH_RESULTS\n if has_score_field:\n return \"search_results\"\n\n return \"generic\"\n
"},{"location":"ARCHITECTURE/#compression-strategy","title":"Compression Strategy","text":"def compress(items, analysis):\n if analysis.pattern == \"time_series\":\n # Keep points around change points\n # Summarize stable regions\n return time_series_compress(items, analysis.change_points)\n\n elif analysis.pattern == \"logs\":\n # Cluster similar messages\n # Keep 1-2 per cluster\n return cluster_compress(items, analysis.clusters)\n\n elif analysis.pattern == \"search_results\":\n # Sort by score\n # Keep top N\n return top_n_compress(items, analysis.score_field)\n
"},{"location":"ARCHITECTURE/#ccr-architecture-compress-cache-retrieve","title":"CCR Architecture: Compress-Cache-Retrieve","text":""},{"location":"ARCHITECTURE/#the-key-insight","title":"The Key Insight","text":"\"Prefer raw > Compaction > Summarization only when compaction no longer yields enough space. Compaction (Reversible) strips out information that is redundant because it exists in the environment\u2014if the agent needs to read the data later, it can use a tool to retrieve it.\" \u2014 Phil Schmid, Context Engineering
The problem with traditional compression: If we guess wrong about what's important, we've permanently lost data. The LLM might need something we threw away.
CCR's solution: Make compression reversible. When SmartCrusher compresses, the original data is cached. If the LLM needs more, it can retrieve instantly.
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 TOOL OUTPUT (1000 items) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 HEADROOM CCR LAYER \u2502\n\u2502 \u2502\n\u2502 1. COMPRESS: Keep 20 items (errors, anomalies, relevant) \u2502\n\u2502 2. CACHE: Store full 1000 items in fast local cache \u2502\n\u2502 3. INJECT: Add retrieval capability to LLM context \u2502\n\u2502 \u2502\n\u2502 \"20 items shown. Use /v1/retrieve?hash=xxx for more.\" \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LLM PROCESSING \u2502\n\u2502 \u2502\n\u2502 Option A: LLM solves task with 20 items \u2192 Done \u2502\n\u2502 Option B: LLM needs more \u2192 retrieves via API \u2502\n\u2502 \u2192 We fetch from cache \u2192 Return instantly \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 FEEDBACK LOOP \u2502\n\u2502 \u2502\n\u2502 Track: What did the LLM retrieve? What queries? \u2502\n\u2502 Learn: \"For this tool, keep items matching common queries\" \u2502\n\u2502 Improve: Next compression uses learned patterns \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"ARCHITECTURE/#ccr-phase-1-compression-store","title":"CCR Phase 1: Compression Store","text":"Location: headroom/cache/compression_store.py
When SmartCrusher compresses, the original content is stored for on-demand retrieval:
@dataclass\nclass CompressionEntry:\n hash: str # 16-char SHA256 for retrieval\n original_content: str # Full JSON before compression\n compressed_content: str # Compressed JSON\n original_item_count: int\n compressed_item_count: int\n tool_name: str | None # For feedback tracking\n created_at: float\n ttl: int = 300 # 5 minute default\n
Features: - Thread-safe in-memory storage - TTL-based expiration (default 5 minutes) - LRU-style eviction when capacity reached - Built-in BM25 search within cached content
Usage:
store = get_compression_store()\n\n# Store compressed content\nhash_key = store.store(\n original=original_json,\n compressed=compressed_json,\n original_item_count=1000,\n compressed_item_count=20,\n tool_name=\"search_api\",\n)\n\n# Retrieve later\nentry = store.retrieve(hash_key)\n\n# Or search within cached content\nresults = store.search(hash_key, \"user query\")\n
"},{"location":"ARCHITECTURE/#ccr-phase-2-retrieval-api","title":"CCR Phase 2: Retrieval API","text":"Endpoints:
Endpoint Method Description /v1/retrieve POST Retrieve original content by hash /v1/retrieve?query=X POST Search within cached content Retrieval Request:
{\n \"hash\": \"abc123def456...\",\n \"query\": \"find errors\" // Optional: search within\n}\n
Response (full retrieval):
{\n \"hash\": \"abc123def456...\",\n \"original_content\": \"[{...}, {...}, ...]\",\n \"original_item_count\": 1000,\n \"tool_name\": \"search_api\"\n}\n
Response (search):
{\n \"hash\": \"abc123def456...\",\n \"query\": \"find errors\",\n \"results\": [{...}, {...}, ...],\n \"count\": 15\n}\n
"},{"location":"ARCHITECTURE/#ccr-phase-3-tool-injection","title":"CCR Phase 3: Tool Injection","text":"When compression happens, Headroom injects retrieval instructions into the LLM context.
Method A: System Message Injection
## Compressed Context Available\nThe following tool outputs have been compressed. If you need more detail,\ncall the retrieve_compressed tool with the hash.\n\nAvailable: hash=abc123 (1000\u219220 items from search_api)\n
Method B: MCP Tool Registration (Hybrid) When running as MCP server, Headroom exposes retrieval as a tool:
{\n \"name\": \"headroom_retrieve\",\n \"description\": \"Retrieve more items from compressed tool output\",\n \"inputSchema\": {\n \"type\": \"object\",\n \"properties\": {\n \"hash\": {\"type\": \"string\"},\n \"query\": {\"type\": \"string\"}\n }\n }\n}\n
Marker Injection: Compressed content includes retrieval markers:
{\n \"__headroom_compressed\": true,\n \"__headroom_hash\": \"abc123def456\",\n \"__headroom_stats\": {\n \"original_items\": 1000,\n \"kept_items\": 20,\n \"errors_preserved\": 5\n },\n \"data\": [...]\n}\n
"},{"location":"ARCHITECTURE/#ccr-phase-4-feedback-loop","title":"CCR Phase 4: Feedback Loop","text":"Location: headroom/cache/compression_feedback.py
The feedback system learns from retrieval patterns to improve future compression.
Tracked Patterns per Tool:
@dataclass\nclass ToolPattern:\n tool_name: str\n total_compressions: int # Times we compressed this tool\n total_retrievals: int # Times LLM asked for more\n full_retrievals: int # Retrieved everything\n search_retrievals: int # Used search query\n common_queries: dict[str, int] # Query frequency\n queried_fields: dict[str, int] # Fields mentioned in queries\n
Key Metrics: - Retrieval Rate: total_retrievals / total_compressions - High (>50%) \u2192 Compressing too aggressively - Low (<20%) \u2192 Compression is effective - Full Retrieval Rate: full_retrievals / total_retrievals - High (>80%) \u2192 Data is unique, consider skipping compression
Compression Hints:
@dataclass\nclass CompressionHints:\n max_items: int = 15 # Target item count\n suggested_items: int | None # Calculated optimal\n skip_compression: bool # Don't compress at all\n preserve_fields: list[str] # Always keep these fields\n aggressiveness: float # 0.0 = aggressive, 1.0 = conservative\n reason: str # Explanation\n
Feedback-Driven Adjustment:
# In SmartCrusher._crush_array()\nif self.config.use_feedback_hints and tool_name:\n feedback = get_compression_feedback()\n hints = feedback.get_compression_hints(tool_name)\n\n if hints.skip_compression:\n return items, f\"skip:feedback({hints.reason})\", None\n\n if hints.suggested_items is not None:\n self.config.max_items_after_crush = hints.suggested_items\n
Feedback Endpoints:
Endpoint Method Description /v1/feedback GET Get all learned patterns /v1/feedback/{tool_name} GET Get hints for specific tool Example Response:
{\n \"total_compressions\": 150,\n \"total_retrievals\": 23,\n \"global_retrieval_rate\": 0.15,\n \"tools_tracked\": 5,\n \"tool_patterns\": {\n \"search_api\": {\n \"compressions\": 50,\n \"retrievals\": 5,\n \"retrieval_rate\": 0.10,\n \"full_rate\": 0.20,\n \"search_rate\": 0.80,\n \"common_queries\": [\"status:error\", \"level:critical\"],\n \"queried_fields\": [\"status\", \"level\", \"message\"]\n }\n }\n}\n
"},{"location":"ARCHITECTURE/#ccr-phase-5-response-handler-automatic-tool-call-handling","title":"CCR Phase 5: Response Handler (Automatic Tool Call Handling)","text":"Location: headroom/ccr/response_handler.py
The Problem: When the proxy injects the headroom_retrieve tool, the LLM might call it. But who handles that tool call? Without response handling, the tool call would go back to the client unhandled.
The Solution: The Response Handler intercepts LLM responses, detects CCR tool calls, executes retrievals automatically, and continues the conversation until the LLM produces a final response.
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 RESPONSE HANDLER FLOW \u2502\n\u2502 \u2502\n\u2502 1. LLM Response arrives \u2502\n\u2502 \u2514\u2500 Contains: tool_use(headroom_retrieve, hash=abc123) \u2502\n\u2502 \u2502\n\u2502 2. Handler detects CCR tool call \u2502\n\u2502 \u2514\u2500 Extracts hash and optional query \u2502\n\u2502 \u2502\n\u2502 3. Handler executes retrieval \u2502\n\u2502 \u2514\u2500 Full retrieval: store.retrieve(hash) \u2502\n\u2502 \u2514\u2500 Search: store.search(hash, query) \u2502\n\u2502 \u2502\n\u2502 4. Handler continues conversation \u2502\n\u2502 \u2514\u2500 Adds tool result to messages \u2502\n\u2502 \u2514\u2500 Makes another API call \u2502\n\u2502 \u2502\n\u2502 5. Repeat until no CCR tool calls \u2502\n\u2502 \u2514\u2500 Max 3 rounds (configurable) \u2502\n\u2502 \u2502\n\u2502 6. Return final response to client \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Key Classes:
@dataclass\nclass CCRToolCall:\n tool_call_id: str # For matching response\n hash_key: str # CCR hash to retrieve\n query: str | None # Optional search query\n\n@dataclass\nclass CCRToolResult:\n tool_call_id: str\n content: str # Retrieved data as JSON\n success: bool\n items_retrieved: int\n was_search: bool # True if search, False if full retrieval\n\nclass CCRResponseHandler:\n async def handle_response(\n self,\n response: dict, # Initial LLM response\n messages: list, # Conversation history\n tools: list, # Tool definitions\n api_call_fn: Callable, # Function to make API calls\n provider: str, # \"anthropic\" or \"openai\"\n ) -> dict:\n \"\"\"Handle CCR tool calls until final response.\"\"\"\n
Streaming Support:
The handler also supports streaming responses via StreamingCCRHandler:
class StreamingCCRBuffer:\n \"\"\"Buffers streaming chunks to detect CCR tool calls.\"\"\"\n chunks: list[bytes]\n detected_ccr: bool\n\nclass StreamingCCRHandler:\n \"\"\"Handles CCR in streaming responses.\"\"\"\n async def process_stream(self, stream, messages, tools, api_call_fn):\n \"\"\"Yields chunks, switching to buffered mode if CCR detected.\"\"\"\n
"},{"location":"ARCHITECTURE/#ccr-phase-6-context-tracker-multi-turn-awareness","title":"CCR Phase 6: Context Tracker (Multi-Turn Awareness)","text":"Location: headroom/ccr/context_tracker.py
The Problem: In multi-turn conversations, earlier compressed data might become relevant later. Without tracking, the LLM has \"context amnesia\" - it can't reference data that was compressed in turn 1 when answering a question in turn 5.
The Solution: The Context Tracker maintains awareness of all compressed content across the conversation and can proactively expand relevant data when a new query might need it.
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 CONTEXT TRACKER FLOW \u2502\n\u2502 \u2502\n\u2502 Turn 1: Search returns 100 files \u2192 compressed to 10 \u2502\n\u2502 Tracker stores: hash=abc123, sample=\"auth.py, db.py\" \u2502\n\u2502 \u2502\n\u2502 Turn 5: User asks \"What about the authentication middleware?\" \u2502\n\u2502 Tracker analyzes query: \u2502\n\u2502 - \"authentication\" matches \"auth.py\" in sample \u2502\n\u2502 - Relevance score: 0.7 (above threshold) \u2502\n\u2502 \u2502\n\u2502 Proactive Expansion: \u2502\n\u2502 - Retrieves abc123 before LLM responds \u2502\n\u2502 - Adds expanded context to request \u2502\n\u2502 \u2502\n\u2502 Result: LLM sees full file list, can answer accurately \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Key Classes:
@dataclass\nclass CompressedContext:\n hash_key: str # CCR hash\n turn_number: int # When compression happened\n timestamp: float # For age-based filtering\n tool_name: str | None # Which tool was compressed\n original_item_count: int\n compressed_item_count: int\n query_context: str # User query at compression time\n sample_content: str # Preview for relevance matching\n\n@dataclass\nclass ExpansionRecommendation:\n hash_key: str\n reason: str # Human-readable explanation\n relevance_score: float # 0-1, higher = more relevant\n expand_full: bool # True = full retrieval\n search_query: str | None # If expand_full=False\n\nclass ContextTracker:\n def track_compression(self, hash_key, turn_number, ...):\n \"\"\"Track a compression event.\"\"\"\n\n def analyze_query(self, query: str) -> list[ExpansionRecommendation]:\n \"\"\"Find relevant compressed contexts for a query.\"\"\"\n\n def execute_expansions(self, recommendations) -> list[dict]:\n \"\"\"Execute recommended expansions.\"\"\"\n
Relevance Calculation:
The tracker uses simple but effective heuristics:
- Keyword overlap with sample content - Extract keywords from query, match against compressed content preview
- Keyword overlap with original query - Match against the query that triggered compression
- Tool name relevance - File operations more likely to need expansion for \"file\", \"where\", \"find\" queries
- Age discount - Older contexts get lower scores
Configuration:
@dataclass\nclass ContextTrackerConfig:\n enabled: bool = True\n max_tracked_contexts: int = 100 # LRU eviction\n relevance_threshold: float = 0.3 # Min score to recommend\n max_context_age_seconds: float = 300 # 5 minutes\n proactive_expansion: bool = True\n max_proactive_expansions: int = 2 # Per query\n
"},{"location":"ARCHITECTURE/#why-ccr-is-a-moat","title":"Why CCR is a Moat","text":" - Reversible: No permanent information loss. Worst case = retrieve everything.
- Transparent: LLM knows it can ask for more data.
- Automatic: Response Handler executes retrievals without client intervention.
- Context-Aware: Context Tracker prevents multi-turn amnesia.
- Feedback Loop: Learn from actual needs, not guesses.
- Network Effect: Retrieval patterns across users improve compression for everyone.
- Zero-Risk: If compression fails, instant fallback to original data.
"},{"location":"ARCHITECTURE/#image-compression-architecture","title":"Image Compression Architecture","text":"Vision models charge by the token, and images are expensive (765-2900 tokens for a typical image). Headroom's image compression uses a trained ML router to automatically select the optimal compression technique.
"},{"location":"ARCHITECTURE/#the-key-insight_1","title":"The Key Insight","text":"Not all image queries need full resolution: - \"What is this?\" \u2192 Low detail is fine (87% savings) - \"Count the whiskers\" \u2192 Need full detail (0% savings) - \"Read the sign\" \u2192 Could convert to text (99% savings)
"},{"location":"ARCHITECTURE/#how-it-works","title":"How It Works","text":"User: [image] + \"What animal is this?\"\n \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 1. Query Analysis \u2502\n\u2502 TrainedRouter (MiniLM) \u2502\n\u2502 Classifies \u2192 full_low \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 2. Image Analysis (Optional) \u2502\n\u2502 SigLIP checks: \u2502\n\u2502 - Has text? Is complex? \u2502\n\u2502 - Fine details needed? \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2193\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 3. Apply Compression \u2502\n\u2502 OpenAI: detail=\"low\" \u2502\n\u2502 Anthropic: Resize to 512px \u2502\n\u2502 Google: Resize to 768px \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2193\nCompressed request \u2192 LLM \u2192 Response\n
"},{"location":"ARCHITECTURE/#the-trained-router","title":"The Trained Router","text":"A fine-tuned MiniLM classifier hosted on HuggingFace:
- Model:
chopratejas/technique-router - Size: ~128MB (downloaded once, cached)
- Accuracy: 93.7% on 1,157 training examples
- Latency: ~10ms CPU, ~2ms GPU
The router learns from examples like: | Query | Technique | |-------|-----------| | \"What is this?\" | full_low | | \"Count the items\" | preserve | | \"Read the text\" | transcode | | \"What's in the corner?\" | crop |
"},{"location":"ARCHITECTURE/#provider-specific-compression","title":"Provider-Specific Compression","text":"Each provider handles images differently:
Provider Method Savings OpenAI detail=\"low\" parameter ~87% Anthropic PIL resize to 512px ~75% Google PIL resize to 768px (tile-optimized) ~75%"},{"location":"ARCHITECTURE/#integration-points","title":"Integration Points","text":"Image compression runs in the proxy before text compression:
Request arrives\n \u2193\n[Image Compression] \u2190 NEW\n \u2193\n[Transform Pipeline: Cache Aligner \u2192 Smart Crusher \u2192 ...]\n \u2193\nForward to LLM\n
This ensures images are compressed first, then text compression (CCR, SmartCrusher) handles the rest.
"},{"location":"ARCHITECTURE/#code-location","title":"Code Location","text":"headroom/\n\u251c\u2500\u2500 image/\n\u2502 \u251c\u2500\u2500 __init__.py # Public API\n\u2502 \u251c\u2500\u2500 compressor.py # ImageCompressor class\n\u2502 \u2514\u2500\u2500 trained_router.py # TrainedRouter (HuggingFace model)\n\u251c\u2500\u2500 proxy/\n\u2502 \u2514\u2500\u2500 server.py # Integration point\n
"},{"location":"ARCHITECTURE/#file-structure-explained","title":"File Structure Explained","text":"headroom/\n\u251c\u2500\u2500 __init__.py # Public exports\n\u251c\u2500\u2500 client.py # HeadroomClient - the main wrapper\n\u251c\u2500\u2500 config.py # All configuration dataclasses\n\u251c\u2500\u2500 parser.py # Message \u2192 Block decomposition\n\u251c\u2500\u2500 tokenizer.py # Token counting abstraction\n\u251c\u2500\u2500 utils.py # Hashing, markers, helpers\n\u2502\n\u251c\u2500\u2500 providers/\n\u2502 \u251c\u2500\u2500 base.py # Provider/TokenCounter protocols\n\u2502 \u251c\u2500\u2500 openai.py # OpenAI-specific (tiktoken)\n\u2502 \u2514\u2500\u2500 anthropic.py # Anthropic-specific\n\u2502\n\u251c\u2500\u2500 transforms/\n\u2502 \u251c\u2500\u2500 base.py # Transform protocol\n\u2502 \u251c\u2500\u2500 pipeline.py # Orchestrates all transforms\n\u2502 \u251c\u2500\u2500 cache_aligner.py # Date extraction for caching\n\u2502 \u251c\u2500\u2500 tool_crusher.py # Naive compression (disabled)\n\u2502 \u251c\u2500\u2500 smart_crusher.py # Statistical compression (default)\n\u2502 \u251c\u2500\u2500 rolling_window.py # Token limit enforcement (position-based)\n\u2502 \u251c\u2500\u2500 intelligent_context.py # Semantic context management (score-based)\n\u2502 \u251c\u2500\u2500 scoring.py # Message importance scoring\n\u2502 \u2514\u2500\u2500 llmlingua_compressor.py # ML-based compression (opt-in)\n\u2502\n\u251c\u2500\u2500 cache/ # CCR Architecture - Caching & Storage\n\u2502 \u251c\u2500\u2500 compression_store.py # Phase 1: Store original content\n\u2502 \u251c\u2500\u2500 compression_feedback.py # Phase 4: Learn from retrievals\n\u2502 \u251c\u2500\u2500 anthropic.py # Anthropic cache optimizer\n\u2502 \u251c\u2500\u2500 openai.py # OpenAI cache optimizer\n\u2502 \u251c\u2500\u2500 google.py # Google cache optimizer\n\u2502 \u2514\u2500\u2500 dynamic_detector.py # Dynamic content detection\n\u2502\n\u251c\u2500\u2500 ccr/ # CCR Architecture - Tool Injection & Response Handling\n\u2502 \u251c\u2500\u2500 __init__.py # CCR module exports\n\u2502 \u251c\u2500\u2500 tool_injection.py # Phase 3: Inject retrieval tool\n\u2502 \u251c\u2500\u2500 response_handler.py # Phase 5: Handle CCR tool calls\n\u2502 \u251c\u2500\u2500 context_tracker.py # Phase 6: Multi-turn context tracking\n\u2502 \u2514\u2500\u2500 mcp_server.py # MCP server for retrieval tool\n\u2502\n\u251c\u2500\u2500 relevance/ # Relevance scoring for compression\n\u2502 \u251c\u2500\u2500 bm25.py # BM25 keyword scorer\n\u2502 \u251c\u2500\u2500 embedding.py # Semantic embedding scorer\n\u2502 \u2514\u2500\u2500 hybrid.py # Adaptive fusion scorer\n\u2502\n\u251c\u2500\u2500 storage/\n\u2502 \u251c\u2500\u2500 base.py # Storage protocol\n\u2502 \u251c\u2500\u2500 sqlite.py # SQLite implementation\n\u2502 \u2514\u2500\u2500 jsonl.py # JSON Lines implementation\n\u2502\n\u251c\u2500\u2500 proxy/\n\u2502 \u2514\u2500\u2500 server.py # Production HTTP proxy (CCR endpoints)\n\u2502\n\u2514\u2500\u2500 reporting/\n \u2514\u2500\u2500 generator.py # HTML report generation\n
"},{"location":"ARCHITECTURE/#key-design-decisions","title":"Key Design Decisions","text":""},{"location":"ARCHITECTURE/#1-provider-agnostic","title":"1. Provider-Agnostic","text":"Works with ANY OpenAI-compatible API: - OpenAI - Azure OpenAI - Anthropic - Groq - Together - Local models (Ollama)
"},{"location":"ARCHITECTURE/#2-deterministic-transforms","title":"2. Deterministic Transforms","text":"No LLM calls for compression. Everything is: - Statistical analysis - Pattern matching - Rule-based
This means: - Predictable results - Fast (<10ms overhead) - No added API costs
"},{"location":"ARCHITECTURE/#3-safety-first","title":"3. Safety First","text":" - Never modify user/assistant TEXT content
- Tool call + response are atomic (drop both or neither)
- Parse failures = no-op (return unchanged)
- Audit mode for testing before optimizing
"},{"location":"ARCHITECTURE/#4-smart-by-default","title":"4. Smart by Default","text":" - SmartCrusher enabled (statistical analysis)
- ToolCrusher disabled (naive rules)
- Conservative settings that preserve important data
"},{"location":"ARCHITECTURE/#what-makes-this-different","title":"What Makes This Different?","text":""},{"location":"ARCHITECTURE/#vs-summarization-llm-based-compression","title":"vs. Summarization (LLM-based compression)","text":"Headroom Summarization Deterministic Non-deterministic ~10ms overhead ~2-5 seconds overhead No extra API cost Costs money to summarize Preserves structure Loses structure Can't hallucinate Can hallucinate"},{"location":"ARCHITECTURE/#vs-simple-truncation","title":"vs. Simple Truncation","text":"Headroom Truncation Keeps important data Loses end of data Statistical analysis No analysis Detects spikes Misses spikes Factors out constants Keeps redundancy"},{"location":"ARCHITECTURE/#the-numbers-from-our-tests","title":"The Numbers (From Our Tests)","text":"Real-world SRE incident investigation: - 5 tool calls: Metrics, logs, status, deployments, runbook - Original: 22,048 tokens - After SmartCrusher: 2,190 tokens - Reduction: 90% - Quality Score: 5.0/5 (no information loss)
The model could still: - Identify the CPU spike (preserved by change point detection) - Reference specific error rates (kept in compressed data) - Provide correct remediation commands
"},{"location":"ARCHITECTURE/#summary","title":"Summary","text":"Headroom is a Context Budget Controller that:
- Wraps your existing LLM client
- Analyzes your messages to find waste
- Compresses tool outputs intelligently (not blindly)
- Preserves important information (spikes, anomalies, unique data)
- Logs everything for observability
- Saves 70-90% of tokens on tool-heavy workloads
The key insight: Most tool output redundancy is statistical (repeated values, constant fields, similar messages). By analyzing the data first, we can compress intelligently without losing the information that matters.
"},{"location":"LATENCY_BENCHMARKS/","title":"Headroom Latency Benchmarks","text":"Measured compression overhead across content types and sizes to answer: does the token savings outweigh the processing time?
Generated: 2026-02-24 01:11 UTC
"},{"location":"LATENCY_BENCHMARKS/#environment","title":"Environment","text":" - Platform: macOS-26.1-arm64-arm-64bit
- Processor: arm
- Python: 3.11.11
- Headroom: v0.3.7
Note: These benchmarks were captured on v0.3.7. Since then, v0.5.6 added parallel message compression, eliminated redundant token counting, and optimized hot-path hashing. Expect lower latency on current versions. Re-benchmarking is planned.
"},{"location":"LATENCY_BENCHMARKS/#tldr","title":"TL;DR","text":" - Average compression: 93% token reduction
- Maximum compression overhead: 12213ms (p50)
- Net latency win: 11/12 scenarios against Claude Sonnet 4.5
"},{"location":"LATENCY_BENCHMARKS/#compression-overhead-by-scenario","title":"Compression Overhead by Scenario","text":"Scenario Tokens In Tokens Out Saved Ratio p50 (ms) p95 (ms) Mean (ms) JSON: Search Results (100 items) 10.2K 1.5K 8.7K 86% 189 231 196 JSON: Search Results (500 items) 50.2K 1.5K 48.7K 97% 943 955 943 JSON: Search Results (1K items) 100.5K 1.5K 99.0K 99% 2012 2198 2032 JSON: Search Results (5K items) 502.6K 1.5K 501.2K 100% 12213 12804 12223 JSON: API Responses (500 items) 38.9K 1.1K 37.8K 97% 743 776 744 JSON: Database Rows (1K rows) 43.7K 605 43.1K 99% 961 1104 986 JSON: String Array (100 strings) 1.1K 231 820 78% 15.0 15.4 15.0 JSON: String Array (500 strings) 4.9K 233 4.6K 95% 71.9 80.3 72.7 JSON: String Array (1K strings) 9.6K 242 9.4K 97% 146 160 147 JSON: Number Array (200 numbers) 1.2K 192 1.1K 85% 30.9 61.9 33.8 JSON: Number Array (1K numbers) 6.1K 243 5.8K 96% 301 307 300 JSON: Mixed Array (250 items) 2.3K 368 1.9K 84% 38.4 39.8 38.4"},{"location":"LATENCY_BENCHMARKS/#per-transform-latency-breakdown","title":"Per-Transform Latency Breakdown","text":"Scenario Transform p50 (ms) % of Total JSON: Search Results (100 items) cache_aligner 2.2 1% JSON: Search Results (100 items) content_router 186 98% JSON: Search Results (100 items) rolling_window <0.01 0% JSON: Search Results (500 items) cache_aligner 10.7 1% JSON: Search Results (500 items) content_router 927 98% JSON: Search Results (500 items) rolling_window <0.01 0% JSON: Search Results (1K items) cache_aligner 21.0 1% JSON: Search Results (1K items) content_router 1980 98% JSON: Search Results (1K items) rolling_window <0.01 0% JSON: Search Results (5K items) cache_aligner 105 1% JSON: Search Results (5K items) content_router 11985 98% JSON: Search Results (5K items) rolling_window <0.01 0% JSON: API Responses (500 items) cache_aligner 8.8 1% JSON: API Responses (500 items) content_router 729 98% JSON: API Responses (500 items) rolling_window <0.01 0% JSON: Database Rows (1K rows) cache_aligner 9.3 1% JSON: Database Rows (1K rows) content_router 946 99% JSON: Database Rows (1K rows) rolling_window <0.01 0% JSON: String Array (100 strings) cache_aligner 0.27 2% JSON: String Array (100 strings) content_router 14.5 97% JSON: String Array (100 strings) rolling_window <0.01 0% JSON: String Array (500 strings) cache_aligner 0.95 1% JSON: String Array (500 strings) content_router 70.2 98% JSON: String Array (500 strings) rolling_window <0.01 0% JSON: String Array (1K strings) cache_aligner 1.9 1% JSON: String Array (1K strings) content_router 143 98% JSON: String Array (1K strings) rolling_window <0.01 0% JSON: Number Array (200 numbers) cache_aligner 0.66 2% JSON: Number Array (200 numbers) content_router 29.6 96% JSON: Number Array (200 numbers) rolling_window <0.01 0% JSON: Number Array (1K numbers) cache_aligner 2.5 1% JSON: Number Array (1K numbers) content_router 297 99% JSON: Number Array (1K numbers) rolling_window <0.01 0% JSON: Mixed Array (250 items) cache_aligner 0.58 1% JSON: Mixed Array (250 items) content_router 37.4 97% JSON: Mixed Array (250 items) rolling_window <0.01 0%"},{"location":"LATENCY_BENCHMARKS/#cost-benefit-analysis","title":"Cost-Benefit Analysis","text":"Net latency benefit = LLM time saved from fewer tokens - compression overhead.
Scenario Compress (ms) LLM Saved (ms)* Net Benefit $/1K Requests** JSON: Search Results (100 items) 189 261 +71.8ms $26.13 JSON: Search Results (500 items) 943 1461 +517.5ms $146.06 JSON: Search Results (1K items) 2012 2969 +956.9ms $296.91 JSON: Search Results (5K items) 12213 15035 +2822.2ms $1503.53 JSON: API Responses (500 items) 743 1134 +390.7ms $113.38 JSON: Database Rows (1K rows) 961 1292 +330.7ms $129.16 JSON: String Array (100 strings) 15.0 24.6 +9.6ms $2.46 JSON: String Array (500 strings) 71.9 139 +67.1ms $13.90 JSON: String Array (1K strings) 146 282 +135.9ms $28.16 JSON: Number Array (200 numbers) 30.9 31.6 +0.7ms $3.16 JSON: Number Array (1K numbers) 301 175 -126.3ms $17.45 JSON: Mixed Array (250 items) 38.4 56.6 +18.2ms $5.66 * LLM time saved based on Claude Sonnet 4.5 prefill rate (0.03ms/token) ** Cost savings at $3.0/MTok input pricing
"},{"location":"LATENCY_BENCHMARKS/#break-even-across-models","title":"Break-Even Across Models","text":"Compression overhead (p50) vs. LLM time saved for different model speed tiers:
Scenario Compress (ms) GPT-4o Mini GPT-4o Claude Sonnet 4.5 Claude Opus 4 JSON: Search Results (100 items) 189 -102ms +71.8ms +71.8ms +507ms JSON: Search Results (500 items) 943 -456ms +518ms +518ms +2952ms JSON: Search Results (1K items) 2012 -1022ms +957ms +957ms +5905ms JSON: Search Results (5K items) 12213 -7201ms +2822ms +2822ms +27881ms JSON: API Responses (500 items) 743 -365ms +391ms +391ms +2280ms JSON: Database Rows (1K rows) 961 -530ms +331ms +331ms +2483ms JSON: String Array (100 strings) 15.0 -6.8ms +9.6ms +9.6ms +50.6ms JSON: String Array (500 strings) 71.9 -25.6ms +67.1ms +67.1ms +299ms JSON: String Array (1K strings) 146 -51.9ms +136ms +136ms +605ms JSON: Number Array (200 numbers) 30.9 -20.4ms +0.68ms +0.68ms +53.3ms JSON: Number Array (1K numbers) 301 -243ms -126ms -126ms +165ms JSON: Mixed Array (250 items) 38.4 -19.5ms +18.2ms +18.2ms +113ms"},{"location":"LATENCY_BENCHMARKS/#key-takeaways","title":"Key Takeaways","text":" - Compression pays for itself in latency for 11/12 compressing scenarios (json). For these, the LLM prefill time saved exceeds compression overhead.
- ContentRouter is 98% of pipeline cost on average \u2014 it does the actual compression work. CacheAligner and context management are <2% of total time.
- Cost savings are substantial regardless of latency. The highest-compression scenario (JSON: Search Results (5K items)) saves $1504/1K requests at Claude Sonnet 4.5 pricing.
- Slower/pricier models benefit most. Claude Opus shows a net latency win in 12/12 scenarios vs 11 for Claude Sonnet 4.5, with 0.08ms/token prefill.
Benchmarks run with python benchmarks/bench_latency.py. Results vary based on hardware, Python version, and content characteristics.
"},{"location":"LIMITATIONS/","title":"Headroom Limitations & Known Behavior","text":"Honest documentation of when Headroom helps, when it doesn't, and what to watch out for.
"},{"location":"LIMITATIONS/#when-headroom-helps-and-when-it-doesnt","title":"When Headroom Helps (and When It Doesn't)","text":"Content Type Compression Latency Impact Best For JSON: Arrays of dicts (search results, API responses, DB rows) 86-100% Net latency win on Sonnet/Opus Primary use case \u2014 always use JSON: Arrays of strings (file paths, log lines, tags) 60-90% Net latency win New \u2014 works with all string arrays JSON: Arrays of numbers (metrics, time series) 70-85% Net latency win New \u2014 includes statistical summary JSON: Mixed-type arrays 50-70% Net latency win New \u2014 groups by type, compresses each Structured logs (as JSON) 82-95% Net latency win Log entries in tool outputs Agentic conversations (25-50 turns) 56-81% Break-even to net win Multi-tool agent sessions Plain text (documentation, articles) 43-46% Adds latency (cost savings only) Cost optimization, not speed Code Passthrough Minimal overhead See Code Compression RAG document contexts Passthrough Minimal overhead Not compressed (plain text in user messages) See LATENCY_BENCHMARKS.md for full data with per-scenario timing.
"},{"location":"LIMITATIONS/#code-compression","title":"Code Compression","text":"Headroom includes an AST-aware CodeCompressor (tree-sitter, 8 languages) but it's gated behind safety protections that prevent it from firing in most real-world scenarios. This is intentional.
Why code mostly passes through:
- Word count gate: Content under 50 words is silently skipped
- Recent code protection (
protect_recent_code=4): Code in the last 4 messages is never compressed. In typical tool-call patterns, the tool result is always \"recent\" - Analysis intent protection (
protect_analysis_context=True): If the most recent user message contains keywords like \"analyze\", \"review\", \"explain\", \"fix\", \"debug\", \"optimize\", \"error\", \"bug\" \u2014 ALL code in the conversation is protected
Why this is the right default: Code is almost always fetched because the user wants to work with it. Compressing function bodies would remove exactly what they need. LLMs like Claude are excellent at navigating large code files without compression.
Where code savings come from: The IntelligentContextManager drops old code messages that are no longer relevant (scoring-based), which is a better strategy than stripping function bodies from active code.
Override: Set protect_analysis_context=False in ContentRouterConfig for aggressive code compression. Requires headroom-ai[code] for tree-sitter.
"},{"location":"LIMITATIONS/#json-compression-constraints","title":"JSON Compression Constraints","text":""},{"location":"LIMITATIONS/#what-gets-compressed","title":"What gets compressed","text":" - Arrays of dicts: Full statistical analysis with adaptive K (Kneedle algorithm)
- Arrays of strings: Dedup + adaptive sampling + error preservation
- Arrays of numbers: Statistical summary + outlier/change-point preservation
- Mixed-type arrays: Grouped by type, each group compressed independently
- Nested objects: Recursed into, arrays within are compressed (up to depth 5)
"},{"location":"LIMITATIONS/#what-passes-through","title":"What passes through","text":" - Arrays below 5 items (
min_items_to_analyze) - Content below 200 tokens (
min_tokens_to_crush) - Bool-only arrays (not useful to compress)
- JSON objects without array values
- Malformed JSON (silently passes through, no error)
- Non-JSON content (handled by other pipeline stages)
"},{"location":"LIMITATIONS/#edge-cases","title":"Edge cases","text":" - NaN/Infinity in numeric fields: Filtered out before statistics are computed
- Nesting depth > 5: Inner arrays not examined for compression
- Mixed-type arrays with small groups: Groups below
min_items_to_analyze are kept as-is
"},{"location":"LIMITATIONS/#adaptive-k-how-item-retention-works","title":"Adaptive K: How Item Retention Works","text":"SmartCrusher doesn't use fixed K values. It uses information-theoretic sizing:
- Kneedle algorithm on bigram coverage curves finds the point where adding more items stops providing new information
- SimHash fingerprinting detects near-duplicate items
- zlib validation ensures the subset captures the full set's diversity
- The resulting K is split: 30% from array start, 15% from end, 55% for importance-scored items
Safety guarantees (additive, never dropped): - Error items (containing \"error\", \"exception\", \"failed\", \"critical\", etc.) \u2014 across ALL array types - Numeric anomalies (> 2\u03c3 from mean) - String length anomalies (> 2\u03c3 from mean length) - Change points (sudden shifts in running values)
These are kept even if they exceed the K budget.
"},{"location":"LIMITATIONS/#text-compression-llmlingua","title":"Text Compression (LLMLingua)","text":" - Requires:
headroom-ai[llmlingua] \u2014 downloads ~2GB model, needs ~1GB RAM - First call: 10-30s model load latency (cached globally after)
- Sequence length: Content chunked at 512 tokens (model limit)
- Content < 100 tokens: Skipped
- Latency: Adds overhead that doesn't break even on fast models (GPT-4o Mini, Sonnet). Use for cost savings, not speed
- Thread safety: Single global model instance with lock \u2014 sequential access under concurrency
"},{"location":"LIMITATIONS/#error-handling","title":"Error Handling","text":"All compressors follow the same principle: fail gracefully, return original content unchanged.
- Invalid JSON \u2192 passthrough (no error raised)
- AST parse failure in CodeCompressor \u2192 falls back to original or LLMLingua
- Compression makes output larger \u2192 original returned
- Missing optional dependencies (tree-sitter, LLMLingua) \u2192 passthrough with warning log
- One exception: LLMLingua out-of-memory during model loading raises
RuntimeError
Errors are logged at WARNING level and never propagated to callers.
"},{"location":"LIMITATIONS/#toin-cold-start","title":"TOIN Cold Start","text":"The Tool Output Intelligence Network (TOIN) learns compression patterns from usage. For new tool types:
- No learned patterns exist \u2192 falls back to statistical heuristics
- Confidence below
toin_confidence_threshold (default 0.3) \u2192 TOIN hints ignored - Patterns build up over time as tools are used repeatedly
- Cross-session learning requires persistence (
TelemetryConfig.storage_path)
"},{"location":"LIMITATIONS/#cachealigner-behavior","title":"CacheAligner Behavior","text":" - Only processes system messages for dynamic content extraction
- Dynamic content in user/assistant/tool messages is not extracted
- May add small markers (
[Dynamic Context] separator) that slightly increase token count - Whitespace normalization may affect content with significant indentation (code blocks, ASCII art)
"},{"location":"LIMITATIONS/#provider-interactions","title":"Provider Interactions","text":" - CacheAligner is designed to maximize Anthropic/OpenAI prefix cache hit rates
- Token counting uses model-specific tokenizers (tiktoken for OpenAI, calibrated estimation for Anthropic)
- Compression works with all providers \u2014 no provider-specific limitations
- Compressed content is valid JSON \u2014 downstream tools and parsers work unchanged
"},{"location":"LIMITATIONS/#performance-characteristics","title":"Performance Characteristics","text":" - ContentRouter accounts for 91-98% of pipeline cost \u2014 it does the actual compression work
- CacheAligner and RollingWindow are sub-millisecond
- Scaling is roughly linear with input size
- Full benchmark data: LATENCY_BENCHMARKS.md
"},{"location":"LIMITATIONS/#configuration-tuning","title":"Configuration Tuning","text":"Parameter Default Effect min_items_to_analyze 5 Arrays below this pass through min_tokens_to_crush 200 Content below this passes through max_items_after_crush 15 Upper bound on retained items variance_threshold 2.0 Std devs for anomaly detection (lower = more preserved) first_fraction 0.3 Fraction of K allocated to array start last_fraction 0.15 Fraction of K allocated to array end protect_analysis_context True Protect code when user asks about it protect_recent_code 4 Messages from end to protect code skip_user_messages True Never compress user messages toin_confidence_threshold 0.3 Minimum TOIN confidence to apply hints"},{"location":"agno/","title":"Agno Integration","text":"Headroom integrates with Agno (formerly Phidata) to provide automatic context optimization for AI agents. This guide covers model wrapping, observability hooks, and multi-provider support.
"},{"location":"agno/#installation","title":"Installation","text":"pip install \"headroom-ai[agno]\"\n
This installs Headroom with Agno support. You'll also need Agno itself:
pip install agno\n
"},{"location":"agno/#quick-start","title":"Quick Start","text":"from agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Wrap your model\nmodel = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n# Create agent as usual\nagent = Agent(model=model)\n\n# Use exactly like before\nresponse = agent.run(\"What's the capital of France?\")\n\n# Check savings\nprint(f\"Tokens saved: {model.total_tokens_saved}\")\nprint(model.get_savings_summary())\n# {'total_requests': 1, 'total_tokens_saved': 245, 'average_savings_percent': 12.3}\n
"},{"location":"agno/#integration-patterns","title":"Integration Patterns","text":""},{"location":"agno/#1-basic-model-wrapping","title":"1. Basic Model Wrapping","text":"The simplest integration - wrap any Agno model with HeadroomAgnoModel:
from agno.models.openai import OpenAIChat\nfrom agno.models.anthropic import Claude\nfrom agno.models.google import Gemini\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Works with any Agno model\nopenai_model = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\nclaude_model = HeadroomAgnoModel(Claude(id=\"claude-3-5-sonnet-20241022\"))\ngemini_model = HeadroomAgnoModel(Gemini(id=\"gemini-2.0-flash\"))\n\n# Each automatically uses the correct provider for accurate token counting\n
Why this matters: Headroom automatically detects the underlying provider and applies the correct tokenizer for accurate optimization metrics.
"},{"location":"agno/#2-agent-with-observability-hooks","title":"2. Agent with Observability Hooks","text":"Use hooks for detailed tracking without modifying your model:
from agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom headroom.integrations.agno import (\n HeadroomAgnoModel,\n HeadroomPreHook,\n HeadroomPostHook,\n)\n\n# Model wrapper for optimization\nmodel = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n# Hooks for observability\npre_hook = HeadroomPreHook()\npost_hook = HeadroomPostHook(token_alert_threshold=10000)\n\nagent = Agent(\n model=model,\n pre_hooks=[pre_hook],\n post_hooks=[post_hook],\n)\n\n# Run agent\nresponse = agent.run(\"Analyze this large dataset...\")\n\n# Check metrics from model\nprint(f\"Tokens saved: {model.total_tokens_saved}\")\n\n# Check observability from hooks\nprint(f\"Post-hook summary: {post_hook.get_summary()}\")\nprint(f\"Alerts triggered: {post_hook.alerts}\")\n
Why this matters: Hooks provide observability into agent behavior and can alert when token usage exceeds thresholds.
"},{"location":"agno/#3-convenience-hook-factory","title":"3. Convenience Hook Factory","text":"Use create_headroom_hooks() to create matched hook pairs:
from headroom.integrations.agno import create_headroom_hooks\n\npre_hook, post_hook = create_headroom_hooks(\n token_alert_threshold=5000,\n log_level=\"DEBUG\",\n)\n\nagent = Agent(\n model=model,\n pre_hooks=[pre_hook],\n post_hooks=[post_hook],\n)\n
"},{"location":"agno/#4-custom-configuration","title":"4. Custom Configuration","text":"Pass a HeadroomConfig for fine-grained control:
from headroom import HeadroomConfig, HeadroomMode\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\nconfig = HeadroomConfig(\n default_mode=HeadroomMode.OPTIMIZE,\n # Add other configuration options as needed\n)\n\nmodel = HeadroomAgnoModel(\n wrapped_model=OpenAIChat(id=\"gpt-4o\"),\n config=config,\n)\n
"},{"location":"agno/#5-standalone-message-optimization","title":"5. Standalone Message Optimization","text":"Optimize messages without wrapping a model:
from headroom.integrations.agno import optimize_messages\n\nmessages = [\n {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Analyze this large JSON: \" + large_json},\n]\n\noptimized_messages, metrics = optimize_messages(messages, model=\"gpt-4o\")\n\nprint(f\"Tokens saved: {metrics['tokens_saved']}\")\nprint(f\"Transforms applied: {metrics['transforms_applied']}\")\n
"},{"location":"agno/#6-async-operations","title":"6. Async Operations","text":"Full async support for high-throughput applications:
import asyncio\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\nasync def process_async():\n model = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n # Async response\n response = await model.aresponse(messages)\n\n # Async streaming\n async for chunk in model.aresponse_stream(messages):\n print(chunk, end=\"\", flush=True)\n\n print(f\"\\nTokens saved: {model.total_tokens_saved}\")\n\nasyncio.run(process_async())\n
"},{"location":"agno/#real-world-examples","title":"Real-World Examples","text":""},{"location":"agno/#example-1-tool-heavy-agent","title":"Example 1: Tool-Heavy Agent","text":"from agno.agent import Agent\nfrom agno.models.openai import OpenAIChat\nfrom agno.tools.duckduckgo import DuckDuckGoTools\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Wrap model for optimization\nmodel = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n# Agent with search tools\nagent = Agent(\n model=model,\n tools=[DuckDuckGoTools()],\n show_tool_calls=True,\n)\n\n# Tool outputs get compressed automatically\nresponse = agent.run(\"Research the latest AI developments and summarize\")\n\n# Impact: Tool outputs (often 10K+ tokens) compressed by 70-90%\nprint(f\"Tokens saved: {model.total_tokens_saved}\")\nprint(model.get_savings_summary())\n
"},{"location":"agno/#example-2-multi-model-routing","title":"Example 2: Multi-Model Routing","text":"from agno.models.openai import OpenAIChat\nfrom agno.models.anthropic import Claude\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Different models for different tasks\nfast_model = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o-mini\"))\npowerful_model = HeadroomAgnoModel(Claude(id=\"claude-3-5-sonnet-20241022\"))\n\n# Use fast model for simple tasks\nsimple_agent = Agent(model=fast_model)\n\n# Use powerful model for complex reasoning\ncomplex_agent = Agent(model=powerful_model)\n\n# Each tracks its own metrics\nprint(f\"Fast model saved: {fast_model.total_tokens_saved}\")\nprint(f\"Powerful model saved: {powerful_model.total_tokens_saved}\")\n
"},{"location":"agno/#example-3-production-monitoring","title":"Example 3: Production Monitoring","text":"from agno.agent import Agent\nfrom headroom.integrations.agno import (\n HeadroomAgnoModel,\n create_headroom_hooks,\n)\n\nmodel = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\npre_hook, post_hook = create_headroom_hooks(\n token_alert_threshold=50000, # Alert on large requests\n log_level=\"WARNING\",\n)\n\nagent = Agent(\n model=model,\n pre_hooks=[pre_hook],\n post_hooks=[post_hook],\n)\n\n# Run multiple requests\nfor query in user_queries:\n response = agent.run(query)\n\n# Check for alerts\nif post_hook.alerts:\n print(f\"WARNING: {len(post_hook.alerts)} requests exceeded threshold\")\n for alert in post_hook.alerts:\n print(f\" - {alert}\")\n\n# Summary stats\nsummary = post_hook.get_summary()\nprint(f\"Total requests: {summary['total_requests']}\")\nprint(f\"Average tokens: {summary['average_tokens']}\")\n
"},{"location":"agno/#example-4-reset-for-new-sessions","title":"Example 4: Reset for New Sessions","text":"model = HeadroomAgnoModel(OpenAIChat(id=\"gpt-4o\"))\n\n# Session 1\nagent.run(\"First conversation...\")\nprint(f\"Session 1 savings: {model.get_savings_summary()}\")\n\n# Reset for new session\nmodel.reset()\n\n# Session 2 - metrics start fresh\nagent.run(\"Second conversation...\")\nprint(f\"Session 2 savings: {model.get_savings_summary()}\")\n
"},{"location":"agno/#supported-providers","title":"Supported Providers","text":"HeadroomAgnoModel automatically detects the provider from the wrapped model:
Provider Agno Models Auto-Detected OpenAI OpenAIChat, OpenAILike Yes Anthropic Claude, AwsBedrock Yes Google Gemini, VertexAI Yes Cohere Cohere, CohereChat Yes Groq Groq Yes (OpenAI-compatible) Mistral Mistral Yes (OpenAI-compatible) Together Together Yes (OpenAI-compatible) Ollama Ollama Yes (OpenAI-compatible) To disable auto-detection:
model = HeadroomAgnoModel(\n wrapped_model=some_model,\n auto_detect_provider=False, # Falls back to OpenAI tokenizer\n)\n
"},{"location":"agno/#feature-coverage","title":"Feature Coverage","text":""},{"location":"agno/#whats-optimized","title":"What's Optimized","text":"HeadroomAgnoModel optimizes messages at the LLM call boundary. This covers:
Feature Optimized Notes User/Assistant Messages \u2705 Yes Full message history compressed Tool Calls \u2705 Yes Tool call arguments optimized Tool Results \u2705 Yes JSON responses compressed 70-90% via SmartCrusher System Prompts \u2705 Yes Included in message optimization Streaming Responses \u2705 Yes Both sync and async Multi-turn Conversations \u2705 Yes Full history available for optimization"},{"location":"agno/#known-limitations","title":"Known Limitations","text":"The integration operates at the model layer, not the agent layer. Some Agno features operate outside this boundary:
Agno Feature Status Explanation Agent Memory \u26a0\ufe0f Partial Memory content is optimized when it enters messages, but the persistent memory store itself is not compressed. If you're storing large amounts of data in agent memory, consider summarizing before storage. Knowledge Bases \u26a0\ufe0f Partial KB retrieval happens before messages reach the model. Retrieved context is optimized as part of the message, but we can't influence KB retrieval itself. Agent Teams \u274c Not supported Each agent's model is wrapped independently. No cross-agent optimization or team-level coordination. Tool Definitions \u26a0\ufe0f Not deduplicated Tool schemas are sent with every request. Future versions may deduplicate repeated tool definitions. Structured Outputs \u2705 Supported response_model works normally; optimization doesn't affect output parsing. Reasoning Models \u2705 Supported Extended thinking works; we don't compress reasoning traces."},{"location":"agno/#best-practices-for-maximum-savings","title":"Best Practices for Maximum Savings","text":" - Tool-heavy agents see the biggest wins \u2014 Tool results (JSON, logs, search results) compress 70-90%
- Long conversations benefit from RollingWindow \u2014 Configure context limits to avoid hitting provider maximums
- Wrap at the model level, not agent level \u2014 This ensures all LLM calls go through optimization
- Use hooks for observability \u2014 Track token usage patterns to identify optimization opportunities
"},{"location":"agno/#future-improvements","title":"Future Improvements","text":"We're tracking these potential enhancements:
- Memory optimization hooks \u2014 Compress data before it enters agent memory
- Knowledge base integration \u2014 Optimize retrieved context at the KB layer
- Tool schema deduplication \u2014 Cache and reference repeated tool definitions
- Team-level optimization \u2014 Shared context compression across agent teams
Contributions welcome! See CONTRIBUTING.md.
"},{"location":"agno/#configuration-reference","title":"Configuration Reference","text":""},{"location":"agno/#headroomagnomodel","title":"HeadroomAgnoModel","text":"Parameter Type Default Description wrapped_model Any Required The Agno model to wrap config HeadroomConfig None Custom configuration auto_detect_provider bool True Auto-detect provider for token counting Properties: - wrapped_model - Access the underlying Agno model - total_tokens_saved - Running total of tokens saved - metrics_history - List of last 100 OptimizationMetrics
Methods: - response(messages, **kwargs) - Sync response with optimization - response_stream(messages, **kwargs) - Sync streaming response - aresponse(messages, **kwargs) - Async response - aresponse_stream(messages, **kwargs) - Async streaming - get_savings_summary() - Returns dict with stats - reset() - Clear all metrics
"},{"location":"agno/#headroomprehook","title":"HeadroomPreHook","text":"Parameter Type Default Description config HeadroomConfig None Configuration (for future use) model str \"gpt-4o\" Model name for estimation"},{"location":"agno/#headroomposthook","title":"HeadroomPostHook","text":"Parameter Type Default Description log_level str \"INFO\" Logging level token_alert_threshold int None Alert if tokens exceed this Properties: - total_requests - Number of requests tracked - alerts - List of alert messages
Methods: - get_summary() - Returns dict with request stats - reset() - Clear history and alerts
"},{"location":"agno/#create_headroom_hooks","title":"create_headroom_hooks()","text":"Parameter Type Default Description config HeadroomConfig None Config for pre-hook model str \"gpt-4o\" Model for pre-hook log_level str \"INFO\" Log level for post-hook token_alert_threshold int None Alert threshold for post-hook Returns: tuple[HeadroomPreHook, HeadroomPostHook]
"},{"location":"agno/#import-reference","title":"Import Reference","text":"# Main integration\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\n# Hooks\nfrom headroom.integrations.agno import HeadroomPreHook\nfrom headroom.integrations.agno import HeadroomPostHook\nfrom headroom.integrations.agno import create_headroom_hooks\n\n# Utilities\nfrom headroom.integrations.agno import optimize_messages\nfrom headroom.integrations.agno import agno_available\nfrom headroom.integrations.agno import get_headroom_provider\nfrom headroom.integrations.agno import get_model_name_from_agno\n\n# Or import everything from parent\nfrom headroom.integrations import (\n HeadroomAgnoModel,\n HeadroomPreHook,\n HeadroomPostHook,\n create_headroom_hooks,\n)\n
"},{"location":"agno/#troubleshooting","title":"Troubleshooting","text":""},{"location":"agno/#check-if-agno-is-available","title":"Check if Agno is Available","text":"from headroom.integrations.agno import agno_available\n\nif agno_available():\n from headroom.integrations.agno import HeadroomAgnoModel\nelse:\n print(\"Install agno: pip install agno\")\n
"},{"location":"agno/#provider-detection-issues","title":"Provider Detection Issues","text":"If auto-detection fails, check the detected provider:
from headroom.integrations.agno import get_headroom_provider, get_model_name_from_agno\n\nmodel = OpenAIChat(id=\"gpt-4o\")\nprovider = get_headroom_provider(model)\nmodel_name = get_model_name_from_agno(model)\n\nprint(f\"Detected provider: {type(provider).__name__}\")\nprint(f\"Model name: {model_name}\")\n
"},{"location":"agno/#metrics-not-updating","title":"Metrics Not Updating","text":"Ensure you're checking the correct object:
# Model metrics (optimization)\nprint(model.total_tokens_saved) # Actual savings\n\n# Hook metrics (observability)\nprint(post_hook.get_summary()) # Request tracking\n
Note: Hooks track request counts, not token savings. Use the model wrapper for optimization metrics.
"},{"location":"api/","title":"API Reference","text":""},{"location":"api/#headroomclient","title":"HeadroomClient","text":"The main entry point for Headroom SDK.
from headroom import HeadroomClient\nfrom openai import OpenAI\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n default_mode=\"optimize\",\n)\n
"},{"location":"api/#constructor-parameters","title":"Constructor Parameters","text":"Parameter Type Default Description original_client OpenAI \\| Anthropic Required The underlying LLM client provider Provider Auto-detected Token counting provider default_mode str \"audit\" Default mode: \"audit\", \"optimize\", \"off\" store_url str None Storage URL for metrics smart_crusher_config SmartCrusherConfig Default Compression settings cache_aligner_config CacheAlignerConfig Default Cache alignment settings rolling_window_config RollingWindowConfig Default Context window settings"},{"location":"api/#methods","title":"Methods","text":""},{"location":"api/#chatcompletionscreatekwargs","title":"chat.completions.create(**kwargs)","text":"Create a chat completion with optional optimization.
response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[...],\n headroom_mode=\"optimize\", # Override default mode\n)\n
Additional Parameters:
Parameter Type Description headroom_mode str Override mode for this request headroom_query str Query for relevance scoring"},{"location":"api/#chatcompletionssimulatekwargs","title":"chat.completions.simulate(**kwargs)","text":"Preview optimization without making an API call.
plan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=[...],\n)\n\nprint(f\"Tokens before: {plan.tokens_before}\")\nprint(f\"Tokens after: {plan.tokens_after}\")\nprint(f\"Savings: {plan.savings_percent:.1f}%\")\n
Returns: SimulationResult
"},{"location":"api/#configuration-classes","title":"Configuration Classes","text":""},{"location":"api/#smartcrusherconfig","title":"SmartCrusherConfig","text":"from headroom import SmartCrusherConfig\n\nconfig = SmartCrusherConfig(\n min_tokens_to_crush=200,\n max_items_after_crush=50,\n keep_first=3,\n keep_last=2,\n relevance_threshold=0.3,\n anomaly_std_threshold=2.0,\n preserve_errors=True,\n)\n
"},{"location":"api/#cachealignerconfig","title":"CacheAlignerConfig","text":"from headroom import CacheAlignerConfig\n\nconfig = CacheAlignerConfig(\n extract_dates=True,\n normalize_whitespace=True,\n stable_prefix_min_tokens=100,\n)\n
"},{"location":"api/#rollingwindowconfig","title":"RollingWindowConfig","text":"from headroom import RollingWindowConfig\n\nconfig = RollingWindowConfig(\n max_tokens=100000,\n preserve_system=True,\n preserve_recent_turns=5,\n drop_oldest_first=True,\n)\n
"},{"location":"api/#intelligentcontextconfig","title":"IntelligentContextConfig","text":"from headroom.config import IntelligentContextConfig, ScoringWeights\n\nweights = ScoringWeights(\n recency=0.20,\n semantic_similarity=0.20,\n toin_importance=0.25,\n error_indicator=0.15,\n forward_reference=0.15,\n token_density=0.05,\n)\n\nconfig = IntelligentContextConfig(\n enabled=True,\n keep_system=True,\n keep_last_turns=2,\n output_buffer_tokens=4000,\n use_importance_scoring=True,\n scoring_weights=weights,\n toin_integration=True,\n recency_decay_rate=0.1,\n compress_threshold=0.1,\n)\n
"},{"location":"api/#scoringweights","title":"ScoringWeights","text":"from headroom.config import ScoringWeights\n\nweights = ScoringWeights(\n recency=0.20, # Exponential decay from end\n semantic_similarity=0.20, # Embedding similarity to recent context\n toin_importance=0.25, # TOIN retrieval_rate\n error_indicator=0.15, # TOIN field_semantics error detection\n forward_reference=0.15, # Messages referenced by later messages\n token_density=0.05, # Unique/total token ratio\n)\n\n# Weights are auto-normalized to sum to 1.0\nnormalized = weights.normalized()\n
"},{"location":"api/#relevancescorerconfig","title":"RelevanceScorerConfig","text":"from headroom import RelevanceScorerConfig\n\nconfig = RelevanceScorerConfig(\n scorer_type=\"bm25\", # \"bm25\", \"embedding\", or \"hybrid\"\n embedding_model=None, # Model name for embedding scorer\n hybrid_alpha=0.5, # Weight for hybrid scoring\n)\n
"},{"location":"api/#data-models","title":"Data Models","text":""},{"location":"api/#simulationresult","title":"SimulationResult","text":"Returned by simulate().
@dataclass\nclass SimulationResult:\n tokens_before: int\n tokens_after: int\n tokens_saved: int\n savings_percent: float\n transforms_applied: list[str]\n waste_signals: WasteSignals\n
"},{"location":"api/#requestmetrics","title":"RequestMetrics","text":"Metrics for a single request.
@dataclass\nclass RequestMetrics:\n request_id: str\n timestamp: datetime\n model: str\n tokens_input_before: int\n tokens_input_after: int\n tokens_output: int\n cost_before: float\n cost_after: float\n transforms_applied: list[str]\n
"},{"location":"api/#wastesignals","title":"WasteSignals","text":"Detected waste in the request.
@dataclass\nclass WasteSignals:\n json_bloat_tokens: int\n html_noise_tokens: int\n whitespace_tokens: int\n dynamic_date_tokens: int\n repetition_tokens: int\n
"},{"location":"api/#providers","title":"Providers","text":""},{"location":"api/#openaiprovider","title":"OpenAIProvider","text":"from headroom import OpenAIProvider\n\nprovider = OpenAIProvider()\n\n# Get token counter\ncounter = provider.get_token_counter(\"gpt-4o\")\ntokens = counter.count_text(\"Hello, world!\")\n\n# Get context limit\nlimit = provider.get_context_limit(\"gpt-4o\") # 128000\n\n# Estimate cost\ncost = provider.estimate_cost(\n input_tokens=1000,\n output_tokens=500,\n model=\"gpt-4o\",\n)\n
"},{"location":"api/#anthropicprovider","title":"AnthropicProvider","text":"from headroom import AnthropicProvider\nfrom anthropic import Anthropic\n\nprovider = AnthropicProvider(client=Anthropic())\n\ncounter = provider.get_token_counter(\"claude-3-5-sonnet-latest\")\ntokens = counter.count_messages(messages) # Accurate count via API\n
"},{"location":"api/#relevance-scoring","title":"Relevance Scoring","text":""},{"location":"api/#bm25scorer","title":"BM25Scorer","text":"Fast keyword-based scoring (zero dependencies).
from headroom import BM25Scorer\n\nscorer = BM25Scorer()\nscores = scorer.score_items(\n items=[\"item 1\", \"item 2\", ...],\n query=\"search query\",\n)\n
"},{"location":"api/#embeddingscorer","title":"EmbeddingScorer","text":"Semantic similarity scoring (requires sentence-transformers).
from headroom import EmbeddingScorer, embedding_available\n\nif embedding_available():\n scorer = EmbeddingScorer(model=\"all-MiniLM-L6-v2\")\n scores = scorer.score_items(items, query)\n
"},{"location":"api/#hybridscorer","title":"HybridScorer","text":"Combines BM25 and embeddings.
from headroom import HybridScorer\n\nscorer = HybridScorer(alpha=0.5) # 50% BM25, 50% embedding\nscores = scorer.score_items(items, query)\n
"},{"location":"api/#create_scorer","title":"create_scorer()","text":"Factory function to create scorers.
from headroom import create_scorer\n\n# Auto-select best available scorer\nscorer = create_scorer()\n\n# Explicitly choose type\nscorer = create_scorer(scorer_type=\"hybrid\", alpha=0.7)\n
"},{"location":"api/#transforms-direct-use","title":"Transforms (Direct Use)","text":""},{"location":"api/#smartcrusher","title":"SmartCrusher","text":"from headroom import SmartCrusher\n\ncrusher = SmartCrusher()\nresult = crusher.crush(\n data={\"results\": [...]},\n query=\"user query\",\n)\n
"},{"location":"api/#cachealigner","title":"CacheAligner","text":"from headroom import CacheAligner\n\naligner = CacheAligner()\nresult = aligner.align(messages)\n
"},{"location":"api/#rollingwindow","title":"RollingWindow","text":"from headroom import RollingWindow\n\nwindow = RollingWindow(config)\nresult = window.apply(messages, max_tokens=100000)\n
"},{"location":"api/#intelligentcontextmanager","title":"IntelligentContextManager","text":"from headroom.transforms import IntelligentContextManager\nfrom headroom.config import IntelligentContextConfig\nfrom headroom.telemetry import get_toin\n\n# With TOIN integration for learned patterns\ntoin = get_toin()\nconfig = IntelligentContextConfig(\n keep_system=True,\n keep_last_turns=2,\n use_importance_scoring=True,\n)\n\nmanager = IntelligentContextManager(config, toin=toin)\nresult = manager.apply(messages, tokenizer, model_limit=128000)\n\n# Access scoring details\nprint(result.transforms_applied) # [\"intelligent_cap:3\"]\nprint(result.tokens_before, result.tokens_after)\n
"},{"location":"api/#messagescorer","title":"MessageScorer","text":"from headroom.transforms import MessageScorer, MessageScore\nfrom headroom.config import ScoringWeights\n\nscorer = MessageScorer(\n weights=ScoringWeights(),\n toin=None, # Optional TOIN for learned patterns\n embedding_provider=None, # Optional for semantic similarity\n recency_decay_rate=0.1,\n)\n\n# Score messages\nscores: list[MessageScore] = scorer.score_messages(\n messages=messages,\n protected_indices={0}, # System message\n tool_unit_indices={2, 3}, # Tool call + response\n)\n\nfor score in scores:\n print(f\"Message {score.message_index}: {score.total_score:.2f}\")\n print(f\" Recency: {score.recency_score:.2f}\")\n print(f\" TOIN: {score.toin_score:.2f}\")\n print(f\" Protected: {score.is_protected}\")\n
"},{"location":"api/#transformpipeline","title":"TransformPipeline","text":"from headroom import TransformPipeline\n\npipeline = TransformPipeline([\n SmartCrusher(),\n CacheAligner(),\n RollingWindow(),\n])\n\nresult = pipeline.transform(messages)\n
"},{"location":"api/#utilities","title":"Utilities","text":""},{"location":"api/#tokenizer","title":"Tokenizer","text":"from headroom import Tokenizer, count_tokens_text, count_tokens_messages\n\n# Quick counting\ntokens = count_tokens_text(\"Hello, world!\", model=\"gpt-4o\")\n\n# With tokenizer instance\ntokenizer = Tokenizer(model=\"gpt-4o\")\ntokens = tokenizer.count_text(\"Hello\")\ntokens = tokenizer.count_messages(messages)\n
"},{"location":"api/#generate_report","title":"generate_report()","text":"Generate HTML/Markdown reports from stored metrics.
from headroom import generate_report\n\nreport = generate_report(\n store_url=\"sqlite:///headroom.db\",\n format=\"html\",\n period=\"day\",\n)\n
"},{"location":"api/#typescript-sdk","title":"TypeScript SDK","text":"For the TypeScript SDK API reference, see TypeScript SDK.
The TypeScript SDK provides compress(), HeadroomClient, and framework adapters for Vercel AI SDK, OpenAI, and Anthropic.
"},{"location":"benchmarks/","title":"Benchmarks","text":"Headroom's core promise: compress context without losing accuracy. This page shows accuracy benchmarks, compression performance, and real-world production telemetry from 250+ active proxy instances.
Key Results
98.2% recall on article extraction with 94.9% compression. 52ms median overhead in production. 1.4 billion tokens saved across 249 instances.
"},{"location":"benchmarks/#compression-performance","title":"Compression Performance","text":"Tested on Apple M-series (CPU), headroom v0.5.18. Each test runs compress() on realistic tool outputs.
Content Type Original Compressed Saved Ratio Latency JSON array (100 items) 3,163 297 2,866 90.6% 1ms JSON array (500 items) 9,526 1,614 7,912 83.1% 2ms Shell output (200 lines) 3,238 469 2,769 85.5% 1ms Build log (200 lines) 2,412 148 2,264 93.9% 1ms grep results (150 hits) 2,624 2,624 0 0.0% <1ms Python source (~480 lines) 2,958 2,958 0 0.0% <1ms Total 23,921 8,110 15,811 66.1% 5ms Notes:
- grep results and Python source show 0% compression \u2014 these are already compact structured formats. SmartCrusher only compresses JSON arrays; code passes through to preserve correctness.
- Latency is for the
compress() SDK call, not the full proxy round-trip.
"},{"location":"benchmarks/#production-telemetry","title":"Production Telemetry","text":"Real-world data from 50,000+ proxy sessions across 250+ unique instances (March 30 \u2013 April 2, 2026). Collected via anonymous telemetry beacon (opt-out: HEADROOM_TELEMETRY=off).
"},{"location":"benchmarks/#proxy-overhead","title":"Proxy Overhead","text":"Percentile Latency Median (P50) 52ms P90 309ms P99 4,172ms Mean 161ms The median 52ms overhead is negligible compared to LLM inference time (typically 2-10 seconds).
"},{"location":"benchmarks/#compression-rate","title":"Compression Rate","text":"Percentile Compression P25 4.8% Median 4.8% P75 6.9% Mean 11.3% Median compression is modest because many requests are short conversational turns. Heavy tool-use sessions (file reads, shell output) see 40-80% compression.
"},{"location":"benchmarks/#pipeline-step-timing-production-median","title":"Pipeline Step Timing (Production Median)","text":"Step Median P90 Description pipeline_total 16.9ms 289ms Full compression pipeline content_router 11.7ms 259ms Content detection + routing compressor:smart_crusher 50.1ms 50ms JSON array compression compressor:text 32.0ms 576ms Text compression (Kompress ONNX) compressor:mixed 316ms 428ms Mixed content compression compressor:code_aware 815ms 886ms Tree-sitter AST compression _initial_token_count 2.9ms 16ms Token counting (tiktoken) _deep_copy 0.1ms 0.3ms Message copy overhead"},{"location":"benchmarks/#fleet-summary","title":"Fleet Summary","text":"Metric Value Clean instances 249 Total tokens saved 1.4 billion Total $ saved ~$4,000 OS distribution Linux 57%, macOS 38%, Windows 5% Top version 0.5.17 (77%) Models used Claude Opus 4.6, Sonnet 4.6, Haiku 4.5"},{"location":"benchmarks/#accuracy-benchmarks","title":"Accuracy Benchmarks","text":""},{"location":"benchmarks/#html-extraction","title":"HTML Extraction","text":"Dataset: Scrapinghub Article Extraction Benchmark Samples: 181 HTML pages with ground truth article bodies Baseline: trafilatura (0.958 F1)
Metric Value Description F1 Score 0.919 Token-level overlap with ground truth Precision 0.879 Proportion of extracted content that's relevant Recall 0.982 Proportion of ground truth content captured Compression 94.9% Average size reduction For LLM applications, recall is critical \u2014 98.2% means nearly all article content is preserved. The slight precision drop (some extra content) doesn't hurt LLM accuracy.
# Run it yourself\npip install \"headroom-ai[html]\" datasets\npytest tests/test_evals/test_html_oss_benchmarks.py::TestExtractionBenchmark -v -s\n
"},{"location":"benchmarks/#json-compression-smartcrusher","title":"JSON Compression (SmartCrusher)","text":"Test: 100 production log entries with critical error at position 67 Task: Find the error, error code, resolution, and affected count
Metric Baseline Headroom Input tokens 10,144 1,260 Correct answers 4/4 4/4 Compression \u2014 87.6% SmartCrusher preserves first N items (schema), last N items (recency), all anomalies (errors, warnings), and statistical distribution.
"},{"location":"benchmarks/#qa-accuracy-preservation","title":"QA Accuracy Preservation","text":"Metric Original HTML Extracted Delta F1 Score 0.85 0.87 +0.02 Exact Match 60% 62% +2% Extraction Can Improve Accuracy
Removing HTML noise sometimes helps LLMs focus on relevant content.
"},{"location":"benchmarks/#limitations","title":"Limitations","text":""},{"location":"benchmarks/#what-headroom-does-not-compress","title":"What Headroom Does NOT Compress","text":" - Short messages (< 300 tokens) \u2014 overhead exceeds savings
- Source code \u2014 passes through unchanged to preserve correctness (unless tree-sitter AST compression is enabled)
- grep/search results \u2014 compact structured format, already minimal
- Images \u2014 counted at fixed token cost (~1,600 tokens), not compressed as text
- System prompts \u2014 preserved for prefix cache compatibility
"},{"location":"benchmarks/#known-overhead-sources","title":"Known Overhead Sources","text":" - Token counting (P90: 16ms) \u2014 runs tiktoken twice (before + after compression)
- Tree-sitter AST parsing (P90: 886ms) \u2014 expensive for large code files
- Kompress ONNX (P90: 576ms) \u2014 ML inference on CPU for text compression
- Content detection (Magika) \u2014 ML classification of content type
"},{"location":"benchmarks/#when-headroom-adds-the-most-value","title":"When Headroom Adds the Most Value","text":" - Long agent sessions with accumulated tool outputs (40-80% compression)
- JSON-heavy workflows (API responses, database queries) \u2014 83-94% compression
- Build/test output \u2014 85-94% compression
- Multi-tool agents \u2014 60-76% compression across tool results
"},{"location":"benchmarks/#when-headroom-adds-little-value","title":"When Headroom Adds Little Value","text":" - Short conversational exchanges \u2014 median 4.8% compression
- Code-only sessions (reading/writing files) \u2014 code passes through
- Single-turn requests \u2014 no accumulated context to compress
"},{"location":"benchmarks/#methodology","title":"Methodology","text":""},{"location":"benchmarks/#token-level-f1","title":"Token-Level F1","text":"Precision = |predicted \u2229 ground_truth| / |predicted|\nRecall = |predicted \u2229 ground_truth| / |ground_truth|\nF1 = 2 * (Precision * Recall) / (Precision + Recall)\n
"},{"location":"benchmarks/#compression-ratio","title":"Compression Ratio","text":"Compression = 1 - (compressed_size / original_size)\n
A 94.9% compression means the output is 5.1% of the original size.
"},{"location":"benchmarks/#production-telemetry_1","title":"Production Telemetry","text":" - Collected via anonymous beacon (no prompts, no content, no PII)
- Image-inflated instances excluded (base64 counted as text tokens \u2014 fixed in v0.5.18)
- Multi-worker beacon spam excluded (per-instance MAX, not SUM)
- Opt-out:
HEADROOM_TELEMETRY=off
"},{"location":"benchmarks/#reproducing-results","title":"Reproducing Results","text":"# Clone the repo\ngit clone https://github.com/chopratejas/headroom.git\ncd headroom\n\n# Install with eval dependencies\npip install -e \".[evals,html]\"\n\n# Run all benchmarks\npytest tests/test_evals/ -v -s\n\n# Run compression benchmark\npython -c \"from headroom import compress; print(compress([{'role':'user','content':'test'}]))\"\n\n# Run local proxy mode benchmark (no API calls)\npython benchmarks/proxy_mode_benchmark.py --turns 12 --show-real-harness\n\n# Replay local Claude Code transcripts (no API calls)\npython benchmarks/claude_session_mode_benchmark.py --workers 1\n\n# Compare two refs on the same local Claude transcript corpus\npython benchmarks/claude_session_branch_compare.py --left-ref upstream/main --right-ref HEAD --recent-turns-per-session 200 --workers 1\n
This benchmark compares token vs cache proxy modes on the same synthetic conversation:
token should show higher compression. cache should preserve prior-turn stability and can win in long sessions with strong prefix-cache reuse.
--show-real-harness prints optional steps for running the same comparison with Claude Code, but does not call APIs by default.
claude_session_branch_compare.py runs the real local session replay benchmark twice, once per git ref, in isolated worktrees. It writes:
- per-ref replay outputs under
benchmark_results/branch_compare/<label>/ - a combined comparison report under
benchmark_results/branch_compare/
Use it when you want a clean PR-vs-main comparison on the same transcript slice.
For a deterministic cache-busting proof case, run:
python benchmarks/synthetic_token_cache_bust_report.py\n
That synthetic replay forces token mode to retroactively rewrite a prior tool result on the second turn while cache mode remains stable. Use it to verify the simulator can distinguish:
token: history rewrite + cache bust cache: no rewrite + no bust
For a reproducible local report bundle that combines:
- full real-session replay summaries
- local-only processed real input/output excerpts
- synthetic token-bust proof
- synthetic long-form stress tests
run:
python benchmarks/cache_validation_bundle.py --workers 1 --output-dir benchmark_results/cache_validation_bundle_full\n
Notes:
- By default the bundle is redaction-safe for sharing:
- real processed reports redact transcript-derived content excerpts
- manifest paths are redacted
- To include local processed content excerpts for private review on your own machine:
python benchmarks/cache_validation_bundle.py --workers 1 --include-content\n
- The bundle writes:
index.html / index.md: top-level summary and links bundle_manifest.json: runtime metadata + corpus fingerprint real/: full real-session replay reports real_processed/: processed before/after excerpts from real transcripts synthetic_token_bust/: minimal explicit cache-bust proof synthetic_long_suite/: long deterministic rewrite/TTL scenarios - Checkpoints are scoped under the bundle output directory and fingerprinted by the selected corpus so stale runs do not contaminate new results.
The Claude session benchmark replays local transcript data from ~/.claude/projects through baseline, token, and cache modes. It estimates raw tokens, cache read/write tokens, paid input/output costs, and prompt-window winners under two assumptions:
- cached tokens count against the model window
- cache reads do not count against the model window
Notes:
- It writes local output to
benchmark_results/, which is gitignored. - It is intentionally conservative on memory. Run with
--workers 1 for the most stable full-corpus replay. Higher worker counts increase memory use. - It uses transcript-visible messages only. Hidden Claude Code system/tool schemas are not available in the local
.jsonl files, so the numbers are comparative estimates rather than exact provider billing replicas.
"},{"location":"ccr/","title":"CCR: Compress-Cache-Retrieve","text":"Headroom's CCR architecture makes compression reversible. When content is compressed, the original data is cached. If the LLM needs more data, it can retrieve it instantly.
"},{"location":"ccr/#the-problem-with-traditional-compression","title":"The Problem with Traditional Compression","text":"Traditional compression is lossy \u2014 if you guess wrong about what's important, data is lost forever. This creates a difficult tradeoff:
- Aggressive compression: Risk losing data the LLM needs
- Conservative compression: Miss out on token savings
CCR eliminates this tradeoff.
"},{"location":"ccr/#ccr-enabled-components","title":"CCR-Enabled Components","text":"Component What it compresses CCR integration SmartCrusher JSON arrays (tool outputs) Stores original array, marker includes hash ContentRouter Code, logs, search results, text Stores original content by strategy IntelligentContextManager Messages (conversation turns) Stores dropped messages, marker includes hash"},{"location":"ccr/#how-ccr-works","title":"How CCR Works","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 TOOL OUTPUT (1000 items) \u2502\n\u2502 \u2514\u2500 SmartCrusher compresses to 20 items \u2502\n\u2502 \u2514\u2500 Original cached with hash=abc123 \u2502\n\u2502 \u2514\u2500 Retrieval tool injected into context \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LLM PROCESSING \u2502\n\u2502 Option A: LLM solves task with 20 items \u2192 Done (90% savings) \u2502\n\u2502 Option B: LLM calls headroom_retrieve(hash=abc123) \u2502\n\u2502 \u2192 Response Handler executes retrieval automatically \u2502\n\u2502 \u2192 LLM receives full data, responds accurately \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"ccr/#phase-1-compression-store","title":"Phase 1: Compression Store","text":"When SmartCrusher compresses tool output: 1. Original content is stored in an LRU cache 2. A hash key is generated for retrieval 3. A marker is added to the compressed output: [1000 items compressed to 20. Retrieve more: hash=abc123]
"},{"location":"ccr/#phase-2-tool-injection","title":"Phase 2: Tool Injection","text":"Headroom injects a headroom_retrieve tool into the LLM's available tools:
{\n \"name\": \"headroom_retrieve\",\n \"description\": \"Retrieve original uncompressed data from Headroom cache\",\n \"parameters\": {\n \"hash\": \"The hash key from the compression marker\",\n \"query\": \"Optional: search within the cached data\"\n }\n}\n
"},{"location":"ccr/#phase-3-response-handler","title":"Phase 3: Response Handler","text":"When the LLM calls headroom_retrieve: 1. Response Handler intercepts the tool call 2. Retrieves data from the local cache (~1ms) 3. Adds the result to the conversation 4. Continues the API call automatically
The client never sees CCR tool calls \u2014 they're handled transparently.
"},{"location":"ccr/#phase-4-context-tracker","title":"Phase 4: Context Tracker","text":"Across multiple turns, the Context Tracker: 1. Remembers what was compressed in earlier turns 2. Analyzes new queries for relevance to compressed content 3. Proactively expands relevant data before the LLM asks
Example:
Turn 1: User searches for files\n \u2192 Tool returns 500 files\n \u2192 SmartCrusher compresses to 15, caches original (hash=abc123)\n \u2192 LLM sees 15 files, answers question\n\nTurn 5: User asks \"What about the auth middleware?\"\n \u2192 Context Tracker detects \"auth\" might be in abc123\n \u2192 Proactively expands compressed content\n \u2192 LLM sees full file list, finds auth_middleware.py\n
"},{"location":"ccr/#message-level-ccr-intelligentcontext","title":"Message-Level CCR (IntelligentContext)","text":"IntelligentContextManager is a message-level compressor. When it drops low-importance messages to fit the context budget, those messages are stored in CCR:
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LONG CONVERSATION (100 messages, 50K tokens) \u2502\n\u2502 \u2514\u2500 IntelligentContext scores messages by importance \u2502\n\u2502 \u2514\u2500 Drops 60 low-scoring messages \u2502\n\u2502 \u2514\u2500 Dropped messages cached with hash=def456 \u2502\n\u2502 \u2514\u2500 Marker inserted: \"60 messages dropped, retrieve: def456\" \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 LLM PROCESSING \u2502\n\u2502 Option A: LLM solves task with remaining messages \u2192 Done \u2502\n\u2502 Option B: LLM needs earlier context \u2502\n\u2502 \u2192 Calls headroom_retrieve(hash=def456) \u2502\n\u2502 \u2192 Full conversation restored \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
The marker includes the CCR reference:
[Earlier context compressed: 60 message(s) dropped by importance scoring.\nFull content available via ccr_retrieve tool with reference 'def456'.]\n
TOIN integration: When users retrieve dropped messages, TOIN learns to score those message patterns higher next time, improving future drop decisions across all users.
"},{"location":"ccr/#features","title":"Features","text":"Feature Description Automatic Response Handling When LLM calls headroom_retrieve, the proxy handles it automatically Multi-Turn Context Tracking Tracks compressed content across turns, proactively expands when relevant BM25 Search LLM can search within compressed data: headroom_retrieve(hash, query=\"errors\") Feedback Learning Learns from retrieval patterns to improve future compression"},{"location":"ccr/#configuration","title":"Configuration","text":"# Proxy with CCR enabled (default)\nheadroom proxy --port 8787\n\n# Disable CCR response handling\nheadroom proxy --no-ccr-responses\n\n# Disable proactive expansion\nheadroom proxy --no-ccr-expansion\n
"},{"location":"ccr/#why-this-matters","title":"Why This Matters","text":"Approach Risk Savings No compression None 0% Traditional compression Data loss 70-90% CCR compression None (reversible) 70-90% CCR gives you the savings of aggressive compression with zero risk \u2014 the LLM can always retrieve the original data if needed.
"},{"location":"ccr/#demo","title":"Demo","text":"Run the CCR demonstration to see it in action:
python examples/ccr_demo.py\n
Output:
1. COMPRESSION STORE\n Original: 100 items (7,059 chars)\n Compressed: 8 items (633 chars)\n Reduction: 91.0%\n\n3. RESPONSE HANDLER\n Detected CCR tool call: True\n Retrieved 100 items automatically\n\n4. CONTEXT TRACKER\n Turn 5: User asks \"show authentication middleware\"\n Tracker found 1 relevant context\n \u2192 relevance=0.73\n Proactively expanded: 100 items\n
"},{"location":"ccr/#architecture","title":"Architecture","text":"For implementation details, see ARCHITECTURE.md.
"},{"location":"compression/","title":"Universal Compression","text":"Headroom's Universal Compression module provides intelligent, automatic compression with ML-based content detection and structure preservation.
"},{"location":"compression/#overview","title":"Overview","text":"Universal Compression combines several techniques:
- ML-based Detection - Automatically detects content type (JSON, code, logs, text) using Magika
- Structure Preservation - Keeps keys, signatures, and templates intact via structure masks
- Intelligent Compression - Compresses content while preserving meaning with LLMLingua
- Reversible via CCR - Stores originals for retrieval when LLM needs full context
"},{"location":"compression/#quick-start","title":"Quick Start","text":""},{"location":"compression/#one-liner","title":"One-Liner","text":"from headroom.compression import compress\n\nresult = compress(content)\nprint(result.compressed)\nprint(f\"Saved {result.savings_percentage:.0f}% tokens\")\n
"},{"location":"compression/#with-configuration","title":"With Configuration","text":"from headroom.compression import UniversalCompressor, UniversalCompressorConfig\n\nconfig = UniversalCompressorConfig(\n compression_ratio_target=0.5, # Keep 50% of content\n use_entropy_preservation=True, # Preserve UUIDs, hashes\n)\n\ncompressor = UniversalCompressor(config=config)\nresult = compressor.compress(content)\n
"},{"location":"compression/#how-it-works","title":"How It Works","text":""},{"location":"compression/#detection-flow","title":"Detection Flow","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Content \u2502\u2500\u2500\u2500>\u2502 Detect \u2502\u2500\u2500\u2500>\u2502 Extract \u2502\u2500\u2500\u2500>\u2502 Compress \u2502\n\u2502 Input \u2502 \u2502 Type \u2502 \u2502 Structure \u2502 \u2502 Content \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502 \u2502\n \u25bc \u25bc \u25bc\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Magika \u2502 \u2502 Handler \u2502 \u2502 LLMLingua \u2502\n \u2502 (ML) \u2502 \u2502 (JSON, \u2502 \u2502 (optional) \u2502\n \u2502 \u2502 \u2502 Code...) \u2502 \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"compression/#structure-masks","title":"Structure Masks","text":"Structure masks identify what to preserve:
Content Type What's Preserved What's Compressed JSON Keys, brackets, booleans, nulls, short values, UUIDs Long string values, whitespace Code Imports, function signatures, class definitions, types Function bodies, comments Logs Timestamps, log levels, error messages Repeated patterns, verbose details Text High-entropy tokens (IDs, hashes) Low-information content"},{"location":"compression/#configuration","title":"Configuration","text":""},{"location":"compression/#universalcompressorconfig","title":"UniversalCompressorConfig","text":"from headroom.compression import UniversalCompressorConfig\n\nconfig = UniversalCompressorConfig(\n # Detection\n use_magika=True, # Use ML-based detection (requires magika)\n\n # Compression\n use_llmlingua=True, # Use LLMLingua for compression\n compression_ratio_target=0.3, # Keep 30% of content (70% reduction)\n min_content_length=100, # Skip content shorter than this\n\n # Structure preservation\n use_entropy_preservation=True, # Preserve high-entropy tokens\n entropy_threshold=0.85, # Entropy threshold for preservation\n\n # CCR\n ccr_enabled=True, # Store originals for retrieval\n)\n
"},{"location":"compression/#configuration-options","title":"Configuration Options","text":"Option Default Description use_magika True Use ML-based content detection use_llmlingua True Use LLMLingua for compression compression_ratio_target 0.3 Target ratio (0.3 = keep 30%) min_content_length 100 Minimum chars to compress use_entropy_preservation True Preserve high-entropy tokens entropy_threshold 0.85 Entropy threshold (0.0-1.0) ccr_enabled True Enable CCR storage"},{"location":"compression/#content-handlers","title":"Content Handlers","text":""},{"location":"compression/#json-handler","title":"JSON Handler","text":"Preserves JSON structure while compressing values:
from headroom.compression.handlers.json_handler import JSONStructureHandler\n\nhandler = JSONStructureHandler(\n preserve_short_values=True, # Keep values < 20 chars\n short_value_threshold=20, # Threshold for \"short\"\n preserve_high_entropy=True, # Keep UUIDs, hashes\n entropy_threshold=0.85, # Entropy threshold\n max_array_items_full=3, # Keep first N array items full\n max_number_digits=10, # Preserve numbers up to N digits\n)\n
What's Preserved: - All keys (navigational - LLM sees schema) - Structural syntax ({, }, [, ], :, ,) - Booleans and nulls (semantically important) - High-entropy strings (UUIDs, hashes - identifiers) - Short numbers (often IDs)
Example:
# Before\n{\n \"id\": \"usr_abc123\",\n \"name\": \"Alice Johnson\",\n \"bio\": \"A long description that goes on and on...\"\n}\n\n# After (structure preserved, long values compressed)\n{\n \"id\": \"usr_abc123\",\n \"name\": \"Alice Johnson\",\n \"bio\": \"A long...[compressed]...\"\n}\n
"},{"location":"compression/#code-handler","title":"Code Handler","text":"Preserves code structure using AST parsing (tree-sitter) or regex fallback:
from headroom.compression.handlers.code_handler import CodeStructureHandler\n\nhandler = CodeStructureHandler(\n preserve_comments=False, # Preserve comments as structural\n use_tree_sitter=True, # Use tree-sitter for parsing\n default_language=\"python\", # Default when detection fails\n)\n
What's Preserved: - Import statements - Function/method signatures - Class definitions - Type annotations - Decorators
What's Compressed: - Function bodies (implementations) - Comments (unless preserve_comments=True)
Example:
# Before\ndef process_data(items: List[str]) -> Dict[str, int]:\n \"\"\"Process items and count occurrences.\"\"\"\n result = {}\n for item in items:\n item = item.strip().lower()\n if item in result:\n result[item] += 1\n else:\n result[item] = 1\n return result\n\n# After (signature preserved, body compressed)\ndef process_data(items: List[str]) -> Dict[str, int]:\n \"\"\"Process items and count occurrences.\"\"\"\n result = {}\n for item in items:\n ...[compressed]...\n
"},{"location":"compression/#supported-languages","title":"Supported Languages","text":"Language Parser Support Level Python tree-sitter Full AST JavaScript tree-sitter Full AST TypeScript tree-sitter Full AST Go tree-sitter Full AST Rust tree-sitter Full AST Java tree-sitter Full AST C tree-sitter Full AST C++ tree-sitter Full AST"},{"location":"compression/#compression-result","title":"Compression Result","text":"from headroom.compression import compress\n\nresult = compress(content)\n\n# Access result fields\nprint(result.compressed) # Compressed content\nprint(result.original) # Original content\nprint(result.compression_ratio) # e.g., 0.35 (35% of original size)\nprint(result.tokens_before) # Estimated tokens before\nprint(result.tokens_after) # Estimated tokens after\nprint(result.tokens_saved) # tokens_before - tokens_after\nprint(result.savings_percentage) # e.g., 65.0 (65% savings)\n\n# Detection info\nprint(result.content_type) # ContentType.JSON, CODE, etc.\nprint(result.detection_confidence) # 0.0-1.0\n\n# Structure info\nprint(result.handler_used) # \"json\", \"code\", etc.\nprint(result.preservation_ratio) # Fraction preserved as structure\n\n# CCR info\nprint(result.ccr_key) # Key for retrieval (if CCR enabled)\n
"},{"location":"compression/#batch-compression","title":"Batch Compression","text":"For multiple contents, batch compression is more efficient:
from headroom.compression import UniversalCompressor\n\ncompressor = UniversalCompressor()\n\ncontents = [\n '{\"users\": [...]}',\n 'def hello(): pass',\n 'Plain text content',\n]\n\nresults = compressor.compress_batch(contents)\n\nfor result in results:\n print(f\"{result.content_type}: {result.savings_percentage:.0f}% saved\")\n
"},{"location":"compression/#custom-handlers","title":"Custom Handlers","text":"Register custom handlers for specific content types:
from headroom.compression import UniversalCompressor\nfrom headroom.compression.detector import ContentType\nfrom headroom.compression.handlers.base import BaseStructureHandler, HandlerResult\nfrom headroom.compression.masks import StructureMask\n\n\nclass LogStructureHandler(BaseStructureHandler):\n \"\"\"Custom handler for log content.\"\"\"\n\n def __init__(self):\n super().__init__(name=\"log\")\n\n def can_handle(self, content: str) -> bool:\n return \"[INFO]\" in content or \"[ERROR]\" in content\n\n def _extract_mask(self, content, tokens, **kwargs):\n # Mark timestamps and log levels as structural\n mask = [False] * len(content)\n # ... (custom logic)\n return HandlerResult(\n mask=StructureMask(tokens=tokens, mask=mask),\n handler_name=self.name,\n confidence=0.9,\n )\n\n\n# Register the custom handler\ncompressor = UniversalCompressor()\ncompressor.register_handler(ContentType.TEXT, LogStructureHandler())\n
"},{"location":"compression/#ccr-integration","title":"CCR Integration","text":"Universal Compression integrates with CCR (Compress-Cache-Retrieve) for reversible compression:
from headroom.compression import UniversalCompressor, UniversalCompressorConfig\n\nconfig = UniversalCompressorConfig(ccr_enabled=True)\ncompressor = UniversalCompressor(config=config)\n\nresult = compressor.compress(large_content)\n\n# CCR key for retrieval\nif result.ccr_key:\n print(f\"Original stored with key: {result.ccr_key}\")\n # LLM can request original via CCR when needed\n
See CCR Guide for full CCR documentation.
"},{"location":"compression/#performance","title":"Performance","text":"Content Type Compression Speed Accuracy JSON (large arrays) 70-90% ~1ms Keys preserved Code (Python) 50-70% ~10ms Signatures preserved Plain text 60-80% ~5ms High-entropy preserved Overhead: ~1-10ms per compression depending on content size and type.
"},{"location":"compression/#installation","title":"Installation","text":"# Basic compression (fallback to simple compression)\npip install headroom-ai\n\n# With ML detection (recommended)\npip install \"headroom-ai[magika]\"\n\n# With LLMLingua compression\npip install \"headroom-ai[llmlingua]\"\n\n# With AST-based code handling\npip install \"headroom-ai[code]\"\n\n# Everything\npip install \"headroom-ai[all]\"\n
"},{"location":"compression/#example-full-pipeline","title":"Example: Full Pipeline","text":"from headroom.compression import UniversalCompressor, UniversalCompressorConfig\n\n# Configure for aggressive compression\nconfig = UniversalCompressorConfig(\n compression_ratio_target=0.25, # Keep 25%\n use_magika=True,\n use_llmlingua=True,\n ccr_enabled=True,\n)\n\ncompressor = UniversalCompressor(config=config)\n\n# Compress JSON API response\njson_content = \"\"\"\n{\n \"users\": [\n {\"id\": \"usr_123\", \"name\": \"Alice\", \"bio\": \"Software engineer...\"},\n {\"id\": \"usr_456\", \"name\": \"Bob\", \"bio\": \"Product manager...\"}\n ],\n \"total\": 2,\n \"page\": 1\n}\n\"\"\"\n\nresult = compressor.compress(json_content)\n\nprint(f\"Type: {result.content_type}\") # ContentType.JSON\nprint(f\"Handler: {result.handler_used}\") # json\nprint(f\"Saved: {result.savings_percentage:.0f}%\") # ~60%\nprint(f\"Structure: {result.preservation_ratio:.0%} preserved\") # ~40%\nprint(f\"CCR Key: {result.ccr_key}\") # For retrieval\n
"},{"location":"compression/#see-also","title":"See Also","text":" - Transforms Reference - Other compression transforms
- CCR Guide - Reversible compression architecture
- Text Compression - Opt-in utilities for search/logs
"},{"location":"configuration/","title":"Configuration","text":"Headroom can be configured via the SDK, proxy command line, or per-request overrides.
"},{"location":"configuration/#sdk-configuration","title":"SDK Configuration","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n\n # Mode: \"audit\" (observe only) or \"optimize\" (apply transforms)\n default_mode=\"optimize\",\n\n # Enable provider-specific cache optimization\n enable_cache_optimizer=True,\n\n # Enable query-level semantic caching\n enable_semantic_cache=False,\n\n # Override default context limits per model\n model_context_limits={\n \"gpt-4o\": 128000,\n \"gpt-4o-mini\": 128000,\n },\n\n # Database location (defaults to temp directory)\n # store_url=\"sqlite:////absolute/path/to/headroom.db\",\n)\n
"},{"location":"configuration/#proxy-configuration","title":"Proxy Configuration","text":""},{"location":"configuration/#command-line-options","title":"Command Line Options","text":"headroom proxy \\\n --port 8787 \\ # Port to listen on\n --host 0.0.0.0 \\ # Host to bind to\n --budget 10.00 \\ # Daily budget limit in USD\n --log-file headroom.jsonl # Log file path\n
"},{"location":"configuration/#feature-flags","title":"Feature Flags","text":"# Disable optimization (passthrough mode)\nheadroom proxy --no-optimize\n\n# Disable semantic caching\nheadroom proxy --no-cache\n\n# Disable CCR response handling\nheadroom proxy --no-ccr-responses\n\n# Disable proactive expansion\nheadroom proxy --no-ccr-expansion\n\n# Enable LLMLingua ML compression\nheadroom proxy --llmlingua\nheadroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.4\n
"},{"location":"configuration/#all-options","title":"All Options","text":"headroom proxy --help\n
"},{"location":"configuration/#per-request-overrides","title":"Per-Request Overrides","text":"Override configuration for specific requests:
response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[...],\n\n # Override mode for this request\n headroom_mode=\"audit\",\n\n # Reserve more tokens for output\n headroom_output_buffer_tokens=8000,\n\n # Keep last N turns (don't compress)\n headroom_keep_turns=5,\n\n # Skip compression for specific tools\n headroom_tool_profiles={\n \"important_tool\": {\"skip_compression\": True}\n }\n)\n
"},{"location":"configuration/#modes","title":"Modes","text":"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"},{"location":"configuration/#simulate-mode","title":"Simulate Mode","text":"Preview what would happen without making an API call:
plan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=large_conversation,\n)\n\nprint(f\"Would save {plan.tokens_saved} tokens\")\nprint(f\"Transforms: {plan.transforms}\")\nprint(f\"Estimated savings: {plan.estimated_savings}\")\n
"},{"location":"configuration/#smartcrusher-configuration","title":"SmartCrusher Configuration","text":"Fine-tune JSON compression behavior:
from headroom.transforms import SmartCrusherConfig\n\nconfig = SmartCrusherConfig(\n # Maximum items to keep after compression\n max_items_after_crush=15,\n\n # Minimum tokens before applying compression\n min_tokens_to_crush=200,\n\n # Relevance scoring tier: \"bm25\" (fast) or \"embedding\" (accurate)\n relevance_tier=\"bm25\",\n\n # Always keep items with these field values\n preserve_fields=[\"error\", \"warning\", \"failure\"],\n)\n
"},{"location":"configuration/#cache-aligner-configuration","title":"Cache Aligner Configuration","text":"Control prefix stabilization:
from headroom.transforms import CacheAlignerConfig\n\nconfig = CacheAlignerConfig(\n # Enable/disable cache alignment\n enabled=True,\n\n # Patterns to extract from system prompt\n dynamic_patterns=[\n r\"Today is \\w+ \\d+, \\d{4}\",\n r\"Current time: .*\",\n ],\n)\n
"},{"location":"configuration/#rolling-window-configuration","title":"Rolling Window Configuration","text":"Control context window management:
from headroom.transforms import RollingWindowConfig\n\nconfig = RollingWindowConfig(\n # Minimum turns to always keep\n min_keep_turns=3,\n\n # Reserve tokens for output\n output_buffer_tokens=4000,\n\n # Drop oldest tool outputs first\n prefer_drop_tool_outputs=True,\n)\n
"},{"location":"configuration/#intelligent-context-manager-configuration","title":"Intelligent Context Manager Configuration","text":"For semantic-aware context management with importance scoring:
from headroom.config import IntelligentContextConfig, ScoringWeights\n\n# Customize scoring weights (must sum to 1.0, or will be normalized)\nweights = ScoringWeights(\n recency=0.20, # Newer messages score higher\n semantic_similarity=0.20, # Similarity to recent context\n toin_importance=0.25, # TOIN-learned retrieval patterns\n error_indicator=0.15, # TOIN-learned error field types\n forward_reference=0.15, # Messages referenced by later messages\n token_density=0.05, # Information density\n)\n\nconfig = IntelligentContextConfig(\n # Enable/disable the manager\n enabled=True,\n\n # Protection settings\n keep_system=True, # Never drop system messages\n keep_last_turns=2, # Protect last N user turns\n\n # Token budget\n output_buffer_tokens=4000, # Reserve for model output\n\n # Scoring settings\n use_importance_scoring=True, # Use semantic scoring (vs position-only)\n scoring_weights=weights, # Custom weights\n toin_integration=True, # Use TOIN patterns if available\n recency_decay_rate=0.1, # Exponential decay lambda\n\n # Strategy thresholds\n compress_threshold=0.1, # Try compression first if <10% over budget\n)\n
"},{"location":"configuration/#ccr-integration","title":"CCR Integration","text":"When IntelligentContext drops messages, they're stored in CCR for potential retrieval:
from headroom.telemetry import get_toin\n\n# Pass TOIN for bidirectional integration\ntoin = get_toin()\nmanager = IntelligentContextManager(config=config, toin=toin)\n\n# Dropped messages are:\n# 1. Stored in CCR (so LLM can retrieve if needed)\n# 2. Recorded to TOIN (so it learns which patterns matter)\n# 3. Marked with CCR reference in the inserted message\n
The marker inserted when messages are dropped includes the CCR reference:
[Earlier context compressed: 14 message(s) dropped by importance scoring.\nFull content available via ccr_retrieve tool with reference 'abc123def456'.]\n
"},{"location":"configuration/#scoring-weights","title":"Scoring Weights","text":"The ScoringWeights class controls how messages are scored:
Weight Default Description recency 0.20 Exponential decay from conversation end semantic_similarity 0.20 Embedding cosine similarity to recent context toin_importance 0.25 TOIN retrieval_rate (high retrieval = important) error_indicator 0.15 TOIN field_semantics error detection forward_reference 0.15 Count of later messages referencing this one token_density 0.05 Unique tokens / total tokens Weights are automatically normalized to sum to 1.0:
weights = ScoringWeights(recency=1.0, toin_importance=1.0)\nnormalized = weights.normalized()\n# recency=0.5, toin_importance=0.5, others=0.0\n
"},{"location":"configuration/#environment-variables","title":"Environment Variables","text":"Some settings can be configured via environment variables:
Variable Description Default HEADROOM_LOG_LEVEL Logging level INFO HEADROOM_STORE_URL Database URL temp directory HEADROOM_DEFAULT_MODE Default mode optimize HEADROOM_MODEL_LIMITS Custom model config (JSON string or file path) -"},{"location":"configuration/#custom-model-configuration","title":"Custom Model Configuration","text":"Configure context limits and pricing for new or custom models. Useful when: - A new model is released before Headroom is updated - You're using fine-tuned or custom models - You want to override built-in limits
"},{"location":"configuration/#configuration-methods","title":"Configuration Methods","text":"Settings are resolved in this order (later overrides earlier): 1. Built-in defaults 2. ~/.headroom/models.json config file 3. HEADROOM_MODEL_LIMITS environment variable 4. SDK constructor arguments
"},{"location":"configuration/#config-file-format","title":"Config File Format","text":"Create ~/.headroom/models.json:
{\n \"anthropic\": {\n \"context_limits\": {\n \"claude-4-opus-20250301\": 200000,\n \"claude-custom-finetune\": 128000\n },\n \"pricing\": {\n \"claude-4-opus-20250301\": {\n \"input\": 15.00,\n \"output\": 75.00,\n \"cached_input\": 1.50\n }\n }\n },\n \"openai\": {\n \"context_limits\": {\n \"gpt-5\": 256000,\n \"ft:gpt-4o:my-org\": 128000\n },\n \"pricing\": {\n \"gpt-5\": [5.00, 15.00]\n }\n }\n}\n
"},{"location":"configuration/#environment-variable","title":"Environment Variable","text":"Set HEADROOM_MODEL_LIMITS as a JSON string or file path:
# JSON string\nexport HEADROOM_MODEL_LIMITS='{\"anthropic\":{\"context_limits\":{\"claude-new\":200000}}}'\n\n# File path\nexport HEADROOM_MODEL_LIMITS=/path/to/models.json\n
"},{"location":"configuration/#pattern-based-inference","title":"Pattern-Based Inference","text":"Unknown models are automatically 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 o1*, o3* 200K context, reasoning model pricing This means new models like claude-4-sonnet-20251201 will work automatically with Sonnet-tier defaults.
"},{"location":"configuration/#sdk-override","title":"SDK Override","text":"Override in code for specific models:
from headroom import HeadroomClient, AnthropicProvider\n\nclient = HeadroomClient(\n original_client=Anthropic(),\n provider=AnthropicProvider(\n context_limits={\n \"claude-new-model\": 300000,\n }\n ),\n)\n
"},{"location":"configuration/#provider-specific-settings","title":"Provider-Specific Settings","text":""},{"location":"configuration/#openai","title":"OpenAI","text":"from headroom import OpenAIProvider\n\nprovider = OpenAIProvider(\n # Enable automatic prefix caching\n enable_prefix_caching=True,\n)\n
"},{"location":"configuration/#anthropic","title":"Anthropic","text":"from headroom import AnthropicProvider\n\nprovider = AnthropicProvider(\n # Enable cache_control blocks\n enable_cache_control=True,\n)\n
"},{"location":"configuration/#google","title":"Google","text":"from headroom import GoogleProvider\n\nprovider = GoogleProvider(\n # Enable context caching\n enable_context_caching=True,\n)\n
"},{"location":"configuration/#configuration-precedence","title":"Configuration Precedence","text":"Settings are applied in this order (later overrides earlier):
- Default values
- Environment variables
- SDK constructor arguments
- Per-request overrides
"},{"location":"configuration/#validation","title":"Validation","text":"Validate your configuration:
result = client.validate_setup()\n\nif not result[\"valid\"]:\n print(\"Configuration issues:\")\n for issue in result[\"issues\"]:\n print(f\" - {issue}\")\n
"},{"location":"configuration/#typescript-sdk-configuration","title":"TypeScript SDK Configuration","text":"The TypeScript SDK is configured via environment variables or constructor options.
"},{"location":"configuration/#environment-variables_1","title":"Environment Variables","text":"Variable Description Default HEADROOM_BASE_URL Base URL of the Headroom proxy or cloud API http://localhost:8787 HEADROOM_API_KEY API key for Headroom Cloud authentication -"},{"location":"configuration/#usage","title":"Usage","text":"export HEADROOM_BASE_URL=http://localhost:8787\nexport HEADROOM_API_KEY=your-api-key\n
import { HeadroomClient } from 'headroom-ai';\n\n// Reads from HEADROOM_BASE_URL and HEADROOM_API_KEY automatically\nconst client = new HeadroomClient();\n\n// Or configure explicitly\nconst client = new HeadroomClient({\n baseUrl: 'http://localhost:8787',\n apiKey: 'your-api-key',\n});\n
See the TypeScript SDK Guide for full configuration options.
"},{"location":"errors/","title":"Error Handling","text":"Headroom provides explicit exceptions for debugging, with a safety guarantee that compression failures never break your LLM calls.
"},{"location":"errors/#exception-hierarchy","title":"Exception Hierarchy","text":"from headroom import (\n HeadroomError, # Base class - catch all Headroom errors\n ConfigurationError, # Invalid configuration\n ProviderError, # Provider issues (unknown model, etc.)\n StorageError, # Database/storage failures\n CompressionError, # Compression failures (rare)\n ValidationError, # Setup validation failures\n)\n
"},{"location":"errors/#usage","title":"Usage","text":"from headroom import (\n HeadroomClient,\n HeadroomError,\n ConfigurationError,\n StorageError,\n)\n\ntry:\n client = HeadroomClient(...)\n response = client.chat.completions.create(...)\n\nexcept ConfigurationError as e:\n print(f\"Config issue: {e}\")\n print(f\"Details: {e.details}\") # Additional context\n\nexcept StorageError as e:\n print(f\"Storage issue: {e}\")\n # Headroom continues to work, just without metrics persistence\n\nexcept HeadroomError as e:\n print(f\"Headroom error: {e}\")\n
"},{"location":"errors/#exception-types","title":"Exception Types","text":""},{"location":"errors/#configurationerror","title":"ConfigurationError","text":"Raised when configuration is invalid.
# Examples:\n# - Invalid mode value\n# - Missing required provider\n# - Invalid model context limit\n\ntry:\n client = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"invalid_mode\", # Will raise ConfigurationError\n )\nexcept ConfigurationError as e:\n print(f\"Config error: {e}\")\n print(f\"Field: {e.details.get('field')}\")\n
"},{"location":"errors/#providererror","title":"ProviderError","text":"Raised for provider-specific issues.
# Examples:\n# - Unknown model name\n# - Provider API error\n# - Token counting failure\n\ntry:\n response = client.chat.completions.create(\n model=\"unknown-model-xyz\",\n messages=[...]\n )\nexcept ProviderError as e:\n print(f\"Provider error: {e}\")\n print(f\"Provider: {e.details.get('provider')}\")\n
"},{"location":"errors/#storageerror","title":"StorageError","text":"Raised when database operations fail.
# Examples:\n# - Database connection failure\n# - Write permission denied\n# - Disk full\n\ntry:\n metrics = client.get_metrics()\nexcept StorageError as e:\n print(f\"Storage error: {e}\")\n # Application can continue - just won't have metrics\n
"},{"location":"errors/#compressionerror","title":"CompressionError","text":"Raised when compression fails (rare).
# Examples:\n# - Malformed JSON in tool output\n# - Unexpected data structure\n\n# Note: In practice, compression errors are caught internally\n# and the original content passes through unchanged.\n# This exception is only raised if you explicitly enable strict mode.\n
"},{"location":"errors/#validationerror","title":"ValidationError","text":"Raised when setup validation fails.
result = client.validate_setup()\nif not result[\"valid\"]:\n raise ValidationError(\n \"Setup validation failed\",\n details={\"issues\": result[\"issues\"]}\n )\n
"},{"location":"errors/#safety-guarantee","title":"Safety Guarantee","text":"If compression fails, the original content passes through unchanged.
This is a core design principle. Your LLM calls never fail due to Headroom:
# Even if SmartCrusher encounters unexpected data:\nmessages = [\n {\"role\": \"tool\", \"content\": \"malformed json {{{\"}\n]\n\n# This will NOT raise an exception\n# Instead, the malformed content passes through unchanged\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages\n)\n
"},{"location":"errors/#logging-errors","title":"Logging Errors","text":"Enable logging to see error details:
import logging\nlogging.basicConfig(level=logging.WARNING)\n\n# Now you'll see warnings when compression is skipped:\n# WARNING:headroom.transforms.smart_crusher:Skipping compression: invalid JSON\n
"},{"location":"errors/#error-details","title":"Error Details","text":"All Headroom exceptions include a details dict with context:
try:\n client = HeadroomClient(...)\nexcept HeadroomError as e:\n print(f\"Error: {e}\")\n print(f\"Type: {type(e).__name__}\")\n print(f\"Details: {e.details}\")\n\n # Details might include:\n # - field: which config field caused the error\n # - provider: which provider was involved\n # - model: which model was requested\n # - original_error: underlying exception\n
"},{"location":"errors/#best-practices","title":"Best Practices","text":""},{"location":"errors/#1-catch-specific-exceptions","title":"1. Catch Specific Exceptions","text":"# Good: catch specific exceptions\ntry:\n response = client.chat.completions.create(...)\nexcept ConfigurationError:\n # Handle config issues\n pass\nexcept ProviderError:\n # Handle provider issues\n pass\n\n# Avoid: catching all exceptions\ntry:\n response = client.chat.completions.create(...)\nexcept Exception:\n # Too broad - might hide real bugs\n pass\n
"},{"location":"errors/#2-let-storageerror-pass","title":"2. Let StorageError Pass","text":"# Storage errors don't affect core functionality\ntry:\n metrics = client.get_metrics()\nexcept StorageError:\n metrics = [] # Continue without historical metrics\n
"},{"location":"errors/#3-validate-on-startup","title":"3. Validate on Startup","text":"client = HeadroomClient(...)\n\n# Validate once at startup\nresult = client.validate_setup()\nif not result[\"valid\"]:\n raise SystemExit(f\"Headroom setup invalid: {result['issues']}\")\n\n# Then use client normally\nresponse = client.chat.completions.create(...)\n
"},{"location":"errors/#debugging","title":"Debugging","text":""},{"location":"errors/#enable-debug-logging","title":"Enable Debug Logging","text":"import logging\nlogging.basicConfig(level=logging.DEBUG)\n\n# Shows detailed transform decisions\n# DEBUG:headroom.transforms.smart_crusher:Analyzing 1000 items...\n# DEBUG:headroom.transforms.smart_crusher:Kept 15 items (errors: 2, anomalies: 3)\n
"},{"location":"errors/#check-stats-after-error","title":"Check Stats After Error","text":"try:\n response = client.chat.completions.create(...)\nexcept HeadroomError:\n # Check what happened\n stats = client.get_stats()\n print(f\"Last request stats: {stats}\")\n
"},{"location":"getting-started/","title":"Getting Started with Headroom","text":"This guide will help you get up and running with Headroom in under 5 minutes.
"},{"location":"getting-started/#installation","title":"Installation","text":"Python:
# Core package (minimal dependencies)\npip install headroom\n\n# With proxy server\npip install headroom[proxy]\n\n# With semantic relevance (for smarter compression)\npip install headroom[relevance]\n\n# Everything\npip install headroom[all]\n
TypeScript / Node.js:
npm install headroom-ai\n
"},{"location":"getting-started/#quick-start-proxy-mode-recommended","title":"Quick Start: Proxy Mode (Recommended)","text":"The easiest way to use Headroom is as a proxy server:
# Start the proxy\nheadroom proxy --port 8787\n
Then point your LLM client at it:
# Claude Code\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n\n# GitHub Copilot CLI (default Anthropic-style proxy route)\nheadroom wrap copilot -- --model claude-sonnet-4-20250514\n\n# OpenAI-compatible clients\nOPENAI_BASE_URL=http://localhost:8787/v1 your-app\n
That's it! All your requests now go through Headroom and get optimized automatically.
"},{"location":"getting-started/#quick-start-python-sdk","title":"Quick Start: Python SDK","text":"If you want programmatic control:
from headroom import HeadroomClient\nfrom openai import OpenAI\n\n# Create a wrapped client\nclient = HeadroomClient(\n original_client=OpenAI(),\n default_mode=\"optimize\",\n)\n\n# Use exactly like the original\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[\n {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Hello!\"},\n ],\n)\n
"},{"location":"getting-started/#modes","title":"Modes","text":""},{"location":"getting-started/#audit-mode","title":"Audit Mode","text":"Observe without modifying:
client = HeadroomClient(\n original_client=OpenAI(),\n default_mode=\"audit\",\n)\n# Logs metrics but doesn't change requests\n
"},{"location":"getting-started/#optimize-mode","title":"Optimize Mode","text":"Apply transforms to reduce tokens:
client = HeadroomClient(\n original_client=OpenAI(),\n default_mode=\"optimize\",\n)\n# Compresses tool outputs, aligns cache prefixes, etc.\n
"},{"location":"getting-started/#simulate-mode","title":"Simulate Mode","text":"Preview what optimizations would do:
plan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=[...],\n)\nprint(f\"Would save {plan.tokens_saved} tokens\")\nprint(f\"Transforms: {plan.transforms_applied}\")\n
"},{"location":"getting-started/#next-steps","title":"Next Steps","text":" - Proxy Server Documentation - Configure the proxy
- Transforms Reference - Understand each transform
- API Reference - Full API documentation
"},{"location":"image-compression/","title":"Image Compression","text":"Headroom automatically compresses images in your LLM requests, reducing token usage by 40-90% while maintaining answer accuracy.
"},{"location":"image-compression/#overview","title":"Overview","text":"Vision models charge by the token, and images are expensive: - A 1024x1024 image costs ~765 tokens (OpenAI) - A 2048x2048 image costs ~2,900 tokens
Headroom's image compression uses a trained ML router to analyze your query and automatically select the optimal compression technique:
Technique Savings When Used full_low ~87% General questions (\"What is this?\") preserve 0% Fine details needed (\"Count the whiskers\") crop 50-90% Region-specific (\"What's in the corner?\") transcode ~99% Text extraction (\"Read the sign\")"},{"location":"image-compression/#how-it-works","title":"How It Works","text":"User uploads image + asks question\n \u2193\n [Query Analysis]\n TrainedRouter (MiniLM from HuggingFace)\n Classifies: \"What animal is this?\" \u2192 full_low\n \u2193\n [Image Analysis]\n SigLIP analyzes image properties\n (has text? complex? fine details?)\n \u2193\n [Apply Compression]\n OpenAI: detail=\"low\"\n Anthropic: Resize to 512px\n Google: Resize to 768px\n \u2193\n Compressed request to LLM\n
"},{"location":"image-compression/#quick-start","title":"Quick Start","text":""},{"location":"image-compression/#with-headroom-proxy-zero-code-changes","title":"With Headroom Proxy (Zero Code Changes)","text":"# Start the proxy\nheadroom proxy --port 8787\n\n# Connect your client\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n
Images are automatically compressed based on your queries.
"},{"location":"image-compression/#with-headroomclient","title":"With HeadroomClient","text":"from headroom import HeadroomClient\n\nclient = HeadroomClient(provider=\"openai\")\n\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\n \"role\": \"user\",\n \"content\": [\n {\"type\": \"text\", \"text\": \"What animal is this?\"},\n {\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/jpeg;base64,...\"}}\n ]\n }]\n)\n# Image automatically compressed with detail=\"low\" (87% savings)\n
"},{"location":"image-compression/#direct-api","title":"Direct API","text":"from headroom.image import ImageCompressor\n\ncompressor = ImageCompressor()\n\n# Compress images in messages\ncompressed_messages = compressor.compress(messages, provider=\"openai\")\n\n# Check savings\nprint(f\"Saved {compressor.last_savings:.0f}% tokens\")\nprint(f\"Technique: {compressor.last_result.technique.value}\")\n
"},{"location":"image-compression/#configuration","title":"Configuration","text":""},{"location":"image-compression/#proxy-configuration","title":"Proxy Configuration","text":"# Enable image compression (default: true)\nheadroom proxy --image-optimize\n\n# Disable image compression\nheadroom proxy --no-image-optimize\n
"},{"location":"image-compression/#programmatic-configuration","title":"Programmatic Configuration","text":"from headroom.image import ImageCompressor\n\ncompressor = ImageCompressor(\n model_id=\"chopratejas/technique-router\", # HuggingFace model\n use_siglip=True, # Enable image analysis\n device=\"cuda\", # Use GPU if available\n)\n
"},{"location":"image-compression/#provider-support","title":"Provider Support","text":"Provider Detection Compression Method OpenAI image_url Sets detail=\"low\" Anthropic image with source Resizes to 512px Google inlineData Resizes to 768px (tile-optimized)"},{"location":"image-compression/#openai","title":"OpenAI","text":"Uses the native detail parameter:
# Before\n{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:...\"}}\n\n# After (full_low technique)\n{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:...\", \"detail\": \"low\"}}\n
"},{"location":"image-compression/#anthropic","title":"Anthropic","text":"Resizes the image using PIL:
# Before: 1024x1024 image (~1,398 tokens)\n# After: 512x512 image (~349 tokens) - 75% savings\n
"},{"location":"image-compression/#google-gemini","title":"Google Gemini","text":"Resizes to 768px (optimal for Gemini's 768x768 tile system):
# Before: 1536x1536 image (4 tiles \u00d7 258 = 1,032 tokens)\n# After: 768x768 image (1 tile \u00d7 258 = 258 tokens) - 75% savings\n
"},{"location":"image-compression/#techniques-explained","title":"Techniques Explained","text":""},{"location":"image-compression/#full_low-87-savings","title":"full_low (87% savings)","text":"Best for general understanding questions: - \"What is this?\" - \"Describe the scene\" - \"Is this indoors or outdoors?\"
The model doesn't need fine details to answer these questions.
"},{"location":"image-compression/#preserve-0-savings","title":"preserve (0% savings)","text":"Required when fine details matter: - \"Count the whiskers\" - \"What brand is shown?\" - \"Read the serial number\" - \"What time does the clock show?\"
"},{"location":"image-compression/#crop-50-90-savings","title":"crop (50-90% savings)","text":"For region-specific queries: - \"What's in the top-right corner?\" - \"Focus on the background\" - \"Zoom into the left side\"
Note: Currently implemented as resize. True cropping coming soon.
"},{"location":"image-compression/#transcode-99-savings","title":"transcode (99% savings)","text":"For text extraction (converts image to text): - \"Read the sign\" - \"What does it say?\" - \"Transcribe the document\"
Note: Requires vision model call. Currently falls back to preserve.
"},{"location":"image-compression/#the-trained-router","title":"The Trained Router","text":"The routing decision is made by a fine-tuned MiniLM classifier:
- Model:
chopratejas/technique-router on HuggingFace - Size: ~128MB
- Accuracy: 93.7% on validation set
- Training data: 1,157 examples across 4 techniques
The model is downloaded automatically on first use and cached locally.
"},{"location":"image-compression/#training-data-examples","title":"Training Data Examples","text":"Query Technique \"What animal is this?\" full_low \"Count the spots\" preserve \"Read the text on the sign\" transcode \"What's in the corner?\" crop"},{"location":"image-compression/#performance","title":"Performance","text":""},{"location":"image-compression/#token-savings-by-query-type","title":"Token Savings by Query Type","text":"Query Type Before After Savings General (\"What is this?\") 765 85 89% Detail (\"Count items\") 765 765 0% Region (\"Top corner?\") 765 85 89% Text (\"Read the sign\") 765 85 89%"},{"location":"image-compression/#latency","title":"Latency","text":" - Router inference: ~10ms (CPU), ~2ms (GPU)
- Image resize: ~5-20ms depending on size
- First request: +2-3s (model download, cached after)
"},{"location":"image-compression/#troubleshooting","title":"Troubleshooting","text":""},{"location":"image-compression/#model-download-issues","title":"Model Download Issues","text":"The HuggingFace model downloads on first use:
# Force a specific cache directory\nimport os\nos.environ[\"HF_HOME\"] = \"/path/to/cache\"\n\nfrom headroom.image import ImageCompressor\ncompressor = ImageCompressor()\n
"},{"location":"image-compression/#gpu-memory","title":"GPU Memory","text":"SigLIP requires ~400MB GPU memory. To use CPU only:
compressor = ImageCompressor(device=\"cpu\")\n
"},{"location":"image-compression/#disable-image-compression","title":"Disable Image Compression","text":"# Proxy\nheadroom proxy --no-image-optimize\n\n# Direct\n# Simply don't call compress()\n
"},{"location":"image-compression/#api-reference","title":"API Reference","text":""},{"location":"image-compression/#imagecompressor","title":"ImageCompressor","text":"class ImageCompressor:\n def __init__(\n self,\n model_id: str = \"chopratejas/technique-router\",\n use_siglip: bool = True,\n device: str | None = None,\n ): ...\n\n def has_images(self, messages: list[dict]) -> bool:\n \"\"\"Check if messages contain images.\"\"\"\n\n def compress(\n self,\n messages: list[dict],\n provider: str = \"openai\",\n ) -> list[dict]:\n \"\"\"Compress images in messages.\"\"\"\n\n @property\n def last_result(self) -> CompressionResult | None:\n \"\"\"Result of last compression.\"\"\"\n\n @property\n def last_savings(self) -> float:\n \"\"\"Savings percentage from last compression.\"\"\"\n
"},{"location":"image-compression/#compressionresult","title":"CompressionResult","text":"@dataclass\nclass CompressionResult:\n technique: Technique # full_low, preserve, crop, transcode\n original_tokens: int # Estimated tokens before\n compressed_tokens: int # Estimated tokens after\n confidence: float # Router confidence (0-1)\n\n @property\n def savings_percent(self) -> float:\n \"\"\"Percentage of tokens saved.\"\"\"\n
"},{"location":"image-compression/#technique","title":"Technique","text":"class Technique(Enum):\n FULL_LOW = \"full_low\" # 87% savings\n PRESERVE = \"preserve\" # 0% savings\n CROP = \"crop\" # 50-90% savings\n TRANSCODE = \"transcode\" # 99% savings\n
"},{"location":"image-compression/#see-also","title":"See Also","text":" - Compression Guide - Text compression techniques
- CCR Guide - Reversible compression with retrieval
- Proxy Guide - Zero-code deployment
- Architecture - System design
"},{"location":"integration-guide/","title":"Integration Guide","text":"You don't need to run the Headroom proxy. Headroom is a compression library that works with any LLM client, proxy, or framework.
"},{"location":"integration-guide/#pick-your-path","title":"Pick Your Path","text":"You have... Use this Setup Any Python app compress() 2 lines LiteLLM LiteLLM callback 1 line A Python proxy (FastAPI, custom) ASGI middleware 1 line Claude Code / Cursor / Copilot CLI Headroom proxy 1 command or env var Agno agents Agno integration Wrap model LangChain LangChain integration Wrap model Non-Python app Headroom proxy HTTP TypeScript SDK compress() npm install headroom-ai Vercel AI SDK headroomMiddleware() Middleware adapter OpenAI Node SDK withHeadroom() Client wrapper Anthropic TS SDK withHeadroom() Client wrapper"},{"location":"integration-guide/#compress-function","title":"compress() Function","text":"The simplest integration. Works with any LLM client.
from headroom import compress\n\n# Before sending to your LLM:\nresult = compress(messages, model=\"claude-sonnet-4-5-20250929\")\nresponse = your_client.create(messages=result.messages) # Fewer tokens, same answer\n\nprint(f\"Saved {result.tokens_saved} tokens ({result.compression_ratio:.0%})\")\n
"},{"location":"integration-guide/#with-anthropic-sdk","title":"With Anthropic SDK","text":"from anthropic import Anthropic\nfrom headroom import compress\n\nclient = Anthropic()\nmessages = [\n {\"role\": \"user\", \"content\": \"What went wrong?\"},\n {\"role\": \"assistant\", \"content\": \"Let me check.\", \"tool_use\": [...]},\n {\"role\": \"user\", \"content\": [{\"type\": \"tool_result\", \"content\": huge_json}]},\n]\n\ncompressed = compress(messages, model=\"claude-sonnet-4-5-20250929\")\nresponse = client.messages.create(\n model=\"claude-sonnet-4-5-20250929\",\n messages=compressed.messages,\n max_tokens=1000,\n)\n
"},{"location":"integration-guide/#with-openai-sdk","title":"With OpenAI SDK","text":"from openai import OpenAI\nfrom headroom import compress\n\nclient = OpenAI()\nmessages = [\n {\"role\": \"user\", \"content\": \"Analyze these results\"},\n {\"role\": \"tool\", \"content\": big_json_output, \"tool_call_id\": \"call_1\"},\n]\n\ncompressed = compress(messages, model=\"gpt-4o\")\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=compressed.messages,\n)\n
"},{"location":"integration-guide/#with-litellm-direct","title":"With LiteLLM (direct)","text":"import litellm\nfrom headroom import compress\n\nmessages = [...]\ncompressed = compress(messages, model=\"bedrock/claude-sonnet\")\nresponse = litellm.completion(model=\"bedrock/claude-sonnet\", messages=compressed.messages)\n
"},{"location":"integration-guide/#with-any-http-client","title":"With any HTTP client","text":"import httpx\nfrom headroom import compress\n\ncompressed = compress(messages, model=\"claude-sonnet-4-5-20250929\")\nhttpx.post(\"https://api.anthropic.com/v1/messages\", json={\n \"model\": \"claude-sonnet-4-5-20250929\",\n \"messages\": compressed.messages,\n}, headers={\"X-Api-Key\": api_key, \"anthropic-version\": \"2023-06-01\"})\n
"},{"location":"integration-guide/#what-compress-returns","title":"What compress() returns","text":"result = compress(messages, model=\"gpt-4o\")\nresult.messages # list[dict] \u2014 compressed messages, same format as input\nresult.tokens_before # int \u2014 original token count\nresult.tokens_after # int \u2014 compressed token count\nresult.tokens_saved # int \u2014 tokens removed\nresult.compression_ratio # float \u2014 0.0 (no savings) to 1.0 (100% removed)\nresult.transforms_applied # list[str] \u2014 what ran (e.g., [\"router:smart_crusher:0.35\"])\n
"},{"location":"integration-guide/#litellm","title":"LiteLLM","text":"If you're already using LiteLLM as your LLM gateway, add Headroom as a callback:
import litellm\nfrom headroom.integrations.litellm_callback import HeadroomCallback\n\nlitellm.callbacks = [HeadroomCallback()]\n\n# All calls now compressed automatically\nresponse = litellm.completion(model=\"gpt-4o\", messages=[...])\nresponse = litellm.completion(model=\"bedrock/claude-sonnet\", messages=[...])\nresponse = litellm.completion(model=\"azure/gpt-4o\", messages=[...])\n
The callback compresses messages in LiteLLM's pre_call_hook before they're sent to the provider. Works with all 100+ LiteLLM-supported providers.
"},{"location":"integration-guide/#with-litellm-proxy","title":"With LiteLLM Proxy","text":"If you run LiteLLM as a proxy server, use the ASGI middleware instead:
# In your LiteLLM proxy startup\nfrom litellm.proxy.proxy_server import app\nfrom headroom.integrations.asgi import CompressionMiddleware\n\napp.add_middleware(CompressionMiddleware)\n
Or use the callback in your LiteLLM config:
# litellm_config.yaml\nlitellm_settings:\n callbacks: [\"headroom.integrations.litellm_callback.HeadroomCallback\"]\n
"},{"location":"integration-guide/#asgi-middleware","title":"ASGI Middleware","text":"Drop-in middleware for any ASGI application (FastAPI, Starlette, LiteLLM proxy, custom proxies).
from headroom.integrations.asgi import CompressionMiddleware\n\n# FastAPI\napp = FastAPI()\napp.add_middleware(CompressionMiddleware)\n\n# Starlette\napp = Starlette(routes=[...])\napp.add_middleware(CompressionMiddleware)\n\n# LiteLLM proxy\nfrom litellm.proxy.proxy_server import app\napp.add_middleware(CompressionMiddleware)\n
The middleware intercepts POST requests to /v1/messages, /v1/chat/completions, /v1/responses, and /chat/completions. All other requests pass through untouched.
Response headers include: - x-headroom-compressed: true \u2014 compression was applied - x-headroom-tokens-saved: 1234 \u2014 tokens removed
"},{"location":"integration-guide/#proxy","title":"Proxy","text":"The Headroom proxy is a standalone HTTP server. Best for non-Python apps or tools that only support base URL configuration (Claude Code, Cursor, GitHub Copilot CLI).
pip install \"headroom-ai[all]\"\nheadroom proxy --port 8787\n
# Claude Code\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n\n# GitHub Copilot CLI\nheadroom wrap copilot -- --model claude-sonnet-4-20250514\n\n# Cursor / Any OpenAI client\nOPENAI_BASE_URL=http://localhost:8787/v1 cursor\n
For translated backends, the Copilot wrapper can switch to Headroom's OpenAI-compatible route:
headroom wrap copilot --backend anyllm --anyllm-provider groq -- --model gpt-4o\n
By default, headroom wrap copilot installs rtk and appends token-optimized shell guidance to .github/copilot-instructions.md so Copilot sessions reuse the same command-saving conventions as other wrapped agent CLIs. Use --no-rtk to skip that step.
"},{"location":"integration-guide/#with-cloud-providers","title":"With Cloud Providers","text":"# AWS Bedrock\nheadroom proxy --backend bedrock --region us-east-1\n\n# Google Vertex AI\nheadroom proxy --backend vertex_ai --region us-central1\n\n# Azure OpenAI\nheadroom proxy --backend azure\n\n# OpenRouter (400+ models)\nOPENROUTER_API_KEY=sk-or-... headroom proxy --backend openrouter\n
See Proxy Documentation for all options.
"},{"location":"integration-guide/#agno","title":"Agno","text":"Full integration with the Agno agent framework.
from agno.agent import Agent\nfrom agno.models.anthropic import Claude\nfrom headroom.integrations.agno import HeadroomAgnoModel\n\nmodel = HeadroomAgnoModel(Claude(id=\"claude-sonnet-4-20250514\"))\nagent = Agent(model=model, tools=[your_tools])\nresponse = agent.run(\"Investigate the issue\")\n\nprint(f\"Tokens saved: {model.total_tokens_saved}\")\n
See Agno Guide for hooks, multi-provider, and streaming.
"},{"location":"integration-guide/#langchain","title":"LangChain","text":"Full integration with LangChain \u2014 chat models, memory, retrievers, tool wrappers, and streaming.
from langchain_openai import ChatOpenAI\nfrom headroom.integrations import HeadroomChatModel\n\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\nresponse = llm.invoke(\"Hello!\")\n
See LangChain Guide for details and known limitations.
"},{"location":"integration-guide/#typescript-sdk","title":"TypeScript SDK","text":"For Node.js, Next.js, and any TypeScript/JavaScript application.
npm install headroom-ai\n
See the TypeScript SDK Guide for full documentation including Vercel AI SDK middleware, OpenAI SDK wrapper, and Anthropic SDK wrapper.
"},{"location":"integration-guide/#openclaw","title":"OpenClaw","text":"Context compression plugin for OpenClaw agents.
pip install \"headroom-ai[proxy]\"\nopenclaw plugins install headroom-openclaw\n
Configure as context engine:
{ \"plugins\": { \"slots\": { \"contextEngine\": \"headroom\" } } }\n
The plugin auto-detects a running Headroom proxy or starts one. Compression happens in assemble() \u2014 zero changes to the agent's behavior.
See the OpenClaw plugin documentation for full setup.
"},{"location":"integration-guide/#compression-hooks-advanced","title":"Compression Hooks (Advanced)","text":"Customize compression behavior without modifying Headroom's code:
from headroom import compress, CompressionHooks, CompressContext\n\nclass MyHooks(CompressionHooks):\n def pre_compress(self, messages, ctx):\n # Modify messages before compression (dedup, filter, inject)\n return messages\n\n def compute_biases(self, messages, ctx):\n # Per-message compression aggressiveness\n # >1.0 = keep more, <1.0 = compress more\n return {5: 1.5, 6: 0.5} # Keep message 5, compress message 6\n\n def post_compress(self, event):\n # Observe results (logging, analytics, learning)\n print(f\"Saved {event.tokens_saved} tokens\")\n\nresult = compress(messages, model=\"gpt-4o\", hooks=MyHooks())\n
See Architecture for how hooks integrate with the pipeline.
"},{"location":"integration-guide/#faq","title":"FAQ","text":"Q: Does Headroom change the response format? No. Your LLM returns the same response format. Headroom only modifies the input messages.
Q: What if compression removes something the LLM needs? Headroom stores originals in CCR (Compress-Cache-Retrieve). The LLM can call headroom_retrieve to get full uncompressed content. Compression summaries tell the LLM what's available.
Q: Does it work with streaming? Yes. Compression happens before the request is sent. Streaming responses are unaffected.
Q: How much latency does it add? 15-200ms depending on content size and type. Small JSON arrays take ~15ms, large tool outputs take 100-200ms. The token savings typically save far more time on the LLM side than compression adds \u2014 a 50% token reduction on a Sonnet call saves seconds of generation time. See Latency Benchmarks for real numbers.
"},{"location":"langchain/","title":"LangChain Integration","text":"Headroom provides seamless integration with LangChain, enabling automatic context optimization across all LangChain patterns: chat models, memory, retrievers, agents, and observability.
"},{"location":"langchain/#installation","title":"Installation","text":"pip install \"headroom-ai[langchain]\"\n
This installs Headroom with LangChain dependencies (langchain-core).
"},{"location":"langchain/#quick-start","title":"Quick Start","text":""},{"location":"langchain/#wrap-any-chat-model-1-line","title":"Wrap Any Chat Model (1 Line)","text":"from langchain_openai import ChatOpenAI\nfrom headroom.integrations import HeadroomChatModel\n\n# Wrap your model - that's it!\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Use exactly like before\nresponse = llm.invoke(\"Hello!\")\n
Headroom automatically: - Detects the provider (OpenAI, Anthropic, Google) - Compresses tool outputs in conversation history - Optimizes for provider caching - Tracks token savings
"},{"location":"langchain/#check-your-savings","title":"Check Your Savings","text":"# After some usage\nprint(llm.get_metrics())\n# {'tokens_saved': 12500, 'savings_percent': 45.2, 'requests': 50}\n
"},{"location":"langchain/#integration-patterns","title":"Integration Patterns","text":""},{"location":"langchain/#1-chat-model-wrapper","title":"1. Chat Model Wrapper","text":"The HeadroomChatModel wraps any LangChain BaseChatModel:
from langchain_openai import ChatOpenAI\nfrom langchain_anthropic import ChatAnthropic\nfrom headroom.integrations import HeadroomChatModel\n\n# OpenAI\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Anthropic (auto-detected)\nllm = HeadroomChatModel(ChatAnthropic(model=\"claude-3-5-sonnet-20241022\"))\n\n# Custom configuration\nfrom headroom import HeadroomConfig, HeadroomMode\n\nconfig = HeadroomConfig(\n default_mode=HeadroomMode.OPTIMIZE,\n smart_crusher_target_ratio=0.3, # Target 70% compression\n)\nllm = HeadroomChatModel(\n ChatOpenAI(model=\"gpt-4o\"),\n headroom_config=config,\n)\n
"},{"location":"langchain/#async-support","title":"Async Support","text":"Full async support for ainvoke and astream:
# Async invoke\nresponse = await llm.ainvoke(\"Hello!\")\n\n# Async streaming\nasync for chunk in llm.astream(\"Tell me a story\"):\n print(chunk.content, end=\"\", flush=True)\n
"},{"location":"langchain/#tool-calling","title":"Tool Calling","text":"Works seamlessly with LangChain tool calling:
from langchain_core.tools import tool\n\n@tool\ndef search(query: str) -> str:\n \"\"\"Search the web.\"\"\"\n return {\"results\": [...]} # Large JSON response\n\nllm_with_tools = llm.bind_tools([search])\nresponse = llm_with_tools.invoke(\"Search for Python tutorials\")\n# Tool outputs are automatically compressed in subsequent turns\n
"},{"location":"langchain/#2-memory-integration","title":"2. Memory Integration","text":"HeadroomChatMessageHistory wraps any chat history with automatic compression:
from langchain.memory import ConversationBufferMemory\nfrom langchain_community.chat_message_histories import ChatMessageHistory\nfrom headroom.integrations import HeadroomChatMessageHistory\n\n# Wrap any history\nbase_history = ChatMessageHistory()\ncompressed_history = HeadroomChatMessageHistory(\n base_history,\n compress_threshold_tokens=4000, # Compress when over 4K tokens\n keep_recent_turns=5, # Always keep last 5 turns\n)\n\n# Use with any memory class\nmemory = ConversationBufferMemory(chat_memory=compressed_history)\n\n# Zero changes to your chain!\nchain = ConversationChain(llm=llm, memory=memory)\n
Why this matters: Long conversations can blow up to 50K+ tokens. HeadroomChatMessageHistory automatically compresses older turns while preserving recent context.
# Check compression stats\nprint(compressed_history.get_compression_stats())\n# {'compression_count': 12, 'total_tokens_saved': 28000}\n
"},{"location":"langchain/#3-retriever-integration","title":"3. Retriever Integration","text":"HeadroomDocumentCompressor filters retrieved documents by relevance:
from langchain.retrievers import ContextualCompressionRetriever\nfrom langchain_community.vectorstores import FAISS\nfrom headroom.integrations import HeadroomDocumentCompressor\n\n# Create vector store retriever (retrieve many for recall)\nvectorstore = FAISS.from_documents(documents, embeddings)\nbase_retriever = vectorstore.as_retriever(search_kwargs={\"k\": 50})\n\n# Wrap with Headroom compression (keep best for precision)\ncompressor = HeadroomDocumentCompressor(\n max_documents=10, # Keep top 10\n min_relevance=0.3, # Minimum relevance score\n prefer_diverse=True, # MMR-style diversity\n)\n\nretriever = ContextualCompressionRetriever(\n base_compressor=compressor,\n base_retriever=base_retriever,\n)\n\n# Retrieves 50 docs, returns best 10\ndocs = retriever.invoke(\"What is Python?\")\n
Why this matters: Vector search often returns many marginally-relevant documents. HeadroomDocumentCompressor uses BM25-style scoring to keep only the most relevant ones, reducing context size while improving answer quality.
"},{"location":"langchain/#4-agent-tool-wrapping","title":"4. Agent Tool Wrapping","text":"wrap_tools_with_headroom compresses tool outputs for agents:
from langchain.agents import create_openai_tools_agent, AgentExecutor\nfrom langchain_core.tools import tool\nfrom headroom.integrations import wrap_tools_with_headroom\n\n@tool\ndef search_database(query: str) -> str:\n \"\"\"Search the database.\"\"\"\n # Returns 1000 results as JSON\n return json.dumps({\"results\": [...], \"total\": 1000})\n\n@tool\ndef fetch_logs(service: str) -> str:\n \"\"\"Fetch service logs.\"\"\"\n # Returns 500 log entries\n return json.dumps({\"logs\": [...]})\n\n# Wrap tools with compression\ntools = [search_database, fetch_logs]\nwrapped_tools = wrap_tools_with_headroom(\n tools,\n min_chars_to_compress=1000, # Only compress large outputs\n)\n\n# Create agent with wrapped tools\nagent = create_openai_tools_agent(llm, wrapped_tools, prompt)\nexecutor = AgentExecutor(agent=agent, tools=wrapped_tools)\n\n# Tool outputs are automatically compressed\nresult = executor.invoke({\"input\": \"Find users who logged in yesterday\"})\n
Per-tool metrics:
from headroom.integrations import get_tool_metrics\n\nmetrics = get_tool_metrics()\nprint(metrics.get_summary())\n# {\n# 'total_invocations': 25,\n# 'total_compressions': 18,\n# 'total_chars_saved': 450000,\n# 'by_tool': {\n# 'search_database': {'invocations': 15, 'chars_saved': 320000},\n# 'fetch_logs': {'invocations': 10, 'chars_saved': 130000},\n# }\n# }\n
"},{"location":"langchain/#5-streaming-metrics","title":"5. Streaming Metrics","text":"Track output tokens during streaming:
from headroom.integrations import StreamingMetricsTracker\n\ntracker = StreamingMetricsTracker(model=\"gpt-4o\")\n\nfor chunk in llm.stream(\"Write a poem about coding\"):\n tracker.add_chunk(chunk)\n print(chunk.content, end=\"\", flush=True)\n\nmetrics = tracker.finish()\nprint(f\"\\nOutput tokens: {metrics.output_tokens}\")\nprint(f\"Duration: {metrics.duration_ms:.0f}ms\")\n
Context manager style:
from headroom.integrations import StreamingMetricsCallback\n\nwith StreamingMetricsCallback(model=\"gpt-4o\") as tracker:\n for chunk in llm.stream(messages):\n tracker.add_chunk(chunk)\n print(chunk.content, end=\"\")\n\nprint(f\"Metrics: {tracker.metrics}\")\n
"},{"location":"langchain/#6-langsmith-integration","title":"6. LangSmith Integration","text":"Add Headroom metrics to LangSmith traces:
from headroom.integrations import HeadroomLangSmithCallbackHandler\n\n# Create callback handler\nlangsmith_handler = HeadroomLangSmithCallbackHandler()\n\n# Use with your LLM\nllm = HeadroomChatModel(\n ChatOpenAI(model=\"gpt-4o\"),\n callbacks=[langsmith_handler],\n)\n\n# After calls, metrics appear in LangSmith traces:\n# - headroom.tokens_before\n# - headroom.tokens_after\n# - headroom.tokens_saved\n# - headroom.compression_ratio\n
"},{"location":"langchain/#real-world-examples","title":"Real-World Examples","text":""},{"location":"langchain/#example-1-langgraph-react-agent","title":"Example 1: LangGraph ReAct Agent","text":"The ReAct pattern is the most common agent architecture. Here's how to optimize it:
from langchain_openai import ChatOpenAI\nfrom langchain_core.tools import tool\nfrom langgraph.prebuilt import create_react_agent\nfrom headroom.integrations import HeadroomChatModel, wrap_tools_with_headroom\n\n# Define tools that return large outputs\n@tool\ndef search_web(query: str) -> str:\n \"\"\"Search the web for information.\"\"\"\n # Simulating large search results\n return json.dumps({\n \"results\": [\n {\"title\": f\"Result {i}\", \"snippet\": \"...\" * 100, \"url\": f\"https://...\"}\n for i in range(100)\n ],\n \"total\": 1000,\n })\n\n@tool\ndef query_database(sql: str) -> str:\n \"\"\"Execute SQL query.\"\"\"\n return json.dumps({\n \"rows\": [{\"id\": i, \"data\": \"...\" * 50} for i in range(500)],\n \"total\": 500,\n })\n\n# Wrap model with Headroom\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Wrap tools with compression\ntools = wrap_tools_with_headroom([search_web, query_database])\n\n# Create ReAct agent\nagent = create_react_agent(llm, tools)\n\n# Run - tool outputs are automatically compressed between iterations\nresult = agent.invoke({\n \"messages\": [(\"user\", \"Find all users who signed up last week and their activity\")]\n})\n\n# Check savings\nprint(f\"Tokens saved: {llm.get_metrics()['tokens_saved']}\")\n
Without Headroom: Each tool call adds 10-50K tokens to context. With Headroom: Tool outputs compressed to 1-2K tokens, agent runs faster and cheaper.
"},{"location":"langchain/#example-1b-langgraph-custom-graph-with-compress_tool_messages-node","title":"Example 1b: LangGraph Custom Graph with compress_tool_messages Node","text":"If you're building a custom LangGraph StateGraph (instead of using create_react_agent), you can insert a compression node between tools and the agent. This compresses all ToolMessage content in the graph state before the LLM sees it.
from langchain_openai import ChatOpenAI\nfrom langchain_core.messages import HumanMessage\nfrom langgraph.graph import StateGraph, MessagesState, START, END\nfrom headroom.integrations.langchain import create_compress_tool_messages_node\n\n# Define your agent and tools nodes\ndef agent_node(state: MessagesState):\n llm = ChatOpenAI(model=\"gpt-4o\")\n response = llm.invoke(state[\"messages\"])\n return {\"messages\": [response]}\n\ndef tools_node(state: MessagesState):\n # Your tool execution logic here\n ...\n\n# Build the graph with a compression step\ngraph = StateGraph(MessagesState)\ngraph.add_node(\"agent\", agent_node)\ngraph.add_node(\"tools\", tools_node)\ngraph.add_node(\"compress\", create_compress_tool_messages_node(\n min_tokens_to_compress=100, # Only compress outputs > ~100 tokens\n))\n\n# Wire: tools -> compress -> agent (instead of tools -> agent directly)\ngraph.add_edge(START, \"agent\")\ngraph.add_edge(\"tools\", \"compress\")\ngraph.add_edge(\"compress\", \"agent\")\n# ... add conditional edges from agent to tools/END as needed\n\napp = graph.compile()\nresult = app.invoke({\"messages\": [HumanMessage(content=\"Find sales data\")]})\n
You can also use compress_tool_messages directly as a standalone function:
from headroom.integrations.langchain import compress_tool_messages\n\n# Compress ToolMessages in any list of LangChain messages\nresult = compress_tool_messages(messages, min_tokens_to_compress=100)\ncompressed_messages = result.messages\nprint(f\"Saved {result.total_tokens_saved} tokens across {result.messages_compressed} messages\")\n
"},{"location":"langchain/#example-2-rag-pipeline-with-document-filtering","title":"Example 2: RAG Pipeline with Document Filtering","text":"from langchain_openai import ChatOpenAI, OpenAIEmbeddings\nfrom langchain_community.vectorstores import Chroma\nfrom langchain.chains import RetrievalQA\nfrom langchain.retrievers import ContextualCompressionRetriever\nfrom headroom.integrations import HeadroomChatModel, HeadroomDocumentCompressor\n\n# Setup vector store\nembeddings = OpenAIEmbeddings()\nvectorstore = Chroma.from_documents(documents, embeddings)\n\n# High-recall retriever (get many candidates)\nbase_retriever = vectorstore.as_retriever(search_kwargs={\"k\": 50})\n\n# Headroom compressor for precision\ncompressor = HeadroomDocumentCompressor(\n max_documents=5, # Keep only top 5\n min_relevance=0.4, # Must be 40%+ relevant\n prefer_diverse=True, # Avoid redundant docs\n)\n\n# Combine into compression retriever\nretriever = ContextualCompressionRetriever(\n base_compressor=compressor,\n base_retriever=base_retriever,\n)\n\n# Wrap LLM\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Create QA chain\nqa_chain = RetrievalQA.from_chain_type(\n llm=llm,\n retriever=retriever,\n return_source_documents=True,\n)\n\n# Query - retrieves 50 docs, uses best 5\nresult = qa_chain.invoke({\"query\": \"How do I configure authentication?\"})\nprint(f\"Answer: {result['result']}\")\nprint(f\"Sources: {len(result['source_documents'])} docs\")\n
Impact: - Without filtering: 50 docs \u00d7 ~500 tokens = 25K context tokens - With Headroom: 5 docs \u00d7 ~500 tokens = 2.5K context tokens (90% reduction)
"},{"location":"langchain/#example-3-conversational-agent-with-memory","title":"Example 3: Conversational Agent with Memory","text":"from langchain_openai import ChatOpenAI\nfrom langchain.memory import ConversationBufferMemory\nfrom langchain_community.chat_message_histories import ChatMessageHistory\nfrom langchain.chains import ConversationChain\nfrom headroom.integrations import HeadroomChatModel, HeadroomChatMessageHistory\n\n# Wrap LLM\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\n\n# Wrap memory with auto-compression\nbase_history = ChatMessageHistory()\ncompressed_history = HeadroomChatMessageHistory(\n base_history,\n compress_threshold_tokens=8000, # Compress when over 8K\n keep_recent_turns=10, # Always keep last 10 turns\n)\n\nmemory = ConversationBufferMemory(\n chat_memory=compressed_history,\n return_messages=True,\n)\n\n# Create conversation chain\nchain = ConversationChain(llm=llm, memory=memory)\n\n# Long conversation - memory auto-compresses\nfor i in range(100):\n response = chain.invoke({\"input\": f\"Tell me about topic {i}\"})\n print(f\"Turn {i}: {len(response['response'])} chars\")\n\n# Check memory stats\nprint(compressed_history.get_compression_stats())\n# {'compression_count': 8, 'total_tokens_saved': 45000}\n
Impact: Without compression, 100-turn conversation = 100K+ tokens. With HeadroomChatMessageHistory, it stays under 8K tokens while preserving recent context.
"},{"location":"langchain/#example-4-multi-tool-research-agent","title":"Example 4: Multi-Tool Research Agent","text":"from langchain_openai import ChatOpenAI\nfrom langchain.agents import AgentExecutor, create_openai_tools_agent\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.tools import tool\nfrom headroom.integrations import (\n HeadroomChatModel,\n wrap_tools_with_headroom,\n get_tool_metrics,\n reset_tool_metrics,\n)\n\n@tool\ndef search_arxiv(query: str) -> str:\n \"\"\"Search arXiv for papers.\"\"\"\n return json.dumps({\"papers\": [{\"title\": f\"Paper {i}\", \"abstract\": \"...\" * 200} for i in range(50)]})\n\n@tool\ndef search_github(query: str) -> str:\n \"\"\"Search GitHub repositories.\"\"\"\n return json.dumps({\"repos\": [{\"name\": f\"repo-{i}\", \"description\": \"...\" * 100, \"stars\": i * 100} for i in range(100)]})\n\n@tool\ndef fetch_documentation(url: str) -> str:\n \"\"\"Fetch documentation from URL.\"\"\"\n return \"...\" * 5000 # Large doc content\n\n# Wrap everything\nllm = HeadroomChatModel(ChatOpenAI(model=\"gpt-4o\"))\ntools = wrap_tools_with_headroom([search_arxiv, search_github, fetch_documentation])\n\nprompt = ChatPromptTemplate.from_messages([\n (\"system\", \"You are a research assistant. Use tools to gather information.\"),\n (\"human\", \"{input}\"),\n (\"placeholder\", \"{agent_scratchpad}\"),\n])\n\nagent = create_openai_tools_agent(llm, tools, prompt)\nexecutor = AgentExecutor(agent=agent, tools=tools, verbose=True)\n\n# Reset metrics for this session\nreset_tool_metrics()\n\n# Run complex research task\nresult = executor.invoke({\n \"input\": \"Research the latest advances in LLM context compression and find relevant GitHub projects\"\n})\n\n# Check per-tool metrics\nmetrics = get_tool_metrics().get_summary()\nprint(f\"Total chars saved: {metrics['total_chars_saved']:,}\")\nprint(f\"Per-tool breakdown: {metrics['by_tool']}\")\n
"},{"location":"langchain/#configuration-options","title":"Configuration Options","text":""},{"location":"langchain/#headroomchatmodel","title":"HeadroomChatModel","text":"HeadroomChatModel(\n wrapped_model, # Any LangChain BaseChatModel\n headroom_config=HeadroomConfig(), # Headroom configuration\n auto_detect_provider=True, # Auto-detect from wrapped model\n)\n
"},{"location":"langchain/#headroomchatmessagehistory","title":"HeadroomChatMessageHistory","text":"HeadroomChatMessageHistory(\n base_history, # Any BaseChatMessageHistory\n compress_threshold_tokens=4000, # Token threshold for compression\n keep_recent_turns=5, # Minimum turns to preserve\n model=\"gpt-4o\", # Model for token counting\n)\n
"},{"location":"langchain/#headroomdocumentcompressor","title":"HeadroomDocumentCompressor","text":"HeadroomDocumentCompressor(\n max_documents=10, # Maximum docs to return\n min_relevance=0.0, # Minimum relevance score (0-1)\n prefer_diverse=False, # Use MMR for diversity\n)\n
"},{"location":"langchain/#wrap_tools_with_headroom","title":"wrap_tools_with_headroom","text":"wrap_tools_with_headroom(\n tools, # List of LangChain tools\n min_chars_to_compress=1000, # Minimum output size\n smart_crusher_config=None, # SmartCrusher configuration\n)\n
"},{"location":"langchain/#import-reference","title":"Import Reference","text":"from headroom.integrations import (\n # Chat Model\n HeadroomChatModel,\n\n # Memory\n HeadroomChatMessageHistory,\n\n # Retrievers\n HeadroomDocumentCompressor,\n\n # Agents\n HeadroomToolWrapper,\n wrap_tools_with_headroom,\n get_tool_metrics,\n reset_tool_metrics,\n\n # Streaming\n StreamingMetricsTracker,\n StreamingMetricsCallback,\n track_streaming_response,\n\n # LangSmith\n HeadroomLangSmithCallbackHandler,\n\n # Provider Detection\n detect_provider,\n get_headroom_provider,\n)\n\n# Or import from subpackage directly\nfrom headroom.integrations.langchain import HeadroomChatModel\nfrom headroom.integrations.langchain.memory import HeadroomChatMessageHistory\n
"},{"location":"langchain/#troubleshooting","title":"Troubleshooting","text":""},{"location":"langchain/#langchain-not-detected","title":"LangChain not detected","text":"from headroom.integrations import langchain_available\n\nif not langchain_available():\n print(\"Install with: pip install headroom-ai[langchain]\")\n
"},{"location":"langchain/#provider-detection-failing","title":"Provider detection failing","text":"# Force a specific provider\nfrom headroom.providers import AnthropicProvider\n\nllm = HeadroomChatModel(\n ChatAnthropic(model=\"claude-3-5-sonnet-20241022\"),\n auto_detect_provider=False,\n)\nllm._provider = AnthropicProvider()\n
"},{"location":"langchain/#memory-not-compressing","title":"Memory not compressing","text":"Check that your message count exceeds the threshold:
history = HeadroomChatMessageHistory(\n base_history,\n compress_threshold_tokens=1000, # Lower threshold\n keep_recent_turns=2, # Fewer preserved turns\n)\n
"},{"location":"langchain/#performance-tips","title":"Performance Tips","text":" - Use tool wrapping for agents - Agents with tools benefit most from compression
- Set appropriate thresholds - Don't compress small conversations
- Enable diversity for RAG -
prefer_diverse=True improves answer quality - Monitor with LangSmith - Use the callback handler to track savings over time
- Batch similar requests - Provider caching works better with stable prefixes
"},{"location":"learn/","title":"Headroom Learn","text":"Offline failure learning for coding agents. Analyzes past conversations, finds what went wrong, correlates it with what eventually worked, and writes specific project-level learnings that prevent the same mistakes next session.
"},{"location":"learn/#quick-start","title":"Quick Start","text":"# See recommendations for current project (dry-run, no changes)\nheadroom learn\n\n# Write recommendations to CLAUDE.md and MEMORY.md\nheadroom learn --apply\n\n# Analyze a specific project\nheadroom learn --project ~/my-project --apply\n\n# Analyze all projects\nheadroom learn --all --apply\n
"},{"location":"learn/#how-it-works","title":"How It Works","text":"Past Sessions \u2192 Plugin \u2192 Analyzer \u2192 Writer \u2192 Agent-native context file\n \u2502 \u2502 \u2502\n \u2502 \u2502 \u2514\u2500 Writes marker-delimited sections\n \u2502 \u2502 (replaced on re-run, not duplicated)\n \u2502 \u2502\n \u2502 \u2514\u2500 LLM-based analysis: finds failure patterns,\n \u2502 success correlations, and actionable rules\n \u2502\n \u2514\u2500 Plugin reads agent-specific logs:\n \u2022 Claude Code: ~/.claude/projects/*.jsonl\n \u2022 Codex: ~/.codex/sessions/*.json\n \u2022 Gemini CLI: ~/.gemini/tmp/*/chats/session-*.json\n
"},{"location":"learn/#success-correlation","title":"Success Correlation","text":"The core innovation. Instead of cataloging failures (\"Read failed 5 times\"), Headroom finds what the model did to fix each failure:
- Failed:
Read axion-formats/src/main/java/.../FirstClassEntity.java - Then succeeded:
Read axion-scala-common/src/main/scala/.../FirstClassEntity.scala - Learning: \"
FirstClassEntity is at axion-scala-common/, not axion-formats/\"
This produces specific, actionable corrections \u2014 not generic advice.
"},{"location":"learn/#what-it-learns","title":"What It Learns","text":""},{"location":"learn/#1-environment-facts-claudemd","title":"1. Environment Facts \u2192 CLAUDE.md","text":"Which runtime commands work vs fail.
### Environment\n- **Python**: use `uv run python` (not `python3` \u2014 modules not available outside venv)\n
"},{"location":"learn/#2-file-path-corrections-claudemd","title":"2. File Path Corrections \u2192 CLAUDE.md","text":"Wrong paths the model keeps guessing, with the correct locations.
### File Path Corrections\n- `axion-common/src/.../AxionSparkConstants.scala`\n \u2192 actually at `axion-spark-common/src/.../AxionSparkConstants.scala`\n
"},{"location":"learn/#3-search-scope-claudemd","title":"3. Search Scope \u2192 CLAUDE.md","text":"Which directories to search in (narrow paths fail, broader ones work).
### Search Scope\n- Don't search `axion-model/` \u2192 use `axion/` (the repo root)\n
"},{"location":"learn/#4-command-patterns-claudemd","title":"4. Command Patterns \u2192 CLAUDE.md","text":"How commands should (and shouldn't) be run.
### Command Patterns\n- **user_prefers_manual**: User rejected gradle 18 times \u2014 show the command, don't execute\n- **python_runtime**: Use `uv run python` not `python3` (ModuleNotFoundError)\n
"},{"location":"learn/#5-known-large-files-claudemd","title":"5. Known Large Files \u2192 CLAUDE.md","text":"Files that need offset/limit with Read.
### Known Large Files\n- `proxy/server.py` (~8000 lines) \u2014 always use offset/limit\n
"},{"location":"learn/#6-retry-prevention-memorymd","title":"6. Retry Prevention \u2192 MEMORY.md","text":"Specific suggestions derived from actual corrections.
"},{"location":"learn/#7-permission-notes-memorymd","title":"7. Permission Notes \u2192 MEMORY.md","text":"Commands repeatedly rejected \u2014 model should suggest them to the user instead.
"},{"location":"learn/#where-learnings-go","title":"Where Learnings Go","text":"Pattern Claude Code Codex Gemini CLI Environment, paths, commands CLAUDE.md AGENTS.md GEMINI.md Retry patterns, permissions MEMORY.md instructions.md GEMINI.md Output files are agent-native: Claude Code uses CLAUDE.md/MEMORY.md, Codex uses AGENTS.md, Gemini uses GEMINI.md. The same learnings, written to the format each agent reads.
"},{"location":"learn/#marker-based-updates","title":"Marker-Based Updates","text":"Headroom manages a clearly-delimited section in each file:
<!-- headroom:learn:start -->\n## Headroom Learned Patterns\n*Auto-generated by `headroom learn` \u2014 do not edit manually*\n...\n<!-- headroom:learn:end -->\n
On re-run, only the content between markers is replaced. Your existing file content is preserved.
"},{"location":"learn/#architecture-plugin-system","title":"Architecture (Plugin System)","text":"Headroom Learn uses a plugin architecture where each agent is a self-contained plugin:
Plugin Registry (auto-discovered)\n\u251c\u2500\u2500 ClaudeCodePlugin \u2192 Analyzer (LLM) \u2192 ClaudeCodeWriter \u2192 CLAUDE.md / MEMORY.md\n\u251c\u2500\u2500 CodexPlugin \u2192 Analyzer (LLM) \u2192 CodexWriter \u2192 AGENTS.md / instructions.md\n\u251c\u2500\u2500 GeminiPlugin \u2192 Analyzer (LLM) \u2192 GeminiWriter \u2192 GEMINI.md\n\u2514\u2500\u2500 (your plugin) \u2192 Analyzer (LLM) \u2192 (your writer) \u2192 (your file)\n
Plugins bundle scanning, detection, and writing for one agent. Built-in plugins are auto-discovered from headroom.learn.plugins.*. External plugins register via the headroom.learn_plugin entry point.
The Analyzer is shared \u2014 it uses an LLM (Sonnet, GPT-4o, or Gemini Flash) to find patterns. Same analysis for any agent.
"},{"location":"learn/#adding-support-for-a-new-agent","title":"Adding Support for a New Agent","text":" - Create
headroom/learn/plugins/myagent.py - Implement
LearnPlugin + ConversationScanner (scanner + writer + detection) - Add
plugin = MyAgentPlugin() at module scope - Done \u2014
headroom learn --agent myagent works automatically
Or install an external plugin: pip install headroom-learn-cursor (registers via entry point).
"},{"location":"learn/#cli-reference","title":"CLI Reference","text":"headroom learn [OPTIONS]\n\nOptions:\n --project PATH Project directory (default: current directory)\n --all Analyze all discovered projects\n --apply Write recommendations (default: dry-run)\n --agent [auto|claude|codex|gemini]\n Which agent to analyze (default: auto-detect)\n --model TEXT LLM for analysis (default: auto from API keys)\n
"},{"location":"learn/#supported-agents","title":"Supported Agents","text":"Agent Scanner Writer Output Files Claude Code Reads ~/.claude/projects/*.jsonl ClaudeCodeWriter CLAUDE.md, MEMORY.md OpenAI Codex Reads ~/.codex/sessions/*.json CodexWriter AGENTS.md, instructions.md Gemini CLI Reads ~/.gemini/tmp/*/chats/session-*.json GeminiWriter GEMINI.md"},{"location":"learn/#real-world-results","title":"Real-World Results","text":"Tested on 67,583 tool calls across 23 projects:
Metric Value Failure rate 7.5% (5,066 failures) Corrections extracted 164 per project (avg) Specific path corrections 22 (axion project) Search scope corrections 24 (axion project) Command patterns learned 5 (axion project) Estimated preventable waste ~27 MB across corpus"},{"location":"llmlingua/","title":"LLMLingua-2 Integration","text":"For maximum compression, Headroom integrates with LLMLingua-2, Microsoft's BERT-based token classifier trained via GPT-4 distillation. It achieves up to 20x compression while preserving semantic meaning.
"},{"location":"llmlingua/#when-to-use-llmlingua-2","title":"When to Use LLMLingua-2","text":"Approach Best For Compression Speed SmartCrusher JSON tool outputs 70-90% ~1ms Text Utilities Search/logs 50-90% ~1ms LLMLingua-2 Any text, max compression 80-95% ~50-200ms LLMLingua-2 is ideal when you need maximum compression and can tolerate slightly higher latency (e.g., compressing large tool outputs before storage, offline processing).
"},{"location":"llmlingua/#installation","title":"Installation","text":"# Adds ~2GB of model weights\npip install \"headroom-ai[llmlingua]\"\n
"},{"location":"llmlingua/#basic-usage","title":"Basic Usage","text":"from headroom.transforms import LLMLinguaCompressor\n\n# Create compressor (model loaded lazily on first use)\ncompressor = LLMLinguaCompressor()\n\n# Compress any text\nlong_output = \"The function processUserData takes a user object and validates...\"\nresult = compressor.compress(long_output)\n\nprint(f\"Before: {result.original_tokens} tokens\")\nprint(f\"After: {result.compressed_tokens} tokens\")\nprint(f\"Saved: {result.savings_percentage:.1f}%\")\nprint(result.compressed)\n
"},{"location":"llmlingua/#content-aware-compression","title":"Content-Aware Compression","text":"LLMLingua-2 automatically adjusts compression based on content type:
from headroom.transforms import LLMLinguaCompressor, LLMLinguaConfig\n\n# Conservative for code (keep 40% of tokens)\nconfig = LLMLinguaConfig(\n code_compression_rate=0.4, # More conservative\n json_compression_rate=0.35, # Moderate\n text_compression_rate=0.25, # Aggressive\n)\n\ncompressor = LLMLinguaCompressor(config)\n\n# Auto-detects content type\ncode_result = compressor.compress(\"def calculate(x): return x * 2\")\ntext_result = compressor.compress(\"This is a verbose explanation...\")\n
"},{"location":"llmlingua/#memory-management","title":"Memory Management","text":"The model uses ~1GB RAM. Unload it when done:
from headroom.transforms import (\n LLMLinguaCompressor,\n unload_llmlingua_model,\n is_llmlingua_model_loaded,\n)\n\ncompressor = LLMLinguaCompressor()\nresult = compressor.compress(content) # Model loaded here\n\n# Check if loaded\nprint(is_llmlingua_model_loaded()) # True\n\n# Free memory when done\nunload_llmlingua_model() # Frees ~1GB\nprint(is_llmlingua_model_loaded()) # False\n\n# Next compression will reload automatically\n
"},{"location":"llmlingua/#device-configuration","title":"Device Configuration","text":"from headroom.transforms import LLMLinguaConfig, LLMLinguaCompressor\n\n# Force CPU (slower but works everywhere)\nconfig = LLMLinguaConfig(device=\"cpu\")\n\n# Force GPU (faster but needs CUDA)\nconfig = LLMLinguaConfig(device=\"cuda\")\n\n# Auto-detect (default): uses CUDA > MPS > CPU\nconfig = LLMLinguaConfig(device=\"auto\")\n\ncompressor = LLMLinguaCompressor(config)\n
"},{"location":"llmlingua/#use-in-pipeline","title":"Use in Pipeline","text":"from headroom.transforms import TransformPipeline, LLMLinguaCompressor, SmartCrusher\n\n# Combine with other transforms\npipeline = TransformPipeline([\n SmartCrusher(), # First: compress JSON\n LLMLinguaCompressor(), # Then: ML compression on remaining text\n])\n\nresult = pipeline.apply(messages, tokenizer)\n
"},{"location":"llmlingua/#proxy-integration","title":"Proxy Integration","text":"Enable LLMLingua in the proxy server for automatic ML compression:
# Enable LLMLingua in proxy (requires: pip install headroom-ai[llmlingua,proxy])\nheadroom proxy --llmlingua\n\n# With custom settings\nheadroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.4\n\n# The proxy shows LLMLingua status at startup:\n# LLMLingua: ENABLED (device=cuda, rate=0.4)\n#\n# If llmlingua is installed but not enabled, you'll see a helpful hint:\n# LLMLingua: available (enable with --llmlingua for ML compression)\n
"},{"location":"llmlingua/#configuration-reference","title":"Configuration Reference","text":"Option Default Description device \"auto\" Device to run model on: auto, cpu, cuda, mps code_compression_rate 0.4 Keep 40% of tokens for code json_compression_rate 0.35 Keep 35% of tokens for JSON text_compression_rate 0.25 Keep 25% of tokens for text force_tokens [] Tokens to always preserve drop_consecutive True Drop consecutive whitespace"},{"location":"llmlingua/#performance-characteristics","title":"Performance Characteristics","text":"Metric Value Model size ~500MB Memory usage ~1GB RAM Cold start 10-30s (first load) Inference 50-200ms per request Compression 80-95%"},{"location":"llmlingua/#why-opt-in","title":"Why Opt-In?","text":"LLMLingua adds significant dependencies and overhead:
Aspect Default Proxy With LLMLingua Dependencies ~50MB ~2GB Cold start <1s 10-30s Per-request ~1-5ms ~50-200ms Compression 70-90% 80-95% The default proxy is lightweight and fast. Enable LLMLingua when you need maximum compression and can accept the tradeoffs.
"},{"location":"llmlingua/#troubleshooting","title":"Troubleshooting","text":""},{"location":"llmlingua/#model-not-found","title":"\"Model not found\"","text":"# Ensure llmlingua extra is installed\npip install \"headroom-ai[llmlingua]\"\n
"},{"location":"llmlingua/#cuda-out-of-memory","title":"\"CUDA out of memory\"","text":"# Force CPU mode\nconfig = LLMLinguaConfig(device=\"cpu\")\n
"},{"location":"llmlingua/#slow-compression","title":"\"Slow compression\"","text":" - Use GPU if available:
device=\"cuda\" - Batch multiple compressions
- Consider using SmartCrusher for JSON (faster, similar results)
"},{"location":"macos-deployment/","title":"macOS Deployment Guide","text":"This guide covers deploying the headroom proxy server as a background service on macOS using LaunchAgent. The service will start automatically on login and restart on crash.
"},{"location":"macos-deployment/#overview","title":"Overview","text":"macOS LaunchAgent provides a native way to run background services with:
- Automatic startup on user login
- Crash recovery with automatic restart
- Standard logging to
~/Library/Logs/ - Native lifecycle management via
launchctl
This is ideal for local development environments where you want \"set and forget\" proxy configuration.
"},{"location":"macos-deployment/#prerequisites","title":"Prerequisites","text":" - macOS 10.13+ (High Sierra or later)
- headroom-ai installed with proxy support
- Anthropic API key configured
"},{"location":"macos-deployment/#installing-headroom-with-proxy-support","title":"Installing Headroom with Proxy Support","text":"# Install with proxy support\npip install headroom-ai[proxy]\n\n# Verify installation\nheadroom proxy --help\n
"},{"location":"macos-deployment/#api-key-configuration","title":"API Key Configuration","text":"Your Anthropic API key can be configured in several ways:
Option 1: Shell environment (recommended)
# Add to ~/.bashrc or ~/.zshrc\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n
Option 2: LaunchAgent plist
<key>EnvironmentVariables</key>\n<dict>\n <key>ANTHROPIC_API_KEY</key>\n <string>sk-ant-...</string>\n</dict>\n
Option 3: System environment
# Add to /etc/launchd.conf (requires admin)\nsetenv ANTHROPIC_API_KEY sk-ant-...\n
"},{"location":"macos-deployment/#quick-install","title":"Quick Install","text":"The automated installer handles all setup:
# Clone or navigate to headroom repository\ncd examples/deployment/macos-launchagent\n\n# Run installer\n./install.sh\n
The installer will:
- Detect your headroom installation
- Prompt for port configuration (default: 8787)
- Create log directory
- Generate LaunchAgent plist
- Load and start the service
- Verify service is running
"},{"location":"macos-deployment/#installation-options","title":"Installation Options","text":"Custom port:
./install.sh --port 9000\n
Unattended install (no prompts):
./install.sh --port 8787 --unattended\n
Reinstall over existing:
# Installer will prompt to reinstall if service exists\n./install.sh\n
"},{"location":"macos-deployment/#manual-installation","title":"Manual Installation","text":"If you prefer full control over the installation:
"},{"location":"macos-deployment/#step-1-create-log-directory","title":"Step 1: Create Log Directory","text":"mkdir -p ~/Library/Logs/headroom\n
"},{"location":"macos-deployment/#step-2-generate-launchagent-plist","title":"Step 2: Generate LaunchAgent Plist","text":"Copy and customize the template:
cd examples/deployment/macos-launchagent\ncp com.headroom.proxy.plist.template ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Edit ~/Library/LaunchAgents/com.headroom.proxy.plist:
- Replace
__HEADROOM_PATH__ with your headroom path:
command -v headroom\n# Example output: /usr/local/bin/headroom\n
-
Replace __PORT__ with your desired port (e.g., 8787)
-
Replace __HOME__ with your home directory:
echo $HOME\n# Example output: /Users/yourusername\n
"},{"location":"macos-deployment/#step-3-load-the-launchagent","title":"Step 3: Load the LaunchAgent","text":"launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.headroom.proxy.plist\n
"},{"location":"macos-deployment/#step-4-verify-service","title":"Step 4: Verify Service","text":"# Check if service is running\nlaunchctl print gui/$(id -u)/com.headroom.proxy\n\n# Check if port is listening\nlsof -iTCP:8787 -sTCP:LISTEN\n\n# Test health endpoint\ncurl http://localhost:8787/health\n
"},{"location":"macos-deployment/#configuration","title":"Configuration","text":""},{"location":"macos-deployment/#port-customization","title":"Port Customization","text":"The default port is 8787. To use a custom port:
During installation:
./install.sh --port 9000\n
After installation:
- Uninstall:
./uninstall.sh - Reinstall with new port:
./install.sh --port 9000 - Update shell integration:
export HEADROOM_PROXY_PORT=9000
"},{"location":"macos-deployment/#log-location","title":"Log Location","text":"Logs are written to standard macOS locations:
- Standard output:
~/Library/Logs/headroom/proxy.log - Error output:
~/Library/Logs/headroom/proxy-error.log
To change log locations, edit the plist:
<key>StandardOutPath</key>\n<string>/custom/path/proxy.log</string>\n
"},{"location":"macos-deployment/#environment-variables","title":"Environment Variables","text":"Configure additional options in the plist EnvironmentVariables section:
<key>EnvironmentVariables</key>\n<dict>\n <!-- Required: Proxy port -->\n <key>HEADROOM_PROXY_PORT</key>\n <string>8787</string>\n\n <!-- Optional: API key (or set in shell) -->\n <key>ANTHROPIC_API_KEY</key>\n <string>sk-ant-...</string>\n\n <!-- Optional: Enable LLMLingua compression -->\n <key>HEADROOM_COMPRESSION_PROVIDER</key>\n <string>llmlingua</string>\n\n <!-- Optional: LLMLingua device (auto, cuda, cpu, mps) -->\n <key>HEADROOM_LLMLINGUA_DEVICE</key>\n <string>mps</string>\n</dict>\n
Note: LLMLingua requires additional installation:
pip install headroom-ai[llmlingua]\n
"},{"location":"macos-deployment/#crash-recovery","title":"Crash Recovery","text":"The LaunchAgent is configured with:
- KeepAlive: Automatically restarts on crash
- ThrottleInterval: 10 seconds between restart attempts
To disable automatic restart, edit the plist:
<key>KeepAlive</key>\n<false/>\n
"},{"location":"macos-deployment/#shell-integration","title":"Shell Integration","text":"Automatically configure your shell to use the proxy when available.
"},{"location":"macos-deployment/#setup","title":"Setup","text":"Add to ~/.bashrc (bash) or ~/.zshrc (zsh):
# Configure port (optional, defaults to 8787)\nexport HEADROOM_PROXY_PORT=8787\n\n# Source shell integration\nsource /path/to/headroom/examples/deployment/macos-launchagent/shell-integration.sh\n
"},{"location":"macos-deployment/#what-it-does","title":"What It Does","text":"The shell integration script:
- Checks if proxy is running on configured port
- If running, sets
ANTHROPIC_BASE_URL=http://localhost:8787 - If not running, attempts to start the LaunchAgent
- Provides status messages on first load
This makes Claude clients automatically use the proxy without manual configuration.
"},{"location":"macos-deployment/#manual-configuration","title":"Manual Configuration","text":"If you prefer not to use shell integration:
# Add to ~/.bashrc or ~/.zshrc\nexport ANTHROPIC_BASE_URL=http://localhost:8787\n
"},{"location":"macos-deployment/#service-management","title":"Service Management","text":""},{"location":"macos-deployment/#check-status","title":"Check Status","text":"# View service status\nlaunchctl print gui/$(id -u)/com.headroom.proxy\n\n# Check if port is listening\nlsof -iTCP:8787 -sTCP:LISTEN\n\n# Test health endpoint\ncurl http://localhost:8787/health\n
"},{"location":"macos-deployment/#view-logs","title":"View Logs","text":"# Tail standard output\ntail -f ~/Library/Logs/headroom/proxy.log\n\n# Tail error output\ntail -f ~/Library/Logs/headroom/proxy-error.log\n\n# View last 50 lines\ntail -n 50 ~/Library/Logs/headroom/proxy-error.log\n
"},{"location":"macos-deployment/#restart-service","title":"Restart Service","text":"# Graceful restart (stop and let KeepAlive restart it)\nlaunchctl kickstart -k gui/$(id -u)/com.headroom.proxy\n\n# Manual stop/start\nlaunchctl bootout gui/$(id -u)/com.headroom.proxy\nlaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.headroom.proxy.plist\n
"},{"location":"macos-deployment/#stop-service-temporarily","title":"Stop Service Temporarily","text":"# Disable without uninstalling\nlaunchctl disable gui/$(id -u)/com.headroom.proxy\n\n# Re-enable\nlaunchctl enable gui/$(id -u)/com.headroom.proxy\n
"},{"location":"macos-deployment/#verification","title":"Verification","text":"After installation, verify everything is working:
"},{"location":"macos-deployment/#1-check-service-status","title":"1. Check Service Status","text":"launchctl print gui/$(id -u)/com.headroom.proxy\n
Expected output includes:
state = running\n
"},{"location":"macos-deployment/#2-check-port","title":"2. Check Port","text":"lsof -iTCP:8787 -sTCP:LISTEN\n
Should show headroom listening on port 8787.
"},{"location":"macos-deployment/#3-test-health-endpoint","title":"3. Test Health Endpoint","text":"curl http://localhost:8787/health\n
Expected response:
{\"status\": \"healthy\"}\n
"},{"location":"macos-deployment/#4-test-proxy-functionality","title":"4. Test Proxy Functionality","text":"# Set base URL\nexport ANTHROPIC_BASE_URL=http://localhost:8787\n\n# Test with Python\npython -c \"\nimport anthropic\nclient = anthropic.Anthropic()\nresponse = client.messages.create(\n model='claude-3-5-sonnet-20241022',\n max_tokens=50,\n messages=[{'role': 'user', 'content': 'Hi'}]\n)\nprint(response.content[0].text)\n\"\n
"},{"location":"macos-deployment/#5-check-logs-for-errors","title":"5. Check Logs for Errors","text":"tail -n 20 ~/Library/Logs/headroom/proxy-error.log\n
Should show no errors. Common startup errors are listed in Troubleshooting.
"},{"location":"macos-deployment/#troubleshooting","title":"Troubleshooting","text":""},{"location":"macos-deployment/#service-wont-start","title":"Service Won't Start","text":"Symptom: launchctl print shows service not loaded or failed state
Check logs:
tail -n 50 ~/Library/Logs/headroom/proxy-error.log\n
Common causes:
Error Solution ANTHROPIC_API_KEY not set Set API key in environment or plist ModuleNotFoundError: No module named 'headroom' Install: pip install headroom-ai[proxy] command not found: headroom Update plist with correct path: command -v headroom Address already in use Change port or stop conflicting service"},{"location":"macos-deployment/#port-already-in-use","title":"Port Already in Use","text":"Symptom: Service starts but port not listening, logs show \"Address already in use\"
Find what's using the port:
lsof -iTCP:8787 -sTCP:LISTEN\n
Solutions:
- Stop conflicting service
- Use different port:
./uninstall.sh && ./install.sh --port 9000
"},{"location":"macos-deployment/#service-crashes-immediately","title":"Service Crashes Immediately","text":"Symptom: Service starts but immediately exits
Check for Python errors:
tail -f ~/Library/Logs/headroom/proxy-error.log\n
Common causes:
- Missing dependencies:
pip install headroom-ai[proxy] - Invalid API key: Verify
ANTHROPIC_API_KEY - Python version incompatible: Requires Python 3.9+
"},{"location":"macos-deployment/#anthropic_base_url-not-set","title":"ANTHROPIC_BASE_URL Not Set","text":"Symptom: Shell integration not setting environment variable
Verify proxy is running:
curl http://localhost:8787/health\n
Reload shell configuration:
source ~/.bashrc # or ~/.zshrc\n
Check shell integration is sourced:
# Should be set to 1\necho $HEADROOM_SHELL_INTEGRATION_LOADED\n
"},{"location":"macos-deployment/#service-not-auto-starting-on-login","title":"Service Not Auto-Starting on Login","text":"Symptom: Service doesn't start after reboot
Verify LaunchAgent is loaded:
launchctl list | grep headroom\n
If not listed:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Check RunAtLoad is enabled:
grep -A1 RunAtLoad ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Should show:
<key>RunAtLoad</key>\n<true/>\n
"},{"location":"macos-deployment/#permission-issues","title":"Permission Issues","text":"Symptom: \"Operation not permitted\" errors
Ensure plist has correct permissions:
chmod 644 ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Verify ownership:
ls -l ~/Library/LaunchAgents/com.headroom.proxy.plist\n
Should be owned by your user, not root.
"},{"location":"macos-deployment/#uninstallation","title":"Uninstallation","text":""},{"location":"macos-deployment/#quick-uninstall","title":"Quick Uninstall","text":"cd examples/deployment/macos-launchagent\n./uninstall.sh\n
This will:
- Stop the service
- Remove LaunchAgent plist
- Optionally remove log directory (prompts)
"},{"location":"macos-deployment/#remove-everything","title":"Remove Everything","text":"# Uninstall service and remove logs\n./uninstall.sh --remove-logs\n\n# Remove shell integration from ~/.bashrc or ~/.zshrc\n# Delete or comment out:\n# export HEADROOM_PROXY_PORT=8787\n# source .../shell-integration.sh\n
"},{"location":"macos-deployment/#manual-uninstall","title":"Manual Uninstall","text":"# Stop service\nlaunchctl bootout gui/$(id -u)/com.headroom.proxy\n\n# Remove plist\nrm ~/Library/LaunchAgents/com.headroom.proxy.plist\n\n# Remove logs (optional)\nrm -rf ~/Library/Logs/headroom\n
"},{"location":"macos-deployment/#production-deployment","title":"Production Deployment","text":"For production environments, consider:
- System-wide LaunchDaemon instead of per-user LaunchAgent
- Resource limits in plist (CPU, memory)
- Log rotation for long-running deployments
- Monitoring via external tools
- Multiple instances on different ports for redundancy
LaunchAgent is designed for single-user development. For production, evaluate:
- Docker deployment for containerized environments
- systemd on Linux servers
- Cloud-native solutions (ECS, Cloud Run, etc.)
"},{"location":"macos-deployment/#related-documentation","title":"Related Documentation","text":" - Proxy Server Documentation - Core proxy configuration and features
- Configuration Guide - Detailed configuration options
- Architecture - How Headroom works internally
- Troubleshooting - General troubleshooting guide
"},{"location":"macos-deployment/#platform-alternatives","title":"Platform Alternatives","text":" - Linux: Use systemd instead of LaunchAgent
- Windows: Use Task Scheduler or NSSM (Non-Sucking Service Manager)
- Docker: See proxy.md for containerized deployment
"},{"location":"macos-deployment/#security-considerations","title":"Security Considerations","text":""},{"location":"macos-deployment/#launchagent-vs-launchdaemon","title":"LaunchAgent vs LaunchDaemon","text":"LaunchAgent (used here):
- Runs in user context
- No root privileges required
- Starts on user login
- Per-user isolation
LaunchDaemon (not covered):
- Runs as root or specific user
- System-wide service
- Starts on boot
- Requires admin privileges
For single-user development, LaunchAgent is recommended for security.
"},{"location":"macos-deployment/#api-key-security","title":"API Key Security","text":"Store API keys securely:
- \u2705 Use environment variables in shell config
- \u2705 Use macOS Keychain (advanced)
- \u2705 Restrict plist file permissions:
chmod 600 - \u274c Don't commit API keys to version control
- \u274c Don't store in world-readable files
"},{"location":"macos-deployment/#network-security","title":"Network Security","text":"The proxy binds to 127.0.0.1 (localhost only) by default:
- \u2705 Only accessible from local machine
- \u2705 No external network exposure
- \u274c Don't bind to
0.0.0.0 without firewall rules
"},{"location":"macos-deployment/#advanced-configuration","title":"Advanced Configuration","text":""},{"location":"macos-deployment/#multiple-proxy-instances","title":"Multiple Proxy Instances","text":"Run multiple proxies on different ports:
# Install first instance\n./install.sh --port 8787\n\n# For second instance, manually create plist with different label\ncp com.headroom.proxy.plist.template ~/Library/LaunchAgents/com.headroom.proxy-2.plist\n# Edit: Change Label to com.headroom.proxy-2, port to 8788\nlaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.headroom.proxy-2.plist\n
"},{"location":"macos-deployment/#custom-launchagent-schedule","title":"Custom LaunchAgent Schedule","text":"Run proxy only during business hours:
<!-- Add to plist -->\n<key>StartCalendarInterval</key>\n<dict>\n <key>Hour</key>\n <integer>9</integer>\n <key>Minute</key>\n <integer>0</integer>\n</dict>\n
"},{"location":"macos-deployment/#resource-limits","title":"Resource Limits","text":"Limit CPU and memory usage:
<!-- Add to plist -->\n<key>HardResourceLimits</key>\n<dict>\n <key>NumberOfProcesses</key>\n <integer>1</integer>\n <key>MemoryMax</key>\n <integer>536870912</integer> <!-- 512 MB -->\n</dict>\n
"},{"location":"macos-deployment/#faq","title":"FAQ","text":"Q: Why LaunchAgent instead of running headroom proxy manually?
A: LaunchAgent provides automatic startup, crash recovery, and proper lifecycle management. You don't have to remember to start the proxy or keep a terminal window open.
Q: Can I use this in production?
A: LaunchAgent is designed for development. For production, use Docker, systemd, or cloud-native deployment.
Q: How much does the proxy impact performance?
A: Minimal. The proxy adds ~10-50ms latency while reducing token costs by 50-90%. The cost savings far outweigh the latency.
Q: Do I need to restart the proxy when configuration changes?
A: Yes. After changing the plist, reload the service:
launchctl kickstart -k gui/$(id -u)/com.headroom.proxy\n
Q: Can I use this with multiple API providers?
A: The LaunchAgent setup is Anthropic-specific. For other providers, see proxy.md for configuration options.
Q: Does this work with Apple Silicon (M1/M2/M3)?
A: Yes, fully compatible. For LLMLingua compression, use --llmlingua-device mps for Apple Silicon acceleration.
"},{"location":"mcp/","title":"MCP Server \u2014 Context Engineering Toolkit","text":"Headroom's MCP server exposes compression, retrieval, and observability as tools that any MCP-compatible AI coding tool can use \u2014 Claude Code, Cursor, Codex, and more.
"},{"location":"mcp/#quick-start","title":"Quick Start","text":"# Install (MCP is included with proxy, or standalone)\npip install \"headroom-ai[proxy]\" # Proxy + MCP tools\npip install \"headroom-ai[mcp]\" # MCP tools only (lightweight)\n\n# Register with Claude Code (one-time)\nheadroom mcp install\n\n# Start Claude Code \u2014 it now has headroom tools!\nclaude\n
That's it. Claude Code can now compress content on demand, retrieve originals, and check session stats \u2014 no proxy required.
For automatic compression of ALL traffic, also run the proxy:
# Terminal 1\nheadroom proxy\n\n# Terminal 2\nANTHROPIC_BASE_URL=http://127.0.0.1:8787 claude\n
"},{"location":"mcp/#tools","title":"Tools","text":"The MCP server provides three tools:
"},{"location":"mcp/#headroom_compress","title":"headroom_compress","text":"Compress content on demand. The LLM calls this when it wants to shrink large content before reasoning over it.
Tool: headroom_compress\n\nParameters:\n - content (required): Text to compress (files, JSON, logs, search results, etc.)\n\nReturns:\n - compressed: Compressed text\n - hash: Key for retrieving the original later\n - original_tokens / compressed_tokens / savings_percent\n - transforms: Which compression algorithms were applied\n
Example \u2014 Claude reads a large file, then compresses it:
Claude: Let me compress this large output to save context space.\n\n\u2192 headroom_compress(content=\"[5000 lines of grep results...]\")\n\n\u2190 {\n \"compressed\": \"[key matches with context...]\",\n \"hash\": \"a1b2c3d4e5f6...\",\n \"original_tokens\": 12000,\n \"compressed_tokens\": 3200,\n \"savings_percent\": 73.3,\n \"transforms\": [\"router:search:0.27\"]\n }\n
The original is stored locally for the session (1-hour TTL). If Claude needs the full content later, it calls headroom_retrieve.
"},{"location":"mcp/#headroom_retrieve","title":"headroom_retrieve","text":"Retrieve original uncompressed content by hash.
Tool: headroom_retrieve\n\nParameters:\n - hash (required): Hash key from compression\n - query (optional): Search within the original to return only matching items\n\nReturns:\n - original_content (full retrieval) or results (search)\n - source: \"local\" or \"proxy\"\n
Retrieval checks the local store first (content compressed via headroom_compress), then falls back to the proxy's store (content compressed automatically by the proxy). Hashes from either source work transparently.
"},{"location":"mcp/#headroom_stats","title":"headroom_stats","text":"Session compression statistics \u2014 including sub-agent stats and proxy cache info.
Tool: headroom_stats\n\nReturns:\n - compressions, retrievals, tokens_saved, savings_percent\n - estimated_cost_saved_usd\n - recent_events (last 10 compression/retrieval events)\n - sub_agents (stats from sub-agent MCP instances, if any)\n - combined (main + sub-agent totals)\n - proxy (request count, cache hits, cost saved \u2014 if proxy is running)\n
Sub-agent stats are aggregated via a shared stats file (~/.headroom/session_stats.jsonl). Each MCP server instance (main session and sub-agents) writes events there, and headroom_stats reads across all of them.
"},{"location":"mcp/#architecture","title":"Architecture","text":""},{"location":"mcp/#mcp-only-no-proxy","title":"MCP Only (no proxy)","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Claude Code / Cursor / Codex \u2502\n\u2502 \u2502\n\u2502 LLM calls headroom_compress on demand \u2502\n\u2502 \u2193 \u2502\n\u2502 Compression happens locally in MCP process \u2502\n\u2502 Original stored in local CompressionStore \u2502\n\u2502 \u2193 \u2502\n\u2502 LLM calls headroom_retrieve when needed \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"mcp/#mcp-proxy-full-setup","title":"MCP + Proxy (full setup)","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Claude Code \u2502\n\u2502 \u2502\n\u2502 1. Sends request \u2500\u2500\u2192 Proxy (auto-compress) \u2502\n\u2502 2. Gets response with compressed outputs \u2502\n\u2502 3. Can call headroom_compress for more \u2502\n\u2502 4. headroom_retrieve checks: \u2502\n\u2502 local store \u2192 proxy store \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 MCP (stdio)\n \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Headroom MCP Server \u2502\n\u2502 \u251c\u2500\u2500 headroom_compress (local compression) \u2502\n\u2502 \u251c\u2500\u2500 headroom_retrieve (local + proxy) \u2502\n\u2502 \u2514\u2500\u2500 headroom_stats (aggregated stats) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
No double-compression: the proxy compresses at the HTTP level (before the LLM sees content). MCP tools operate after the LLM receives content. They don't touch the same data.
"},{"location":"mcp/#cli-commands","title":"CLI Commands","text":""},{"location":"mcp/#install","title":"Install","text":"headroom mcp install # Default setup\nheadroom mcp install --proxy-url http://host:9000 # Custom proxy URL\nheadroom mcp install --force # Overwrite existing\n
"},{"location":"mcp/#status","title":"Status","text":"headroom mcp status\n
Headroom MCP Status\n========================================\nMCP SDK: \u2713 Installed\nClaude Config: \u2713 Configured\n /Users/you/.claude/mcp.json\nProxy URL: http://127.0.0.1:8787\nProxy Status: \u2713 Running at http://127.0.0.1:8787\n
"},{"location":"mcp/#uninstall","title":"Uninstall","text":"headroom mcp uninstall\n
"},{"location":"mcp/#debug","title":"Debug","text":"headroom mcp serve --debug\n
"},{"location":"mcp/#cross-tool-compatibility","title":"Cross-Tool Compatibility","text":"The MCP server works with any MCP-compatible host:
Tool MCP Support Setup Claude Code Native headroom mcp install Cursor Supported Add to Cursor MCP settings Codex If supported Configure MCP server Any MCP host Yes Point to headroom mcp serve"},{"location":"mcp/#troubleshooting","title":"Troubleshooting","text":""},{"location":"mcp/#mcp-sdk-not-installed","title":"\"MCP SDK not installed\"","text":"pip install \"headroom-ai[mcp]\"\n
"},{"location":"mcp/#proxy-not-running-when-using-proxy-features","title":"\"Proxy not running\" (when using proxy features)","text":"headroom proxy # In another terminal\n
"},{"location":"mcp/#entry-not-found-or-expired","title":"\"Entry not found or expired\"","text":" - Content compressed via
headroom_compress: stored for 1 hour (session TTL) - Content compressed by the proxy: stored for 5 minutes (proxy TTL)
- The proxy must be running for proxy-compressed content
"},{"location":"mcp/#claude-doesnt-see-headroom-tools","title":"Claude doesn't see headroom tools","text":" - Check:
headroom mcp status - Restart Claude Code after installing MCP
- Verify with
/mcp in Claude Code \u2014 should show 3 headroom tools
"},{"location":"mcp/#sub-agent-stats-not-showing","title":"Sub-agent stats not showing","text":"Sub-agent stats appear in headroom_stats only after sub-agents have run compressions. The shared stats file is at ~/.headroom/session_stats.jsonl.
"},{"location":"memory/","title":"Memory","text":"Hierarchical, temporal memory for LLM applications. Enable your AI to remember across conversations with intelligent scoping and versioning.
"},{"location":"memory/#why-memory","title":"Why Memory?","text":"LLMs have two fundamental limitations: 1. Context windows overflow - Too much history, need to truncate 2. No persistence - Every conversation starts from zero
Memory solves both: extract key facts, persist them, inject when relevant.
This is temporal compression - instead of carrying 10,000 tokens of conversation history, carry 100 tokens of extracted memories.
"},{"location":"memory/#what-makes-headroom-memory-different","title":"What Makes Headroom Memory Different?","text":"Feature Headroom Letta (MemGPT) Mem0 Cross-Agent Memory Any agent shares one DB via proxy Per-agent only Per-user, no cross-agent Agent Provenance Tracks which agent saved/updated each memory No No LLM-Mediated Dedup Piggybacks on user's own LLM for merge decisions No Separate LLM call ($) Transparent Proxy Zero code changes \u2014 just route through proxy Requires agent framework Requires SDK integration Hierarchical Scoping User \u2192 Session \u2192 Agent \u2192 Turn Flat (per-agent) Flat (per-user) Temporal Versioning Full supersession chains No No Zero-Latency Extraction Inline (Letta-style) Inline Separate call One-Liner Integration with_memory(client) Requires agent setup Requires separate client Pluggable Backends SQLite, HNSW, FTS5, any embedder PostgreSQL Qdrant/Chroma Semantic + Full-Text Search Both Semantic only Semantic only Memory Bubbling Auto-promote important memories No No Protocol-Based Architecture Yes (dependency injection) No No"},{"location":"memory/#cross-agent-memory-proxy","title":"Cross-Agent Memory (Proxy)","text":"The most powerful way to use memory: any agent that routes through the proxy shares the same memory store. Claude saves a fact, Codex reads it back. Zero configuration needed.
# Start the proxy with memory enabled\nheadroom proxy --memory\n\n# Or use wrap (auto-starts proxy)\nheadroom wrap claude --memory # Claude Code with persistent memory\nheadroom wrap codex --memory # Codex with the SAME memory store\nheadroom wrap aider --memory # Aider shares it too\n
"},{"location":"memory/#how-it-works","title":"How It Works","text":"Claude Code Codex CLI Gemini CLI\n \u2502 \u2502 \u2502\n \u2514\u2500\u2500 /v1/messages \u2500\u2500\u2510 \u2514\u2500\u2500 /v1/chat/completions \u2500\u2500\u2524 \u2514\u2500\u2500 /generateContent \u2500\u2500\u2510\n \u2502 \u2502 \u2502\n \u25bc \u25bc \u25bc\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Headroom Proxy (--memory) \u2502\n \u2502 \u2502\n \u2502 1. Search memory DB for relevant context \u2502\n \u2502 2. Inject memories as system context (provider-native format) \u2502\n \u2502 3. Add memory_save/search/update/delete tools \u2502\n \u2502 4. Forward to upstream LLM \u2502\n \u2502 5. Handle memory tool calls in response \u2502\n \u2502 6. Async background dedup (>92% cosine \u2192 auto-remove) \u2502\n \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502\n \u25bc\n .headroom/memory.db\n (project-scoped SQLite)\n
"},{"location":"memory/#project-scoped-database","title":"Project-Scoped Database","text":"Memory is stored per-project at {cwd}/.headroom/memory.db. Each project has its own memory \u2014 no cross-project contamination. Override with --memory-db-path for a custom location.
"},{"location":"memory/#user-identity","title":"User Identity","text":"User ID is auto-detected from $USER (your OS username). Override per-request with the x-headroom-user-id header. All memories are scoped to the user \u2014 multiple developers on the same project have separate memory stores.
"},{"location":"memory/#agent-provenance","title":"Agent Provenance","text":"Every memory tracks which agent created or updated it:
{\n \"content\": \"Project uses alembic for migrations\",\n \"metadata\": {\n \"source_agent\": \"claude\",\n \"source_provider\": \"anthropic\",\n \"created_via\": \"tool_call\",\n \"created_at_utc\": \"2026-04-10T17:30:00Z\"\n }\n}\n
When an agent updates a memory, the update is tracked:
{\n \"reason\": \"Updated by codex via openai: Added version info\"\n}\n
"},{"location":"memory/#intelligent-deduplication","title":"Intelligent Deduplication","text":"When the LLM calls memory_save, headroom:
- Saves immediately (zero latency)
- Searches for similar existing memories (cosine similarity)
- Returns an enriched hint if duplicates found:
{\n \"status\": \"saved\",\n \"memory_id\": \"abc123\",\n \"note\": \"Similar memory exists (id: def456, 89% match, saved by codex):\n 'DB migration tool is alembic'. Call memory_update('def456',\n '<merged content>') to consolidate.\"\n}\n
The LLM then decides whether to merge \u2014 using the user's own LLM, not a separate model. No extra cost to headroom.
- Background auto-dedup: If similarity >92%, the older duplicate is automatically removed (async, non-blocking).
"},{"location":"memory/#supported-providers","title":"Supported Providers","text":"Memory works with ALL providers routing through the proxy:
Provider Context Injection Memory Tools Format Anthropic (Claude) System parameter Anthropic tool_use Native OpenAI (Codex, GPT) System message OpenAI function calling Native Gemini systemInstruction functionDeclarations Native Any OpenAI-compatible System message Function calling OpenAI format"},{"location":"memory/#quick-start","title":"Quick Start","text":"from openai import OpenAI\nfrom headroom import with_memory\n\n# One line - that's it\nclient = with_memory(OpenAI(), user_id=\"alice\")\n\n# Use exactly like normal\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": \"I prefer Python for backend work\"}]\n)\n# Memory extracted INLINE - zero extra latency\n\n# Later, in a new conversation...\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": \"What language should I use?\"}]\n)\n# \u2192 Response uses the Python preference from memory\n
"},{"location":"memory/#how-it-works_1","title":"How It Works","text":"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 with_memory() \u2502\n\u2502 \u2502\n\u2502 1. INJECT: Semantic search \u2192 prepend to user message \u2502\n\u2502 2. INSTRUCT: Add memory extraction instruction \u2502\n\u2502 3. CALL: Forward to LLM \u2502\n\u2502 4. PARSE: Extract <memory> block from response \u2502\n\u2502 5. STORE: Save with embeddings + vector index + FTS \u2502\n\u2502 6. RETURN: Clean response (without memory block) \u2502\n\u2502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
Key insight: Memory extraction happens inline as part of the LLM response (Letta-style). No extra API calls, no extra latency.
"},{"location":"memory/#hierarchical-scoping","title":"Hierarchical Scoping","text":"Memories exist at different scope levels, enabling fine-grained control:
USER (broadest)\n \u2514\u2500\u2500 SESSION\n \u2514\u2500\u2500 AGENT\n \u2514\u2500\u2500 TURN (narrowest)\n
"},{"location":"memory/#scope-levels","title":"Scope Levels","text":"Scope Persists Across Use Case USER All sessions, all time Long-term preferences, identity SESSION Current session only Current task context AGENT Current agent in session Agent-specific context TURN Single turn only Ephemeral working memory"},{"location":"memory/#example-multi-session-memory","title":"Example: Multi-Session Memory","text":"from openai import OpenAI\nfrom headroom import with_memory\n\n# Session 1: Morning\nclient1 = with_memory(\n OpenAI(),\n user_id=\"bob\",\n session_id=\"morning-session\",\n)\nresponse = client1.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": \"I prefer Go for performance-critical code\"}]\n)\n# Memory stored at USER level (persists across sessions)\n\n# Session 2: Afternoon (different session, same user)\nclient2 = with_memory(\n OpenAI(),\n user_id=\"bob\", # Same user\n session_id=\"afternoon-session\", # Different session\n)\nresponse = client2.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": \"What language for my new microservice?\"}]\n)\n# \u2192 Recalls Go preference from morning session!\n
"},{"location":"memory/#temporal-versioning-supersession","title":"Temporal Versioning (Supersession)","text":"Memories evolve over time. When facts change, Headroom creates a supersession chain preserving history:
from headroom.memory import HierarchicalMemory, MemoryConfig\n\nmemory = await HierarchicalMemory.create()\n\n# Original fact\norig = await memory.add(\n content=\"User works at Google\",\n user_id=\"alice\",\n category=MemoryCategory.FACT,\n)\n\n# User changes jobs - supersede the old memory\nnew = await memory.supersede(\n old_memory_id=orig.id,\n new_content=\"User now works at Anthropic\",\n)\n\n# Query current state (excludes superseded)\ncurrent = await memory.query(MemoryFilter(\n user_id=\"alice\",\n include_superseded=False, # Default\n))\n# \u2192 Returns only \"User now works at Anthropic\"\n\n# Query full history (includes superseded)\nhistory = await memory.query(MemoryFilter(\n user_id=\"alice\",\n include_superseded=True,\n))\n# \u2192 Returns both memories with validity timestamps\n\n# Get the chain\nchain = await memory.get_history(new.id)\n# \u2192 [\n# Memory(content=\"User works at Google\", valid_until=..., is_current=False),\n# Memory(content=\"User now works at Anthropic\", valid_until=None, is_current=True),\n# ]\n
"},{"location":"memory/#why-temporal-versioning-matters","title":"Why Temporal Versioning Matters","text":" - Audit trail - Know what was true at any point in time
- Debugging - Understand why the LLM made certain decisions
- Rollback - Restore previous state if needed
- Analytics - Track how user preferences evolve
"},{"location":"memory/#memory-categories","title":"Memory Categories","text":"Memories are categorized for better organization and retrieval:
Category Description Examples PREFERENCE Likes, dislikes, preferred approaches \"Prefers Python\", \"Likes dark mode\" FACT Identity, role, constraints \"Works at fintech startup\", \"Senior engineer\" CONTEXT Current goals, ongoing tasks \"Migrating to microservices\", \"Working on auth\" ENTITY Information about entities \"Project Apollo uses React\", \"Team lead is Sarah\" DECISION Decisions made \"Chose PostgreSQL over MySQL\", \"Using REST not GraphQL\" INSIGHT Derived insights \"User tends to prefer typed languages\""},{"location":"memory/#memory-api","title":"Memory API","text":"The with_memory() wrapper provides a .memory API for direct access:
client = with_memory(OpenAI(), user_id=\"alice\")\n\n# Search memories (semantic)\nresults = client.memory.search(\"python preferences\", top_k=5)\nfor memory in results:\n print(f\"{memory.content}\")\n\n# Add manual memory\nclient.memory.add(\n \"User is a senior engineer\",\n category=\"fact\",\n importance=0.9,\n)\n\n# Get all memories\nall_memories = client.memory.get_all()\n\n# Clear memories\nclient.memory.clear()\n\n# Get stats\nstats = client.memory.stats()\nprint(f\"Total memories: {stats['total']}\")\nprint(f\"By category: {stats['categories']}\")\n
"},{"location":"memory/#advanced-usage-direct-hierarchicalmemory-api","title":"Advanced Usage: Direct HierarchicalMemory API","text":"For full control, use the HierarchicalMemory class directly:
import asyncio\nfrom headroom.memory import (\n HierarchicalMemory,\n MemoryConfig,\n MemoryCategory,\n EmbedderBackend,\n)\nfrom headroom.memory.ports import MemoryFilter, VectorFilter\n\nasync def main():\n # Create with custom configuration\n config = MemoryConfig(\n db_path=\"my_memory.db\",\n embedder_backend=EmbedderBackend.LOCAL, # or OPENAI, OLLAMA\n vector_dimension=384,\n cache_max_size=2000,\n )\n memory = await HierarchicalMemory.create(config)\n\n # Add memory with full control\n mem = await memory.add(\n content=\"User prefers functional programming\",\n user_id=\"alice\",\n session_id=\"sess-123\",\n agent_id=\"code-assistant\",\n category=MemoryCategory.PREFERENCE,\n importance=0.9,\n entity_refs=[\"functional-programming\", \"coding-style\"],\n metadata={\"source\": \"conversation\", \"confidence\": 0.95},\n )\n\n # Semantic search\n results = await memory.search(\n query=\"programming paradigm preferences\",\n user_id=\"alice\",\n top_k=5,\n min_similarity=0.5,\n categories=[MemoryCategory.PREFERENCE],\n )\n for r in results:\n print(f\"[{r.similarity:.3f}] {r.memory.content}\")\n\n # Full-text search\n text_results = await memory.text_search(\n query=\"functional\",\n user_id=\"alice\",\n )\n\n # Query with filters\n memories = await memory.query(MemoryFilter(\n user_id=\"alice\",\n categories=[MemoryCategory.PREFERENCE, MemoryCategory.FACT],\n min_importance=0.7,\n limit=10,\n ))\n\n # Convenience methods\n await memory.remember(\"Likes coffee\", user_id=\"alice\", importance=0.6)\n relevant = await memory.recall(\"beverage preferences\", user_id=\"alice\")\n\nasyncio.run(main())\n
"},{"location":"memory/#configuration","title":"Configuration","text":""},{"location":"memory/#embedder-backends","title":"Embedder Backends","text":"from headroom.memory import MemoryConfig, EmbedderBackend\n\n# Local embeddings (recommended - fast, free, private)\nconfig = MemoryConfig(\n embedder_backend=EmbedderBackend.LOCAL,\n embedder_model=\"all-MiniLM-L6-v2\", # 384 dimensions, fast\n)\n\n# OpenAI embeddings (higher quality, costs money)\nconfig = MemoryConfig(\n embedder_backend=EmbedderBackend.OPENAI,\n openai_api_key=\"sk-...\",\n embedder_model=\"text-embedding-3-small\",\n)\n\n# Ollama embeddings (local server, many models)\nconfig = MemoryConfig(\n embedder_backend=EmbedderBackend.OLLAMA,\n ollama_base_url=\"http://localhost:11434\",\n embedder_model=\"nomic-embed-text\",\n)\n
"},{"location":"memory/#storage-configuration","title":"Storage Configuration","text":"config = MemoryConfig(\n db_path=\"memory.db\", # SQLite database path\n vector_dimension=384, # Must match embedder output\n hnsw_ef_construction=200, # HNSW index quality (higher = better, slower)\n hnsw_m=16, # HNSW connections per node\n hnsw_ef_search=50, # HNSW search quality\n cache_enabled=True, # Enable LRU cache\n cache_max_size=1000, # Max cached memories\n)\n
"},{"location":"memory/#wrapper-configuration","title":"Wrapper Configuration","text":"client = with_memory(\n OpenAI(),\n user_id=\"alice\",\n db_path=\"memory.db\",\n top_k=5, # Memories to inject per request\n session_id=\"optional-session\",\n agent_id=\"optional-agent\",\n embedder_backend=EmbedderBackend.LOCAL,\n)\n
"},{"location":"memory/#architecture","title":"Architecture","text":""},{"location":"memory/#protocol-based-design","title":"Protocol-Based Design","text":"Headroom Memory uses Protocol interfaces (ports) for all components, enabling easy swapping:
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 HierarchicalMemory \u2502\n\u2502 (Orchestrator) \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 MemoryStore \u2502 \u2502 VectorIndex \u2502 \u2502 TextIndex \u2502 \u2502\n\u2502 \u2502 Protocol \u2502 \u2502 Protocol \u2502 \u2502 Protocol \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 SQLite \u2502 \u2502 HNSW \u2502 \u2502 FTS5 \u2502 \u2502\n\u2502 \u2502 Adapter \u2502 \u2502 Adapter \u2502 \u2502 Adapter \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502 Embedder \u2502 \u2502 MemoryCache \u2502 \u2502\n\u2502 \u2502 Protocol \u2502 \u2502 Protocol \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502 \u2502 \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502\n\u2502 \u2502Local/OpenAI/\u2502 \u2502 LRU Cache \u2502 \u2502\n\u2502 \u2502 Ollama \u2502 \u2502 \u2502 \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502\n\u2502 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"memory/#components","title":"Components","text":"Component Protocol Default Adapter Purpose MemoryStore MemoryStore SQLiteMemoryStore CRUD + filtering + supersession VectorIndex VectorIndex HNSWVectorIndex Semantic similarity search TextIndex TextIndex FTS5TextIndex Full-text keyword search Embedder Embedder LocalEmbedder Text \u2192 vector conversion Cache MemoryCache LRUMemoryCache Hot memory caching"},{"location":"memory/#comparison-with-state-of-the-art","title":"Comparison with State of the Art","text":""},{"location":"memory/#vs-letta-memgpt","title":"vs Letta (MemGPT)","text":"Letta pioneered inline memory extraction. Headroom builds on this with:
Aspect Headroom Letta Scoping 4-level hierarchy (user/session/agent/turn) Flat per-agent Temporal Full supersession chains with history No versioning Integration One-liner wrapper for any client Requires Letta agent framework Search Semantic + full-text Semantic only Storage SQLite + HNSW (embedded) PostgreSQL (external) Extensibility Protocol-based adapters Monolithic When to use Letta: You want a full agent framework with built-in memory. When to use Headroom: You want memory as a layer on your existing stack.
"},{"location":"memory/#vs-mem0","title":"vs Mem0","text":"Mem0 provides a managed memory service. Headroom differs:
Aspect Headroom Mem0 Deployment Embedded (no server) Managed service or self-hosted Scoping 4-level hierarchy Flat per-user Temporal Supersession chains No versioning Extraction Inline (zero latency) Separate API call Search Semantic + full-text Semantic only Cost Free (local embeddings) API costs or infra costs Privacy All local Data leaves your infra When to use Mem0: You want a managed service and don't mind external dependencies. When to use Headroom: You want embedded memory with no external services.
"},{"location":"memory/#feature-matrix","title":"Feature Matrix","text":"Feature Headroom Letta Mem0 Cross-agent sharing (proxy) \u2705 \u274c \u274c Agent provenance tracking \u2705 \u274c \u274c LLM-mediated dedup (no extra cost) \u2705 \u274c \u274c (uses separate LLM) Transparent proxy (zero code) \u2705 \u274c \u274c Hierarchical scoping \u2705 \u274c \u274c Temporal versioning \u2705 \u274c \u274c Zero-latency extraction \u2705 \u2705 \u274c Full-text search \u2705 \u274c \u274c Embedded (no server) \u2705 \u274c \u274c One-liner integration \u2705 \u274c \u274c Protocol-based extensibility \u2705 \u274c \u274c Memory bubbling \u2705 \u274c \u274c Local embeddings \u2705 \u274c \u2705 Managed service option \u274c \u274c \u2705"},{"location":"memory/#multi-user-isolation","title":"Multi-User Isolation","text":"Memories are isolated by user_id:
# Alice's memories\nalice_client = with_memory(OpenAI(), user_id=\"alice\")\n\n# Bob's memories (completely separate)\nbob_client = with_memory(OpenAI(), user_id=\"bob\")\n\n# Bob cannot see Alice's memories, even with the same database\n
"},{"location":"memory/#performance","title":"Performance","text":"Operation Latency Notes Memory injection <50ms Local embeddings + HNSW search Memory extraction +50-100 tokens Part of LLM response (inline) Memory storage <10ms SQLite + HNSW + FTS5 indexing Cache hit <1ms LRU cache lookup Overhead: ~100 extra output tokens per response for the <memory> block.
"},{"location":"memory/#providers","title":"Providers","text":"Memory works with any OpenAI-compatible client:
from openai import OpenAI\nfrom headroom import with_memory\n\n# OpenAI\nclient = with_memory(OpenAI(), user_id=\"alice\")\n\n# Azure OpenAI\nclient = with_memory(\n OpenAI(base_url=\"https://your-resource.openai.azure.com/...\"),\n user_id=\"alice\",\n)\n\n# Groq\nfrom groq import Groq\nclient = with_memory(Groq(), user_id=\"alice\")\n\n# Any OpenAI-compatible client\nclient = with_memory(YourClient(), user_id=\"alice\")\n
"},{"location":"memory/#example-full-conversation-flow","title":"Example: Full Conversation Flow","text":"from openai import OpenAI\nfrom headroom import with_memory\n\nclient = with_memory(OpenAI(), user_id=\"developer_jane\")\n\n# Conversation 1: User shares context\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\n \"role\": \"user\",\n \"content\": \"I'm a Python developer at a fintech startup. We use PostgreSQL and FastAPI.\"\n }]\n)\n# Memories extracted:\n# - [FACT] Python developer at fintech startup\n# - [PREFERENCE] Uses PostgreSQL for databases\n# - [PREFERENCE] Uses FastAPI for web APIs\n\n# Conversation 2 (new session): User asks question\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\n \"role\": \"user\",\n \"content\": \"What database should I use for my new project?\"\n }]\n)\n# Response references PostgreSQL preference from memory:\n# \u2192 \"Given your experience with PostgreSQL at your fintech company,\n# I'd recommend sticking with it for consistency...\"\n\n# Check stored memories\nprint(\"Stored memories:\")\nfor m in client.memory.get_all():\n print(f\" [{m.category.value}] {m.content}\")\n
"},{"location":"memory/#troubleshooting","title":"Troubleshooting","text":""},{"location":"memory/#memories-not-being-extracted","title":"Memories not being extracted","text":" - Check if the conversation has memory-worthy content (not just greetings)
- Verify the LLM is following the memory instruction
- Enable logging:
import logging; logging.basicConfig(level=logging.DEBUG)
"},{"location":"memory/#memories-not-being-retrieved","title":"Memories not being retrieved","text":" - Verify
user_id matches between sessions - Check if memories exist:
client.memory.get_all() - Try a more specific search query
- Check similarity threshold
"},{"location":"memory/#high-latency","title":"High latency","text":" - Use local embeddings:
embedder_backend=EmbedderBackend.LOCAL - Reduce
top_k for fewer memories to retrieve - Enable caching (enabled by default)
"},{"location":"memory/#memory-not-persisting","title":"Memory not persisting","text":" - Check
db_path is the same across sessions - Ensure the database file is writable
- Check for exceptions in logs
"},{"location":"memory/#best-practices","title":"Best Practices","text":" - Use consistent
user_id - Same ID across sessions for continuity - Use session scoping - Set
session_id for session-specific context - Start with local embeddings - Faster, free, good enough for most cases
- Monitor memory growth - Use
client.memory.stats() to track - Use importance scores - Higher importance = more likely to be retrieved
- Leverage categories - Helps with debugging and selective retrieval
- Consider supersession - Use
supersede() when facts change, not add()
"},{"location":"metrics/","title":"Metrics & Monitoring","text":"Headroom provides comprehensive metrics for monitoring compression performance, cost savings, and system health.
"},{"location":"metrics/#proxy-metrics","title":"Proxy Metrics","text":""},{"location":"metrics/#stats-endpoint","title":"Stats Endpoint","text":"curl http://localhost:8787/stats\n
{\n \"persistent_savings\": {\n \"lifetime\": {\n \"tokens_saved\": 12500,\n \"compression_savings_usd\": 0.04\n },\n \"recent_history\": [\n {\n \"timestamp\": \"2026-03-27T09:00:00Z\",\n \"total_tokens_saved\": 12500,\n \"compression_savings_usd\": 0.04\n }\n ]\n },\n \"requests\": {\n \"total\": 42,\n \"cached\": 5,\n \"rate_limited\": 0,\n \"failed\": 0\n },\n \"tokens\": {\n \"input\": 50000,\n \"output\": 8000,\n \"saved\": 12500,\n \"savings_percent\": 25.0\n },\n \"cost\": {\n \"total_cost_usd\": 0.15,\n \"total_savings_usd\": 0.04\n },\n \"cache\": {\n \"entries\": 10,\n \"total_hits\": 5\n }\n}\n
/stats keeps the existing live/session fields, including savings_history, for backward compatibility. The new persistent_savings block is durable local proxy compression history stored by default at ~/.headroom/proxy_savings.json. Use HEADROOM_SAVINGS_PATH to override the file location.
For Anthropic-style providers that return cache-write TTL buckets, /stats also surfaces observed cache TTL usage under prefix_cache:
{\n \"prefix_cache\": {\n \"by_provider\": {\n \"anthropic\": {\n \"observed_ttl_buckets\": {\n \"5m\": {\"tokens\": 20000, \"requests\": 8},\n \"1h\": {\"tokens\": 50000, \"requests\": 12}\n },\n \"observed_ttl_mix\": {\n \"5m_pct\": 28.6,\n \"1h_pct\": 71.4,\n \"active_buckets\": [\"5m\", \"1h\"]\n }\n }\n },\n \"totals\": {\n \"observed_ttl_buckets\": {\n \"5m\": {\"tokens\": 20000, \"requests\": 8},\n \"1h\": {\"tokens\": 50000, \"requests\": 12}\n }\n }\n }\n}\n
These fields are observational only:
- they reflect provider-reported cache write buckets
- they do not configure TTL
- they do not represent remaining expiration time
"},{"location":"metrics/#historical-savings-endpoint","title":"Historical Savings Endpoint","text":"curl http://localhost:8787/stats-history\n
{\n \"schema_version\": 1,\n \"generated_at\": \"2026-03-27T09:10:00Z\",\n \"lifetime\": {\n \"tokens_saved\": 12500,\n \"compression_savings_usd\": 0.04\n },\n \"history\": [\n {\n \"timestamp\": \"2026-03-27T09:00:00Z\",\n \"total_tokens_saved\": 12000,\n \"compression_savings_usd\": 0.038\n }\n ],\n \"series\": {\n \"hourly\": [],\n \"daily\": [],\n \"weekly\": [],\n \"monthly\": []\n },\n \"exports\": {\n \"default_format\": \"json\",\n \"available_formats\": [\"json\", \"csv\"],\n \"available_series\": [\"history\", \"hourly\", \"daily\", \"weekly\", \"monthly\"]\n }\n}\n
/stats-history is the stable frontend-facing API for durable proxy compression history. It survives proxy restarts, tolerates missing or malformed state files, and powers the historical view in /dashboard. It now includes hourly, daily, weekly, and monthly chart-ready rollups.
For export-friendly downloads:
curl \"http://localhost:8787/stats-history?format=csv&series=daily\"\ncurl \"http://localhost:8787/stats-history?format=csv&series=monthly\"\n
CSV exports are available for history, hourly, daily, weekly, and monthly. Plain JSON remains the default response format.
"},{"location":"metrics/#prometheus-metrics","title":"Prometheus Metrics","text":"curl http://localhost:8787/metrics\n
# HELP headroom_requests_total Total number of requests\nheadroom_requests_total 1234\n\n# HELP headroom_latency_ms_count Count of observed request latencies\nheadroom_latency_ms_count 1234\n\n# HELP headroom_tokens_saved_total Tokens saved by optimization\nheadroom_tokens_saved_total 5678900\n\n# HELP headroom_requests_by_provider Requests by provider\nheadroom_requests_by_provider{provider=\"anthropic\"} 800\nheadroom_requests_by_provider{provider=\"openai\"} 434\n\n# HELP headroom_transform_timing_ms_sum Sum of transform timing in milliseconds\nheadroom_transform_timing_ms_sum{transform=\"router\"} 5123.7\n\n# HELP headroom_cache_write_ttl_tokens_total Provider cache write tokens by observed TTL bucket\nheadroom_cache_write_ttl_tokens_total{provider=\"anthropic\",ttl=\"5m\"} 20000\nheadroom_cache_write_ttl_tokens_total{provider=\"anthropic\",ttl=\"1h\"} 50000\n
The built-in Prometheus endpoint exposes the proxy's in-memory operational state, including:
- request counters
- token totals and savings
- latency / overhead / TTFB summaries
- per-provider and per-model request counts
- per-stage pipeline timing
- waste signal token totals
- provider cache read/write and TTL-bucket counters
- cache bust counters
"},{"location":"metrics/#otel-metrics","title":"OTEL Metrics","text":"Headroom now emits the same operational events through a shared OTEL metrics facade.
There are two integration modes:
- Ambient OTEL app setup - if your application already configures a global OTEL meter provider, Headroom records into that provider automatically.
- Headroom-managed export - if you want the proxy to configure its own OTEL metrics exporter, install:
pip install \"headroom-ai[proxy,otel]\"\n
Then set:
HEADROOM_OTEL_METRICS_ENABLED=1\nHEADROOM_OTEL_METRICS_EXPORTER=otlp_http\nHEADROOM_OTEL_METRICS_ENDPOINT=http://127.0.0.1:4318/v1/metrics\nHEADROOM_OTEL_SERVICE_NAME=headroom-proxy\nHEADROOM_OTEL_RESOURCE_ATTRIBUTES=deployment.environment=dev,service.namespace=headroom\n
For local validation without a collector:
HEADROOM_OTEL_METRICS_ENABLED=1\nHEADROOM_OTEL_METRICS_EXPORTER=console\nheadroom proxy\n
The proxy's /stats response now includes an otel block that reports whether Headroom is managing an OTEL exporter for the current process.
Headroom's managed OTEL exporters are intentionally scoped to Headroom's own instrumentation. If you already manage global OTEL providers in your app, keep using those and let Headroom record into the ambient providers instead of enabling HEADROOM_OTEL_*.
"},{"location":"metrics/#otel-environment-variables","title":"OTEL Environment Variables","text":"Variable Default Description HEADROOM_OTEL_METRICS_ENABLED 0 Enables Headroom-managed OTEL metric export HEADROOM_OTEL_METRICS_EXPORTER otlp_http Exporter type: otlp_http or console HEADROOM_OTEL_METRICS_ENDPOINT unset OTLP HTTP metrics endpoint HEADROOM_OTEL_METRICS_HEADERS unset Comma-separated key=value headers for OTLP export HEADROOM_OTEL_METRICS_EXPORT_INTERVAL_MS 10000 Periodic export interval in milliseconds HEADROOM_OTEL_SERVICE_NAME headroom-proxy in proxy mode OTEL service.name HEADROOM_OTEL_RESOURCE_ATTRIBUTES unset Comma-separated resource attributes"},{"location":"metrics/#anonymous-telemetry-vs-otel","title":"Anonymous Telemetry vs OTEL","text":"Headroom has two separate systems:
HEADROOM_TELEMETRY / --no-telemetry controls the privacy-preserving anonymous data-flywheel beacon and TOIN-related aggregate reporting. HEADROOM_OTEL_* controls operational OTEL metric export.
They are independent by design so you can disable the anonymous beacon while keeping OTEL metrics enabled, or vice versa.
"},{"location":"metrics/#langfuse","title":"Langfuse","text":"Langfuse fits next to this implementation as a trace backend, not as a metrics backend.
- Headroom metrics continue to go to
/metrics and/or your OTEL metrics exporter. - Langfuse receives OTLP traces for Headroom's compression pipeline.
- Headroom's
/stats response includes a langfuse block when Headroom is managing Langfuse trace export for the process.
Enable it with:
HEADROOM_LANGFUSE_ENABLED=1\nLANGFUSE_PUBLIC_KEY=pk-lf-...\nLANGFUSE_SECRET_KEY=sk-lf-...\nLANGFUSE_BASE_URL=https://cloud.langfuse.com\n
For self-hosted Langfuse, set LANGFUSE_BASE_URL to your instance URL.
"},{"location":"metrics/#health-check","title":"Health Check","text":"curl http://localhost:8787/health\n
{\n \"status\": \"healthy\",\n \"version\": \"0.1.0\",\n \"uptime_seconds\": 3600,\n \"llmlingua_enabled\": false\n}\n
"},{"location":"metrics/#sdk-metrics","title":"SDK Metrics","text":""},{"location":"metrics/#session-stats","title":"Session Stats","text":"Quick stats for the current session (no database query):
stats = client.get_stats()\nprint(stats)\n
{\n \"session\": {\n \"requests_total\": 10,\n \"tokens_input_before\": 50000,\n \"tokens_input_after\": 35000,\n \"tokens_saved_total\": 15000,\n \"tokens_output_total\": 8000,\n \"cache_hits\": 3,\n \"compression_ratio_avg\": 0.70\n },\n \"config\": {\n \"mode\": \"optimize\",\n \"provider\": \"openai\",\n \"cache_optimizer_enabled\": True,\n \"semantic_cache_enabled\": False\n },\n \"transforms\": {\n \"smart_crusher_enabled\": True,\n \"cache_aligner_enabled\": True,\n \"rolling_window_enabled\": True\n }\n}\n
"},{"location":"metrics/#historical-metrics","title":"Historical Metrics","text":"Query stored metrics from the database:
from datetime import datetime, timedelta\n\n# Get recent metrics\nmetrics = client.get_metrics(\n start_time=datetime.utcnow() - timedelta(hours=1),\n limit=100,\n)\n\nfor m in metrics:\n print(f\"{m.timestamp}: {m.tokens_input_before} -> {m.tokens_input_after}\")\n
"},{"location":"metrics/#summary-statistics","title":"Summary Statistics","text":"Aggregate statistics across all stored metrics:
summary = client.get_summary()\nprint(f\"Total requests: {summary['total_requests']}\")\nprint(f\"Total tokens saved: {summary['total_tokens_saved']}\")\nprint(f\"Average compression: {summary['avg_compression_ratio']:.1%}\")\nprint(f\"Total cost savings: ${summary['total_cost_saved_usd']:.2f}\")\n
"},{"location":"metrics/#logging","title":"Logging","text":""},{"location":"metrics/#enable-logging","title":"Enable Logging","text":"import logging\n\n# INFO level shows compression summaries\nlogging.basicConfig(level=logging.INFO)\n\n# DEBUG level shows detailed transform decisions\nlogging.basicConfig(level=logging.DEBUG)\n
"},{"location":"metrics/#log-output-examples","title":"Log Output Examples","text":"INFO:headroom.transforms.pipeline:Pipeline complete: 45000 -> 4500 tokens (saved 40500, 90.0% reduction)\nINFO:headroom.transforms.smart_crusher:SmartCrusher applied top_n strategy: kept 15 of 1000 items\nINFO:headroom.cache.compression_store:CCR cache hit: hash=abc123, retrieved 1000 items\nDEBUG:headroom.transforms.smart_crusher:Kept items: [0,1,2,42,77,97,98,99] (errors at 42, warnings at 77)\n
"},{"location":"metrics/#proxy-logging","title":"Proxy Logging","text":"# Log to file\nheadroom proxy --log-file headroom.jsonl\n\n# Increase verbosity\nheadroom proxy --log-level debug\n
"},{"location":"metrics/#grafana-dashboard","title":"Grafana Dashboard","text":"Example Grafana dashboard configuration for Prometheus metrics:
{\n \"panels\": [\n {\n \"title\": \"Tokens Saved\",\n \"type\": \"stat\",\n \"targets\": [{\"expr\": \"headroom_tokens_saved_total\"}]\n },\n {\n \"title\": \"Average Request Latency (ms)\",\n \"type\": \"gauge\",\n \"targets\": [{\"expr\": \"headroom_latency_ms_sum / clamp_min(headroom_latency_ms_count, 1)\"}]\n },\n {\n \"title\": \"Max Request Latency (ms)\",\n \"type\": \"graph\",\n \"targets\": [{\"expr\": \"headroom_latency_ms_max\"}]\n },\n {\n \"title\": \"Provider Cache Hit Rate\",\n \"type\": \"gauge\",\n \"targets\": [{\"expr\": \"headroom_provider_cache_hit_requests_total / clamp_min(headroom_provider_cache_requests_total, 1)\"}]\n }\n ]\n}\n
"},{"location":"metrics/#cost-tracking","title":"Cost Tracking","text":""},{"location":"metrics/#per-request-cost","title":"Per-Request Cost","text":"Each request includes cost metadata in the response:
response = client.chat.completions.create(...)\n\n# Access via response metadata (if available)\n# Cost is calculated based on model pricing and token counts\n
"},{"location":"metrics/#budget-alerts","title":"Budget Alerts","text":"Set a budget limit in the proxy:
headroom proxy --budget 10.00\n
When the budget is exceeded: - Requests return a budget exceeded error - The /stats endpoint shows budget status - Logs indicate budget state
"},{"location":"metrics/#validation","title":"Validation","text":"Validate your setup is correct:
result = client.validate_setup()\n\nif result[\"valid\"]:\n print(\"Setup is correct!\")\nelse:\n print(\"Issues found:\")\n for issue in result[\"issues\"]:\n print(f\" - {issue}\")\n
"},{"location":"metrics/#key-metrics-to-monitor","title":"Key Metrics to Monitor","text":"Metric What It Tells You Target tokens_saved_total Total cost savings Higher is better compression_ratio_avg Efficiency 0.7-0.9 typical cache_hit_rate Cache effectiveness >20% is good latency_p99 Performance impact <10ms failed_requests Reliability 0"},{"location":"proxy/","title":"Proxy Server Documentation","text":"The Headroom proxy server is a production-ready HTTP server that applies context optimization to all requests passing through it.
New: The proxy now supports the TypeScript SDK via the POST /v1/compress endpoint, enabling compression-as-a-service for any HTTP client without calling an LLM.
"},{"location":"proxy/#starting-the-proxy","title":"Starting the Proxy","text":"# Basic usage\nheadroom proxy\n\n# Custom port\nheadroom proxy --port 8080\n\n# With all options\nheadroom proxy \\\n --host 0.0.0.0 \\\n --port 8787 \\\n --log-file /var/log/headroom.jsonl \\\n --budget 100.0\n
"},{"location":"proxy/#common-agent-cli-entrypoints","title":"Common agent CLI entrypoints","text":"# Claude Code\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n\n# GitHub Copilot CLI\nheadroom wrap copilot -- --model claude-sonnet-4-20250514\n\n# OpenAI-compatible clients\nOPENAI_BASE_URL=http://localhost:8787/v1 your-app\n
headroom wrap copilot uses Copilot CLI's BYOK provider settings under the hood. In provider-type=auto, it chooses Headroom's Anthropic route for the default proxy backend and the OpenAI-compatible /v1 route for translated backends such as anyllm and LiteLLM.
Anonymous aggregate telemetry is enabled by default. Opt out with HEADROOM_TELEMETRY=off or headroom proxy --no-telemetry. Downstream apps can set HEADROOM_SDK=headroom-app to override the anonymous telemetry sdk label; the default remains proxy.
Operational OTEL metrics are configured separately and are off by default. Install headroom-ai[proxy,otel] and set:
HEADROOM_OTEL_METRICS_ENABLED=1\nHEADROOM_OTEL_METRICS_EXPORTER=otlp_http\nHEADROOM_OTEL_METRICS_ENDPOINT=http://127.0.0.1:4318/v1/metrics\nHEADROOM_OTEL_SERVICE_NAME=headroom-proxy\n
Use HEADROOM_OTEL_METRICS_EXPORTER=console for local smoke testing. HEADROOM_TELEMETRY controls the anonymous data-flywheel beacon only; it does not disable or enable OTEL export.
Langfuse can be enabled alongside this OTEL path for trace ingestion. Langfuse does not ingest OTEL metrics, so Headroom keeps metrics and Langfuse traces as complementary signals:
HEADROOM_LANGFUSE_ENABLED=1\nLANGFUSE_PUBLIC_KEY=pk-lf-...\nLANGFUSE_SECRET_KEY=sk-lf-...\nLANGFUSE_BASE_URL=https://cloud.langfuse.com\n
When configured, Headroom emits OTLP traces for the shared compression pipeline to Langfuse while continuing to expose metrics through /metrics and OTEL metric exporters.
"},{"location":"proxy/#command-line-options","title":"Command Line Options","text":""},{"location":"proxy/#core-options","title":"Core Options","text":"Option Default Description --host 127.0.0.1 Host to bind to --port 8787 Port to bind to --mode token Run mode: token (maximize compression) or cache (freeze prior turns) --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 --budget None Daily budget limit in USD --openai-api-url https://api.openai.com Custom OpenAI API URL endpoint"},{"location":"proxy/#run-modes","title":"Run Modes","text":"Headroom proxy has two explicit run modes:
token mode: prioritize token reduction. Prior history may be rewritten when that improves compression. cache mode: prioritize provider prefix cache stability. Prior turns are frozen; only the newest turn is mutable.
Set via CLI or env:
headroom proxy --mode token\nHEADROOM_MODE=cache headroom proxy\n
When to pick each:
token: best for maximizing immediate compression savings. cache: best for long conversations where preserving prior-turn bytes improves prefix-cache reuse.
Legacy values (token_headroom, cost_savings) are still accepted as aliases.
"},{"location":"proxy/#context-management-options","title":"Context Management Options","text":"Option Default Description --no-intelligent-context false Disable IntelligentContextManager (fall back to RollingWindow) --no-intelligent-scoring false Disable multi-factor importance scoring (use position-based) --no-compress-first false Disable trying deeper compression before dropping messages By default, the proxy uses IntelligentContextManager which scores messages by multiple factors (recency, semantic similarity, TOIN-learned patterns, error indicators, forward references) and drops lowest-scored messages first. This is smarter than simple age-based truncation.
CCR Integration: When messages are dropped, they're stored in CCR so the LLM can retrieve them if needed. The inserted marker includes the CCR reference. Drops are also recorded to TOIN, so the system learns which message patterns are important across all users.
# Use legacy RollingWindow (drops oldest first)\nheadroom proxy --no-intelligent-context\n\n# Disable semantic scoring (faster, but less intelligent)\nheadroom proxy --no-intelligent-scoring\n
"},{"location":"proxy/#llmlingua-options-ml-compression","title":"LLMLingua Options (ML Compression)","text":"Option Default Description --llmlingua false Enable LLMLingua-2 ML-based compression --llmlingua-device auto Device for model: auto, cuda, cpu, mps --llmlingua-rate 0.3 Target compression rate (0.3 = keep 30% of tokens) Note: LLMLingua requires additional dependencies: pip install headroom-ai[llmlingua]
# Enable LLMLingua with GPU acceleration\nheadroom proxy --llmlingua --llmlingua-device cuda\n\n# More aggressive compression (keep only 20%)\nheadroom proxy --llmlingua --llmlingua-rate 0.2\n\n# Conservative compression for code (keep 50%)\nheadroom proxy --llmlingua --llmlingua-rate 0.5\n
"},{"location":"proxy/#api-endpoints","title":"API Endpoints","text":""},{"location":"proxy/#liveness","title":"Liveness","text":"curl http://localhost:8787/livez\n
Response:
{\n \"service\": \"headroom-proxy\",\n \"status\": \"healthy\",\n \"alive\": true,\n \"version\": \"0.5.21\",\n \"timestamp\": \"2026-04-10T16:36:25Z\",\n \"uptime_seconds\": 12.483\n}\n
"},{"location":"proxy/#readiness","title":"Readiness","text":"curl http://localhost:8787/readyz\n
Response:
{\n \"service\": \"headroom-proxy\",\n \"status\": \"healthy\",\n \"ready\": true,\n \"version\": \"0.5.21\",\n \"timestamp\": \"2026-04-10T16:36:25Z\",\n \"uptime_seconds\": 12.483,\n \"checks\": {\n \"startup\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"http_client\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"cache\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"rate_limiter\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"memory\": {\"enabled\": false, \"ready\": true, \"status\": \"disabled\"}\n }\n}\n
/readyz returns HTTP 503 when Headroom has not completed startup or a required enabled subsystem is unavailable. This is the endpoint used by the container health checks.
"},{"location":"proxy/#aggregate-health","title":"Aggregate Health","text":"curl http://localhost:8787/health\n
Response:
{\n \"status\": \"healthy\",\n \"ready\": true,\n \"version\": \"0.5.21\",\n \"config\": {\n \"backend\": \"anthropic\",\n \"optimize\": true,\n \"cache\": true,\n \"rate_limit\": true\n },\n \"checks\": {\n \"startup\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"},\n \"http_client\": {\"enabled\": true, \"ready\": true, \"status\": \"healthy\"}\n }\n}\n
"},{"location":"proxy/#detailed-statistics","title":"Detailed Statistics","text":"curl http://localhost:8787/stats\n
/stats remains the live/session-oriented endpoint and now also includes a persistent_savings block with durable proxy compression lifetime totals plus a small recent preview. The existing savings_history field is still present and remains session-scoped for backward compatibility.
For providers that return cache-write TTL bucket usage, /stats also includes observed TTL breakdowns under prefix_cache:
observed_ttl_buckets.5m.tokens observed_ttl_buckets.1h.tokens observed_ttl_mix
These are provider-reported observations, not configured TTL and not remaining expiration time.
"},{"location":"proxy/#historical-savings","title":"Historical Savings","text":"curl http://localhost:8787/stats-history\n
/stats-history exposes durable proxy compression history for dashboards and other Headroom frontends. It returns:
- lifetime proxy compression totals
- bounded persisted checkpoint history
- derived hourly, daily, weekly, and monthly rollups for charts
- UTC timestamps throughout
By default the proxy stores this history at ~/.headroom/proxy_savings.json. Set HEADROOM_SAVINGS_PATH to override the location.
/dashboard uses this endpoint directly for its historical view, including the daily/weekly/monthly rollups and built-in JSON / CSV export buttons.
curl \"http://localhost:8787/stats-history?format=csv&series=weekly\"\ncurl \"http://localhost:8787/stats-history?format=csv&series=monthly\"\n
"},{"location":"proxy/#prometheus-metrics","title":"Prometheus Metrics","text":"curl http://localhost:8787/metrics\n
/metrics remains the built-in Prometheus-formatted operational view. The proxy now also emits the same operational events through the OTEL facade when OTEL metrics are configured.
"},{"location":"proxy/#llm-apis","title":"LLM APIs","text":"The proxy supports both Anthropic and OpenAI API formats:
# Anthropic format\nPOST /v1/messages\n\n# OpenAI format\nPOST /v1/chat/completions\n
"},{"location":"proxy/#post-v1compress","title":"POST /v1/compress","text":"Compression-only endpoint. Compresses messages without calling any LLM. Used by the TypeScript SDK and any HTTP client that wants compression as a service.
Request:
{\n \"messages\": [...], // OpenAI chat format\n \"model\": \"gpt-4o\" // model name (for token counting)\n}\n
Response:
{\n \"messages\": [...], // compressed messages\n \"tokens_before\": 15000,\n \"tokens_after\": 3500,\n \"tokens_saved\": 11500,\n \"compression_ratio\": 0.23,\n \"transforms_applied\": [\"router:smart_crusher:0.35\"],\n \"ccr_hashes\": [\"a1b2c3\"]\n}\n
Headers: - x-headroom-bypass: true \u2014 skip compression, return messages as-is
Error responses: 400 (missing fields), 401 (bad API key), 503 (compression failed)
"},{"location":"proxy/#using-with-claude-code","title":"Using with Claude Code","text":"# Start proxy\nheadroom proxy --port 8787\n\n# In another terminal\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n
"},{"location":"proxy/#using-with-cursor","title":"Using with Cursor","text":" - Start the proxy:
headroom proxy - In Cursor settings, set the base URL to
http://localhost:8787
"},{"location":"proxy/#using-with-openai-sdk","title":"Using with OpenAI SDK","text":"from openai import OpenAI\n\nclient = OpenAI(\n base_url=\"http://localhost:8787/v1\",\n api_key=\"your-api-key\", # Still needed for upstream\n)\n
"},{"location":"proxy/#features","title":"Features","text":""},{"location":"proxy/#llmlingua-ml-compression-opt-in","title":"LLMLingua ML Compression (Opt-In)","text":"When enabled, the proxy uses Microsoft's LLMLingua-2 model for ML-based token compression:
headroom proxy --llmlingua\n
How it works: - LLMLinguaCompressor is added to the transform pipeline (before RollingWindow) - Automatically detects content type (JSON, code, text) and adjusts compression - Stores original content in CCR for retrieval if needed
Startup feedback:
# When enabled and available:\nLLMLingua: ENABLED (device=cuda, rate=0.3)\n\n# When installed but not enabled (helpful hint):\nLLMLingua: available (enable with --llmlingua for ML compression)\n\n# When enabled but not installed:\nWARNING: LLMLingua requested but not installed. Install with: pip install headroom-ai[llmlingua]\n
Why opt-in? | Concern | Default Proxy | With LLMLingua | |---------|---------------|----------------| | Dependencies | ~50MB | +2GB (torch, transformers) | | Cold start | <1s | 10-30s (model load) | | Memory | ~100MB | +1GB (model in RAM) | | Overhead | <5ms | 50-200ms per request |
Enable LLMLingua when maximum compression justifies the resource cost.
"},{"location":"proxy/#semantic-caching","title":"Semantic Caching","text":"The proxy caches responses for repeated queries:
- LRU eviction with configurable max entries
- TTL-based expiration
- Cache key based on message content hash
"},{"location":"proxy/#rate-limiting","title":"Rate Limiting","text":"Token bucket rate limiting protects against runaway costs:
- Configurable requests per minute
- Configurable tokens per minute
- Per-API-key tracking
"},{"location":"proxy/#cost-tracking","title":"Cost Tracking","text":"Track spending and enforce budgets:
- Real-time cost estimation
- Budget periods: hourly, daily, monthly
- Automatic request rejection when over budget
"},{"location":"proxy/#prometheus-metrics_1","title":"Prometheus Metrics","text":"Export metrics for monitoring:
headroom_requests_total\nheadroom_tokens_saved_total\nheadroom_cost_usd_total\nheadroom_latency_ms_sum\n
"},{"location":"proxy/#configuration-via-environment","title":"Configuration via Environment","text":"export HEADROOM_HOST=0.0.0.0\nexport HEADROOM_PORT=8787\nexport HEADROOM_BUDGET=100.0\nexport OPENAI_TARGET_API_URL=https://custom.openai.endpoint.com\nheadroom proxy\n
"},{"location":"proxy/#running-in-production","title":"Running in Production","text":"For production deployments:
# Use a process manager\npip install gunicorn\n\n# Run with gunicorn\ngunicorn headroom.proxy.server:app \\\n --workers 4 \\\n --bind 0.0.0.0:8787 \\\n --worker-class uvicorn.workers.UvicornWorker\n
Or with Docker:
FROM python:3.11-slim\nRUN apt-get update && apt-get install -y --no-install-recommends build-essential \\\n && pip install \"headroom-ai[proxy]\" \\\n && apt-get purge -y build-essential && apt-get autoremove -y \\\n && rm -rf /var/lib/apt/lists/*\nEXPOSE 8787\nCMD [\"headroom\", \"proxy\", \"--host\", \"0.0.0.0\"]\n
Note: build-essential is required at install time because headroom-ai includes hnswlib, a C++ extension that must be compiled from source. It is removed after installation to keep the image slim.
"},{"location":"quickstart/","title":"Quickstart Guide","text":"Get Headroom running in 5 minutes with these copy-paste examples.
"},{"location":"quickstart/#installation","title":"Installation","text":"Python:
# Core only (minimal dependencies)\npip install headroom-ai\n\n# With proxy server\npip install \"headroom-ai[proxy]\"\n\n# Everything\npip install \"headroom-ai[all]\"\n
TypeScript / Node.js:
npm install headroom-ai\n
"},{"location":"quickstart/#option-1-proxy-server-zero-code-changes","title":"Option 1: Proxy Server (Zero Code Changes)","text":"The fastest way to start saving tokens. Works with any OpenAI-compatible client.
"},{"location":"quickstart/#step-1-start-the-proxy","title":"Step 1: Start the Proxy","text":"headroom proxy --port 8787\n
"},{"location":"quickstart/#step-2-verify-its-running","title":"Step 2: Verify It's Running","text":"curl http://localhost:8787/health\n# Expected: {\"status\":\"healthy\",\"ready\":true,\"config\":{\"backend\":\"anthropic\",...},...}\n
"},{"location":"quickstart/#step-3-point-your-client","title":"Step 3: Point Your Client","text":"# Claude Code\nANTHROPIC_BASE_URL=http://localhost:8787 claude\n\n# GitHub Copilot CLI (default Anthropic-style proxy route)\nheadroom wrap copilot -- --model claude-sonnet-4-20250514\n\n# Cursor / Continue / any OpenAI client\nOPENAI_BASE_URL=http://localhost:8787/v1 your-app\n\n# Python\nexport OPENAI_BASE_URL=http://localhost:8787/v1\npython your_script.py\n
"},{"location":"quickstart/#step-4-check-savings","title":"Step 4: Check Savings","text":"curl http://localhost:8787/stats\n# {\"requests_total\": 42, \"tokens_saved_total\": 125000, ...}\n
"},{"location":"quickstart/#option-2-python-sdk","title":"Option 2: Python SDK","text":"Wrap your existing client for fine-grained control.
"},{"location":"quickstart/#basic-example","title":"Basic Example","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\n\n# Create wrapped client\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n)\n\n# Use exactly like OpenAI client\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[\n {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Hello!\"},\n ],\n)\n\nprint(response.choices[0].message.content)\n\n# Check what happened\nstats = client.get_stats()\nprint(f\"Tokens saved: {stats['session']['tokens_saved_total']}\")\n
"},{"location":"quickstart/#with-tool-outputs-where-savings-happen","title":"With Tool Outputs (Where Savings Happen)","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\nimport json\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n)\n\n# Simulate a conversation with large tool outputs\nmessages = [\n {\"role\": \"system\", \"content\": \"You analyze search results.\"},\n {\"role\": \"user\", \"content\": \"Search for Python tutorials.\"},\n {\n \"role\": \"assistant\",\n \"content\": None,\n \"tool_calls\": [{\n \"id\": \"call_1\",\n \"type\": \"function\",\n \"function\": {\"name\": \"search\", \"arguments\": '{\"q\": \"python\"}'},\n }],\n },\n {\n \"role\": \"tool\",\n \"tool_call_id\": \"call_1\",\n # This is where Headroom shines - compressing large outputs\n \"content\": json.dumps({\n \"results\": [{\"title\": f\"Result {i}\", \"score\": 100-i} for i in range(500)]\n }),\n },\n {\"role\": \"user\", \"content\": \"What are the top 3 results?\"},\n]\n\n# Headroom compresses the 500 results to ~20, keeping the most relevant\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n)\n\nprint(response.choices[0].message.content)\n
"},{"location":"quickstart/#simulate-before-sending","title":"Simulate Before Sending","text":"Preview optimizations without making an API call:
# See what would happen without calling the API\nplan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=messages,\n)\n\nprint(f\"Tokens before: {plan.tokens_before}\")\nprint(f\"Tokens after: {plan.tokens_after}\")\nprint(f\"Would save: {plan.tokens_saved} tokens ({plan.tokens_saved/plan.tokens_before*100:.0f}%)\")\nprint(f\"Transforms: {plan.transforms}\")\nprint(f\"Estimated savings: {plan.estimated_savings}\")\n
"},{"location":"quickstart/#option-3-anthropic-sdk","title":"Option 3: Anthropic SDK","text":"from headroom import HeadroomClient, AnthropicProvider\nfrom anthropic import Anthropic\n\nclient = HeadroomClient(\n original_client=Anthropic(),\n provider=AnthropicProvider(),\n default_mode=\"optimize\",\n)\n\n# Use Anthropic-style API\nresponse = client.messages.create(\n model=\"claude-sonnet-4-20250514\",\n max_tokens=1024,\n messages=[\n {\"role\": \"user\", \"content\": \"Hello, Claude!\"},\n ],\n)\n\nprint(response.content[0].text)\n
"},{"location":"quickstart/#verify-its-working","title":"Verify It's Working","text":""},{"location":"quickstart/#method-1-enable-logging","title":"Method 1: Enable Logging","text":"import logging\nlogging.basicConfig(level=logging.INFO)\n\n# Now you'll see:\n# INFO:headroom.transforms.pipeline:Pipeline complete: 45000 -> 4500 tokens (saved 40500, 90.0% reduction)\n# INFO:headroom.transforms.smart_crusher:SmartCrusher: keeping 15 of 500 items\n
"},{"location":"quickstart/#method-2-check-session-stats","title":"Method 2: Check Session Stats","text":"stats = client.get_stats()\nprint(stats)\n# {\n# \"session\": {\"requests_total\": 10, \"tokens_saved_total\": 5000, ...},\n# \"config\": {\"mode\": \"optimize\", \"provider\": \"openai\", ...},\n# \"transforms\": {\"smart_crusher_enabled\": True, ...}\n# }\n
"},{"location":"quickstart/#method-3-validate-setup","title":"Method 3: Validate Setup","text":"result = client.validate_setup()\nif not result[\"valid\"]:\n print(\"Setup issues:\", result)\nelse:\n print(\"Setup OK!\")\n print(f\"Provider: {result['provider']['name']}\")\n print(f\"Storage: {result['storage']['url']}\")\n
"},{"location":"quickstart/#common-configuration","title":"Common Configuration","text":""},{"location":"quickstart/#adjust-compression","title":"Adjust Compression","text":"from headroom import HeadroomClient, OpenAIProvider, HeadroomConfig\n\nconfig = HeadroomConfig()\n\n# Keep more items after compression (default: 15)\nconfig.smart_crusher.max_items_after_crush = 30\n\n# Only compress if tool output has > 500 tokens (default: 200)\nconfig.smart_crusher.min_tokens_to_crush = 500\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n config=config, # Pass custom config\n default_mode=\"optimize\",\n)\n
"},{"location":"quickstart/#skip-compression-for-specific-tools","title":"Skip Compression for Specific Tools","text":"response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n headroom_tool_profiles={\n \"database_query\": {\"skip_compression\": True}, # Never compress\n \"search\": {\"max_items\": 50}, # Keep more items\n },\n)\n
"},{"location":"quickstart/#audit-mode-observe-only","title":"Audit Mode (Observe Only)","text":"# Start in audit mode - see what WOULD be optimized\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"audit\", # No modifications, just logging\n)\n\n# Override per-request\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n headroom_mode=\"optimize\", # Enable for this request only\n)\n
"},{"location":"quickstart/#what-gets-optimized","title":"What Gets Optimized?","text":"Content Type What Headroom Does Typical Savings Tool outputs with lists Keeps errors, anomalies, high-score items 70-90% Repeated search results Deduplicates and samples 60-80% Long conversations Drops old turns, keeps recent 40-60% System prompts with dates Stabilizes for cache hits Cache savings"},{"location":"quickstart/#next-steps","title":"Next Steps","text":" - Configuration Reference - All configuration options
- Transform Reference - How each transform works
- Troubleshooting - Common issues and solutions
- Examples - More complete examples
"},{"location":"quickstart/#quick-troubleshooting","title":"Quick Troubleshooting","text":""},{"location":"quickstart/#no-token-savings","title":"\"No token savings\"","text":"# 1. Check mode\nstats = client.get_stats()\nprint(stats[\"config\"][\"mode\"]) # Should be \"optimize\"\n\n# 2. Enable logging to see what's happening\nimport logging\nlogging.basicConfig(level=logging.DEBUG)\n
"},{"location":"quickstart/#high-latency","title":"\"High latency\"","text":"# Use BM25 instead of embeddings for faster relevance scoring\nconfig.smart_crusher.relevance.tier = \"bm25\"\n
"},{"location":"quickstart/#compression-too-aggressive","title":"\"Compression too aggressive\"","text":"# Keep more items\nconfig.smart_crusher.max_items_after_crush = 50\n
See Troubleshooting Guide for more solutions.
"},{"location":"sdk/","title":"SDK Guide","text":"The Headroom SDK wraps your existing LLM client to add compression and optimization transparently.
"},{"location":"sdk/#installation","title":"Installation","text":"pip install headroom-ai openai\n
"},{"location":"sdk/#quick-start","title":"Quick Start","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\n\n# Create wrapped client\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n)\n\n# Use exactly like the original client\nresponse = client.chat.completions.create(\n model=\"gpt-4o-mini\",\n messages=[\n {\"role\": \"user\", \"content\": \"Hello!\"},\n ],\n)\n\nprint(response.choices[0].message.content)\n
"},{"location":"sdk/#tool-output-compression","title":"Tool Output Compression","text":"Real savings happen with tool outputs. Here's where Headroom shines:
import json\n\n# Conversation with large tool output\nmessages = [\n {\"role\": \"user\", \"content\": \"Search for Python tutorials\"},\n {\n \"role\": \"assistant\",\n \"content\": None,\n \"tool_calls\": [{\n \"id\": \"call_123\",\n \"type\": \"function\",\n \"function\": {\"name\": \"search\", \"arguments\": '{\"q\": \"python\"}'},\n }],\n },\n {\n \"role\": \"tool\",\n \"tool_call_id\": \"call_123\",\n \"content\": json.dumps({\n \"results\": [\n {\"title\": f\"Tutorial {i}\", \"score\": 100-i}\n for i in range(500)\n ]\n }),\n },\n {\"role\": \"user\", \"content\": \"What are the top 3?\"},\n]\n\n# Headroom compresses 500 results to ~15, keeping highest-scoring items\nresponse = client.chat.completions.create(\n model=\"gpt-4o-mini\",\n messages=messages\n)\n\n# Check savings\nstats = client.get_stats()\nprint(f\"Tokens saved: {stats['session']['tokens_saved_total']}\")\n# Typical output: \"Tokens saved: 3500\"\n
"},{"location":"sdk/#supported-providers","title":"Supported Providers","text":""},{"location":"sdk/#openai","title":"OpenAI","text":"from headroom import HeadroomClient, OpenAIProvider\nfrom openai import OpenAI\n\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n)\n
"},{"location":"sdk/#anthropic","title":"Anthropic","text":"from headroom import HeadroomClient, AnthropicProvider\nfrom anthropic import Anthropic\n\nclient = HeadroomClient(\n original_client=Anthropic(),\n provider=AnthropicProvider(),\n)\n\nresponse = client.messages.create(\n model=\"claude-3-5-sonnet-20241022\",\n max_tokens=1024,\n messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n)\n
"},{"location":"sdk/#google","title":"Google","text":"from headroom import HeadroomClient, GoogleProvider\nimport google.generativeai as genai\n\nclient = HeadroomClient(\n original_client=genai,\n provider=GoogleProvider(),\n)\n
"},{"location":"sdk/#check-stats","title":"Check Stats","text":"# Session stats (no database query)\nstats = client.get_stats()\nprint(stats)\n# {\n# \"session\": {\"requests_total\": 10, \"tokens_saved_total\": 5000, ...},\n# \"config\": {\"mode\": \"optimize\", \"provider\": \"openai\", ...},\n# \"transforms\": {\"smart_crusher_enabled\": True, ...}\n# }\n
"},{"location":"sdk/#validate-setup","title":"Validate Setup","text":"result = client.validate_setup()\nif not result[\"valid\"]:\n print(\"Setup issues:\", result[\"issues\"])\n
"},{"location":"sdk/#modes","title":"Modes","text":""},{"location":"sdk/#optimize-default","title":"Optimize (Default)","text":"Applies all safe transforms:
client = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n)\n
"},{"location":"sdk/#audit","title":"Audit","text":"Observes and logs without modifying:
client = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"audit\",\n)\n
"},{"location":"sdk/#simulate","title":"Simulate","text":"Returns a plan without making the API call:
plan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=large_conversation,\n)\n\nprint(f\"Would save {plan.tokens_saved} tokens\")\nprint(f\"Transforms: {plan.transforms}\")\n
"},{"location":"sdk/#per-request-overrides","title":"Per-Request Overrides","text":"response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[...],\n\n # Override mode for this request\n headroom_mode=\"audit\",\n\n # Reserve more tokens for output\n headroom_output_buffer_tokens=8000,\n\n # Keep last N turns\n headroom_keep_turns=5,\n)\n
"},{"location":"sdk/#enable-logging","title":"Enable Logging","text":"import logging\nlogging.basicConfig(level=logging.INFO)\n\n# Now you'll see:\n# INFO:headroom.transforms.pipeline:Pipeline complete: 45000 -> 4500 tokens\n# INFO:headroom.transforms.smart_crusher:SmartCrusher: kept 15 of 1000 items\n
"},{"location":"sdk/#streaming","title":"Streaming","text":"Streaming works transparently:
stream = client.chat.completions.create(\n model=\"gpt-4o-mini\",\n messages=[{\"role\": \"user\", \"content\": \"Hello!\"}],\n stream=True,\n)\n\nfor chunk in stream:\n if chunk.choices[0].delta.content:\n print(chunk.choices[0].delta.content, end=\"\")\n
"},{"location":"sdk/#error-handling","title":"Error Handling","text":"from headroom import (\n HeadroomClient,\n HeadroomError,\n ConfigurationError,\n ProviderError,\n)\n\ntry:\n response = client.chat.completions.create(...)\nexcept ConfigurationError as e:\n print(f\"Config issue: {e}\")\nexcept ProviderError as e:\n print(f\"Provider issue: {e}\")\nexcept HeadroomError as e:\n print(f\"Headroom error: {e}\")\n
"},{"location":"sdk/#historical-metrics","title":"Historical Metrics","text":"Query stored metrics:
from datetime import datetime, timedelta\n\nmetrics = client.get_metrics(\n start_time=datetime.utcnow() - timedelta(hours=1),\n limit=100,\n)\n\nfor m in metrics:\n print(f\"{m.timestamp}: {m.tokens_input_before} -> {m.tokens_input_after}\")\n
"},{"location":"sdk/#advanced-configuration","title":"Advanced Configuration","text":"See Configuration for full options:
client = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\",\n enable_cache_optimizer=True,\n enable_semantic_cache=False,\n model_context_limits={\n \"gpt-4o\": 128000,\n \"gpt-4o-mini\": 128000,\n },\n)\n
"},{"location":"sdk/#comparison-with-proxy","title":"Comparison with Proxy","text":"Aspect SDK Proxy Setup Wrap client Point URL Control Fine-grained Global Metrics In-process Centralized Best for Custom apps Existing tools Use the SDK when you need fine-grained control. Use the proxy for existing tools like Claude Code, Cursor, etc.
"},{"location":"shared-context/","title":"SharedContext \u2014 Compressed Inter-Agent Context Sharing","text":"When agents hand off to each other, context gets replayed in full. SharedContext compresses what moves between agents using Headroom's compression pipeline.
"},{"location":"shared-context/#quick-start","title":"Quick Start","text":"from headroom import SharedContext\n\nctx = SharedContext()\n\n# Agent A stores large output\nctx.put(\"research\", big_research_output, agent=\"researcher\")\n\n# Agent B gets compressed version (~80% smaller)\nsummary = ctx.get(\"research\")\n\n# Agent B needs full details\nfull = ctx.get(\"research\", full=True)\n
"},{"location":"shared-context/#api","title":"API","text":""},{"location":"shared-context/#putkey-content-agentnone","title":"put(key, content, *, agent=None)","text":"Store content under a key. Compresses automatically using Headroom's full pipeline (SmartCrusher for JSON, CodeCompressor for code, Kompress for text).
entry = ctx.put(\"findings\", big_json_output, agent=\"researcher\")\n\nentry.original_tokens # 20,000\nentry.compressed_tokens # 4,000\nentry.savings_percent # 80.0\nentry.transforms # [\"router:json:0.20\"]\n
"},{"location":"shared-context/#getkey-fullfalse","title":"get(key, *, full=False)","text":"Retrieve content. Returns compressed version by default, original with full=True.
compressed = ctx.get(\"findings\") # 4K tokens\noriginal = ctx.get(\"findings\", full=True) # 20K tokens\nmissing = ctx.get(\"nonexistent\") # None\n
"},{"location":"shared-context/#get_entrykey","title":"get_entry(key)","text":"Get the full ContextEntry with metadata.
entry = ctx.get_entry(\"findings\")\nentry.key # \"findings\"\nentry.agent # \"researcher\"\nentry.original_tokens # 20000\nentry.compressed_tokens # 4000\nentry.savings_percent # 80.0\nentry.timestamp # 1710000000.0\nentry.transforms # [\"router:json:0.20\"]\n
"},{"location":"shared-context/#keys","title":"keys()","text":"List all non-expired keys.
"},{"location":"shared-context/#stats","title":"stats()","text":"Aggregated stats across all entries.
stats = ctx.stats()\nstats.entries # 3\nstats.total_original_tokens # 60000\nstats.total_compressed_tokens # 12000\nstats.total_tokens_saved # 48000\nstats.savings_percent # 80.0\n
"},{"location":"shared-context/#clear","title":"clear()","text":"Remove all entries.
"},{"location":"shared-context/#configuration","title":"Configuration","text":"ctx = SharedContext(\n model=\"claude-sonnet-4-5-20250929\", # For token counting\n ttl=3600, # 1 hour (default)\n max_entries=100, # Evicts oldest when full\n)\n
"},{"location":"shared-context/#framework-examples","title":"Framework Examples","text":""},{"location":"shared-context/#crewai","title":"CrewAI","text":"from headroom import SharedContext\n\nctx = SharedContext()\n\n# After researcher task\nctx.put(\"findings\", researcher_task.output.raw)\n\n# Coder task gets compressed context\ncoder_context = ctx.get(\"findings\")\n
"},{"location":"shared-context/#langgraph","title":"LangGraph","text":"from headroom import SharedContext\n\nctx = SharedContext()\n\ndef researcher_node(state):\n result = do_research()\n ctx.put(\"research\", result)\n return {\"research_summary\": ctx.get(\"research\")}\n\ndef coder_node(state):\n # Compressed summary in state, full details on demand\n full = ctx.get(\"research\", full=True)\n return {\"code\": write_code(full)}\n
"},{"location":"shared-context/#openai-agents-sdk","title":"OpenAI Agents SDK","text":"from headroom import SharedContext\n\nctx = SharedContext()\n\ndef compress_handoff(messages):\n for msg in messages:\n if len(msg.content) > 1000:\n ctx.put(msg.id, msg.content)\n msg.content = ctx.get(msg.id)\n return messages\n\nhandoff(agent=coder, input_filter=compress_handoff)\n
"},{"location":"shared-context/#any-framework","title":"Any Framework","text":"SharedContext is framework-agnostic. It's just put() and get(). Use it wherever context moves between agents.
"},{"location":"shared-context/#how-it-works","title":"How It Works","text":"Under the hood, put() calls headroom.compress() (the same pipeline used by the proxy) and stores the original in memory. get() returns the compressed version. get(full=True) returns the original.
- JSON arrays \u2192 SmartCrusher (70-95% compression)
- Code \u2192 CodeCompressor (AST-aware, with
[code] extra) - Text \u2192 Kompress (ModernBERT, with
[ml] extra) or passthrough - Entries expire after TTL (default 1 hour)
- Oldest entries evicted when max_entries reached
"},{"location":"strands/","title":"Strands Integration","text":"Headroom integrates with Strands Agents to provide automatic context optimization. Two integration patterns: wrap the model, or hook into tool calls.
"},{"location":"strands/#installation","title":"Installation","text":"pip install headroom-ai strands-agents\n
"},{"location":"strands/#quick-start","title":"Quick Start","text":"from strands import Agent\nfrom strands.models.bedrock import BedrockModel\nfrom headroom.integrations.strands import HeadroomStrandsModel\n\n# Wrap your model\nmodel = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-20250514-v1:0\")\noptimized = HeadroomStrandsModel(wrapped_model=model)\n\n# Create agent as usual\nagent = Agent(model=optimized)\nresponse = agent(\"Investigate the production incident\")\n\n# Check savings\nprint(f\"Tokens saved: {optimized.total_tokens_saved}\")\n
Every API call the agent makes \u2014 including tool result round-trips \u2014 gets compressed automatically.
"},{"location":"strands/#integration-patterns","title":"Integration Patterns","text":""},{"location":"strands/#1-model-wrapping","title":"1. Model Wrapping","text":"Wraps the Strands Model interface. Every call to stream() compresses the messages before they hit the provider.
from strands.models.bedrock import BedrockModel\nfrom headroom.integrations.strands import HeadroomStrandsModel\n\nmodel = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-20250514-v1:0\")\noptimized = HeadroomStrandsModel(wrapped_model=model)\n\n# Streaming works identically\nagent = Agent(model=optimized)\nresponse = agent(\"Analyze these logs\")\n
With custom config:
from headroom import HeadroomConfig\n\nconfig = HeadroomConfig()\noptimized = HeadroomStrandsModel(wrapped_model=model, config=config)\n
"},{"location":"strands/#2-hook-provider-tool-output-compression","title":"2. Hook Provider (Tool Output Compression)","text":"Compresses tool call results via Strands' hook system. Uses SmartCrusher on JSON arrays returned by tools.
from strands import Agent\nfrom strands.models.bedrock import BedrockModel\nfrom headroom.integrations.strands import HeadroomHookProvider\n\nmodel = BedrockModel(model_id=\"us.anthropic.claude-sonnet-4-20250514-v1:0\")\nhooks = HeadroomHookProvider(\n compress_tool_outputs=True,\n min_tokens_to_compress=200,\n preserve_errors=True,\n)\n\nagent = Agent(model=model, hooks=[hooks])\nresponse = agent(\"Search the database for recent failures\")\n\n# Check tool compression savings\nprint(f\"Tokens saved by hooks: {hooks.total_tokens_saved}\")\n
The hook preserves:
- Error items (error indicators, exceptions)
- Anomalous values (statistical outliers)
- Items matching the user's query context
- First/last items for boundary context
"},{"location":"strands/#3-both-together","title":"3. Both Together","text":"Model wrapping compresses conversation history. Hooks compress individual tool results. Use both for maximum savings.
from headroom.integrations.strands import HeadroomStrandsModel, HeadroomHookProvider\n\noptimized = HeadroomStrandsModel(wrapped_model=model)\nhooks = HeadroomHookProvider(compress_tool_outputs=True)\n\nagent = Agent(model=optimized, hooks=[hooks])\n
"},{"location":"strands/#structured-output","title":"Structured Output","text":"HeadroomStrandsModel supports Strands' structured output feature:
from pydantic import BaseModel\n\nclass Analysis(BaseModel):\n severity: str\n root_cause: str\n recommendation: str\n\nresult = optimized.structured_output(Analysis, messages)\n
"},{"location":"strands/#metrics","title":"Metrics","text":"# Per-request metrics\nfor m in optimized.metrics_history:\n print(f\" {m.tokens_before} \u2192 {m.tokens_after} ({m.tokens_saved} saved)\")\n\n# Running total\nprint(f\"Total saved: {optimized.total_tokens_saved}\")\n
"},{"location":"strands/#how-it-works","title":"How It Works","text":"Agent decides to call tool\n \u2502\n \u25bc\nTool executes, returns result\n \u2502\n \u25bc\nHeadroomHookProvider (optional)\n compresses tool result JSON\n \u2502\n \u25bc\nAgent builds next API request\n \u2502\n \u25bc\nHeadroomStrandsModel.stream()\n compresses full message list\n \u2502\n \u25bc\nProvider API (Bedrock, etc.)\n
The model wrapper uses Headroom's full pipeline (CacheAligner \u2192 ContentRouter \u2192 IntelligentContext). The hook provider uses SmartCrusher directly for fast JSON compression of individual tool results.
"},{"location":"strands/#supported-providers","title":"Supported Providers","text":"HeadroomStrandsModel auto-detects the provider from the wrapped model:
Strands Model Provider Detected BedrockModel Anthropic (via Bedrock) OllamaModel OpenAI-compatible Custom Model Falls back to estimation"},{"location":"text-compression/","title":"Text Compression Utilities","text":"For coding tasks, Headroom provides standalone text compression utilities that applications can use explicitly. These are opt-in \u2014 they're not applied automatically, giving you full control over when and how to compress text content.
Design Philosophy: SmartCrusher compresses JSON automatically because it's structure-preserving and safe. Text compression is lossy and context-dependent, so applications should decide when to use it.
"},{"location":"text-compression/#available-utilities","title":"Available Utilities","text":"Utility Input Type Use Case SearchCompressor grep/ripgrep output Search results with file:line:content format LogCompressor Build/test logs pytest, npm, cargo, make output TextCompressor Generic text Any plain text with anchor preservation detect_content_type Any content Detect content type for routing decisions"},{"location":"text-compression/#searchcompressor","title":"SearchCompressor","text":"Compresses search results (grep, ripgrep, ag) while preserving relevant matches.
from headroom.transforms import SearchCompressor\n\n# Your grep/ripgrep output (could be 1000s of lines)\nsearch_results = \"\"\"\nsrc/utils.py:42:def process_data(items):\nsrc/utils.py:43: \\\"\\\"\\\"Process items.\\\"\\\"\\\"\nsrc/models.py:15:class DataProcessor:\nsrc/models.py:89: def process(self, items):\n... hundreds more matches ...\n\"\"\"\n\n# Explicitly compress when you decide it's appropriate\ncompressor = SearchCompressor()\nresult = compressor.compress(search_results, context=\"find process\")\n\nprint(f\"Compressed {result.original_match_count} matches to {result.compressed_match_count}\")\nprint(result.compressed)\n
"},{"location":"text-compression/#what-gets-preserved","title":"What Gets Preserved","text":" - Exact query matches: Lines containing the search term
- High-relevance matches: Scored by BM25 similarity to context
- File diversity: Ensures results from different files are kept
- First/last matches: Context from start and end of results
"},{"location":"text-compression/#logcompressor","title":"LogCompressor","text":"Compresses build and test output while preserving errors, warnings, and summaries.
from headroom.transforms import LogCompressor\n\n# pytest output with 1000s of lines\nbuild_output = \"\"\"\n===== test session starts =====\ncollected 500 items\ntests/test_foo.py::test_1 PASSED\n... hundreds of passed tests ...\ntests/test_bar.py::test_fail FAILED\nAssertionError: expected 5, got 3\n===== 1 failed, 499 passed =====\n\"\"\"\n\n# Compress logs, preserving errors and stack traces\ncompressor = LogCompressor()\nresult = compressor.compress(build_output)\n\n# Errors, stack traces, and summary are preserved\nprint(result.compressed)\nprint(f\"Compression ratio: {result.compression_ratio:.1%}\")\n
"},{"location":"text-compression/#what-gets-preserved_1","title":"What Gets Preserved","text":" - Errors and failures: Any line with ERROR, FAILED, Exception, etc.
- Warnings: Warning messages that might be important
- Stack traces: Full tracebacks for debugging
- Summaries: Test/build summary lines
- Section headers: Structural markers like
=====
"},{"location":"text-compression/#textcompressor","title":"TextCompressor","text":"General-purpose text compression with anchor preservation.
from headroom.transforms import TextCompressor\n\nlong_text = \"\"\"\n... thousands of lines of documentation ...\n\"\"\"\n\ncompressor = TextCompressor()\nresult = compressor.compress(long_text, context=\"authentication\")\n\nprint(result.compressed)\n
"},{"location":"text-compression/#what-gets-preserved_2","title":"What Gets Preserved","text":" - Relevant paragraphs: Scored by similarity to context
- Anchors: Headers, section markers, important keywords
- Structure: Document organization is maintained
"},{"location":"text-compression/#content-type-detection","title":"Content Type Detection","text":"Automatically detect content type to route to the right compressor.
from headroom.transforms import detect_content_type, ContentType\n\ncontent = \"src/main.py:42:def process():\"\n\ndetection = detect_content_type(content)\nif detection.content_type == ContentType.SEARCH_RESULTS:\n # Route to SearchCompressor\n pass\nelif detection.content_type == ContentType.BUILD_OUTPUT:\n # Route to LogCompressor\n pass\nelif detection.content_type == ContentType.PLAIN_TEXT:\n # Route to TextCompressor\n pass\n
"},{"location":"text-compression/#content-types","title":"Content Types","text":"Type Detection Pattern SEARCH_RESULTS file:line:content format BUILD_OUTPUT pytest, npm, cargo markers JSON Valid JSON structure PLAIN_TEXT Default fallback"},{"location":"text-compression/#integration-pattern","title":"Integration Pattern","text":"from headroom.transforms import (\n detect_content_type, ContentType,\n SearchCompressor, LogCompressor, TextCompressor\n)\n\ndef compress_tool_output(content: str, context: str = \"\") -> str:\n \"\"\"Application-level compression with explicit control.\"\"\"\n detection = detect_content_type(content)\n\n if detection.content_type == ContentType.SEARCH_RESULTS:\n result = SearchCompressor().compress(content, context)\n return result.compressed\n elif detection.content_type == ContentType.BUILD_OUTPUT:\n result = LogCompressor().compress(content)\n return result.compressed\n elif detection.content_type == ContentType.PLAIN_TEXT:\n result = TextCompressor().compress(content, context)\n return result.compressed\n else:\n # JSON or other - let SmartCrusher handle it automatically\n return content\n
"},{"location":"text-compression/#configuration","title":"Configuration","text":"Each compressor accepts configuration options:
from headroom.transforms import SearchCompressor, SearchCompressorConfig\n\nconfig = SearchCompressorConfig(\n max_results=50, # Keep up to 50 matches\n preserve_file_diversity=True, # Ensure different files represented\n relevance_threshold=0.3, # Minimum relevance score to keep\n)\n\ncompressor = SearchCompressor(config)\n
"},{"location":"text-compression/#performance","title":"Performance","text":"Compressor Typical Input Output Speed SearchCompressor 1000 matches 30-50 matches ~2ms LogCompressor 5000 lines 100-200 lines ~3ms TextCompressor 10000 chars 2000 chars ~2ms"},{"location":"text-compression/#when-to-use","title":"When to Use","text":"Scenario Recommendation JSON tool output Let SmartCrusher handle automatically grep/ripgrep results Use SearchCompressor pytest/npm/cargo output Use LogCompressor Documentation/README Use TextCompressor Unknown content Use detect_content_type to route"},{"location":"transforms/","title":"Transform Reference","text":"Headroom provides several transforms that work together to optimize LLM context.
"},{"location":"transforms/#smartcrusher","title":"SmartCrusher","text":"Statistical compression for JSON tool outputs.
"},{"location":"transforms/#how-it-works","title":"How It Works","text":"SmartCrusher analyzes JSON arrays and selectively keeps important items:
- First/Last items - Context for pagination and recency
- Error items - 100% preservation of error states
- Anomalies - Statistical outliers (> 2 std dev from mean)
- Relevant items - Matches to user's query via BM25/embeddings
- Change points - Significant transitions in data
"},{"location":"transforms/#configuration","title":"Configuration","text":"from headroom import SmartCrusherConfig\n\nconfig = SmartCrusherConfig(\n min_tokens_to_crush=200, # Only compress if > 200 tokens\n max_items_after_crush=50, # Keep at most 50 items\n keep_first=3, # Always keep first 3 items\n keep_last=2, # Always keep last 2 items\n relevance_threshold=0.3, # Keep items with relevance > 0.3\n anomaly_std_threshold=2.0, # Keep items > 2 std dev from mean\n preserve_errors=True, # Always keep error items\n)\n
"},{"location":"transforms/#example","title":"Example","text":"from headroom import SmartCrusher\n\ncrusher = SmartCrusher(config)\n\n# Before: 1000 search results (45,000 tokens)\ntool_output = {\"results\": [...1000 items...]}\n\n# After: ~50 important items (4,500 tokens) - 90% reduction\ncompressed = crusher.crush(tool_output, query=\"user's question\")\n
"},{"location":"transforms/#what-gets-preserved","title":"What Gets Preserved","text":"Category Preserved Why Errors 100% Critical for debugging First N 100% Context/pagination Last N 100% Recency Anomalies All Unusual values matter Relevant Top K Match user's query Others Sampled Statistical representation"},{"location":"transforms/#cachealigner","title":"CacheAligner","text":"Prefix stabilization for improved cache hit rates.
"},{"location":"transforms/#the-problem","title":"The Problem","text":"LLM providers cache request prefixes. But dynamic content breaks caching:
\"You are helpful. Today is January 7, 2025.\" # Changes daily = no cache\n
"},{"location":"transforms/#the-solution","title":"The Solution","text":"CacheAligner extracts dynamic content to stabilize the prefix:
from headroom import CacheAligner\n\naligner = CacheAligner()\nresult = aligner.align(messages)\n\n# Static prefix (cacheable):\n# \"You are helpful.\"\n\n# Dynamic content moved to end:\n# [Current date context]\n
"},{"location":"transforms/#configuration_1","title":"Configuration","text":"from headroom import CacheAlignerConfig\n\nconfig = CacheAlignerConfig(\n extract_dates=True, # Move dates to dynamic section\n normalize_whitespace=True, # Consistent spacing\n stable_prefix_min_tokens=100, # Min prefix size for alignment\n)\n
"},{"location":"transforms/#cache-hit-improvement","title":"Cache Hit Improvement","text":"Scenario Before After Daily date in prompt 0% hits ~95% hits Dynamic user context ~10% hits ~80% hits Consistent prompts ~90% hits ~95% hits"},{"location":"transforms/#rollingwindow","title":"RollingWindow","text":"Context management within token limits.
"},{"location":"transforms/#the-problem_1","title":"The Problem","text":"Long conversations exceed context limits. Naive truncation breaks tool calls:
[tool_call: search] # Kept\n[tool_result: ...] # Dropped = orphaned call!\n
"},{"location":"transforms/#the-solution_1","title":"The Solution","text":"RollingWindow drops complete tool units, preserving pairs:
from headroom import RollingWindow\n\nwindow = RollingWindow(config)\nresult = window.apply(messages, max_tokens=100000)\n\n# Guarantees:\n# 1. Tool calls paired with results\n# 2. System prompt preserved\n# 3. Recent turns kept\n# 4. Oldest tool outputs dropped first\n
"},{"location":"transforms/#configuration_2","title":"Configuration","text":"from headroom import RollingWindowConfig\n\nconfig = RollingWindowConfig(\n max_tokens=100000, # Target token limit\n preserve_system=True, # Always keep system prompt\n preserve_recent_turns=5, # Keep last 5 user/assistant turns\n drop_oldest_first=True, # Remove oldest tool outputs\n)\n
"},{"location":"transforms/#drop-priority","title":"Drop Priority","text":" - Oldest tool outputs - First to go
- Old assistant messages - Summary preserved
- Old user messages - Only if necessary
- Never dropped: System prompt, recent turns, active tool pairs
Note: For more intelligent context management based on semantic importance rather than just position, see IntelligentContextManager below.
"},{"location":"transforms/#intelligentcontextmanager","title":"IntelligentContextManager","text":"Semantic-aware context management with TOIN-learned importance scoring.
"},{"location":"transforms/#the-problem_2","title":"The Problem","text":"RollingWindow drops messages by position (oldest first), but position doesn't equal importance:
- An error message from turn 3 might be critical
- A verbose success response from turn 10 might be expendable
- Messages referenced by later turns should be preserved
"},{"location":"transforms/#the-solution_2","title":"The Solution","text":"IntelligentContextManager uses multi-factor importance scoring:
from headroom.transforms import IntelligentContextManager, IntelligentContextConfig\n\nmanager = IntelligentContextManager(config)\nresult = manager.apply(messages, tokenizer, model_limit=128000)\n\n# Guarantees:\n# 1. System messages never dropped (configurable)\n# 2. Last N turns always protected\n# 3. Tool calls/responses dropped atomically\n# 4. Drops by importance score, not just position\n
"},{"location":"transforms/#how-scoring-works","title":"How Scoring Works","text":"Messages are scored on multiple factors (all learned, no hardcodes):
Factor Weight Description Recency 20% Exponential decay from conversation end Semantic Similarity 20% Embedding similarity to recent context TOIN Importance 25% Learned from retrieval patterns Error Indicators 15% TOIN-learned error field detection Forward References 15% Messages referenced by later messages Token Density 5% Information density (unique/total tokens) Key principle: No hardcoded patterns. Error detection uses TOIN's field_semantics.inferred_type == \"error_indicator\", not keyword matching.
"},{"location":"transforms/#configuration_3","title":"Configuration","text":"from headroom.transforms import IntelligentContextManager\nfrom headroom.config import IntelligentContextConfig, ScoringWeights\n\n# Custom scoring weights\nweights = ScoringWeights(\n recency=0.20,\n semantic_similarity=0.20,\n toin_importance=0.25,\n error_indicator=0.15,\n forward_reference=0.15,\n token_density=0.05,\n)\n\nconfig = IntelligentContextConfig(\n enabled=True,\n keep_system=True, # Never drop system messages\n keep_last_turns=2, # Protect last N user turns\n output_buffer_tokens=4000, # Reserve for model output\n use_importance_scoring=True, # Enable semantic scoring\n scoring_weights=weights, # Custom weights\n toin_integration=True, # Use TOIN patterns\n recency_decay_rate=0.1, # Exponential decay lambda\n compress_threshold=0.1, # Try compression first if <10% over\n)\n\nmanager = IntelligentContextManager(config)\n
"},{"location":"transforms/#strategy-selection","title":"Strategy Selection","text":"Based on how much over budget you are:
Overage Strategy Action Under budget NONE No action needed < 10% over COMPRESS_FIRST Try deeper compression >= 10% over DROP_BY_SCORE Drop lowest-scored messages"},{"location":"transforms/#toin-ccr-integration","title":"TOIN + CCR Integration","text":"IntelligentContextManager is a message-level compressor. Just like SmartCrusher compresses items in a JSON array, IntelligentContext \"compresses\" messages in a conversation by dropping low-value ones.
Bidirectional TOIN integration:
- Scoring uses TOIN patterns: Learned retrieval rates and field semantics inform importance scores
- Drops are recorded to TOIN: When messages are dropped, TOIN learns the pattern
- CCR stores originals: Dropped messages are stored in CCR for potential retrieval
- Retrievals feed back to TOIN: If users retrieve dropped messages, TOIN learns to score those patterns higher
from headroom.telemetry import get_toin\n\ntoin = get_toin()\nmanager = IntelligentContextManager(config, toin=toin)\n\n# TOIN provides (for scoring):\n# - retrieval_rate: How often this message pattern is retrieved (high = important)\n# - field_semantics: Learned field types (error_indicator, identifier, etc.)\n# - commonly_retrieved_fields: Fields that users frequently need\n\n# TOIN receives (from drops):\n# - Message pattern signatures (role counts, has_tools, has_errors)\n# - Token counts (original vs marker size)\n# - Retrieval feedback when users access CCR\n
What this means: - When you drop a message pattern and users frequently retrieve it, TOIN learns to score it higher next time - When you drop a pattern and no one retrieves it, that confirms it was safe to drop - The feedback loop improves drop decisions across all users, not just in one session
"},{"location":"transforms/#example-before-vs-after","title":"Example: Before vs After","text":"RollingWindow (position-based):
Messages: [sys, user1, asst1, user2, asst2_error, user3, asst3, user4, asst4]\nOver budget by 3 messages.\nDrops: user1, asst1, user2 (oldest first)\nResult: Loses context, keeps verbose asst3\n
IntelligentContextManager (score-based):
Messages scored:\n - asst2_error: 0.85 (TOIN learned error indicator)\n - asst1: 0.45 (old, low density)\n - asst3: 0.40 (verbose, low unique tokens)\n\nDrops: asst1, asst3, user1 (lowest scores)\nResult: Preserves critical error message\n
"},{"location":"transforms/#backwards-compatibility","title":"Backwards Compatibility","text":"Convert from RollingWindowConfig:
from headroom.config import IntelligentContextConfig, RollingWindowConfig\n\nrolling_config = RollingWindowConfig(\n max_tokens=100000,\n preserve_system=True,\n preserve_recent_turns=3,\n)\n\n# Convert to intelligent context config\nintelligent_config = IntelligentContextConfig(\n keep_system=rolling_config.preserve_system,\n keep_last_turns=rolling_config.preserve_recent_turns,\n)\n
"},{"location":"transforms/#llmlinguacompressor-optional","title":"LLMLinguaCompressor (Optional)","text":"ML-based compression using Microsoft's LLMLingua-2 model.
"},{"location":"transforms/#when-to-use","title":"When to Use","text":"Transform Best For Speed Compression SmartCrusher JSON arrays ~1ms 70-90% Text Utilities Search/logs ~1ms 50-90% LLMLinguaCompressor Any text, max compression 50-200ms 80-95%"},{"location":"transforms/#installation","title":"Installation","text":"pip install \"headroom-ai[llmlingua]\" # Adds ~2GB\n
"},{"location":"transforms/#configuration_4","title":"Configuration","text":"from headroom.transforms import LLMLinguaCompressor, LLMLinguaConfig\n\nconfig = LLMLinguaConfig(\n device=\"auto\", # auto, cuda, cpu, mps\n target_compression_rate=0.3, # Keep 30% of tokens\n min_tokens_for_compression=100, # Skip small content\n code_compression_rate=0.4, # Conservative for code\n json_compression_rate=0.35, # Moderate for JSON\n text_compression_rate=0.25, # Aggressive for text\n enable_ccr=True, # Store original for retrieval\n)\n\ncompressor = LLMLinguaCompressor(config)\n
"},{"location":"transforms/#content-aware-rates","title":"Content-Aware Rates","text":"LLMLinguaCompressor auto-detects content type:
Content Type Default Rate Behavior Code 0.4 Conservative - preserves syntax JSON 0.35 Moderate - keeps structure Text 0.3 Aggressive - maximum compression"},{"location":"transforms/#memory-management","title":"Memory Management","text":"from headroom.transforms import (\n is_llmlingua_model_loaded,\n unload_llmlingua_model,\n)\n\n# Check if model is loaded\nprint(is_llmlingua_model_loaded()) # True/False\n\n# Free ~1GB RAM when done\nunload_llmlingua_model()\n
"},{"location":"transforms/#proxy-integration","title":"Proxy Integration","text":"# Enable in proxy\nheadroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.3\n
"},{"location":"transforms/#codeawarecompressor-optional","title":"CodeAwareCompressor (Optional)","text":"AST-based compression for source code using tree-sitter.
"},{"location":"transforms/#when-to-use_1","title":"When to Use","text":"Transform Best For Speed Compression SmartCrusher JSON arrays ~1ms 70-90% CodeAwareCompressor Source code ~10-50ms 40-70% LLMLinguaCompressor Any text 50-200ms 80-95%"},{"location":"transforms/#key-benefits","title":"Key Benefits","text":" - Syntax validity guaranteed \u2014 Output always parses correctly
- Preserves critical structure \u2014 Imports, signatures, types, error handlers
- Multi-language support \u2014 Python, JavaScript, TypeScript, Go, Rust, Java, C, C++
- Lightweight \u2014 ~50MB vs ~1GB for LLMLingua
"},{"location":"transforms/#installation_1","title":"Installation","text":"pip install \"headroom-ai[code]\" # Adds tree-sitter-language-pack\n
"},{"location":"transforms/#configuration_5","title":"Configuration","text":"from headroom.transforms import CodeAwareCompressor, CodeCompressorConfig, DocstringMode\n\nconfig = CodeCompressorConfig(\n preserve_imports=True, # Always keep imports\n preserve_signatures=True, # Always keep function signatures\n preserve_type_annotations=True, # Keep type hints\n preserve_error_handlers=True, # Keep try/except blocks\n preserve_decorators=True, # Keep decorators\n docstring_mode=DocstringMode.FIRST_LINE, # FULL, FIRST_LINE, REMOVE\n target_compression_rate=0.2, # Keep 20% of tokens\n max_body_lines=5, # Lines to keep per function body\n min_tokens_for_compression=100, # Skip small content\n language_hint=None, # Auto-detect if None\n fallback_to_llmlingua=True, # Use LLMLingua for unknown langs\n)\n\ncompressor = CodeAwareCompressor(config)\n
"},{"location":"transforms/#example_1","title":"Example","text":"from headroom.transforms import CodeAwareCompressor\n\ncompressor = CodeAwareCompressor()\n\ncode = '''\nimport os\nfrom typing import List\n\ndef process_items(items: List[str]) -> List[str]:\n \"\"\"Process a list of items.\"\"\"\n results = []\n for item in items:\n if not item:\n continue\n processed = item.strip().lower()\n results.append(processed)\n return results\n'''\n\nresult = compressor.compress(code, language=\"python\")\nprint(result.compressed)\n# import os\n# from typing import List\n#\n# def process_items(items: List[str]) -> List[str]:\n# \"\"\"Process a list of items.\"\"\"\n# results = []\n# for item in items:\n# # ... (5 lines compressed)\n# pass\n\nprint(f\"Compression: {result.compression_ratio:.0%}\") # ~55%\nprint(f\"Syntax valid: {result.syntax_valid}\") # True\n
"},{"location":"transforms/#supported-languages","title":"Supported Languages","text":"Tier Languages Support Level 1 Python, JavaScript, TypeScript Full AST analysis 2 Go, Rust, Java, C, C++ Function body compression"},{"location":"transforms/#memory-management_1","title":"Memory Management","text":"from headroom.transforms import is_tree_sitter_available, unload_tree_sitter\n\n# Check if tree-sitter is installed\nprint(is_tree_sitter_available()) # True/False\n\n# Free memory when done (parsers are lazy-loaded)\nunload_tree_sitter()\n
"},{"location":"transforms/#contentrouter","title":"ContentRouter","text":"Intelligent compression orchestrator that routes content to the optimal compressor.
"},{"location":"transforms/#how-it-works_1","title":"How It Works","text":"ContentRouter analyzes content and selects the best compression strategy:
- Detect content type \u2014 JSON, code, logs, search results, plain text
- Consider source hints \u2014 File paths, tool names for high-confidence routing
- Route to compressor \u2014 SmartCrusher, CodeAwareCompressor, SearchCompressor, etc.
- Log decisions \u2014 Transparent routing for debugging
"},{"location":"transforms/#configuration_6","title":"Configuration","text":"from headroom.transforms import ContentRouter, ContentRouterConfig, CompressionStrategy\n\nconfig = ContentRouterConfig(\n min_section_tokens=100, # Minimum tokens to compress\n enable_code_aware=True, # Use CodeAwareCompressor for code\n enable_search_compression=True, # Use SearchCompressor for grep output\n enable_log_compression=True, # Use LogCompressor for logs\n default_strategy=CompressionStrategy.TEXT, # Fallback strategy\n)\n\nrouter = ContentRouter(config)\n
"},{"location":"transforms/#example_2","title":"Example","text":"from headroom.transforms import ContentRouter\n\nrouter = ContentRouter()\n\n# Router auto-detects content type and routes to optimal compressor\nresult = router.compress(content)\n\nprint(result.strategy_used) # CompressionStrategy.CODE_AWARE, SMART_CRUSHER, etc.\nprint(result.routing_log) # List of routing decisions\n
"},{"location":"transforms/#compression-strategies","title":"Compression Strategies","text":"Strategy Used For Compressor CODE_AWARE Source code CodeAwareCompressor SMART_CRUSHER JSON arrays SmartCrusher SEARCH Grep/find output SearchCompressor LOG Log files LogCompressor TEXT Plain text TextCompressor LLMLINGUA Any (max compression) LLMLinguaCompressor PASSTHROUGH Small content None"},{"location":"transforms/#content-detection","title":"Content Detection","text":"The router automatically detects content types by analyzing the content itself:
- Source code: Detected by syntax patterns, indentation, keywords
- JSON arrays: Detected by JSON structure with array elements
- Search results: Detected by
file:line: patterns - Log output: Detected by timestamp and log level patterns
- Plain text: Fallback for prose content
No manual hints required - the router inspects content directly.
"},{"location":"transforms/#toin-integration","title":"TOIN Integration","text":"ContentRouter records all compressions to TOIN (Tool Output Intelligence Network) for cross-user learning:
- All strategies tracked: Code, search, logs, text, and LLMLingua compressions are recorded
- Retrieval feedback: When users retrieve original content via CCR, TOIN learns which compressions need expansion
- Pattern learning: TOIN builds signatures for each content type to improve future compressions
This enables the feedback loop where compression decisions improve based on actual user behavior across all content types, not just JSON arrays.
"},{"location":"transforms/#transformpipeline","title":"TransformPipeline","text":"Combine transforms for optimal results.
from headroom import TransformPipeline, SmartCrusher, CacheAligner, RollingWindow\n\npipeline = TransformPipeline([\n SmartCrusher(), # First: compress tool outputs\n CacheAligner(), # Then: stabilize prefix\n RollingWindow(), # Finally: fit in context\n])\n\nresult = pipeline.transform(messages)\nprint(f\"Saved {result.tokens_saved} tokens\")\n
"},{"location":"transforms/#with-llmlingua-optional","title":"With LLMLingua (Optional)","text":"from headroom.transforms import (\n TransformPipeline, SmartCrusher, CacheAligner,\n RollingWindow, LLMLinguaCompressor\n)\n\npipeline = TransformPipeline([\n CacheAligner(), # 1. Stabilize prefix\n SmartCrusher(), # 2. Compress JSON arrays\n LLMLinguaCompressor(), # 3. ML compression on remaining text\n RollingWindow(), # 4. Final size constraint (always last)\n])\n
"},{"location":"transforms/#recommended-order","title":"Recommended Order","text":"Order Transform Purpose 1 CacheAligner Stabilize prefix for caching 2 SmartCrusher Compress JSON tool outputs 3 LLMLinguaCompressor ML compression (optional) 4 RollingWindow Enforce token limits (always last) Why this order? - CacheAligner first to maximize prefix stability - SmartCrusher handles JSON arrays efficiently - LLMLingua compresses remaining long text - RollingWindow truncates only if still over limit
"},{"location":"transforms/#safety-guarantees","title":"Safety Guarantees","text":"All transforms follow strict safety rules:
- Never remove human content - User/assistant text is sacred
- Never break tool ordering - Calls and results stay paired
- Parse failures are no-ops - Malformed content passes through
- Preserves recency - Last N turns always kept
- 100% error preservation - Error items never dropped
"},{"location":"troubleshooting/","title":"Troubleshooting Guide","text":"Solutions for common Headroom issues.
"},{"location":"troubleshooting/#proxy-server-issues","title":"Proxy Server Issues","text":""},{"location":"troubleshooting/#proxy-wont-start","title":"\"Proxy won't start\"","text":"Symptom: headroom proxy fails or hangs.
Solutions:
# 1. Check if port is already in use\nlsof -i :8787\n# If something is using the port, either kill it or use a different port\n\n# 2. Try a different port\nheadroom proxy --port 8788\n\n# 3. Check for missing dependencies\npip install \"headroom-ai[proxy]\"\n\n# 4. Run with debug logging\nheadroom proxy --log-level debug\n
"},{"location":"troubleshooting/#connection-refused-when-calling-proxy","title":"\"Connection refused\" when calling proxy","text":"Symptom: curl: (7) Failed to connect to localhost port 8787
Solutions:
# 1. Verify proxy is running\ncurl http://localhost:8787/health\n\n# 2. Check if proxy started on a different port\nps aux | grep headroom\n\n# 3. Check firewall settings (macOS)\nsudo pfctl -s rules | grep 8787\n
"},{"location":"troubleshooting/#proxy-returns-errors-for-some-requests","title":"\"Proxy returns errors for some requests\"","text":"Symptom: Some requests work, others fail with 502/503.
Solutions:
# 1. Check proxy logs for the actual error\nheadroom proxy --log-level debug\n\n# 2. Verify API key is set\necho $OPENAI_API_KEY # or ANTHROPIC_API_KEY\n\n# 3. Test the underlying API directly\ncurl https://api.openai.com/v1/models -H \"Authorization: Bearer $OPENAI_API_KEY\"\n
"},{"location":"troubleshooting/#sdk-issues","title":"SDK Issues","text":""},{"location":"troubleshooting/#no-token-savings","title":"\"No token savings\"","text":"Symptom: stats['session']['tokens_saved_total'] is 0.
Diagnosis:
# 1. Check mode\nstats = client.get_stats()\nprint(f\"Mode: {stats['config']['mode']}\") # Should be \"optimize\"\n\n# 2. Check transforms are enabled\nprint(f\"SmartCrusher: {stats['transforms']['smart_crusher_enabled']}\")\n\n# 3. Check if content meets threshold\n# SmartCrusher only compresses tool outputs > 200 tokens by default\n
Solutions:
# 1. Ensure mode is \"optimize\"\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n default_mode=\"optimize\", # NOT \"audit\"\n)\n\n# 2. Or override per-request\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n headroom_mode=\"optimize\",\n)\n\n# 3. Lower the compression threshold\nconfig = HeadroomConfig()\nconfig.smart_crusher.min_tokens_to_crush = 100 # Default is 200\n
Why It Might Be 0: - Mode is \"audit\" (observation only) - Messages don't contain tool outputs - Tool outputs are below the token threshold - Data isn't compressible (high uniqueness)
"},{"location":"troubleshooting/#compression-too-aggressive","title":"\"Compression too aggressive\"","text":"Symptom: LLM responses are missing information that was in tool outputs.
Solutions:
# 1. Keep more items\nconfig = HeadroomConfig()\nconfig.smart_crusher.max_items_after_crush = 50 # Default: 15\n\n# 2. Skip compression for specific tools\nresponse = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=messages,\n headroom_tool_profiles={\n \"important_tool\": {\"skip_compression\": True},\n },\n)\n\n# 3. Disable SmartCrusher entirely\nconfig.smart_crusher.enabled = False\n
"},{"location":"troubleshooting/#high-latency","title":"\"High latency\"","text":"Symptom: Requests take longer than expected.
Diagnosis:
import time\nimport logging\n\nlogging.basicConfig(level=logging.DEBUG)\n\nstart = time.time()\nresponse = client.chat.completions.create(...)\nprint(f\"Total time: {time.time() - start:.2f}s\")\n\n# Check logs for:\n# - \"SmartCrusher\" timing\n# - \"EmbeddingScorer\" timing (slow if using embeddings)\n
Solutions:
# 1. Use BM25 instead of embeddings (faster)\nconfig = HeadroomConfig()\nconfig.smart_crusher.relevance.tier = \"bm25\" # Default may use embeddings\n\n# 2. Increase threshold to skip small payloads\nconfig.smart_crusher.min_tokens_to_crush = 500\n\n# 3. Disable transforms you don't need\nconfig.cache_aligner.enabled = False\nconfig.rolling_window.enabled = False\n
"},{"location":"troubleshooting/#validationerror-on-setup","title":"\"ValidationError on setup\"","text":"Symptom: validate_setup() returns errors.
Common Issues:
result = client.validate_setup()\nprint(result)\n\n# Provider error:\n# {\"provider\": {\"ok\": False, \"error\": \"No API key\"}}\n# \u2192 Set OPENAI_API_KEY or pass api_key to OpenAI()\n\n# Storage error:\n# {\"storage\": {\"ok\": False, \"error\": \"unable to open database\"}}\n# \u2192 Check path permissions, use :memory: for testing\n\n# Config error:\n# {\"config\": {\"ok\": False, \"error\": \"Invalid mode\"}}\n# \u2192 Use \"audit\" or \"optimize\" only\n
Solutions:
# 1. For testing, use in-memory storage\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n store_url=\"sqlite:///:memory:\", # No file created\n)\n\n# 2. For temp directory storage\nimport tempfile\nimport os\ndb_path = os.path.join(tempfile.gettempdir(), \"headroom.db\")\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n store_url=f\"sqlite:///{db_path}\",\n)\n
"},{"location":"troubleshooting/#importinstallation-issues","title":"Import/Installation Issues","text":""},{"location":"troubleshooting/#pip-install-fails-with-c-compilation-error","title":"\"pip install fails with C++ compilation error\"","text":"Symptom: Installation fails with an error like:
RuntimeError: Unsupported compiler -- at least C++11 support is needed!\nERROR: Failed building wheel for hnswlib\n
Cause: headroom-ai depends on hnswlib, a C++ extension that must be compiled from source. Slim environments (Docker slim images, minimal CI runners) lack the required build tools.
Solutions:
# Linux / Debian-based (including Docker)\napt-get install -y build-essential && pip install headroom-ai\n\n# macOS (Xcode command line tools)\nxcode-select --install && pip install headroom-ai\n
In a Dockerfile, install and remove build tools in one layer to keep the image slim:
FROM python:3.11-slim\nRUN apt-get update && apt-get install -y --no-install-recommends build-essential \\\n && pip install \"headroom-ai[proxy]\" \\\n && apt-get purge -y build-essential && apt-get autoremove -y \\\n && rm -rf /var/lib/apt/lists/*\n
"},{"location":"troubleshooting/#modulenotfounderror-no-module-named-headroom","title":"\"ModuleNotFoundError: No module named 'headroom'\"","text":"# 1. Check it's installed in the right environment\npip show headroom-ai\n\n# 2. If using virtual environment, ensure it's activated\nsource venv/bin/activate # or equivalent\n\n# 3. Reinstall\npip install --upgrade headroom-ai\n
"},{"location":"troubleshooting/#importerror-cannot-import-name-x-from-headroom","title":"\"ImportError: cannot import name 'X' from 'headroom'\"","text":"# Check available imports\nimport headroom\nprint(dir(headroom))\n\n# Common imports:\nfrom headroom import (\n HeadroomClient,\n OpenAIProvider,\n AnthropicProvider,\n HeadroomConfig,\n # Exceptions\n HeadroomError,\n ConfigurationError,\n ProviderError,\n)\n
"},{"location":"troubleshooting/#missing-optional-dependency","title":"\"Missing optional dependency\"","text":"# For proxy server\npip install \"headroom-ai[proxy]\"\n\n# For embedding-based relevance scoring\npip install \"headroom-ai[relevance]\"\n\n# For everything\npip install \"headroom-ai[all]\"\n
"},{"location":"troubleshooting/#provider-specific-issues","title":"Provider-Specific Issues","text":""},{"location":"troubleshooting/#openai-invalid-api-key","title":"OpenAI: \"Invalid API key\"","text":"from openai import OpenAI\nimport os\n\n# Ensure key is set\napi_key = os.environ.get(\"OPENAI_API_KEY\")\nif not api_key:\n raise ValueError(\"OPENAI_API_KEY not set\")\n\nclient = HeadroomClient(\n original_client=OpenAI(api_key=api_key),\n provider=OpenAIProvider(),\n)\n
"},{"location":"troubleshooting/#anthropic-authentication-error","title":"Anthropic: \"Authentication error\"","text":"from anthropic import Anthropic\nimport os\n\napi_key = os.environ.get(\"ANTHROPIC_API_KEY\")\nclient = HeadroomClient(\n original_client=Anthropic(api_key=api_key),\n provider=AnthropicProvider(),\n)\n
"},{"location":"troubleshooting/#unknown-model-warnings","title":"\"Unknown model\" warnings","text":"# For custom/fine-tuned models, specify context limit\nclient = HeadroomClient(\n original_client=OpenAI(),\n provider=OpenAIProvider(),\n model_context_limits={\n \"ft:gpt-4o-2024-08-06:my-org::abc123\": 128000,\n \"my-custom-model\": 32000,\n },\n)\n
"},{"location":"troubleshooting/#debugging-techniques","title":"Debugging Techniques","text":""},{"location":"troubleshooting/#enable-full-logging","title":"Enable Full Logging","text":"import logging\n\n# See everything\nlogging.basicConfig(\n level=logging.DEBUG,\n format=\"%(asctime)s %(name)s %(levelname)s %(message)s\",\n)\n\n# Or just Headroom logs\nlogging.getLogger(\"headroom\").setLevel(logging.DEBUG)\n
"},{"location":"troubleshooting/#inspect-transform-results","title":"Inspect Transform Results","text":"# Use simulate to see what would happen\nplan = client.chat.completions.simulate(\n model=\"gpt-4o\",\n messages=messages,\n)\n\nprint(f\"Tokens: {plan.tokens_before} -> {plan.tokens_after}\")\nprint(f\"Transforms: {plan.transforms}\")\nprint(f\"Waste signals: {plan.waste_signals}\")\n\n# See the actual optimized messages\nimport json\nprint(json.dumps(plan.messages_optimized, indent=2))\n
"},{"location":"troubleshooting/#check-storage-contents","title":"Check Storage Contents","text":"from datetime import datetime, timedelta\n\n# Get recent metrics\nmetrics = client.get_metrics(\n start_time=datetime.utcnow() - timedelta(hours=1),\n limit=10,\n)\n\nfor m in metrics:\n print(f\"{m.timestamp}: {m.tokens_input_before} -> {m.tokens_input_after}\")\n print(f\" Transforms: {m.transforms_applied}\")\n if m.error:\n print(f\" ERROR: {m.error}\")\n
"},{"location":"troubleshooting/#manual-transform-testing","title":"Manual Transform Testing","text":"from headroom import SmartCrusher, Tokenizer\nfrom headroom.config import SmartCrusherConfig\nimport json\n\n# Test compression directly\nconfig = SmartCrusherConfig()\ncrusher = SmartCrusher(config)\ntokenizer = Tokenizer()\n\nmessages = [\n {\"role\": \"tool\", \"content\": json.dumps({\"items\": list(range(100))}), \"tool_call_id\": \"1\"}\n]\n\nresult = crusher.apply(messages, tokenizer)\nprint(f\"Tokens: {result.tokens_before} -> {result.tokens_after}\")\nprint(f\"Compressed content: {result.messages[0]['content'][:200]}...\")\n
"},{"location":"troubleshooting/#error-reference","title":"Error Reference","text":"Exception Meaning Solution ConfigurationError Invalid config values Check config parameters ProviderError Provider issue (unknown model, etc.) Set model_context_limits StorageError Database issue Check path/permissions CompressionError Compression failed Rare - check data format TokenizationError Token counting failed Check model name ValidationError Setup validation failed Run validate_setup()"},{"location":"troubleshooting/#handling-errors","title":"Handling Errors","text":"from headroom import (\n HeadroomClient,\n HeadroomError,\n ConfigurationError,\n StorageError,\n)\n\ntry:\n client = HeadroomClient(...)\n response = client.chat.completions.create(...)\nexcept ConfigurationError as e:\n print(f\"Config issue: {e}\")\n print(f\"Details: {e.details}\")\nexcept StorageError as e:\n print(f\"Storage issue: {e}\")\n # Headroom continues to work, just without metrics persistence\nexcept HeadroomError as e:\n print(f\"Headroom error: {e}\")\n
"},{"location":"troubleshooting/#getting-help","title":"Getting Help","text":" - Enable debug logging and check the output
- Use simulate() to see what transforms would apply
- Check validate_setup() for configuration issues
- File an issue at https://github.com/headroom-sdk/headroom/issues
When filing an issue, include: - Headroom version (pip show headroom) - Python version - Provider (OpenAI/Anthropic) - Debug log output - Minimal reproduction code
"},{"location":"typescript-sdk/","title":"TypeScript SDK","text":"The Headroom TypeScript SDK lets any JavaScript or TypeScript application compress LLM messages before sending them to a model. It saves tokens, reduces costs, and fits more context into every request.
"},{"location":"typescript-sdk/#install","title":"Install","text":"npm install headroom-ai\n
Requires a running Headroom proxy or Headroom Cloud API key.
"},{"location":"typescript-sdk/#quick-start","title":"Quick Start","text":"import { compress } from 'headroom-ai';\n\nconst result = await compress(messages, { model: 'gpt-4o' });\nconsole.log(`Saved ${result.tokensSaved} tokens`);\n\nconst response = await openai.chat.completions.create({\n model: 'gpt-4o',\n messages: result.messages,\n});\n
"},{"location":"typescript-sdk/#how-it-works","title":"How It Works","text":"The TypeScript SDK is an HTTP client. When you call compress(), it sends your messages to the Headroom proxy's POST /v1/compress endpoint. The proxy runs the full compression pipeline (SmartCrusher, ContentRouter, CacheAligner, etc.) and returns compressed messages. No compression logic runs in Node.js \u2014 all the heavy lifting happens in the proxy.
Your TypeScript App\n \u2502\n \u2502 compress(messages)\n \u25bc\nheadroom-ai (npm) \u2190 HTTP client\n \u2502\n \u2502 POST /v1/compress\n \u25bc\nHeadroom Proxy / Cloud \u2190 compression pipeline (Python)\n \u2502\n \u2502 compressed messages\n \u25bc\nYour TypeScript App\n \u2502\n \u2502 openai.chat.completions.create(compressed)\n \u25bc\nLLM Provider\n
"},{"location":"typescript-sdk/#core-api-compress","title":"Core API: compress()","text":"import { compress } from 'headroom-ai';\n\nconst result = await compress(messages, {\n model: 'gpt-4o', // model name (for token counting)\n baseUrl: 'http://localhost:8787', // proxy URL (default)\n apiKey: 'hr_...', // Headroom Cloud key\n timeout: 30000, // ms (default)\n fallback: true, // return uncompressed if proxy down (default)\n retries: 1, // retry on transient errors (default)\n});\n\nresult.messages // compressed messages (same format as input)\nresult.tokensBefore // original token count\nresult.tokensAfter // compressed token count\nresult.tokensSaved // tokens removed\nresult.compressionRatio // tokensAfter / tokensBefore\nresult.transformsApplied // e.g. ['router:smart_crusher:0.35']\nresult.compressed // false if fallback kicked in\n
Messages use standard OpenAI chat format: { role, content, tool_calls?, tool_call_id? }.
"},{"location":"typescript-sdk/#environment-variables","title":"Environment Variables","text":"Instead of passing options, set environment variables:
HEADROOM_BASE_URL \u2014 proxy or cloud URL (default: http://localhost:8787) HEADROOM_API_KEY \u2014 Headroom Cloud API key
"},{"location":"typescript-sdk/#reusable-client","title":"Reusable Client","text":"For apps making many calls, create a client once and reuse it:
import { HeadroomClient } from 'headroom-ai';\n\nconst client = new HeadroomClient({\n baseUrl: 'http://localhost:8787',\n apiKey: 'hr_...',\n});\n\nconst r1 = await client.compress(messages1, { model: 'gpt-4o' });\nconst r2 = await client.compress(messages2, { model: 'gpt-4o' });\n
"},{"location":"typescript-sdk/#framework-adapters","title":"Framework Adapters","text":""},{"location":"typescript-sdk/#vercel-ai-sdk","title":"Vercel AI SDK","text":"The Headroom middleware plugs directly into Vercel AI SDK's wrapLanguageModel():
import { headroomMiddleware } from 'headroom-ai/vercel-ai';\nimport { wrapLanguageModel, generateText } from 'ai';\nimport { openai } from '@ai-sdk/openai';\n\nconst model = wrapLanguageModel({\n model: openai('gpt-4o'),\n middleware: headroomMiddleware(),\n});\n\n// All calls through this model are automatically compressed\nconst { text } = await generateText({ model, messages });\n
The middleware intercepts messages in the transformParams hook, converts Vercel's internal format to OpenAI format, compresses via the proxy, and converts back. Your app code doesn't change.
You can also compress Vercel messages directly:
import { compressVercelMessages } from 'headroom-ai/vercel-ai';\n\nconst result = await compressVercelMessages(modelMessages, { model: 'gpt-4o' });\n// result.messages is in Vercel ModelMessage[] format\n
"},{"location":"typescript-sdk/#openai-sdk","title":"OpenAI SDK","text":"Wrap your OpenAI client to auto-compress messages on every chat.completions.create() call:
import { withHeadroom } from 'headroom-ai/openai';\nimport OpenAI from 'openai';\n\nconst client = withHeadroom(new OpenAI());\n\n// Messages are compressed before sending \u2014 transparent to your code\nconst response = await client.chat.completions.create({\n model: 'gpt-4o',\n messages: longConversation,\n});\n
Only chat.completions.create() is intercepted. All other methods (embeddings, images, audio) pass through unchanged.
"},{"location":"typescript-sdk/#anthropic-sdk","title":"Anthropic SDK","text":"Same pattern for the Anthropic client:
import { withHeadroom } from 'headroom-ai/anthropic';\nimport Anthropic from '@anthropic-ai/sdk';\n\nconst client = withHeadroom(new Anthropic());\n\nconst response = await client.messages.create({\n model: 'claude-sonnet-4-5-20250929',\n messages: longConversation,\n max_tokens: 1024,\n});\n
Only messages.create() is intercepted. The adapter converts between Anthropic's content block format and OpenAI format automatically.
"},{"location":"typescript-sdk/#error-handling","title":"Error Handling","text":"import { compress, HeadroomConnectionError, HeadroomAuthError } from 'headroom-ai';\n\ntry {\n const result = await compress(messages, { model: 'gpt-4o', fallback: false });\n} catch (error) {\n if (error instanceof HeadroomAuthError) {\n // Invalid API key (401)\n } else if (error instanceof HeadroomConnectionError) {\n // Proxy unreachable\n }\n}\n
With fallback: true (the default), connection errors and 5xx responses return the original messages uncompressed instead of throwing. Auth errors (401) and client errors (400) always throw.
"},{"location":"typescript-sdk/#fallback-behavior","title":"Fallback Behavior","text":"By default, compress() never blocks your app. If the proxy is unreachable:
Scenario fallback: true (default) fallback: false Proxy unreachable Returns uncompressed, compressed: false Throws HeadroomConnectionError Proxy 503 error Returns uncompressed after retries Throws HeadroomCompressError Invalid API key (401) Throws HeadroomAuthError Throws HeadroomAuthError Bad request (400) Throws HeadroomCompressError Throws HeadroomCompressError"},{"location":"typescript-sdk/#zero-dependencies","title":"Zero Dependencies","text":"The headroom-ai package has no runtime dependencies. Framework SDKs (Vercel AI, OpenAI, Anthropic) are optional peer dependencies \u2014 only install what you use.
"},{"location":"typescript-sdk/#openclaw-plugin","title":"OpenClaw Plugin","text":"The TypeScript SDK powers the headroom-openclaw plugin for OpenClaw agents. The plugin uses HeadroomClient internally to compress context during the assemble() lifecycle hook. Install it with openclaw plugins install headroom-openclaw. See the plugin source for details.
"},{"location":"typescript-sdk/#comparison-with-python-sdk","title":"Comparison with Python SDK","text":"Feature Python SDK TypeScript SDK compress() Native (runs locally) HTTP client (calls proxy) Proxy Built-in server Connects to proxy Vercel AI SDK N/A Middleware adapter OpenAI SDK HeadroomClient wrapper withHeadroom() wrapper Anthropic SDK HeadroomClient wrapper withHeadroom() wrapper LangChain HeadroomChatModel Use compress() directly Memory system Full (SQLite + HNSW) Not yet (use proxy) MCP server Built-in Not yet CLI tools headroom proxy, headroom wrap, etc. N/A (use Python CLI)"},{"location":"plans/dynamic-smart-crusher/","title":"Dynamic SmartCrusher Preservation Plan","text":""},{"location":"plans/dynamic-smart-crusher/#problem-statement","title":"Problem Statement","text":"Current SmartCrusher uses static \"First 3 + Last 2\" preservation regardless of: - Array size (100 items vs 10,000 items get same treatment) - Data pattern (time series vs search results vs logs) - Query context (user asking about \"latest\" vs \"oldest\") - Position importance (first items might all be identical/wasteful) - Learned retrieval patterns (which positions do users actually need?)
This is too simplistic for a production-grade compression system.
"},{"location":"plans/dynamic-smart-crusher/#current-implementation-analysis","title":"Current Implementation Analysis","text":"Location: headroom/transforms/smart_crusher.py
Current Logic (lines 2273-2279, 2353-2359, 2561-2567):
# Always keep first 3\nfor i in range(min(3, n)):\n keep_indices.add(i)\n\n# Always keep last 2\nfor i in range(max(0, n - 2), n):\n keep_indices.add(i)\n
Problems: 1. Fixed slots waste budget - If first 3 items are identical, we've wasted 3 slots 2. No size adaptation - 20-item array loses 25% to anchors; 1000-item array loses 0.5% 3. Pattern-agnostic - Search results don't need \"last 2\"; time series might need more recency 4. No learning - Doesn't adapt based on what users actually retrieve
"},{"location":"plans/dynamic-smart-crusher/#proposed-solution-adaptive-slot-allocation","title":"Proposed Solution: Adaptive Slot Allocation","text":""},{"location":"plans/dynamic-smart-crusher/#idea-1-size-proportional-anchor-budget","title":"Idea 1: Size-Proportional Anchor Budget","text":"Instead of fixed counts, allocate a percentage budget for position-based anchors:
def calculate_anchor_budget(array_size: int, max_items: int) -> AnchorBudget:\n \"\"\"Allocate slots proportionally, with floors and ceilings.\"\"\"\n\n # Base: 20% of output budget for position anchors\n total_anchor_slots = max(3, min(10, int(max_items * 0.20)))\n\n # Distribution: 60% front, 40% back (front-weighted for context)\n front_slots = max(1, int(total_anchor_slots * 0.6))\n back_slots = max(1, total_anchor_slots - front_slots)\n\n return AnchorBudget(front=front_slots, back=back_slots)\n
Example: | Array Size | Max Items | Anchor Budget | Front | Back | |------------|-----------|---------------|-------|------| | 50 | 10 | 3 | 2 | 1 | | 200 | 15 | 3 | 2 | 1 | | 1000 | 20 | 4 | 3 | 1 | | 5000 | 30 | 6 | 4 | 2 |
"},{"location":"plans/dynamic-smart-crusher/#idea-2-pattern-aware-anchor-weighting","title":"Idea 2: Pattern-Aware Anchor Weighting","text":"Different data patterns need different position importance:
class AnchorStrategy(Enum):\n FRONT_HEAVY = \"front_heavy\" # Search results: top items matter most\n BACK_HEAVY = \"back_heavy\" # Logs: recent items matter most\n BALANCED = \"balanced\" # Time series: both ends matter\n MIDDLE_AWARE = \"middle_aware\" # Database: order might be arbitrary\n\ndef get_anchor_strategy(pattern: DataPattern) -> AnchorStrategy:\n return {\n DataPattern.SEARCH_RESULTS: AnchorStrategy.FRONT_HEAVY, # Top N by score\n DataPattern.LOGS: AnchorStrategy.BACK_HEAVY, # Recency matters\n DataPattern.TIME_SERIES: AnchorStrategy.BALANCED, # Both ends for trend\n DataPattern.GENERIC: AnchorStrategy.MIDDLE_AWARE, # Don't assume order\n }.get(pattern, AnchorStrategy.BALANCED)\n
FRONT_HEAVY (Search Results): - Front: 80% of anchor budget - Back: 20% of anchor budget - Rationale: Top search results are ranked by relevance
BACK_HEAVY (Logs): - Front: 20% of anchor budget - Back: 80% of anchor budget - Rationale: Most recent logs are usually most relevant
BALANCED (Time Series): - Front: 50% of anchor budget - Back: 50% of anchor budget - Rationale: Need both start and end for trend analysis
MIDDLE_AWARE (Generic/Database): - Front: 30% of anchor budget - Back: 30% of anchor budget - Middle sample: 40% of anchor budget (stratified) - Rationale: Order might be arbitrary; sample across positions
"},{"location":"plans/dynamic-smart-crusher/#idea-3-query-aware-dynamic-weighting","title":"Idea 3: Query-Aware Dynamic Weighting","text":"Adjust anchor strategy based on user's query:
def adjust_for_query(base_strategy: AnchorStrategy, query: str) -> AnchorWeights:\n \"\"\"Shift anchor weights based on query intent.\"\"\"\n\n weights = base_strategy.default_weights()\n\n # Recency signals\n recency_keywords = [\"latest\", \"recent\", \"last\", \"newest\", \"current\"]\n if any(kw in query.lower() for kw in recency_keywords):\n weights.back_weight *= 1.5\n weights.front_weight *= 0.7\n\n # Historical signals\n historical_keywords = [\"first\", \"oldest\", \"earliest\", \"original\", \"initial\"]\n if any(kw in query.lower() for kw in historical_keywords):\n weights.front_weight *= 1.5\n weights.back_weight *= 0.7\n\n # Range signals\n range_keywords = [\"all\", \"every\", \"complete\", \"full\"]\n if any(kw in query.lower() for kw in range_keywords):\n weights.middle_weight *= 1.3 # Better coverage\n\n return weights.normalize()\n
"},{"location":"plans/dynamic-smart-crusher/#idea-4-information-density-anchor-selection","title":"Idea 4: Information-Density Anchor Selection","text":"Don't blindly take first N - select most informative items from anchor regions:
def select_informative_anchors(\n items: list[dict],\n region: str, # \"front\", \"back\", \"middle\"\n slots: int,\n all_items_hash: set[str]\n) -> list[int]:\n \"\"\"Select most informative items from a region.\"\"\"\n\n if region == \"front\":\n candidates = list(range(min(slots * 3, len(items)))) # Consider 3x candidates\n elif region == \"back\":\n start = max(0, len(items) - slots * 3)\n candidates = list(range(start, len(items)))\n else: # middle\n step = len(items) // (slots * 3 + 1)\n candidates = [i * step for i in range(1, slots * 3 + 1)]\n\n # Score each candidate by information content\n scored = []\n for idx in candidates:\n item = items[idx]\n item_hash = hash_item(item)\n\n # Skip if we've seen identical item\n if item_hash in all_items_hash:\n continue\n\n score = calculate_information_score(item, items)\n scored.append((idx, score, item_hash))\n\n # Select top N by information score\n scored.sort(key=lambda x: x[1], reverse=True)\n selected = []\n for idx, _, item_hash in scored[:slots]:\n selected.append(idx)\n all_items_hash.add(item_hash)\n\n return sorted(selected)\n\n\ndef calculate_information_score(item: dict, all_items: list[dict]) -> float:\n \"\"\"Score item by how much unique information it contributes.\"\"\"\n\n score = 0.0\n\n # 1. Field uniqueness - rare field values score higher\n for field, value in item.items():\n field_values = [i.get(field) for i in all_items if field in i]\n value_frequency = field_values.count(value) / len(field_values)\n score += (1 - value_frequency) # Rare values score higher\n\n # 2. Structural uniqueness - different fields than typical\n typical_fields = get_typical_fields(all_items)\n unique_fields = set(item.keys()) - typical_fields\n score += len(unique_fields) * 0.5\n\n # 3. Content length - longer items often more informative\n content_length = len(json.dumps(item))\n avg_length = sum(len(json.dumps(i)) for i in all_items) / len(all_items)\n if content_length > avg_length:\n score += 0.3\n\n return score\n
"},{"location":"plans/dynamic-smart-crusher/#idea-5-toin-learned-position-importance","title":"Idea 5: TOIN-Learned Position Importance","text":"Track which positions users actually retrieve and learn from it:
@dataclass\nclass PositionRetrievalPattern:\n \"\"\"Learned position importance from retrieval data.\"\"\"\n tool_name: str\n total_compressions: int\n position_retrievals: dict[str, int] # \"front_10%\", \"middle\", \"back_10%\"\n\n def get_position_weights(self) -> dict[str, float]:\n \"\"\"Convert retrieval counts to weights.\"\"\"\n total = sum(self.position_retrievals.values())\n if total == 0:\n return {\"front\": 0.5, \"middle\": 0.0, \"back\": 0.5}\n\n return {\n position: count / total\n for position, count in self.position_retrievals.items()\n }\n\n\nclass TOINPositionLearning:\n \"\"\"Learn position importance from retrieval patterns.\"\"\"\n\n def record_retrieval(\n self,\n tool_name: str,\n original_size: int,\n retrieved_indices: list[int]\n ):\n \"\"\"Record which positions were retrieved.\"\"\"\n for idx in retrieved_indices:\n position = self._classify_position(idx, original_size)\n self._increment_position_count(tool_name, position)\n\n def _classify_position(self, idx: int, size: int) -> str:\n \"\"\"Classify index into position bucket.\"\"\"\n relative_pos = idx / size\n if relative_pos < 0.1:\n return \"front_10%\"\n elif relative_pos < 0.3:\n return \"front_30%\"\n elif relative_pos > 0.9:\n return \"back_10%\"\n elif relative_pos > 0.7:\n return \"back_30%\"\n else:\n return \"middle\"\n\n def get_anchor_recommendation(self, tool_name: str) -> AnchorWeights:\n \"\"\"Get learned anchor weights for a tool.\"\"\"\n pattern = self._get_pattern(tool_name)\n if pattern.total_compressions < 10:\n return AnchorWeights.default() # Not enough data\n\n weights = pattern.get_position_weights()\n return AnchorWeights(\n front=weights.get(\"front_10%\", 0.3) + weights.get(\"front_30%\", 0.1),\n middle=weights.get(\"middle\", 0.2),\n back=weights.get(\"back_10%\", 0.3) + weights.get(\"back_30%\", 0.1),\n )\n
"},{"location":"plans/dynamic-smart-crusher/#idea-6-stratified-sampling-for-middle-positions","title":"Idea 6: Stratified Sampling for Middle Positions","text":"For large arrays, sample strategically from middle:
def stratified_middle_sample(\n items: list[dict],\n num_samples: int,\n analysis: ArrayAnalysis\n) -> list[int]:\n \"\"\"Sample middle positions using stratified approach.\"\"\"\n\n n = len(items)\n front_boundary = int(n * 0.1)\n back_boundary = int(n * 0.9)\n middle_items = list(range(front_boundary, back_boundary))\n\n if not middle_items or num_samples <= 0:\n return []\n\n # Strategy 1: Cluster-based sampling\n if analysis.has_clusterable_field:\n clusters = cluster_by_field(items, analysis.cluster_field)\n return sample_from_clusters(clusters, num_samples, middle_items)\n\n # Strategy 2: Variance-based sampling (pick high-variance points)\n if analysis.numeric_fields:\n variance_scores = calculate_position_variance(items, analysis.numeric_fields)\n sorted_by_variance = sorted(\n middle_items,\n key=lambda i: variance_scores.get(i, 0),\n reverse=True\n )\n return sorted(sorted_by_variance[:num_samples])\n\n # Strategy 3: Even spacing (fallback)\n step = len(middle_items) // (num_samples + 1)\n return [middle_items[i * step] for i in range(1, num_samples + 1)]\n
"},{"location":"plans/dynamic-smart-crusher/#testing-strategy","title":"Testing Strategy","text":""},{"location":"plans/dynamic-smart-crusher/#test-category-1-adversarial-position-tests","title":"Test Category 1: Adversarial Position Tests","text":"Test cases where important data is NOT at expected positions:
class TestAdversarialPositions:\n \"\"\"Test scenarios that break 'first 3 + last 2' assumption.\"\"\"\n\n def test_important_data_in_middle(self):\n \"\"\"Critical error at position 50 of 100-item array.\"\"\"\n items = [{\"status\": \"ok\", \"value\": i} for i in range(100)]\n items[50] = {\"status\": \"error\", \"error_code\": \"CRITICAL\", \"value\": 50}\n\n result = smart_crusher.crush(items, max_items=10)\n\n # Error item MUST be preserved regardless of position\n assert any(item.get(\"error_code\") == \"CRITICAL\" for item in result)\n\n def test_spike_not_at_boundaries(self):\n \"\"\"Numeric spike at position 75 of 100-item array.\"\"\"\n items = [{\"metric\": 10.0 + random.random()} for _ in range(100)]\n items[75][\"metric\"] = 1000.0 # Spike\n\n result = smart_crusher.crush(items, max_items=10)\n\n # Spike MUST be preserved as anomaly\n assert any(item[\"metric\"] > 500 for item in result)\n\n def test_first_items_identical(self):\n \"\"\"First 10 items are identical - shouldn't waste slots.\"\"\"\n items = [{\"id\": \"same\", \"value\": 0}] * 10 + [\n {\"id\": f\"unique_{i}\", \"value\": i} for i in range(90)\n ]\n\n result = smart_crusher.crush(items, max_items=10)\n\n # Should NOT have multiple identical items\n ids = [item[\"id\"] for item in result]\n # At most 1-2 of the identical items, not 3\n assert ids.count(\"same\") <= 2\n\n def test_last_items_identical(self):\n \"\"\"Last 10 items are identical - shouldn't waste slots.\"\"\"\n items = [{\"id\": f\"unique_{i}\", \"value\": i} for i in range(90)] + [\n {\"id\": \"same\", \"value\": 100}\n ] * 10\n\n result = smart_crusher.crush(items, max_items=10)\n\n ids = [item[\"id\"] for item in result]\n assert ids.count(\"same\") <= 2\n\n def test_relevant_item_in_middle(self):\n \"\"\"Item matching user query is in middle of array.\"\"\"\n items = [{\"name\": f\"item_{i}\", \"status\": \"active\"} for i in range(100)]\n items[42][\"name\"] = \"target_item\"\n items[42][\"description\"] = \"This is what user asked about\"\n\n result = smart_crusher.crush(\n items,\n max_items=10,\n query=\"find target_item\"\n )\n\n # Query-matched item MUST be preserved\n assert any(\"target_item\" in item.get(\"name\", \"\") for item in result)\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-2-size-adaptation-tests","title":"Test Category 2: Size Adaptation Tests","text":"class TestSizeAdaptation:\n \"\"\"Test that anchor allocation scales with array size.\"\"\"\n\n @pytest.mark.parametrize(\"size,expected_min_anchors\", [\n (20, 3), # Small array: at least 3 anchors\n (100, 4), # Medium array: at least 4 anchors\n (500, 5), # Large array: at least 5 anchors\n (2000, 6), # Very large: at least 6 anchors\n ])\n def test_anchor_count_scales(self, size, expected_min_anchors):\n \"\"\"Anchor count should increase with array size.\"\"\"\n items = [{\"id\": i, \"value\": i * 10} for i in range(size)]\n\n result = smart_crusher.crush(items, max_items=20)\n\n # Count items from first 10% and last 10%\n anchor_count = sum(\n 1 for item in result\n if item[\"id\"] < size * 0.1 or item[\"id\"] > size * 0.9\n )\n\n assert anchor_count >= expected_min_anchors\n\n def test_small_array_high_preservation(self):\n \"\"\"Small arrays should preserve higher percentage.\"\"\"\n items = [{\"id\": i} for i in range(15)]\n\n result = smart_crusher.crush(items, max_items=20)\n\n # Should preserve most/all of small array\n assert len(result) >= 10 # At least 66%\n\n def test_large_array_efficient_sampling(self):\n \"\"\"Large arrays should sample efficiently.\"\"\"\n items = [{\"id\": i, \"value\": i} for i in range(5000)]\n\n result = smart_crusher.crush(items, max_items=20)\n\n # Should have good distribution across positions\n positions = [item[\"id\"] for item in result]\n\n has_front = any(p < 500 for p in positions)\n has_middle = any(500 < p < 4500 for p in positions)\n has_back = any(p > 4500 for p in positions)\n\n assert has_front and has_back\n # Middle should be represented if array is large enough\n assert has_middle\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-3-pattern-specific-tests","title":"Test Category 3: Pattern-Specific Tests","text":"class TestPatternAwareAnchoring:\n \"\"\"Test pattern-specific anchor strategies.\"\"\"\n\n def test_search_results_front_heavy(self):\n \"\"\"Search results should preserve more from front.\"\"\"\n items = [\n {\"title\": f\"Result {i}\", \"score\": 1.0 - (i * 0.01)}\n for i in range(100)\n ]\n\n result = smart_crusher.crush(items, max_items=10)\n\n # More items should be from front (high scores)\n front_count = sum(1 for item in result if item[\"score\"] > 0.9)\n back_count = sum(1 for item in result if item[\"score\"] < 0.1)\n\n assert front_count > back_count\n\n def test_logs_back_heavy(self):\n \"\"\"Logs should preserve more from back (recent).\"\"\"\n items = [\n {\"timestamp\": f\"2024-01-{i:02d}\", \"level\": \"INFO\", \"message\": f\"Log {i}\"}\n for i in range(1, 31)\n ]\n\n result = smart_crusher.crush(items, max_items=10)\n\n # More items should be from back (recent logs)\n timestamps = [item[\"timestamp\"] for item in result]\n recent_count = sum(1 for ts in timestamps if int(ts[-2:]) > 20)\n old_count = sum(1 for ts in timestamps if int(ts[-2:]) < 10)\n\n assert recent_count >= old_count\n\n def test_time_series_balanced(self):\n \"\"\"Time series should have balanced front/back.\"\"\"\n items = [\n {\"timestamp\": f\"2024-01-01T{i:02d}:00:00\", \"value\": 100 + i}\n for i in range(24)\n ]\n\n result = smart_crusher.crush(items, max_items=8)\n\n hours = [int(item[\"timestamp\"][11:13]) for item in result]\n front_count = sum(1 for h in hours if h < 8)\n back_count = sum(1 for h in hours if h > 16)\n\n # Should be roughly balanced\n assert abs(front_count - back_count) <= 2\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-4-query-aware-tests","title":"Test Category 4: Query-Aware Tests","text":"class TestQueryAwareAnchoring:\n \"\"\"Test query-based anchor adjustment.\"\"\"\n\n def test_latest_query_shifts_to_back(self):\n \"\"\"'Latest' in query should preserve more recent items.\"\"\"\n items = [{\"id\": i, \"created\": f\"2024-01-{i:02d}\"} for i in range(1, 31)]\n\n result = smart_crusher.crush(\n items,\n max_items=8,\n query=\"Show me the latest entries\"\n )\n\n ids = [item[\"id\"] for item in result]\n recent_count = sum(1 for id in ids if id > 20)\n\n assert recent_count >= 3 # At least 3 recent items\n\n def test_first_query_shifts_to_front(self):\n \"\"\"'First' in query should preserve earlier items.\"\"\"\n items = [{\"id\": i, \"created\": f\"2024-01-{i:02d}\"} for i in range(1, 31)]\n\n result = smart_crusher.crush(\n items,\n max_items=8,\n query=\"Show me the first entries\"\n )\n\n ids = [item[\"id\"] for item in result]\n early_count = sum(1 for id in ids if id < 10)\n\n assert early_count >= 3\n\n def test_specific_id_query_finds_item(self):\n \"\"\"Query for specific ID should find it regardless of position.\"\"\"\n items = [{\"id\": f\"item_{i:04d}\", \"value\": i} for i in range(1000)]\n\n result = smart_crusher.crush(\n items,\n max_items=10,\n query=\"Find item_0567\"\n )\n\n assert any(item[\"id\"] == \"item_0567\" for item in result)\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-5-coverage-metrics-tests","title":"Test Category 5: Coverage Metrics Tests","text":"class TestCoverageMetrics:\n \"\"\"Test that preserved items represent the full distribution.\"\"\"\n\n def test_value_range_coverage(self):\n \"\"\"Preserved items should cover the value range.\"\"\"\n items = [{\"value\": i} for i in range(100)]\n\n result = smart_crusher.crush(items, max_items=10)\n\n values = [item[\"value\"] for item in result]\n\n # Should cover most of the range\n assert min(values) < 10 # Has low values\n assert max(values) > 90 # Has high values\n\n # Should have some middle values too\n middle_count = sum(1 for v in values if 30 < v < 70)\n assert middle_count >= 1\n\n def test_category_coverage(self):\n \"\"\"Preserved items should represent all categories.\"\"\"\n items = [\n {\"category\": cat, \"id\": i}\n for i, cat in enumerate([\"A\"] * 30 + [\"B\"] * 30 + [\"C\"] * 40)\n ]\n\n result = smart_crusher.crush(items, max_items=10)\n\n categories = set(item[\"category\"] for item in result)\n\n # Should have at least 2 of 3 categories\n assert len(categories) >= 2\n\n def test_temporal_coverage(self):\n \"\"\"Preserved items should span the time range.\"\"\"\n items = [\n {\"timestamp\": f\"2024-{m:02d}-15\", \"event\": f\"event_{i}\"}\n for i, m in enumerate(range(1, 13))\n ]\n\n result = smart_crusher.crush(items, max_items=5)\n\n months = [int(item[\"timestamp\"][5:7]) for item in result]\n\n # Should span at least 6 months of the year\n assert max(months) - min(months) >= 6\n
"},{"location":"plans/dynamic-smart-crusher/#test-category-6-retrieval-simulation-tests","title":"Test Category 6: Retrieval Simulation Tests","text":"class TestRetrievalSimulation:\n \"\"\"Simulate user retrieval patterns to measure effectiveness.\"\"\"\n\n def test_retrieval_hit_rate_random_queries(self):\n \"\"\"Measure how often preserved items satisfy random queries.\"\"\"\n items = [\n {\"id\": i, \"name\": f\"Item {i}\", \"category\": f\"cat_{i % 5}\"}\n for i in range(100)\n ]\n\n compressed = smart_crusher.crush(items, max_items=15)\n compressed_ids = {item[\"id\"] for item in compressed}\n\n # Simulate 100 random \"queries\" (random item lookups)\n hits = 0\n for _ in range(100):\n target_id = random.randint(0, 99)\n if target_id in compressed_ids:\n hits += 1\n\n # Should hit at least 15% (we keep 15 of 100)\n assert hits >= 15\n\n def test_retrieval_hit_rate_weighted_queries(self):\n \"\"\"Measure hits for queries weighted toward common patterns.\"\"\"\n items = [{\"id\": i, \"value\": i * 10} for i in range(100)]\n\n compressed = smart_crusher.crush(items, max_items=15)\n compressed_ids = {item[\"id\"] for item in compressed}\n\n # Weight queries toward front (30%), back (30%), anomalies (40%)\n hits = 0\n queries = (\n list(range(10)) * 3 + # Front queries\n list(range(90, 100)) * 3 + # Back queries\n [50] * 4 # Middle anomaly queries\n )\n\n for target_id in queries:\n if target_id in compressed_ids:\n hits += 1\n\n # Should hit more often than random due to anchor strategy\n assert hits >= 20 # At least 20% hit rate\n
"},{"location":"plans/dynamic-smart-crusher/#implementation-phases","title":"Implementation Phases","text":""},{"location":"plans/dynamic-smart-crusher/#phase-1-refactor-anchor-logic-foundation","title":"Phase 1: Refactor Anchor Logic (Foundation)","text":" - Extract anchor selection into
AnchorSelector class - Make slot counts configurable via
AnchorConfig - Add size-proportional allocation
- Maintain backward compatibility with current defaults
"},{"location":"plans/dynamic-smart-crusher/#phase-2-pattern-aware-anchoring","title":"Phase 2: Pattern-Aware Anchoring","text":" - Map
DataPattern to AnchorStrategy - Implement front-heavy, back-heavy, balanced, middle-aware strategies
- Add pattern-specific anchor weight configs
"},{"location":"plans/dynamic-smart-crusher/#phase-3-information-density-selection","title":"Phase 3: Information-Density Selection","text":" - Add
calculate_information_score() for items - Select from candidate region instead of fixed positions
- Deduplicate identical items across regions
"},{"location":"plans/dynamic-smart-crusher/#phase-4-query-aware-adjustment","title":"Phase 4: Query-Aware Adjustment","text":" - Parse query for position intent keywords
- Adjust anchor weights dynamically
- Add query-position relevance scoring
"},{"location":"plans/dynamic-smart-crusher/#phase-5-toin-position-learning","title":"Phase 5: TOIN Position Learning","text":" - Track retrieval positions in TOIN
- Learn per-tool position importance
- Use learned weights to adjust anchor strategy
"},{"location":"plans/dynamic-smart-crusher/#phase-6-comprehensive-testing","title":"Phase 6: Comprehensive Testing","text":" - Implement all adversarial tests
- Add coverage metric tests
- Add retrieval simulation tests
- Performance benchmarks
"},{"location":"plans/dynamic-smart-crusher/#configuration-schema","title":"Configuration Schema","text":"@dataclass\nclass AnchorConfig:\n \"\"\"Configuration for dynamic anchor allocation.\"\"\"\n\n # Base anchor budget as percentage of max_items\n anchor_budget_pct: float = 0.20 # 20% of slots for position anchors\n\n # Minimum and maximum anchor slots\n min_anchor_slots: int = 3\n max_anchor_slots: int = 10\n\n # Default distribution (overridden by pattern)\n default_front_weight: float = 0.5\n default_back_weight: float = 0.5\n default_middle_weight: float = 0.0\n\n # Pattern-specific overrides\n search_front_weight: float = 0.8\n logs_back_weight: float = 0.8\n time_series_balance: float = 0.5\n\n # Query keyword detection\n recency_keywords: list[str] = field(default_factory=lambda: [\n \"latest\", \"recent\", \"last\", \"newest\", \"current\"\n ])\n historical_keywords: list[str] = field(default_factory=lambda: [\n \"first\", \"oldest\", \"earliest\", \"original\", \"initial\"\n ])\n\n # Information density selection\n use_information_density: bool = True\n candidate_multiplier: int = 3 # Consider 3x candidates per slot\n\n # TOIN learning\n use_learned_positions: bool = True\n min_samples_for_learning: int = 10\n
"},{"location":"plans/dynamic-smart-crusher/#success-metrics","title":"Success Metrics","text":" - Retrieval Coverage: % of user retrievals that hit preserved items (target: >80%)
- Information Density: Unique information per preserved slot (target: no duplicate items)
- Distribution Coverage: Preserved items span full value/time/category ranges
- Adversarial Robustness: All adversarial tests pass
- Backward Compatibility: Existing tests still pass
- Performance: <5ms additional latency for anchor selection
"},{"location":"plans/dynamic-smart-crusher/#risks-and-mitigations","title":"Risks and Mitigations","text":"Risk Mitigation Information density calculation is expensive Cache scores, limit candidate pool Query keyword detection is brittle Use as soft signal, not hard rule TOIN learning needs cold start Fall back to pattern-based defaults Breaking existing behavior Feature flag, A/B testing Middle sampling misses important items Always include anomalies/errors regardless"},{"location":"plans/dynamic-smart-crusher/#next-steps","title":"Next Steps","text":" - Review and approve this plan
- Write failing tests first (TDD approach)
- Implement Phase 1 (refactor foundation)
- Iterate through phases with test validation
- Benchmark against current implementation
- A/B test in production with telemetry
"}]}
\ No newline at end of file
diff --git a/sitemap.xml b/sitemap.xml
index 71614f975..18ee4f8e8 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -2,126 +2,126 @@
https://chopratejas.github.io/headroom/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/ARCHITECTURE/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/LATENCY_BENCHMARKS/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/LIMITATIONS/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/agno/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/api/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/benchmarks/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/ccr/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/compression/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/configuration/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/errors/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/getting-started/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/image-compression/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/integration-guide/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/langchain/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/learn/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/llmlingua/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/macos-deployment/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/mcp/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/memory/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/metrics/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/proxy/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/quickstart/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/sdk/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/shared-context/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/strands/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/text-compression/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/transforms/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/troubleshooting/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/typescript-sdk/
- 2026-04-10
+ 2026-04-11
https://chopratejas.github.io/headroom/plans/dynamic-smart-crusher/
- 2026-04-10
+ 2026-04-11
\ No newline at end of file
diff --git a/sitemap.xml.gz b/sitemap.xml.gz
index 0261da2a43623f03f9240029d625e1be4895eb63..2c598d1a6acad025d7cdb2a83c8f8b64dd8ca1fa 100644
GIT binary patch
delta 418
zcmV;T0bTyq1J(lvABzYG0FT*`2OS8Kq8PRzaghd*EGK{8PyPUsi(ygCnrStGprUrc
zEUM@3OZ~CBZLj77_5^uSi~}tyn{)as#Nlvg$Yt;{Q4`S!QntL1c%2l}=2O*PP0AP-
zxICT5ypO6KSD~_uvN55|@Jmu-T(-lmaZy;18e{ZgE=J@3zm=4I)a~{3`a?avsb{ld
z@;Y}B6aRnI-k`hOuI@M8deuE|*K82qmfiYs_2cJVx!yl4w_l(5P&N;nZrN?VKk_k1
z6C$5@a%|eWawx5rxF$h5Ed?EFqfdH&EPb!k@PYcWu@cuMGq1g)q8*6m)IO68-bT9QmrBN@#G<7J`k1o
zw82L_G0JCjiZ4SZj7PrR{~ny$pOZ5i0_i=Uwv`CF)P<6ikBlzJD1+vU!Ey8NqBxFz
zpqGfA(H9tpT@Peq96J|2j#C0s=u)mZPe>|V0ttNz(mPdC`K2h4f&*E2LwA3^5#;tS
M2ml76l}``=0KK8eZU6uP
delta 418
zcmV;T0bTyq1J(lvABzYGfIZle2OS99Vi>j|agha)EGK_I%>Dq9i(ye+H*Gb8prUrc
zEUKseOZ}m`?XKn{4g`5ti~}tyn{)ak#Nlvg$Yt;{Q4`S!QntL1c$pM!^Reo#W@StZ
zT%OKjK19_`t58`+*_cpf_$8?^F57X}xF{@0jWK#L7nAY--%83p>h7k!d0)5h>g%?c
zyv$w1#6N$vSLiOctGi9VUiDAgH5VtnDN^FjjjlfG4sn(E&c=8Z3ABakP
z+TbIe809lM#g`!y#v|YEe~(V>&&in$f%KkF+e!po>Ox7%M@AQ9ltJ^w;I#QSQ5+{f
z&~rr3=nIVFZUC||PMr%M$0-3RbSc-ICnS|FfrLH<>7A;n{8E%i!I3PyqPsue2y*uq
MS?C7iicbO(c8q5uE@