mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
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:
parent
547b15dab2
commit
8e0dadfe02
3 changed files with 81 additions and 0 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue