#3561·headroom

[BUG] No content compressor runs in proxy mode on the Anthropic /v1/messages path (0.30.0 through 0.37.0)

Author: alanbemCreated Sep 11, 2026Updated Sep 16, 2026

Description

With headroom proxy in front of Claude Code on the Anthropic /v1/messages path, no content compressor ever runs. Only tool_schema_compaction and tool_search_deferral appear in headroom inspect, and the payload ships byte-identical. The same content compresses fine through the library's compress(), so the compressors work. The proxy never reaches them.

Issue 3363 reports the same symptom on the OpenAI path. PR 2297 notes the Anthropic and OpenAI tool-search-deferral paths are separate code, so I am filing this for the Anthropic side rather than commenting there. Issue 2291 is the same signature on this path and was fixed in 0.32.0, but the symptom is present on 0.32.0 as well (see Versions).

What this looks like from the outside

I installed headroom, pointed Claude Code at the proxy with ANTHROPIC_BASE_URL, and ran ordinary sessions. That is the documented setup and it needs nothing unusual. Everything behaves as though it is working: the proxy starts cleanly, headroom doctor comes back green, sessions run normally, and the savings dashboard fills in.

The compression is the part that does not happen. On this path the headline capability produces no change to the content, and nothing in the normal output indicates that. I only found out by capturing the requests on the wire and comparing them. Anyone running the same setup would have good reason to believe it was working, which is the main reason I thought it was worth writing up carefully rather than just moving on.

To Reproduce

  1. Write a 26 KB JSON array of 102 records to small.json. The size is deliberate: above roughly 32 KB Claude Code replaces tool output with a <persisted-output> preview before it leaves the CLI, so this stays under that threshold and ships whole.

  2. Start the proxy:

headroom proxy --port 8791 --mode token --log-messages \
  --no-telemetry --no-memory-context --no-memory-tools --no-learn
  1. Run one session with the JSON in the prompt, so no tool call is involved and nothing can route around loading it:
ANTHROPIC_BASE_URL=http://127.0.0.1:8791 claude -p "Below is a JSON array. Do not use any tool. Answer from the text below only.

$(cat small.json)

How many records? Reply with the number, then: done."
  1. headroom inspect --port 8791 --last 1

Expected

SmartCrusher compresses the JSON array, as it does when the same content is passed to compress().

Actual

hr_1789099090_000002  claude-fable-5-1
   tokens 26082 - 25834  (saved 248, 0.95%)   transforms: anthropic:tool_schema_compaction, router:tool_search_deferral:6tools:4993tok
   (no per-message content changes - savings came from structural / transform-level edits)

The 248 tokens are the tool-definition trim. Logging proxies on both sides of headroom confirm the JSON itself is byte-identical, with all 102 records present before and after.

Same outcome for a 28 KB log file, 20 KB of PHP at 35 percent whitespace, and 11 KB of markdown. Every one byte-identical.

Repetition does not trigger dedup

With HEADROOM_DEDUPE=1, the same 26 KB payload forced into one session three times:

request 3   102 records   116,079 B sent -> 116,079 B shipped
request 4   204 records   149,740 B sent -> 149,740 B shipped
request 5   306 records   180,985 B sent -> 180,985 B shipped

Versions

0.37.0   saved 248   tool schema only
0.36.5   saved 248   tool schema only
0.32.0   saved 0     (the release that fixed issue 2291)
0.30.0   requests_compressed: 0, total_tokens_removed: 0 on a 26,005 token request

0.30.0 predates tool_search_deferral entirely, so the deferral tag is not the trigger.

The library compresses the same content

Same machine, no ML extras:

SmartCrusher       121,258 -> 77,300 tokens   36.3%   (JSON array)
SearchCompressor    24,288 -> 12,186 tokens   49.8%   (grep output)

Environment

headroom-ai 0.37.0, [proxy] extra, no ML extras
Python 3.14, Linux aarch64
Client: Claude Code CLI, routed via ANTHROPIC_BASE_URL
Streaming: yes, Claude Code default
Rollout channel: stable (canary also tested)
Deterministic: reproduces on every run, roughly 30 sessions

headroom doctor with the proxy up and the shell routed:

proxy       pass   running at http://127.0.0.1:8791 (v0.37.0)
version     pass   proxy matches installed v0.37.0
shell env   pass   routed via ANTHROPIC_BASE_URL
savings     pass   6,379 tokens / $0.11 saved lifetime

Ruled out

Configuration. Tried individually and then all together: --mode token and --mode cache, --lossless, --intercept-tool-results on the canary channel, --target-ratio 0.2, HEADROOM_SAVINGS_PROFILE=coding, HEADROOM_COMPRESS_USER_MESSAGES=1, HEADROOM_COMPRESS_SYSTEM_MESSAGES=1, HEADROOM_DEDUPE=1, HEADROOM_SMART_CRUSHER_COMPACTION=1, HEADROOM_MIN_TOKENS=1, HEADROOM_MIN_CHARS_FOR_BLOCK=1, HEADROOM_LOSSY_MIN_EXTRA_SAVINGS=0, HEADROOM_SYSTEM_COMPACT=1, HEADROOM_BACKGROUND_COMPRESSION=1. Identical result in every case.

Client-side truncation. Fixtures sit under Claude Code's threshold and arrive whole. Verified by searching each captured request for a persisted-output block.

Delivery shape. Tested with the content embedded in the prompt and as a Read tool result.

My measurement. A control run with the two logging proxies and nothing between them is byte-identical on every pair. Killing headroom mid-chain makes Claude Code time out, so traffic genuinely traverses it.

Corporate TLS interception. This machine sits behind a TLS-inspecting corporate proxy, but every hop in the reproduction is loopback, and the upstream leg behaves the same with and without it.

Additional context

/stats reports tool_schema_tokens_saved: 4993, which comes from router:tool_search_deferral:6tools:4993tok. On the wire the tools block only moves from 27,009 to 26,140 bytes, about 210 tokens. Tool-search deferral is Claude Code's own deferred-tool mechanism, so counting it makes total_tokens_saved_all_layers: 5241 read as real savings when 248 of it is. That may be why this has gone unreported for so long: the dashboard looks healthy while no content compressor runs.

Note on how this was produced

For transparency: I investigated this and wrote it up with the assistance of a coding agent. Everything above is measured rather than inferred. The inspect output, the byte counts and the version results all come from actual runs, captured by logging proxies on either side of headroom, with a control run confirming the measurement adds nothing of its own.

Two limits worth stating plainly. I did not install the ML extras, so the Kompress path was never exercised on this machine; issue 3363 reports the same outcome with them installed, but I cannot confirm that myself. And I did not try --read-maturation, which /stats shows as a beta-gated feature I never requested.

Source: headroomlabs-ai/headroom