fix: restore token-mode compression on frozen prefixes (#1489)

## Description

Fixes token-mode compression for continued Claude Code turns with a
frozen prefix when the client has not already supplied
`headroom_retrieve`.

The previous guard returned before request-side compression could run in
token mode. This keeps the non-token safety behavior, but lets token
mode use the existing marker-triggered CCR tool injection override so
emitted markers stay redeemable.

Closes #1487.

## 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

- Let Anthropic token mode run request-side compression even when the
client did not pre-register `headroom_retrieve`.
- Kept the deferred-injection skip for cache-mode coverage.
- Added a regression for the frozen-prefix token-mode path.
- Updated `CHANGELOG.md` for the user-facing behavior change.

## Testing

<!-- Check what you actually ran, then paste the real command output
below. -->

- [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
$ rtk uv run pytest -q tests/test_proxy/test_anthropic_ccr_deferred_injection.py
15 passed, 1 warning in 2.73s

$ rtk uv run ruff check headroom/proxy/handlers/anthropic.py tests/test_proxy/test_anthropic_ccr_deferred_injection.py
All checks passed!

$ rtk uv run ruff format --check headroom/proxy/handlers/anthropic.py tests/test_proxy/test_anthropic_ccr_deferred_injection.py
2 files already formatted
```

## Real Behavior Proof

- Environment: macOS, Python 3.12.9, local FastAPI `TestClient`,
Anthropic proxy path, `mode=token`, `ccr_inject_tool=True`, frozen
prefix count = 1, no client-supplied `headroom_retrieve`.
- Exact command / steps: ran a local `rtk uv run python` repro that
builds `create_app(ProxyConfig(...))`, forces compression on the
Anthropic path, simulates a frozen prefix, and posts `/v1/messages`.
- Observed result: local `TestClient` request returned `STATUS=200`;
token-mode frozen-prefix compression ran once with
`FROZEN_MESSAGE_COUNT=1`; the forwarded message was the CCR marker;
forwarded tools included `headroom_retrieve`.

```text
STATUS= 200
FROZEN_MESSAGE_COUNT= 1
COMPRESSION_CALLS= 1
FORWARDED_MESSAGES= [{'role': 'user', 'content': '[100 items compressed to 10. Retrieve more: hash=abc123def456abc123def456]'}]
FORWARDED_TOOLS= ['headroom_retrieve']
```

- Not tested: live Claude Code session against a real Anthropic
upstream, full repo-wide `uv run pytest`, and `mypy headroom`.

## 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
(N/A: no new hard-to-follow block needed)
- [x] I have made corresponding changes to the documentation (N/A:
changelog update covers this user-facing bug fix)
- [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

## Screenshots (if applicable)

N/A; proxy behavior only.

## Additional Notes

The pytest run still emits the existing Starlette/httpx deprecation
warning from `fastapi.testclient`; this PR does not touch that
dependency path.
This commit is contained in:
Omar Garcia 2026-06-28 22:21:52 +02:00 committed by GitHub
parent 547b15dab2
commit 8e0dadfe02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 81 additions and 0 deletions

View file

@ -115,6 +115,7 @@ def test_frozen_prefix_skips_marker_emission_when_tool_injection_is_deferred(mon
proxy.config.optimize = True
proxy.config.image_optimize = False
proxy.config.ccr_inject_tool = True
proxy.config.mode = "cache"
_disable_pipeline_extensions(proxy)
fake_tracker = _FakePrefixTracker(frozen_count=1)
@ -333,6 +334,83 @@ def test_token_mode_reclamp_keeps_reversible_ccr_path_when_effective_prefix_drop
assert any(tool.get("name") == "headroom_retrieve" for tool in forwarded["tools"])
def test_token_mode_compresses_frozen_prefix_turns_when_tool_is_not_already_present(
monkeypatch,
) -> None:
captured: dict[str, object] = {}
marker_message = {
"role": "user",
"content": "[100 items compressed to 10. Retrieve more: hash=abc123def456abc123def456]",
}
_force_compression(monkeypatch)
with _make_proxy_client() as client:
proxy = client.app.state.proxy
proxy.config.optimize = True
proxy.config.image_optimize = False
proxy.config.ccr_inject_tool = True
proxy.config.mode = "token"
_disable_pipeline_extensions(proxy)
fake_tracker = _FakePrefixTracker(frozen_count=1)
proxy.session_tracker_store.compute_session_id = lambda request, model, messages: (
"stable-session"
)
proxy.session_tracker_store.get_or_create = lambda session_id, provider: fake_tracker
proxy._get_compression_cache = lambda session_id: _FakeCompressionCache(frozen_count=1)
def _fake_apply(**kwargs):
captured.setdefault("compression_calls", []).append(kwargs["messages"])
captured["frozen_message_count"] = kwargs["frozen_message_count"]
return SimpleNamespace(
messages=[marker_message],
transforms_applied=["fake:ccr"],
timing={},
tokens_before=40,
tokens_after=10,
waste_signals=None,
)
proxy.anthropic_pipeline.apply = _fake_apply
async def _fake_retry(method, url, headers, body, stream=False, **kwargs): # noqa: ANN001
captured["body"] = body
return httpx.Response(
200,
json={
"id": "msg_ccr_token_frozen",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "ok"}],
"usage": {
"input_tokens": 20,
"output_tokens": 3,
"cache_read_input_tokens": 0,
"cache_creation_input_tokens": 0,
},
},
)
proxy._retry_request = _fake_retry
response = client.post(
"/v1/messages",
headers={"x-api-key": "test-key", "anthropic-version": "2023-06-01"},
json={
"model": "claude-sonnet-4-6",
"max_tokens": 64,
"messages": [{"role": "user", "content": _RAW_TRANSCRIPT}],
},
)
assert response.status_code == 200
assert captured.get("frozen_message_count") == 1
assert len(captured.get("compression_calls", [])) == 1
forwarded = captured["body"]
assert forwarded["messages"] == [marker_message]
assert any(tool.get("name") == "headroom_retrieve" for tool in forwarded["tools"])
def test_existing_retrieve_tool_keeps_reversible_ccr_path_when_prefix_is_frozen(
monkeypatch,
) -> None: