headroom/wiki/proxy.md

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

459 lines
16 KiB
Markdown
Raw Normal View History

# Proxy Server Documentation
The Headroom proxy server is a production-ready HTTP server that applies context optimization to all requests passing through it.
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
> The proxy exposes compression-as-a-service via the `POST /v1/compress` endpoint — used by the [TypeScript SDK](typescript-sdk.md), LiteLLM's `headroom` guardrail, and gateway sidecars. It is loopback-only by default; see the endpoint section below.
## Starting the Proxy
```bash
# Basic usage
headroom proxy
# Custom port
headroom proxy --port 8080
# With all options
headroom proxy \
--host 0.0.0.0 \
--port 8787 \
--log-file /var/log/headroom.jsonl \
--budget 100.0
```
### Common agent CLI entrypoints
```bash
# 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.
fix(telemetry): switch anonymous telemetry to opt-in (off by default) (#1223) ## Description Anonymous usage telemetry was **on by default** (opt-out). This flips it to **opt-in**: nothing is collected or shipped unless the user explicitly turns it on. Small change, but it makes "no data leaves the proxy by default" the actual default rather than something users have to discover and disable. Closes # <!-- N/A: no tracking issue --> ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) — adds `--telemetry` opt-in flag - [x] Breaking change (fix or feature that would cause existing functionality to change) — telemetry no longer runs unless opted in - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `is_telemetry_enabled()` is now **fail-closed**: only explicit on-values (`on`/`true`/`1`/`yes`/`enable`/`enabled`) enable telemetry; unset, empty, or unrecognized values stay disabled. This single predicate gates both the Supabase beacon and the local `/v1/telemetry` collector. - Added `--telemetry` opt-in flag to `headroom proxy` and `headroom install apply`; kept `--no-telemetry` and `HEADROOM_TELEMETRY=off` for back-compat. If both are passed, opt-out wins. - Install manifests now write `HEADROOM_TELEMETRY` explicitly (`on`/`off`) plus the matching flag, so generated systemd/docker/launchd deployments are unambiguous and don't rely on the runtime default. - Startup banner and proxy log show `DISABLED` by default and surface how to opt in. - Updated tests for opt-in defaults; added coverage for the default-off banner, the `--telemetry` flag, and the explicit-on manifest path. - Updated docs (proxy/configuration/installation/benchmarks/community-savings mdx, spec 011/015, wiki proxy/cli/benchmarks/metrics) and `CHANGELOG.md`. ## Testing - [x] Unit tests pass (`pytest`) — targeted telemetry + install-planner suites - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_telemetry_warning.py tests/test_telemetry.py tests/test_install/test_planner.py -q 81 passed $ ruff check <changed source + test files> All checks passed! $ mypy headroom/telemetry/beacon.py headroom/cli/proxy.py headroom/cli/install.py \ headroom/install/planner.py headroom/proxy/server.py Success: no issues found in 5 source files ``` ## Real Behavior Proof - **Environment:** macOS (darwin 25.4.0), project `.venv`, `headroom` CLI. - **Exact command / steps:** ``` $ python -c "import os; from headroom.telemetry.beacon import is_telemetry_enabled; \ os.environ.pop('HEADROOM_TELEMETRY', None); print('unset ->', is_telemetry_enabled()); \ [ (os.environ.__setitem__('HEADROOM_TELEMETRY', v), print(repr(v), '->', is_telemetry_enabled())) \ for v in ['on','TRUE','1','yes','off','0','garbage',''] ]" $ headroom proxy --help | grep -i telemetry $ headroom install apply --help | grep -i telemetry ``` - **Observed result:** ``` unset -> False # off by default 'on' -> True 'TRUE' -> True '1' -> True 'yes' -> True 'off' -> False '0' -> False 'garbage' -> False '' -> False # fail-closed proxy: --telemetry Opt in to anonymous usage telemetry — off by default (env: HEADROOM_TELEMETRY=on) --no-telemetry Force anonymous usage telemetry off (already the default; env: HEADROOM_TELEMETRY=off) install: --telemetry Opt in to anonymous telemetry in the runtime (off by default). --no-telemetry Force anonymous telemetry off in the runtime (already the default). ``` - **Not tested:** live Supabase beacon network round-trip (no opt-in network call was made); full `pytest` suite was not run — only the telemetry + install-planner targeted suites. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - "Breaking change" is checked because the default behavior changes (telemetry stops running unless opted in). It is **not** an API break — `--no-telemetry` and `HEADROOM_TELEMETRY=off` still work, so existing opt-out configs are unaffected. - No tracking issue, so `Closes #` is left N/A.
2026-06-20 21:26:04 -07:00
Anonymous aggregate telemetry is **off by default** (opt-in). Opt in with `HEADROOM_TELEMETRY=on` or `headroom proxy --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:
```bash
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:
```bash
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
### Core Options
| 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 |
docs(proxy): correct --code-aware default to disabled (#1710) ## Description The wiki proxy page (`wiki/proxy.md`, which feeds the published docs site) claimed `--code-aware` defaults to **true**. The CLI deliberately defaults it to **disabled**: `headroom/cli/proxy.py` resolves the paired flag to off unless `--code-aware` is passed or `HEADROOM_CODE_AWARE_ENABLED` is truthy, and the Click help text plus `docs/content/docs/proxy.mdx` and `wiki/cli.md` already document it as disabled. This PR aligns the one remaining stale table row and collapses the self-contradictory separate `--no-code-aware` row into a single paired-flag entry, matching the style used in `docs/content/docs/proxy.mdx`. Fixes #1700 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `wiki/proxy.md`: replaced the two flag-table rows claiming `--code-aware` default `true` / `--no-code-aware` default `false` with one `--code-aware` / `--no-code-aware` row documenting the actual default (`disabled`), the `headroom-ai[code]` requirement, and the `HEADROOM_CODE_AWARE_ENABLED=1` env opt-in. ## Testing - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python -c " from click.testing import CliRunner from headroom.cli.proxy import proxy r = CliRunner().invoke(proxy, ['--help']) print([l.strip() for l in r.output.splitlines() if 'code-aware' in l][0]) " --code-aware / --no-code-aware Enable/disable AST-based code compression. $ grep -n "code-aware" wiki/proxy.md 77:| `--code-aware` / `--no-code-aware` | disabled | Enable or disable AST-based code compression. Requires `headroom-ai[code]` (env: HEADROOM_CODE_AWARE_ENABLED=1 to enable) | ``` ## Real Behavior Proof - Environment: Windows 11, local checkout at `upstream/main` (9fbd47ba), Python 3.13. - Exact command / steps: `headroom proxy --help` (via Click test runner) to confirm the CLI help says "Default: disabled"; inspected `headroom/cli/proxy.py` flag resolution (explicit flag → `HEADROOM_CODE_AWARE_ENABLED` → off); `grep -rn "code.aware" wiki/ docs/` to find every doc stating a default. - Observed result: CLI default is disabled; `docs/content/docs/proxy.mdx:61` and `wiki/cli.md:255-256` already say disabled/off; only `wiki/proxy.md:77-78` claimed true. After the change the table matches actual behavior. - Not tested: rendered docs-site build (content-only table edit). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:19:12 +02:00
| `--code-aware` / `--no-code-aware` | disabled | Enable or disable AST-based code compression. Requires `headroom-ai[code]` (env: HEADROOM_CODE_AWARE_ENABLED=1 to enable) |
| `--anthropic-api-url` | `https://api.anthropic.com` | Custom Anthropic API URL endpoint |
2026-01-19 23:35:16 +01:00
| `--openai-api-url` | `https://api.openai.com` | Custom OpenAI API URL endpoint |
fix(proxy): dedupe Codex WS request logging for accurate mixed-provider dashboards (#2189) ## Description Running Claude Code (Anthropic) and Codex (OpenAI) against the **same** Headroom proxy instance on one port produced incorrect, unstable dashboard data. The proxy core is provider-isolated and multi-provider-safe by design; the defect was in the observability layer. The Codex `/v1/responses` **WebSocket** handler was the only path in the proxy that wrote to the request logger by hand instead of through the unified `emit_request_outcome` funnel, and it did so twice per session close: the per-turn funnel record plus an unconditional cumulative session-summary `RequestLog`. This PR removes the duplicate summary log so Codex WS emits exactly one request log per turn, matching the HTTP provider paths. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Dropped the duplicate cumulative session-summary `RequestLog` in the Codex WS handler while preserving the per-turn `emit_request_outcome` path. - Preserved gated `request_messages` and `turn_id` on residual outcomes so dashboard telemetry keeps the useful attribution without double-counting tokens. - Ensured explicit `--anyllm-provider` wins over a leaked `HEADROOM_ANYLLM_PROVIDER` environment variable. - Registered retry delay settings that had drifted out of the settings registry. - Hardened tests against developer-shell `HEADROOM_*` / `ANTHROPIC_CUSTOM_HEADERS` leakage and stabilized several focused proxy/wrap test fixtures. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/pytest tests/ -q -p no:cacheprovider 8529 passed, 537 skipped, 5831 warnings in 276.25s (0:04:36) $ .venv/bin/ruff check <touched files> All checks passed! ``` ## Real Behavior Proof - Environment: macOS (Darwin 25.4.0), Python 3.13.14, pytest 9.0.3, ruff via project venv, branch `fix/multi-provider-runtime`. - Exact command / steps: Ran the full test suite without pytest cache provider and Ruff on all touched files; used `git stash` to confirm the stale fake-config failures pre-existed this change. - Observed result: Full suite passed with no failures; Ruff passed; Codex WS now routes end-of-session logging through `emit_request_outcome`, emitting one request log per turn with the same accounting model as Anthropic HTTP turns. - Not tested: Live simultaneous Claude + Codex dashboard run. `mypy headroom` was not run to completion; a scoped run reported one pre-existing `settings_store.py:470` coercion error outside this diff. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A - server-side observability fix; no UI markup changed. ## Additional Notes - The proxy's multi-provider routing, header/auth isolation, and per-model cache keying are already correct and unchanged here; only the WS observability write path was double-counting. - Architectural assessment: `plans/reports/research-260714-0004-multi-provider-upstream-compression-report.md`; root-cause + resolution trail: `plans/reports/debug-assessment-260714-0011-dashboard-instability-mixed-claude-codex-report.md`. - No live simultaneous Claude + Codex dashboard run was performed; validation is from test coverage and code review of the WS logging path. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-16 01:18:34 +07:00
| `--anthropic-extra-headers` | unset | JSON object of extra headers merged into (and overriding) forwarded Anthropic requests, e.g. `'{"Api-Key": "..."}'` |
| `--openai-extra-headers` | unset | JSON object of extra headers merged into (and overriding) forwarded OpenAI requests |
### Run Modes
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:
```bash
headroom proxy --mode token
HEADROOM_MODE=cache headroom proxy
```
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.
### Context Management Options
fix(cli): harden all CLI surfaces + fix docs accuracy (#1491) ## Summary Full CLI audit + documentation accuracy pass. All 5 commits on this branch: ### CLI Hardening (4 commits) - **Clean errors instead of tracebacks**: corrupt manifests, missing Docker, malformed JSONL, bad `--profile`, invalid env-var values all now raise `click.ClickException` with helpful messages - **Range validation**: ~25 numeric flags across 10 files now use `click.IntRange`/`FloatRange` — `--port 0`, `--hours -1`, `--limit 0` etc. produce clean usage errors instead of silent wrong behavior - **Flag combination warnings**: conflicting combos (`--no-rate-limit` + `--rpm`, `--no-optimize` + `--target-ratio`, `--telemetry` + `--no-telemetry`) emit yellow warnings on stderr - **`memory --db-path` default fixed**: was resolving to `headroom_memory.db` (wrong bare file); now uses project store `./.headroom/memory.db` if present, else `~/.headroom/memory.db` - **`memory list --search` + filters**: `--scope`/`--session`/`--since` were silently ignored when `--search` was also set; now filters are applied to search results - **`learn --verbosity --apply` now works**: the output shaper is off by default (`HEADROOM_OUTPUT_SHAPER`); `--apply` now hot-enables it via `POST /admin/runtime-env` on a running proxy, or prints explicit `export HEADROOM_OUTPUT_SHAPER=1` instructions when no proxy is running - **`perf --hours` overflow**: `1e9` hours no longer raises `OverflowError`; treated as "all data" - **`evals memory --categories` invalid input**: `abc,1,2` now raises `BadParameter` instead of a raw `ValueError` traceback ### Documentation (1 commit, 20 files) Corrected factual errors found by 3 parallel audit agents across root docs, wiki, and the published Fumadocs site: **Critical (caused runtime errors or wrong behavior if followed):** - `simulation.mdx`: `plan.transforms_applied` -> `plan.transforms`; `plan.savings_percent` -> computed from available fields (both raised `AttributeError`) - `shared-context.mdx`: `import { SharedContext } from "headroom"` -> `"headroom-ai"` (5x `ImportError`) - `claude-code-azure-foundry.mdx`: `pip install headroom` -> `pip install headroom-ai` - `api-reference.mdx` + `configuration.mdx`: `from headroom import GoogleProvider` -> `from headroom.providers import GoogleProvider` - `ccr.mdx`: CCR TTL default 300s -> 1800s (30 min) **Fabricated flags removed:** - `wiki/proxy.md` + `wiki/cli.md`: `--no-intelligent-context`, `--no-intelligent-scoring`, `--no-compress-first` (none exist); replaced with real CCR flags - `wiki/configuration.md`: `--no-ccr-responses`, `--no-ccr-expansion` (none exist); replaced with real flags - `wiki/troubleshooting.md`, `wiki/metrics.md`, `docs/troubleshooting.mdx`: `headroom proxy --log-level debug` (flag doesn't exist) **Stale content corrected:** - `llms.txt`: telemetry stated as enabled-by-default (it's opt-in); wrap list had 5 tools (now 11) - `README.md`: compatibility matrix added 5 missing `wrap` targets; `unwrap`, `doctor`, `init`/`install`, savings-analytics now mentioned - `SECURITY.md`: supported version table showed 0.2.x (current: 0.27.x) - `wiki/learn.md`: 5 missing flags added; verbosity shaper-off behavior documented - `wiki/quickstart.md`: "Configuration Reference" linked to `api.md` (wrong) -> `configuration.md` - `CacheAlignerConfig.enabled` default corrected: `True` -> `False` - `opencode.mdx`: `--port` default wrong ("random") -> 8787; `openai` backend removed - `CONTRIBUTING.md`: broken Markdown table cell fixed - `docs/meta.json`: `claude-code-azure-foundry` added to nav (was unreachable orphan page) - `configuration.mdx`: SDK modes vs proxy `--mode` now clearly distinguished ## Test plan - [x] `python -m pytest tests/ -x -q` — 857 passed, 0 failures - [x] 41-combination CLI smoke test (all flag combos across 8 commands) — 0 tracebacks - [x] `ruff check` on all modified Python files — clean - [x] Docs changes are removals/corrections of fabricated or stale content; no new claims introduced
2026-06-27 14:48:43 -07:00
Context management in the proxy is handled automatically by the compression pipeline. CCR (Compress-Cache-Retrieve) ensures that when content is compressed or messages are dropped, the original data remains accessible for the LLM to retrieve on demand. See [CCR documentation](ccr.md) for details.
fix(cli): harden all CLI surfaces + fix docs accuracy (#1491) ## Summary Full CLI audit + documentation accuracy pass. All 5 commits on this branch: ### CLI Hardening (4 commits) - **Clean errors instead of tracebacks**: corrupt manifests, missing Docker, malformed JSONL, bad `--profile`, invalid env-var values all now raise `click.ClickException` with helpful messages - **Range validation**: ~25 numeric flags across 10 files now use `click.IntRange`/`FloatRange` — `--port 0`, `--hours -1`, `--limit 0` etc. produce clean usage errors instead of silent wrong behavior - **Flag combination warnings**: conflicting combos (`--no-rate-limit` + `--rpm`, `--no-optimize` + `--target-ratio`, `--telemetry` + `--no-telemetry`) emit yellow warnings on stderr - **`memory --db-path` default fixed**: was resolving to `headroom_memory.db` (wrong bare file); now uses project store `./.headroom/memory.db` if present, else `~/.headroom/memory.db` - **`memory list --search` + filters**: `--scope`/`--session`/`--since` were silently ignored when `--search` was also set; now filters are applied to search results - **`learn --verbosity --apply` now works**: the output shaper is off by default (`HEADROOM_OUTPUT_SHAPER`); `--apply` now hot-enables it via `POST /admin/runtime-env` on a running proxy, or prints explicit `export HEADROOM_OUTPUT_SHAPER=1` instructions when no proxy is running - **`perf --hours` overflow**: `1e9` hours no longer raises `OverflowError`; treated as "all data" - **`evals memory --categories` invalid input**: `abc,1,2` now raises `BadParameter` instead of a raw `ValueError` traceback ### Documentation (1 commit, 20 files) Corrected factual errors found by 3 parallel audit agents across root docs, wiki, and the published Fumadocs site: **Critical (caused runtime errors or wrong behavior if followed):** - `simulation.mdx`: `plan.transforms_applied` -> `plan.transforms`; `plan.savings_percent` -> computed from available fields (both raised `AttributeError`) - `shared-context.mdx`: `import { SharedContext } from "headroom"` -> `"headroom-ai"` (5x `ImportError`) - `claude-code-azure-foundry.mdx`: `pip install headroom` -> `pip install headroom-ai` - `api-reference.mdx` + `configuration.mdx`: `from headroom import GoogleProvider` -> `from headroom.providers import GoogleProvider` - `ccr.mdx`: CCR TTL default 300s -> 1800s (30 min) **Fabricated flags removed:** - `wiki/proxy.md` + `wiki/cli.md`: `--no-intelligent-context`, `--no-intelligent-scoring`, `--no-compress-first` (none exist); replaced with real CCR flags - `wiki/configuration.md`: `--no-ccr-responses`, `--no-ccr-expansion` (none exist); replaced with real flags - `wiki/troubleshooting.md`, `wiki/metrics.md`, `docs/troubleshooting.mdx`: `headroom proxy --log-level debug` (flag doesn't exist) **Stale content corrected:** - `llms.txt`: telemetry stated as enabled-by-default (it's opt-in); wrap list had 5 tools (now 11) - `README.md`: compatibility matrix added 5 missing `wrap` targets; `unwrap`, `doctor`, `init`/`install`, savings-analytics now mentioned - `SECURITY.md`: supported version table showed 0.2.x (current: 0.27.x) - `wiki/learn.md`: 5 missing flags added; verbosity shaper-off behavior documented - `wiki/quickstart.md`: "Configuration Reference" linked to `api.md` (wrong) -> `configuration.md` - `CacheAlignerConfig.enabled` default corrected: `True` -> `False` - `opencode.mdx`: `--port` default wrong ("random") -> 8787; `openai` backend removed - `CONTRIBUTING.md`: broken Markdown table cell fixed - `docs/meta.json`: `claude-code-azure-foundry` added to nav (was unreachable orphan page) - `configuration.mdx`: SDK modes vs proxy `--mode` now clearly distinguished ## Test plan - [x] `python -m pytest tests/ -x -q` — 857 passed, 0 failures - [x] 41-combination CLI smoke test (all flag combos across 8 commands) — 0 tracebacks - [x] `ruff check` on all modified Python files — clean - [x] Docs changes are removals/corrections of fabricated or stale content; no new claims introduced
2026-06-27 14:48:43 -07:00
Key CCR-related proxy flags:
fix(cli): harden all CLI surfaces + fix docs accuracy (#1491) ## Summary Full CLI audit + documentation accuracy pass. All 5 commits on this branch: ### CLI Hardening (4 commits) - **Clean errors instead of tracebacks**: corrupt manifests, missing Docker, malformed JSONL, bad `--profile`, invalid env-var values all now raise `click.ClickException` with helpful messages - **Range validation**: ~25 numeric flags across 10 files now use `click.IntRange`/`FloatRange` — `--port 0`, `--hours -1`, `--limit 0` etc. produce clean usage errors instead of silent wrong behavior - **Flag combination warnings**: conflicting combos (`--no-rate-limit` + `--rpm`, `--no-optimize` + `--target-ratio`, `--telemetry` + `--no-telemetry`) emit yellow warnings on stderr - **`memory --db-path` default fixed**: was resolving to `headroom_memory.db` (wrong bare file); now uses project store `./.headroom/memory.db` if present, else `~/.headroom/memory.db` - **`memory list --search` + filters**: `--scope`/`--session`/`--since` were silently ignored when `--search` was also set; now filters are applied to search results - **`learn --verbosity --apply` now works**: the output shaper is off by default (`HEADROOM_OUTPUT_SHAPER`); `--apply` now hot-enables it via `POST /admin/runtime-env` on a running proxy, or prints explicit `export HEADROOM_OUTPUT_SHAPER=1` instructions when no proxy is running - **`perf --hours` overflow**: `1e9` hours no longer raises `OverflowError`; treated as "all data" - **`evals memory --categories` invalid input**: `abc,1,2` now raises `BadParameter` instead of a raw `ValueError` traceback ### Documentation (1 commit, 20 files) Corrected factual errors found by 3 parallel audit agents across root docs, wiki, and the published Fumadocs site: **Critical (caused runtime errors or wrong behavior if followed):** - `simulation.mdx`: `plan.transforms_applied` -> `plan.transforms`; `plan.savings_percent` -> computed from available fields (both raised `AttributeError`) - `shared-context.mdx`: `import { SharedContext } from "headroom"` -> `"headroom-ai"` (5x `ImportError`) - `claude-code-azure-foundry.mdx`: `pip install headroom` -> `pip install headroom-ai` - `api-reference.mdx` + `configuration.mdx`: `from headroom import GoogleProvider` -> `from headroom.providers import GoogleProvider` - `ccr.mdx`: CCR TTL default 300s -> 1800s (30 min) **Fabricated flags removed:** - `wiki/proxy.md` + `wiki/cli.md`: `--no-intelligent-context`, `--no-intelligent-scoring`, `--no-compress-first` (none exist); replaced with real CCR flags - `wiki/configuration.md`: `--no-ccr-responses`, `--no-ccr-expansion` (none exist); replaced with real flags - `wiki/troubleshooting.md`, `wiki/metrics.md`, `docs/troubleshooting.mdx`: `headroom proxy --log-level debug` (flag doesn't exist) **Stale content corrected:** - `llms.txt`: telemetry stated as enabled-by-default (it's opt-in); wrap list had 5 tools (now 11) - `README.md`: compatibility matrix added 5 missing `wrap` targets; `unwrap`, `doctor`, `init`/`install`, savings-analytics now mentioned - `SECURITY.md`: supported version table showed 0.2.x (current: 0.27.x) - `wiki/learn.md`: 5 missing flags added; verbosity shaper-off behavior documented - `wiki/quickstart.md`: "Configuration Reference" linked to `api.md` (wrong) -> `configuration.md` - `CacheAlignerConfig.enabled` default corrected: `True` -> `False` - `opencode.mdx`: `--port` default wrong ("random") -> 8787; `openai` backend removed - `CONTRIBUTING.md`: broken Markdown table cell fixed - `docs/meta.json`: `claude-code-azure-foundry` added to nav (was unreachable orphan page) - `configuration.mdx`: SDK modes vs proxy `--mode` now clearly distinguished ## Test plan - [x] `python -m pytest tests/ -x -q` — 857 passed, 0 failures - [x] 41-combination CLI smoke test (all flag combos across 8 commands) — 0 tracebacks - [x] `ruff check` on all modified Python files — clean - [x] Docs changes are removals/corrections of fabricated or stale content; no new claims introduced
2026-06-27 14:48:43 -07:00
| Option | Description |
|--------|-------------|
feat(content-router): lossless-first dispatch, cross-turn dedup, and A7 lossy-after-fold (#1818) ## Description <!-- Briefly explain the change and why it is needed. --> Closes # ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text # Paste relevant command output or artifact links here ``` ## Real Behavior Proof - Environment: - Exact command / steps: - Observed result: - Not tested: ## Review Readiness - [ ] I have performed a self-review - [ ] This PR is ready for human review ## Checklist - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) Add screenshots to help explain your changes. ## Additional Notes <!-- Mention any N/A checklist items, tradeoffs, follow-ups, or maintainer context. --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:32:06 -07:00
| `--no-ccr` | Disable CCR entirely — no retrieval markers in compressed output and no injected `headroom_retrieve` tool (lossy, no recovery path) |
fix(cli): harden all CLI surfaces + fix docs accuracy (#1491) ## Summary Full CLI audit + documentation accuracy pass. All 5 commits on this branch: ### CLI Hardening (4 commits) - **Clean errors instead of tracebacks**: corrupt manifests, missing Docker, malformed JSONL, bad `--profile`, invalid env-var values all now raise `click.ClickException` with helpful messages - **Range validation**: ~25 numeric flags across 10 files now use `click.IntRange`/`FloatRange` — `--port 0`, `--hours -1`, `--limit 0` etc. produce clean usage errors instead of silent wrong behavior - **Flag combination warnings**: conflicting combos (`--no-rate-limit` + `--rpm`, `--no-optimize` + `--target-ratio`, `--telemetry` + `--no-telemetry`) emit yellow warnings on stderr - **`memory --db-path` default fixed**: was resolving to `headroom_memory.db` (wrong bare file); now uses project store `./.headroom/memory.db` if present, else `~/.headroom/memory.db` - **`memory list --search` + filters**: `--scope`/`--session`/`--since` were silently ignored when `--search` was also set; now filters are applied to search results - **`learn --verbosity --apply` now works**: the output shaper is off by default (`HEADROOM_OUTPUT_SHAPER`); `--apply` now hot-enables it via `POST /admin/runtime-env` on a running proxy, or prints explicit `export HEADROOM_OUTPUT_SHAPER=1` instructions when no proxy is running - **`perf --hours` overflow**: `1e9` hours no longer raises `OverflowError`; treated as "all data" - **`evals memory --categories` invalid input**: `abc,1,2` now raises `BadParameter` instead of a raw `ValueError` traceback ### Documentation (1 commit, 20 files) Corrected factual errors found by 3 parallel audit agents across root docs, wiki, and the published Fumadocs site: **Critical (caused runtime errors or wrong behavior if followed):** - `simulation.mdx`: `plan.transforms_applied` -> `plan.transforms`; `plan.savings_percent` -> computed from available fields (both raised `AttributeError`) - `shared-context.mdx`: `import { SharedContext } from "headroom"` -> `"headroom-ai"` (5x `ImportError`) - `claude-code-azure-foundry.mdx`: `pip install headroom` -> `pip install headroom-ai` - `api-reference.mdx` + `configuration.mdx`: `from headroom import GoogleProvider` -> `from headroom.providers import GoogleProvider` - `ccr.mdx`: CCR TTL default 300s -> 1800s (30 min) **Fabricated flags removed:** - `wiki/proxy.md` + `wiki/cli.md`: `--no-intelligent-context`, `--no-intelligent-scoring`, `--no-compress-first` (none exist); replaced with real CCR flags - `wiki/configuration.md`: `--no-ccr-responses`, `--no-ccr-expansion` (none exist); replaced with real flags - `wiki/troubleshooting.md`, `wiki/metrics.md`, `docs/troubleshooting.mdx`: `headroom proxy --log-level debug` (flag doesn't exist) **Stale content corrected:** - `llms.txt`: telemetry stated as enabled-by-default (it's opt-in); wrap list had 5 tools (now 11) - `README.md`: compatibility matrix added 5 missing `wrap` targets; `unwrap`, `doctor`, `init`/`install`, savings-analytics now mentioned - `SECURITY.md`: supported version table showed 0.2.x (current: 0.27.x) - `wiki/learn.md`: 5 missing flags added; verbosity shaper-off behavior documented - `wiki/quickstart.md`: "Configuration Reference" linked to `api.md` (wrong) -> `configuration.md` - `CacheAlignerConfig.enabled` default corrected: `True` -> `False` - `opencode.mdx`: `--port` default wrong ("random") -> 8787; `openai` backend removed - `CONTRIBUTING.md`: broken Markdown table cell fixed - `docs/meta.json`: `claude-code-azure-foundry` added to nav (was unreachable orphan page) - `configuration.mdx`: SDK modes vs proxy `--mode` now clearly distinguished ## Test plan - [x] `python -m pytest tests/ -x -q` — 857 passed, 0 failures - [x] 41-combination CLI smoke test (all flag combos across 8 commands) — 0 tracebacks - [x] `ruff check` on all modified Python files — clean - [x] Docs changes are removals/corrections of fabricated or stale content; no new claims introduced
2026-06-27 14:48:43 -07:00
| `--no-ccr-proactive-expansion` | Disable proactive context expansion before the LLM asks |
fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness Address user-reported UX gaps across the CLI surface: - code-aware: add --code-aware/--no-code-aware (+ HEADROOM_CODE_AWARE_ENABLED env) to the Click CLI. PR #411 had added these only to the orphaned argparse main; the user-facing CLI couldn't reach the flag. Banner status text "remove --no-code-aware to enable" referenced a flag that didn't exist — fix to point at the actual flag/env. Surface code-aware in the click banner and add print_banner=False plumbing to run_server so the click path doesn't print two banners back-to-back. - --mode: hide alias clutter via metavar=[token|cache] and rewrite help to lead with the two real modes. Legacy aliases (token_mode/token_savings/...) still validate. - perf --hours: was documented but ignored. Records are now actually filtered, the report shows the actual time-range covered, and the count of records filtered out (so users can tell when raising --hours helps). - perf TOIN: replace the hash-keyed pattern dump with a strategy-distribution view + recommendation-eligibility from the live store — actionable signal rather than opaque rows. - code-graph: clarify in --help that it indexes cwd / project root. - wrap: spell out supported tools, wrap-vs-proxy distinction, and that `headroom wrap opencode` isn't a thing (use `proxy` directly for opencode; openclaw is not opencode). - mcp: note that mcp__headroom__headroom_retrieve is correct MCP namespacing, not a doubled-prefix bug. Renaming would break the proxy's tool injection. - LLMLingua cleanup: remove [llmlingua] extra from pyproject (no live code uses it). Delete wiki/llmlingua.md and clean retired flag/class references in 6 other wiki pages. Point at [ml] (Kompress) where ML compression is documented. - init -g openclaw: strip mcpServers from existing plugin entries before re-writing — newer openclaw schemas reject it, leaving stale entries from older installs unhealable. Pinned with regression test. Tests: mock_run_server signatures in two existing tests accept **kwargs (needed for the new print_banner plumbing). New test for the openclaw mcpServers strip. Full suite: 4847 passed, 262 skipped, 0 failed.
2026-05-07 16:43:35 -07:00
### ML Compression — RETIRED `--llmlingua` flag
fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness Address user-reported UX gaps across the CLI surface: - code-aware: add --code-aware/--no-code-aware (+ HEADROOM_CODE_AWARE_ENABLED env) to the Click CLI. PR #411 had added these only to the orphaned argparse main; the user-facing CLI couldn't reach the flag. Banner status text "remove --no-code-aware to enable" referenced a flag that didn't exist — fix to point at the actual flag/env. Surface code-aware in the click banner and add print_banner=False plumbing to run_server so the click path doesn't print two banners back-to-back. - --mode: hide alias clutter via metavar=[token|cache] and rewrite help to lead with the two real modes. Legacy aliases (token_mode/token_savings/...) still validate. - perf --hours: was documented but ignored. Records are now actually filtered, the report shows the actual time-range covered, and the count of records filtered out (so users can tell when raising --hours helps). - perf TOIN: replace the hash-keyed pattern dump with a strategy-distribution view + recommendation-eligibility from the live store — actionable signal rather than opaque rows. - code-graph: clarify in --help that it indexes cwd / project root. - wrap: spell out supported tools, wrap-vs-proxy distinction, and that `headroom wrap opencode` isn't a thing (use `proxy` directly for opencode; openclaw is not opencode). - mcp: note that mcp__headroom__headroom_retrieve is correct MCP namespacing, not a doubled-prefix bug. Renaming would break the proxy's tool injection. - LLMLingua cleanup: remove [llmlingua] extra from pyproject (no live code uses it). Delete wiki/llmlingua.md and clean retired flag/class references in 6 other wiki pages. Point at [ml] (Kompress) where ML compression is documented. - init -g openclaw: strip mcpServers from existing plugin entries before re-writing — newer openclaw schemas reject it, leaving stale entries from older installs unhealable. Pinned with regression test. Tests: mock_run_server signatures in two existing tests accept **kwargs (needed for the new print_banner plumbing). New test for the openclaw mcpServers strip. Full suite: 4847 passed, 262 skipped, 0 failed.
2026-05-07 16:43:35 -07:00
The `--llmlingua` / `--llmlingua-device` / `--llmlingua-rate` flags and
the `headroom-ai[llmlingua]` extra were retired and replaced by Kompress
(ModernBERT). For the current opt-in path, install `headroom-ai[ml]`
and see [transforms.md](transforms.md) and [ARCHITECTURE.md](ARCHITECTURE.md).
## API Endpoints
### Liveness
```bash
curl http://localhost:8787/livez
```
Response:
```json
{
"service": "headroom-proxy",
"status": "healthy",
"alive": true,
"version": "0.5.21",
"timestamp": "2026-04-10T16:36:25Z",
"uptime_seconds": 12.483
}
```
### Readiness
```bash
curl http://localhost:8787/readyz
```
Response:
```json
{
"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
```bash
curl http://localhost:8787/health
```
Response:
```json
{
"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
```bash
curl http://localhost:8787/stats
```
2026-03-27 15:27:05 +01:00
`/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.
2026-03-27 15:27:05 +01:00
### Historical Savings
```bash
curl http://localhost:8787/stats-history
```
`/stats-history` exposes durable proxy compression history for dashboards and
other Headroom frontends. It returns:
- lifetime proxy compression totals
- compact checkpoint history by default, with `history_mode=full` available for
export/debug flows
- derived hourly, daily, weekly, and monthly rollups for charts
- a `history_summary` block describing stored versus returned checkpoint counts
2026-03-27 15:27:05 +01:00
- UTC timestamps throughout
By default the proxy stores this history at
`${HEADROOM_WORKSPACE_DIR}/proxy_savings.json` (i.e.
`~/.headroom/proxy_savings.json` when `HEADROOM_WORKSPACE_DIR` is unset).
Set `HEADROOM_SAVINGS_PATH` to override the location directly, or set
`HEADROOM_WORKSPACE_DIR` to relocate the full state root. See the
[Filesystem Contract](filesystem-contract.md).
2026-03-27 15:27:05 +01:00
`/dashboard` uses this endpoint directly for its historical view, including the
daily/weekly/monthly rollups and built-in JSON / CSV export buttons.
```bash
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?history_mode=full"
```
### Prometheus Metrics
```bash
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:
```bash
# Anthropic format
POST /v1/messages
# OpenAI format
POST /v1/chat/completions
```
### `POST /v1/compress`
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
Compression-only endpoint. Compresses messages without ever making a **completion request to an LLM provider** — no generation, no provider API key. Used by the [TypeScript SDK](typescript-sdk.md), LiteLLM's `headroom` guardrail, and gateway sidecars.
**It does run local ML models.** Compression is ML-backed: Kompress is a ModernBERT encoder scoring tokens for retention (classification, not generation) and Magika classifies content types, both in-process by default. If `HEADROOM_KOMPRESS_ENDPOINT` is set, Kompress inference is offloaded over HTTP to that model server — real egress from the sidecar. Only inference goes remote; the CCR store and markers stay proxy-local. `HEADROOM_DISABLE_KOMPRESS=1` gives structural compression only.
**Loopback-only by default.** Non-loopback callers get **404** (not 403 — the route stays invisible to scanners). Set `HEADROOM_COMPRESS_ALLOW_REMOTE=1` to allow remote callers.
**No format conversion.** `messages` may be OpenAI-shaped (`role: "tool"` + `tool_call_id`) or Anthropic-shaped (`tool_use` / `tool_result` content blocks); the same shape comes back. `model` selects the tokenizer and context limit — send the real name, including gateway-prefixed forms like `bedrock/anthropic.claude-3-5-sonnet`.
**`system` and `tools` are ignored.** Anthropic sends both out of band. This endpoint accepts them without complaint (200, no warning) and returns neither, so neither is compressed — keep carrying them yourself. That means the Anthropic system prompt is not compressed here, and tool-schema compaction / tool-search deferral are not reachable through this route; run Headroom as the proxy if you need those.
**Request:**
```json
{
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
"messages": [...], // either wire format
"model": "gpt-4o", // tokenizer + context limit
"token_budget": 8000, // optional: override the context limit
"config": { // optional
"mode": "lossy_inline", // ccr | lossy_inline | lossless_then_lossy
"frozen_message_count": 12, // pin an already-cached prefix
"compress_user_messages": false,
"target_ratio": 0.5,
"protect_recent": 2,
"protect_analysis_context": true
}
}
```
**Response:**
```json
{
"messages": [...], // compressed messages
"tokens_before": 15000,
"tokens_after": 3500,
"tokens_saved": 11500,
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
"compression_ratio": 0.23, // tokens_after / tokens_before — LOWER is better
"transforms_applied": ["router:smart_crusher:0.35"],
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
"transforms_summary": {"router:smart_crusher:0.35": 1},
"ccr_hashes": [] // non-empty only with mode="ccr"
}
```
**Headers:**
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
- `x-headroom-bypass: true` — skip compression, return messages as-is with zeroed metrics
**Error responses:** 400 (missing/invalid fields, bad `config.mode` or `config.frozen_message_count`), 401 (bad `HEADROOM_PROXY_TOKEN`), 404 (non-loopback without `HEADROOM_COMPRESS_ALLOW_REMOTE=1`), 503 (compression failed)
**Fail-open:** on timeout you get 200 with the original messages plus `compression_skipped: true` and `skip_reason: "compression_timeout"`.
**Multi-turn callers — don't lose the prefix cache.** This endpoint is stateless: unlike the proxy's own request path (which runs a CacheAligner and tracks provider cache hits across turns), it has no idea what the provider already cached.
The provider caches the bytes you *forwarded*, which compression already changed — so your originals and the cached prefix are no longer the same thing, and it is the forwarded version you must keep reproducing. Compression also varies with position: an older tool result can fall outside the recent-read protection window as the conversation grows and be compressed harder than last turn, so re-compression is not guaranteed to reproduce earlier output either. Two rules:
1. Pass `config.frozen_message_count` = the number of leading messages already cached upstream.
2. Send back the messages you **previously forwarded**, not the pristine originals. `frozen_message_count` returns leading messages exactly as passed in, so feeding it originals hands the provider different bytes than last turn and busts the cache anyway.
```python
forwarded = []
deps: bump ruff from 0.15.22 to 0.16.2 in the pip-minor-patch group across 1 directory (#2962) Bumps the pip-minor-patch group with 1 update in the / directory: [ruff](https://github.com/astral-sh/ruff). Updates `ruff` from 0.15.22 to 0.16.2 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/releases">ruff's releases</a>.</em></p> <blockquote> <h2>0.16.2</h2> <h2>Release Notes</h2> <p>Released on 2026-08-06.</p> <h3>Bug fixes</h3> <ul> <li>[<code>flake8-pyi</code>] Avoid false positives on <code>singledispatch</code> functions (<code>PYI041</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27335">#27335</a>)</li> </ul> <h3>Server</h3> <ul> <li>Register formatting capabilities dynamically to exclude TOML files (<a href="https://redirect.github.com/astral-sh/ruff/pull/27332">#27332</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/MeGaGiGaGon"><code>@​MeGaGiGaGon</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@​charliermarsh</code></a></li> <li><a href="https://github.com/epage"><code>@​epage</code></a></li> <li><a href="https://github.com/sharkdp"><code>@​sharkdp</code></a></li> <li><a href="https://github.com/ntBre"><code>@​ntBre</code></a></li> </ul> <h2>Install ruff 0.16.2</h2> <h3>Install prebuilt binaries via shell script</h3> <pre lang="sh"><code>curl --proto '=https' --tlsv1.2 -LsSf https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-installer.sh | sh </code></pre> <h3>Install prebuilt binaries via powershell script</h3> <pre lang="sh"><code>powershell -ExecutionPolicy Bypass -c &quot;irm https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-installer.ps1 | iex&quot; </code></pre> <h2>Download ruff 0.16.2</h2> <table> <thead> <tr> <th>File</th> <th>Platform</th> <th>Checksum</th> </tr> </thead> <tbody> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-apple-darwin.tar.gz">ruff-aarch64-apple-darwin.tar.gz</a></td> <td>Apple Silicon macOS</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-apple-darwin.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-apple-darwin.tar.gz">ruff-x86_64-apple-darwin.tar.gz</a></td> <td>Intel macOS</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-apple-darwin.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-pc-windows-msvc.zip">ruff-aarch64-pc-windows-msvc.zip</a></td> <td>ARM64 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-pc-windows-msvc.zip">ruff-i686-pc-windows-msvc.zip</a></td> <td>x86 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-pc-windows-msvc.zip">ruff-x86_64-pc-windows-msvc.zip</a></td> <td>x64 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-unknown-linux-gnu.tar.gz">ruff-aarch64-unknown-linux-gnu.tar.gz</a></td> <td>ARM64 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-unknown-linux-gnu.tar.gz">ruff-i686-unknown-linux-gnu.tar.gz</a></td> <td>x86 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64-unknown-linux-gnu.tar.gz">ruff-powerpc64-unknown-linux-gnu.tar.gz</a></td> <td>PPC64 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64le-unknown-linux-gnu.tar.gz">ruff-powerpc64le-unknown-linux-gnu.tar.gz</a></td> <td>PPC64LE Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64le-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-riscv64gc-unknown-linux-gnu.tar.gz">ruff-riscv64gc-unknown-linux-gnu.tar.gz</a></td> <td>RISCV Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-riscv64gc-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-s390x-unknown-linux-gnu.tar.gz">ruff-s390x-unknown-linux-gnu.tar.gz</a></td> <td>S390x Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-s390x-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> </tbody> </table> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md">ruff's changelog</a>.</em></p> <blockquote> <h2>0.16.2</h2> <p>Released on 2026-08-06.</p> <h3>Bug fixes</h3> <ul> <li>[<code>flake8-pyi</code>] Avoid false positives on <code>singledispatch</code> functions (<code>PYI041</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27335">#27335</a>)</li> </ul> <h3>Server</h3> <ul> <li>Register formatting capabilities dynamically to exclude TOML files (<a href="https://redirect.github.com/astral-sh/ruff/pull/27332">#27332</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/MeGaGiGaGon"><code>@​MeGaGiGaGon</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@​charliermarsh</code></a></li> <li><a href="https://github.com/epage"><code>@​epage</code></a></li> <li><a href="https://github.com/sharkdp"><code>@​sharkdp</code></a></li> <li><a href="https://github.com/ntBre"><code>@​ntBre</code></a></li> </ul> <h2>0.16.1</h2> <p>Released on 2026-07-30.</p> <h3>Preview features</h3> <ul> <li>Add an option to opt out of human-readable names (<a href="https://redirect.github.com/astral-sh/ruff/pull/27160">#27160</a>)</li> <li>[<code>flake8-pytest-style</code>] Make fixes safe by default and unsafe only when comments are present (<code>PT018</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27201">#27201</a>)</li> <li>[<code>pyupgrade</code>] Skip fix when a defaulted <code>TypeVar</code> precedes a non-defaulted one (<code>UP040</code>, <code>UP046</code>, <code>UP047</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27133">#27133</a>)</li> <li>[<code>ruff</code>] Fix false positive with unpacked arguments (<code>RUF065</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/26959">#26959</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>Bump <code>gen-lsp-types</code> to gracefully handle unknown enumeration values in LSP messages (<a href="https://redirect.github.com/astral-sh/ruff/pull/27230">#27230</a>)</li> <li>[<code>flake8-bugbear</code>] Mark <code>range</code> as immutable (<code>B008</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27247">#27247</a>)</li> <li>[<code>flake8-comprehensions</code>] NFKC-normalize keyword names in <code>C408</code> fix (<a href="https://redirect.github.com/astral-sh/ruff/pull/26813">#26813</a>)</li> <li>[<code>flake8-return</code>] Fix false positive when variable is read in <code>finally</code> clause (<code>RET504</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25441">#25441</a>)</li> <li>[<code>pydocstyle</code>] Skip section detection inside RST directive bodies (<code>D214</code>, <code>D405</code>, <code>D413</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/23635">#23635</a>)</li> <li>[<code>refurb</code>] Parenthesize <code>yield</code> arguments in the <code>FURB192</code> fix (<a href="https://redirect.github.com/astral-sh/ruff/pull/27192">#27192</a>)</li> </ul> <h3>Rule changes</h3> <ul> <li>[<code>flake8-pytest-style</code>] Mark <code>PT022</code> fixes as unsafe (<a href="https://redirect.github.com/astral-sh/ruff/pull/26440">#26440</a>)</li> <li>[<code>refurb</code>] Mark fixes that remove unknown separators as unsafe (<code>FURB105</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27200">#27200</a>)</li> </ul> <h3>Server</h3> <ul> <li>Fix indexing of excluded nested Ruff workspaces (<a href="https://redirect.github.com/astral-sh/ruff/pull/27303">#27303</a>)</li> <li>Lint TOML files in the LSP (<a href="https://redirect.github.com/astral-sh/ruff/pull/26862">#26862</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/astral-sh/ruff/commit/5b48a040974781ba90b47c8df628f8fd9b6c95dd"><code>5b48a04</code></a> Bump 0.16.2 (<a href="https://redirect.github.com/astral-sh/ruff/issues/27555">#27555</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/1b9e5fc483b95a01fe02ff104820280b1b32e8ae"><code>1b9e5fc</code></a> Update Swatinem/rust-cache action to v2.9.2 (<a href="https://redirect.github.com/astral-sh/ruff/issues/27568">#27568</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/c4e86fc0394c92a9334ba2eb026c77c21db403be"><code>c4e86fc</code></a> [ty] Add helper extension methods for half-range and equality constraints (<a href="https://redirect.github.com/astral-sh/ruff/issues/2">#2</a>...</li> <li><a href="https://github.com/astral-sh/ruff/commit/17a00de2e298612201a8fe30790e9399204af1b9"><code>17a00de</code></a> [ty] Reuse primer commands in memory reports (<a href="https://redirect.github.com/astral-sh/ruff/issues/27553">#27553</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/6ea296b96923e142eb13af2bc6ad261c280d8eb1"><code>6ea296b</code></a> [ty] Normalize type labels in structured docstrings (<a href="https://redirect.github.com/astral-sh/ruff/issues/26923">#26923</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/2fc445f0053f4ec27c717fae0de3671d73c103be"><code>2fc445f</code></a> [ty] Diagnose invalid <strong>getattr</strong> calls (<a href="https://redirect.github.com/astral-sh/ruff/issues/27502">#27502</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/22c7823c4e8bffcca97688d8438c9b567d6817d8"><code>22c7823</code></a> [ty] Enable (but downrank) auto-import completion suggestions from stub-only ...</li> <li><a href="https://github.com/astral-sh/ruff/commit/05160d507f05345a72db9c28ab4edf7c92334819"><code>05160d5</code></a> [ty] Diagnose invalid descriptor <code>__get__</code> calls (<a href="https://redirect.github.com/astral-sh/ruff/issues/27400">#27400</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/baea3d0dcec6d6f6d1659321940f3725771c5f45"><code>baea3d0</code></a> [ty] Expose strict analysis options in the playground (<a href="https://redirect.github.com/astral-sh/ruff/issues/27543">#27543</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/c88946ebeb92be6d276087f0d528cd6471df4ead"><code>c88946e</code></a> [ty] Bump ecosystem-analyzer for strict project settings (<a href="https://redirect.github.com/astral-sh/ruff/issues/27542">#27542</a>)</li> <li>Additional commits viewable in <a href="https://github.com/astral-sh/ruff/compare/0.15.22...0.16.2">compare view</a></li> </ul> </details> <br /> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-14 16:38:08 -05:00
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
def next_turn(new_messages):
deps: bump ruff from 0.15.22 to 0.16.2 in the pip-minor-patch group across 1 directory (#2962) Bumps the pip-minor-patch group with 1 update in the / directory: [ruff](https://github.com/astral-sh/ruff). Updates `ruff` from 0.15.22 to 0.16.2 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/releases">ruff's releases</a>.</em></p> <blockquote> <h2>0.16.2</h2> <h2>Release Notes</h2> <p>Released on 2026-08-06.</p> <h3>Bug fixes</h3> <ul> <li>[<code>flake8-pyi</code>] Avoid false positives on <code>singledispatch</code> functions (<code>PYI041</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27335">#27335</a>)</li> </ul> <h3>Server</h3> <ul> <li>Register formatting capabilities dynamically to exclude TOML files (<a href="https://redirect.github.com/astral-sh/ruff/pull/27332">#27332</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/MeGaGiGaGon"><code>@​MeGaGiGaGon</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@​charliermarsh</code></a></li> <li><a href="https://github.com/epage"><code>@​epage</code></a></li> <li><a href="https://github.com/sharkdp"><code>@​sharkdp</code></a></li> <li><a href="https://github.com/ntBre"><code>@​ntBre</code></a></li> </ul> <h2>Install ruff 0.16.2</h2> <h3>Install prebuilt binaries via shell script</h3> <pre lang="sh"><code>curl --proto '=https' --tlsv1.2 -LsSf https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-installer.sh | sh </code></pre> <h3>Install prebuilt binaries via powershell script</h3> <pre lang="sh"><code>powershell -ExecutionPolicy Bypass -c &quot;irm https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-installer.ps1 | iex&quot; </code></pre> <h2>Download ruff 0.16.2</h2> <table> <thead> <tr> <th>File</th> <th>Platform</th> <th>Checksum</th> </tr> </thead> <tbody> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-apple-darwin.tar.gz">ruff-aarch64-apple-darwin.tar.gz</a></td> <td>Apple Silicon macOS</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-apple-darwin.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-apple-darwin.tar.gz">ruff-x86_64-apple-darwin.tar.gz</a></td> <td>Intel macOS</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-apple-darwin.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-pc-windows-msvc.zip">ruff-aarch64-pc-windows-msvc.zip</a></td> <td>ARM64 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-pc-windows-msvc.zip">ruff-i686-pc-windows-msvc.zip</a></td> <td>x86 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-pc-windows-msvc.zip">ruff-x86_64-pc-windows-msvc.zip</a></td> <td>x64 Windows</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-x86_64-pc-windows-msvc.zip.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-unknown-linux-gnu.tar.gz">ruff-aarch64-unknown-linux-gnu.tar.gz</a></td> <td>ARM64 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-aarch64-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-unknown-linux-gnu.tar.gz">ruff-i686-unknown-linux-gnu.tar.gz</a></td> <td>x86 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-i686-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64-unknown-linux-gnu.tar.gz">ruff-powerpc64-unknown-linux-gnu.tar.gz</a></td> <td>PPC64 Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64le-unknown-linux-gnu.tar.gz">ruff-powerpc64le-unknown-linux-gnu.tar.gz</a></td> <td>PPC64LE Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-powerpc64le-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-riscv64gc-unknown-linux-gnu.tar.gz">ruff-riscv64gc-unknown-linux-gnu.tar.gz</a></td> <td>RISCV Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-riscv64gc-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> <tr> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-s390x-unknown-linux-gnu.tar.gz">ruff-s390x-unknown-linux-gnu.tar.gz</a></td> <td>S390x Linux</td> <td><a href="https://releases.astral.sh/github/ruff/releases/download/0.16.2/ruff-s390x-unknown-linux-gnu.tar.gz.sha256">checksum</a></td> </tr> </tbody> </table> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md">ruff's changelog</a>.</em></p> <blockquote> <h2>0.16.2</h2> <p>Released on 2026-08-06.</p> <h3>Bug fixes</h3> <ul> <li>[<code>flake8-pyi</code>] Avoid false positives on <code>singledispatch</code> functions (<code>PYI041</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27335">#27335</a>)</li> </ul> <h3>Server</h3> <ul> <li>Register formatting capabilities dynamically to exclude TOML files (<a href="https://redirect.github.com/astral-sh/ruff/pull/27332">#27332</a>)</li> </ul> <h3>Contributors</h3> <ul> <li><a href="https://github.com/MeGaGiGaGon"><code>@​MeGaGiGaGon</code></a></li> <li><a href="https://github.com/charliermarsh"><code>@​charliermarsh</code></a></li> <li><a href="https://github.com/epage"><code>@​epage</code></a></li> <li><a href="https://github.com/sharkdp"><code>@​sharkdp</code></a></li> <li><a href="https://github.com/ntBre"><code>@​ntBre</code></a></li> </ul> <h2>0.16.1</h2> <p>Released on 2026-07-30.</p> <h3>Preview features</h3> <ul> <li>Add an option to opt out of human-readable names (<a href="https://redirect.github.com/astral-sh/ruff/pull/27160">#27160</a>)</li> <li>[<code>flake8-pytest-style</code>] Make fixes safe by default and unsafe only when comments are present (<code>PT018</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27201">#27201</a>)</li> <li>[<code>pyupgrade</code>] Skip fix when a defaulted <code>TypeVar</code> precedes a non-defaulted one (<code>UP040</code>, <code>UP046</code>, <code>UP047</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27133">#27133</a>)</li> <li>[<code>ruff</code>] Fix false positive with unpacked arguments (<code>RUF065</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/26959">#26959</a>)</li> </ul> <h3>Bug fixes</h3> <ul> <li>Bump <code>gen-lsp-types</code> to gracefully handle unknown enumeration values in LSP messages (<a href="https://redirect.github.com/astral-sh/ruff/pull/27230">#27230</a>)</li> <li>[<code>flake8-bugbear</code>] Mark <code>range</code> as immutable (<code>B008</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27247">#27247</a>)</li> <li>[<code>flake8-comprehensions</code>] NFKC-normalize keyword names in <code>C408</code> fix (<a href="https://redirect.github.com/astral-sh/ruff/pull/26813">#26813</a>)</li> <li>[<code>flake8-return</code>] Fix false positive when variable is read in <code>finally</code> clause (<code>RET504</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/25441">#25441</a>)</li> <li>[<code>pydocstyle</code>] Skip section detection inside RST directive bodies (<code>D214</code>, <code>D405</code>, <code>D413</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/23635">#23635</a>)</li> <li>[<code>refurb</code>] Parenthesize <code>yield</code> arguments in the <code>FURB192</code> fix (<a href="https://redirect.github.com/astral-sh/ruff/pull/27192">#27192</a>)</li> </ul> <h3>Rule changes</h3> <ul> <li>[<code>flake8-pytest-style</code>] Mark <code>PT022</code> fixes as unsafe (<a href="https://redirect.github.com/astral-sh/ruff/pull/26440">#26440</a>)</li> <li>[<code>refurb</code>] Mark fixes that remove unknown separators as unsafe (<code>FURB105</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/27200">#27200</a>)</li> </ul> <h3>Server</h3> <ul> <li>Fix indexing of excluded nested Ruff workspaces (<a href="https://redirect.github.com/astral-sh/ruff/pull/27303">#27303</a>)</li> <li>Lint TOML files in the LSP (<a href="https://redirect.github.com/astral-sh/ruff/pull/26862">#26862</a>)</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/astral-sh/ruff/commit/5b48a040974781ba90b47c8df628f8fd9b6c95dd"><code>5b48a04</code></a> Bump 0.16.2 (<a href="https://redirect.github.com/astral-sh/ruff/issues/27555">#27555</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/1b9e5fc483b95a01fe02ff104820280b1b32e8ae"><code>1b9e5fc</code></a> Update Swatinem/rust-cache action to v2.9.2 (<a href="https://redirect.github.com/astral-sh/ruff/issues/27568">#27568</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/c4e86fc0394c92a9334ba2eb026c77c21db403be"><code>c4e86fc</code></a> [ty] Add helper extension methods for half-range and equality constraints (<a href="https://redirect.github.com/astral-sh/ruff/issues/2">#2</a>...</li> <li><a href="https://github.com/astral-sh/ruff/commit/17a00de2e298612201a8fe30790e9399204af1b9"><code>17a00de</code></a> [ty] Reuse primer commands in memory reports (<a href="https://redirect.github.com/astral-sh/ruff/issues/27553">#27553</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/6ea296b96923e142eb13af2bc6ad261c280d8eb1"><code>6ea296b</code></a> [ty] Normalize type labels in structured docstrings (<a href="https://redirect.github.com/astral-sh/ruff/issues/26923">#26923</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/2fc445f0053f4ec27c717fae0de3671d73c103be"><code>2fc445f</code></a> [ty] Diagnose invalid <strong>getattr</strong> calls (<a href="https://redirect.github.com/astral-sh/ruff/issues/27502">#27502</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/22c7823c4e8bffcca97688d8438c9b567d6817d8"><code>22c7823</code></a> [ty] Enable (but downrank) auto-import completion suggestions from stub-only ...</li> <li><a href="https://github.com/astral-sh/ruff/commit/05160d507f05345a72db9c28ab4edf7c92334819"><code>05160d5</code></a> [ty] Diagnose invalid descriptor <code>__get__</code> calls (<a href="https://redirect.github.com/astral-sh/ruff/issues/27400">#27400</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/baea3d0dcec6d6f6d1659321940f3725771c5f45"><code>baea3d0</code></a> [ty] Expose strict analysis options in the playground (<a href="https://redirect.github.com/astral-sh/ruff/issues/27543">#27543</a>)</li> <li><a href="https://github.com/astral-sh/ruff/commit/c88946ebeb92be6d276087f0d528cd6471df4ead"><code>c88946e</code></a> [ty] Bump ecosystem-analyzer for strict project settings (<a href="https://redirect.github.com/astral-sh/ruff/issues/27542">#27542</a>)</li> <li>Additional commits viewable in <a href="https://github.com/astral-sh/ruff/compare/0.15.22...0.16.2">compare view</a></li> </ul> </details> <br /> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-14 16:38:08 -05:00
r = requests.post(
f"{proxy}/v1/compress",
json={
"messages": forwarded + new_messages,
"model": "claude-sonnet-4-6",
"config": {"frozen_message_count": len(forwarded)},
},
).json()
forwarded[:] = r["messages"] # next turn's frozen prefix
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
return forwarded
```
fix(compress): resolve the /v1/compress tokenizer per model, and document the real contract (#2743) ## Description `/v1/compress` does no format conversion — callers send whichever wire shape they already use — but the pipeline pinned **one provider's token counter for the whole route**. `OpenAITokenCounter.count_message` walks list content for `text` and `image_url` only and has **no else branch**, so Anthropic content blocks contributed literally zero. A 599-token `tool_result` scored 8. A request that really removed 235 characters reported `tokens_saved: 0` — so a caller gating on `tokens_saved > 0` concludes compression is broken while it is working. Prompted by a Kong integration question ("do you support the Anthropic native format?"). The answer is that we already did — we just reported zeros for it, and the docs said otherwise. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [x] Documentation update ## Changes Made ### Tokenizer resolution (no hardcoded lists) Build the derived pipelines with `provider=None` so `TransformPipeline` resolves the tokenizer from the **per-model registry**. Every registry tokenizer derives from `BaseTokenizer`, whose `_count_content_parts` ends in a serialize-and-count catch-all, which means: - No block type counts as zero, and there is **no per-provider block-type list to keep in sync**. An enumerated set was the first thing I tried and it already missed `mcp_tool_result`, `web_search_tool_result`, `document`, and `thinking`. - Gemini / Mistral / DeepSeek / Kimi stop defaulting to a tiktoken count when the registry already has a calibrated counter for them. - Gateway aliases matching no vendor pattern still count correctly. `mode="ccr"` now runs a derived pipeline too, for the same reason — sharing `openai_pipeline` pinned its provider. Costs that mode its own cold compression cache; correct metrics win. ### Tokenizer selection stays separate from context-limit resolution Deliberately not welded together. `model_limit` feeds `context_pressure -> min_ratio`, so letting a tokenizer decision pick the limit table changes compression aggressiveness: `gpt-4-32k` answered by the Anthropic table is **8,192 instead of 32,768**, a 4× under-estimate. `test_tokenizer_choice_does_not_move_the_context_limit` pins the independence. ### Docs, rewritten from the code - **`proxy.mdx`** — the loopback-only default and **404-not-403** behavior, previously undocumented *anywhere* in `docs/` despite shipping in #2458 explicitly for gateway sidecars; `HEADROOM_COMPRESS_ALLOW_REMOTE`; all four request fields; the whole `config` object including every `mode` value and `frozen_message_count`; `transforms_summary`; the 400/401/404/503 contract; and the timeout fail-open shape (`compression_skipped` / `skip_reason`). - **Corrected "never calls an LLM"** — accurate about *generative* provider requests, misleading for a sidecar operator. Kompress (a ModernBERT **encoder**, classification not generation) and Magika run **in-process**, and `HEADROOM_KOMPRESS_ENDPOINT` offloads inference over HTTP — **real egress**. Now stated explicitly, with `HEADROOM_DISABLE_KOMPRESS=1` as the structural-only option. - **Both wire formats documented as accepted**, and removed `anthropic-sdk.mdx`'s claim that OpenAI format is "the compression engine's native format" — the exact misconception that prompted this work. The SDK's conversion is now framed as an SDK choice, not an API requirement. - **`litellm.mdx`** had no mention of the endpoint at all, despite the code naming LiteLLM's guardrail as its primary consumer. Added the HTTP deployment path, the `HEADROOM_COMPRESS_ALLOW_REMOTE` requirement, and why to leave `config.mode` unset. - **`index.mdx`** printed `compressionRatio * 100` labelled "Saved …%", so a 77% saving displayed as **23%**. `api-reference.mdx` already defined it correctly, so the docs contradicted each other. - `openai-sdk.mdx`, `wiki/proxy.md`, `wiki/typescript-sdk.md` — same corrections; dropped "any HTTP client", "Cloud", and a CacheAligner claim (it is detector-only). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ .venv/bin/ruff check headroom/ tests/ --exclude headroom/dashboard/templates All checks passed! $ .venv/bin/mypy headroom/ Success: no issues found in 511 source files $ python -m pytest tests/test_compress_route_tokenizer_by_model.py \ tests/test_proxy_compress_endpoint.py tests/test_compress_api.py \ tests/test_platform_stabilization_functional.py tests/test_proxy_eager_preload_bind.py -q 99 passed, 2 warnings in 47.15s ``` Broader sweep (`-k "compress or litellm or gateway or guardrail"`): **1625 passed, 4 failed** — all 4 pre-existing, verified by stashing this diff and re-running on clean `main` (2 strands hook tests, 1 codex WS semaphore-tail timing test, 1 unrelated local WIP test). ## Real Behavior Proof - **Environment:** macOS 26.4 arm64, Python 3.12.6, repo `.venv`, branch rebased on `upstream/main`. **(1) Before → after, same request** (60-line grep payload in an Anthropic `tool_result`): | model | before | after | | --- | --- | --- | | `claude-sonnet-4-6` | `before=28 saved=0` | `before=1223 saved=58` | | `bedrock/anthropic.claude-3-5-sonnet` | `saved=0` | `before=1037 saved=59` | | `my-gateway/big-model` (alias) | `saved=0` | `before=1037 saved=59` | | `gemini-2.5-pro` | `saved=0` | `before=1036 saved=59` | | `gpt-4o` + OpenAI shape | `before=1225 saved=58` | `before=1225 saved=58` (unchanged) | All three `config.mode` values verified for each. Response shape preserved: `type=tool_result`, `tool_use_id` intact. **(2) Counter-level root cause**, 6.8 KB body, `count_message()`: ```text OpenAITokenCounter string-content -> 1406 tool_result block -> 5 registry (BaseTokenizer) claude tool_result=408 thinking=418 mcp_tool_result=421 web_search_tool_result=421 document=422 ``` **(3) Every documented behavior asserted against the running app** — 13 checks, all PASS: 400s for missing `messages`/`model`, invalid `config.mode`, and all four invalid `frozen_message_count` forms; 200 for valid ones; non-dict `config` ignored; bypass and empty-messages omit `transforms_summary`; success returns exactly the 8 documented keys. - **Not tested:** the docs site was not built (`docs/node_modules` absent) — MDX was checked for balanced `<Callout>` tags only, so a reviewer with the site running should eyeball rendering. No live gateway/Kong request; verification is via `TestClient` against the real ASGI app. - **Note:** `HEADROOM_DISABLE_KOMPRESS` is read into `ProxyConfig` at `server.py:4919` and by the CLI, not by `create_app(ProxyConfig(...))` directly — I confirmed `disable_kompress=True` does reach the derived pipeline. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-03 12:20:33 -07:00
Note `protect_recent` is not a substitute — it guards the newest messages, while `frozen_message_count` guards the oldest, which is the cached end.
## Using with Claude Code
```bash
# Start proxy
headroom proxy --port 8787
# In another terminal
ANTHROPIC_BASE_URL=http://localhost:8787 claude
```
## Using with Cursor
1. Start the proxy: `headroom proxy`
2. In Cursor settings, set the base URL to `http://localhost:8787`
## Using with OpenAI SDK
```python
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8787/v1",
api_key="your-api-key", # Still needed for upstream
)
```
## Features
fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness Address user-reported UX gaps across the CLI surface: - code-aware: add --code-aware/--no-code-aware (+ HEADROOM_CODE_AWARE_ENABLED env) to the Click CLI. PR #411 had added these only to the orphaned argparse main; the user-facing CLI couldn't reach the flag. Banner status text "remove --no-code-aware to enable" referenced a flag that didn't exist — fix to point at the actual flag/env. Surface code-aware in the click banner and add print_banner=False plumbing to run_server so the click path doesn't print two banners back-to-back. - --mode: hide alias clutter via metavar=[token|cache] and rewrite help to lead with the two real modes. Legacy aliases (token_mode/token_savings/...) still validate. - perf --hours: was documented but ignored. Records are now actually filtered, the report shows the actual time-range covered, and the count of records filtered out (so users can tell when raising --hours helps). - perf TOIN: replace the hash-keyed pattern dump with a strategy-distribution view + recommendation-eligibility from the live store — actionable signal rather than opaque rows. - code-graph: clarify in --help that it indexes cwd / project root. - wrap: spell out supported tools, wrap-vs-proxy distinction, and that `headroom wrap opencode` isn't a thing (use `proxy` directly for opencode; openclaw is not opencode). - mcp: note that mcp__headroom__headroom_retrieve is correct MCP namespacing, not a doubled-prefix bug. Renaming would break the proxy's tool injection. - LLMLingua cleanup: remove [llmlingua] extra from pyproject (no live code uses it). Delete wiki/llmlingua.md and clean retired flag/class references in 6 other wiki pages. Point at [ml] (Kompress) where ML compression is documented. - init -g openclaw: strip mcpServers from existing plugin entries before re-writing — newer openclaw schemas reject it, leaving stale entries from older installs unhealable. Pinned with regression test. Tests: mock_run_server signatures in two existing tests accept **kwargs (needed for the new print_banner plumbing). New test for the openclaw mcpServers strip. Full suite: 4847 passed, 262 skipped, 0 failed.
2026-05-07 16:43:35 -07:00
### ML Compression (Opt-In, Kompress)
fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness Address user-reported UX gaps across the CLI surface: - code-aware: add --code-aware/--no-code-aware (+ HEADROOM_CODE_AWARE_ENABLED env) to the Click CLI. PR #411 had added these only to the orphaned argparse main; the user-facing CLI couldn't reach the flag. Banner status text "remove --no-code-aware to enable" referenced a flag that didn't exist — fix to point at the actual flag/env. Surface code-aware in the click banner and add print_banner=False plumbing to run_server so the click path doesn't print two banners back-to-back. - --mode: hide alias clutter via metavar=[token|cache] and rewrite help to lead with the two real modes. Legacy aliases (token_mode/token_savings/...) still validate. - perf --hours: was documented but ignored. Records are now actually filtered, the report shows the actual time-range covered, and the count of records filtered out (so users can tell when raising --hours helps). - perf TOIN: replace the hash-keyed pattern dump with a strategy-distribution view + recommendation-eligibility from the live store — actionable signal rather than opaque rows. - code-graph: clarify in --help that it indexes cwd / project root. - wrap: spell out supported tools, wrap-vs-proxy distinction, and that `headroom wrap opencode` isn't a thing (use `proxy` directly for opencode; openclaw is not opencode). - mcp: note that mcp__headroom__headroom_retrieve is correct MCP namespacing, not a doubled-prefix bug. Renaming would break the proxy's tool injection. - LLMLingua cleanup: remove [llmlingua] extra from pyproject (no live code uses it). Delete wiki/llmlingua.md and clean retired flag/class references in 6 other wiki pages. Point at [ml] (Kompress) where ML compression is documented. - init -g openclaw: strip mcpServers from existing plugin entries before re-writing — newer openclaw schemas reject it, leaving stale entries from older installs unhealable. Pinned with regression test. Tests: mock_run_server signatures in two existing tests accept **kwargs (needed for the new print_banner plumbing). New test for the openclaw mcpServers strip. Full suite: 4847 passed, 262 skipped, 0 failed.
2026-05-07 16:43:35 -07:00
> The earlier LLMLingua-2 integration documented in this section
> (`--llmlingua`, `--llmlingua-device`, `--llmlingua-rate`,
> `headroom-ai[llmlingua]`, `LLMLinguaCompressor`) was retired and
> replaced by **Kompress** (ModernBERT). Install with `pip install
> 'headroom-ai[ml]'`. See [transforms.md](transforms.md) and
> [ARCHITECTURE.md](ARCHITECTURE.md) for current configuration.
### Semantic Caching
The proxy caches responses for repeated queries:
- LRU eviction with configurable max entries
- TTL-based expiration
- Cache key based on message content hash
### Rate Limiting
Token bucket rate limiting protects against runaway costs:
- Configurable requests per minute
- Configurable tokens per minute
- Per-API-key tracking
### Cost Tracking
Track spending and enforce budgets:
- Real-time cost estimation
- Budget periods: hourly, daily, monthly
- Automatic request rejection when over budget
### Prometheus Metrics
Export metrics for monitoring:
```
headroom_requests_total
headroom_tokens_saved_total
headroom_cost_usd_total
headroom_latency_ms_sum
```
## Configuration via Environment
```bash
export HEADROOM_HOST=0.0.0.0
export HEADROOM_PORT=8787
export HEADROOM_BUDGET=100.0
# Route OpenAI passthrough requests to a custom endpoint
2026-01-19 23:35:16 +01:00
export OPENAI_TARGET_API_URL=https://custom.openai.endpoint.com
# Route Anthropic passthrough requests to a custom endpoint
export ANTHROPIC_TARGET_API_URL=https://litellm.company.internal
headroom proxy
```
## Running in Production
For production deployments:
```bash
# 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:
```dockerfile
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.