2026-03-19 09:48:52 -07:00
|
|
|
"""Tests for CompressionCache with LRU eviction."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from headroom.cache.compression_cache import CompressionCache
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def cache() -> CompressionCache:
|
|
|
|
|
return CompressionCache()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def small_cache() -> CompressionCache:
|
|
|
|
|
return CompressionCache(max_entries=3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCompressionCache:
|
|
|
|
|
def test_cache_miss_returns_none(self, cache: CompressionCache) -> None:
|
|
|
|
|
h = CompressionCache.content_hash("some content")
|
|
|
|
|
assert cache.get_compressed(h) is None
|
|
|
|
|
|
|
|
|
|
def test_store_and_retrieve(self, cache: CompressionCache) -> None:
|
|
|
|
|
content = "hello world this is a long message"
|
|
|
|
|
h = CompressionCache.content_hash(content)
|
|
|
|
|
cache.store_compressed(h, "hello world...compressed", tokens_saved=15)
|
|
|
|
|
assert cache.get_compressed(h) == "hello world...compressed"
|
|
|
|
|
|
|
|
|
|
def test_different_content_different_hash(self) -> None:
|
|
|
|
|
h1 = CompressionCache.content_hash("content A")
|
|
|
|
|
h2 = CompressionCache.content_hash("content B")
|
|
|
|
|
assert h1 != h2
|
|
|
|
|
|
|
|
|
|
def test_overwrite_same_hash(self, cache: CompressionCache) -> None:
|
|
|
|
|
h = CompressionCache.content_hash("some content")
|
|
|
|
|
cache.store_compressed(h, "v1", tokens_saved=10)
|
|
|
|
|
cache.store_compressed(h, "v2", tokens_saved=20)
|
|
|
|
|
assert cache.get_compressed(h) == "v2"
|
|
|
|
|
|
|
|
|
|
def test_stats_tracking(self, cache: CompressionCache) -> None:
|
|
|
|
|
h = CompressionCache.content_hash("content")
|
|
|
|
|
cache.store_compressed(h, "compressed", tokens_saved=5)
|
|
|
|
|
|
|
|
|
|
# One hit
|
|
|
|
|
cache.get_compressed(h)
|
|
|
|
|
# One miss
|
|
|
|
|
cache.get_compressed("nonexistent")
|
|
|
|
|
|
|
|
|
|
stats = cache.get_stats()
|
|
|
|
|
assert stats["hits"] == 1
|
|
|
|
|
assert stats["misses"] == 1
|
|
|
|
|
assert stats["entries"] == 1
|
|
|
|
|
assert stats["tokens_saved"] == 5
|
|
|
|
|
|
|
|
|
|
def test_eviction_at_max_entries(self, small_cache: CompressionCache) -> None:
|
|
|
|
|
h1 = CompressionCache.content_hash("a")
|
|
|
|
|
h2 = CompressionCache.content_hash("b")
|
|
|
|
|
h3 = CompressionCache.content_hash("c")
|
|
|
|
|
h4 = CompressionCache.content_hash("d")
|
|
|
|
|
|
|
|
|
|
small_cache.store_compressed(h1, "ca", tokens_saved=1)
|
|
|
|
|
small_cache.store_compressed(h2, "cb", tokens_saved=1)
|
|
|
|
|
small_cache.store_compressed(h3, "cc", tokens_saved=1)
|
|
|
|
|
|
|
|
|
|
# Adding a 4th should evict the oldest (h1)
|
|
|
|
|
small_cache.store_compressed(h4, "cd", tokens_saved=1)
|
|
|
|
|
|
|
|
|
|
assert small_cache.get_compressed(h1) is None
|
|
|
|
|
assert small_cache.get_compressed(h2) == "cb"
|
|
|
|
|
assert small_cache.get_compressed(h4) == "cd"
|
|
|
|
|
|
|
|
|
|
def test_access_refreshes_lru(self, small_cache: CompressionCache) -> None:
|
|
|
|
|
h1 = CompressionCache.content_hash("a")
|
|
|
|
|
h2 = CompressionCache.content_hash("b")
|
|
|
|
|
h3 = CompressionCache.content_hash("c")
|
|
|
|
|
h4 = CompressionCache.content_hash("d")
|
|
|
|
|
|
|
|
|
|
small_cache.store_compressed(h1, "ca", tokens_saved=1)
|
|
|
|
|
small_cache.store_compressed(h2, "cb", tokens_saved=1)
|
|
|
|
|
small_cache.store_compressed(h3, "cc", tokens_saved=1)
|
|
|
|
|
|
|
|
|
|
# Access h1 to refresh it
|
|
|
|
|
small_cache.get_compressed(h1)
|
|
|
|
|
|
|
|
|
|
# Adding h4 should evict h2 (oldest untouched), not h1
|
|
|
|
|
small_cache.store_compressed(h4, "cd", tokens_saved=1)
|
|
|
|
|
|
|
|
|
|
assert small_cache.get_compressed(h1) == "ca"
|
|
|
|
|
assert small_cache.get_compressed(h2) is None
|
|
|
|
|
assert small_cache.get_compressed(h4) == "cd"
|
|
|
|
|
|
|
|
|
|
def test_content_hash_list_content(self) -> None:
|
|
|
|
|
"""content_hash handles Anthropic-format list content."""
|
|
|
|
|
list_content = [
|
|
|
|
|
{"type": "text", "text": "hello"},
|
|
|
|
|
{"type": "text", "text": "world"},
|
|
|
|
|
]
|
|
|
|
|
h = CompressionCache.content_hash(list_content)
|
|
|
|
|
assert isinstance(h, str)
|
|
|
|
|
assert len(h) == 16
|
|
|
|
|
|
|
|
|
|
# Same content produces same hash
|
|
|
|
|
assert CompressionCache.content_hash(list_content) == h
|
|
|
|
|
|
|
|
|
|
def test_content_hash_string_length(self) -> None:
|
|
|
|
|
h = CompressionCache.content_hash("test")
|
|
|
|
|
assert len(h) == 16
|
2026-03-19 09:52:33 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCompressionCacheFrozenCount:
|
|
|
|
|
def test_empty_cache_returns_zero(self, cache: CompressionCache) -> None:
|
|
|
|
|
assert cache.compute_frozen_count([]) == 0
|
|
|
|
|
|
|
|
|
|
def test_user_assistant_always_stable(self, cache: CompressionCache) -> None:
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "hello"},
|
|
|
|
|
{"role": "assistant", "content": "hi there"},
|
|
|
|
|
{"role": "user", "content": "how are you"},
|
|
|
|
|
]
|
|
|
|
|
assert cache.compute_frozen_count(messages) == 3
|
|
|
|
|
|
|
|
|
|
def test_tool_result_with_cache_hit_is_stable(self, cache: CompressionCache) -> None:
|
|
|
|
|
tool_content = "tool output data"
|
|
|
|
|
h = CompressionCache.content_hash(tool_content)
|
|
|
|
|
cache.store_compressed(h, "compressed tool output", tokens_saved=5)
|
|
|
|
|
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "do something"},
|
|
|
|
|
{
|
|
|
|
|
"role": "assistant",
|
|
|
|
|
"content": [{"type": "tool_use", "id": "t1", "name": "my_tool", "input": {}}],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": tool_content}],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
assert cache.compute_frozen_count(messages) == 3
|
|
|
|
|
|
|
|
|
|
def test_tool_result_cache_miss_stops_frozen(self, cache: CompressionCache) -> None:
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "hello"},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": "uncached stuff"}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{"role": "user", "content": "follow up"},
|
|
|
|
|
]
|
|
|
|
|
assert cache.compute_frozen_count(messages) == 1
|
|
|
|
|
|
|
|
|
|
def test_frozen_count_with_dropped_messages(self, cache: CompressionCache) -> None:
|
|
|
|
|
cached_content = "cached tool output"
|
|
|
|
|
h = CompressionCache.content_hash(cached_content)
|
|
|
|
|
cache.store_compressed(h, "compressed", tokens_saved=3)
|
|
|
|
|
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "start"},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": cached_content}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t2", "content": "not cached"}],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
assert cache.compute_frozen_count(messages) == 2
|
|
|
|
|
|
2026-04-07 17:10:20 -07:00
|
|
|
def test_stable_hash_allows_frozen_count_past_uncached_tool_result(
|
|
|
|
|
self, cache: CompressionCache
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Tool_results marked stable should not stop the frozen count walk."""
|
|
|
|
|
tool_content = "excluded Read output — big file contents"
|
|
|
|
|
h = CompressionCache.content_hash(tool_content)
|
|
|
|
|
cache.mark_stable(h)
|
|
|
|
|
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "hello"},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
2026-04-07 23:27:31 -05:00
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": tool_content}],
|
2026-04-07 17:10:20 -07:00
|
|
|
},
|
|
|
|
|
{"role": "user", "content": "follow up"},
|
|
|
|
|
]
|
|
|
|
|
# Without mark_stable, this would stop at msg[1] → frozen=1.
|
|
|
|
|
# With stable hash, the walk continues past msg[1] → frozen=3.
|
|
|
|
|
assert cache.compute_frozen_count(messages) == 3
|
|
|
|
|
|
|
|
|
|
def test_update_from_result_identical_content_marks_stable(
|
|
|
|
|
self, cache: CompressionCache
|
|
|
|
|
) -> None:
|
|
|
|
|
"""When orig == compressed, update_from_result marks the hash as stable."""
|
|
|
|
|
tool_content = "unchanged tool output"
|
|
|
|
|
originals = [
|
|
|
|
|
{"role": "user", "content": "hi"},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
2026-04-07 23:27:31 -05:00
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": tool_content}],
|
2026-04-07 17:10:20 -07:00
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
# Compressed is identical to originals (no compression happened)
|
|
|
|
|
compressed = [
|
|
|
|
|
{"role": "user", "content": "hi"},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
2026-04-07 23:27:31 -05:00
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": tool_content}],
|
2026-04-07 17:10:20 -07:00
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
cache.update_from_result(originals, compressed)
|
|
|
|
|
|
|
|
|
|
h = CompressionCache.content_hash(tool_content)
|
|
|
|
|
assert h in cache._stable_hashes
|
|
|
|
|
|
|
|
|
|
# Frozen count should now walk past this tool_result
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "hello"},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
2026-04-07 23:27:31 -05:00
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": tool_content}],
|
2026-04-07 17:10:20 -07:00
|
|
|
},
|
|
|
|
|
{"role": "user", "content": "more stuff"},
|
|
|
|
|
]
|
|
|
|
|
assert cache.compute_frozen_count(messages) == 3
|
|
|
|
|
|
|
|
|
|
def test_mark_stable_from_messages(self, cache: CompressionCache) -> None:
|
|
|
|
|
"""mark_stable_from_messages records hashes for tool_results."""
|
|
|
|
|
content_a = "tool output A"
|
|
|
|
|
content_b = "tool output B"
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "hi"},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
2026-04-07 23:27:31 -05:00
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": content_a}],
|
2026-04-07 17:10:20 -07:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
2026-04-07 23:27:31 -05:00
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t2", "content": content_b}],
|
2026-04-07 17:10:20 -07:00
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
# Mark first 2 messages (msg[0] + msg[1])
|
|
|
|
|
cache.mark_stable_from_messages(messages, 2)
|
|
|
|
|
|
|
|
|
|
ha = CompressionCache.content_hash(content_a)
|
|
|
|
|
hb = CompressionCache.content_hash(content_b)
|
|
|
|
|
assert ha in cache._stable_hashes
|
|
|
|
|
assert hb not in cache._stable_hashes # msg[2] not included
|
|
|
|
|
|
|
|
|
|
def test_should_defer_compression_new_content(self, cache: CompressionCache) -> None:
|
fix(proxy): restore Anthropic compression on token mode (issue #327)
Three bugs combined to drive end-to-end compression on the Anthropic
backend to ~0% in token mode (the default). User report #327 saw a
~9× drop in dashboard savings from one day to the next on Claude
Code traffic; the dashboard headline was technically correct but the
underlying compression genuinely was not running. After this change
the same Claude Code-shape multi-turn conversation goes from
14987 → 14371 tokens at the request boundary on turn 1 and only
recompresses the freshest tool_result on subsequent turns, with the
prior turns frozen byte-identical to preserve the upstream prefix
cache.
Bug 1 — IntelligentContextManager inner ContentRouter has no observer
PR #302 (commit cf979958, 2026-04-28) wired CompressionObserver onto
the outer ContentRouter in proxy/server.py and onto SmartCrusher.
The inner ContentRouter constructed lazily inside
IntelligentContextManager._get_content_router (added Jan 18, 2026
in 57b2de5 alongside the COMPRESS_FIRST strategy) was missed. That
inner router handles the bulk of Claude Code's tool_result-block
compression, so per-strategy counters surfaced by PR #314 in v0.15.0
showed compressions_by_strategy={"text": 6} while
summary.compression.total_tokens_removed=1.3M — math-impossible.
Fix: add observer= parameter to IntelligentContextManager.__init__,
forward it to the inner ContentRouter at intelligent_context.py:525,
and pass observer=self.metrics from proxy/server.py.
Bug 2 — TTL deferral marks every fresh tool_result as stable
should_defer_compression in compression_cache.py returned True on
first-sight (added 2026-04-07 in commit 22dad13 with the intent of
batching first-time compressions near the 5-min cache TTL boundary
to trade many small busts for one). The token-mode walker at
anthropic.py:766-787 walks every message past frozen_message_count,
calls should_defer_compression on each fresh tool_result, gets True,
and advances ttl_frozen += 1 — every iteration. Result:
frozen_message_count grows to len(messages), the pipeline freezes
the entire request, and nothing reaches a real compressor.
The defer-first-sight rationale assumes recurring content within
TTL. Real Claude Code traffic produces unique content per turn, so
"defer until next sight" defers forever. Compressing fresh content
on first sight does not bust any prefix cache because Anthropic has
not cached that byte position yet — it's a cache write either way.
Fix: should_defer_compression returns False on first-sight (record
the timestamp; compress now). Subsequent sightings within TTL still
defer (batch window preserved for genuinely repeating content).
Updated tests in test_compression_cache.py to assert the corrected
semantics and verify _first_seen is recorded on first call.
Bug 3 — cross-tokenizer comparison in token-mode inflation guard
anthropic.py:634 sets original_tokens = tokenizer.count_messages(...)
using the proxy-side EstimatingTokenCounter. The token-mode branch
at line 816 set optimized_tokens = result.tokens_after from
pipeline, which uses the provider-side AnthropicProvider tiktoken
estimator. The two tokenizers disagree by ~25% on the same payload.
The inflation guard at line 901
(if optimized_tokens > original_tokens: revert to originals) treats
those two numbers as comparable. After a real 12% compression the
provider-tokenizer figure was still higher than the proxy-tokenizer
baseline, so the guard fired, optimized_messages was reset to the
original input, transforms_applied was emptied, and tokens_saved
went to 0. The dashboard showed no compression even when the
pipeline successfully compressed.
Fix: recount optimized_tokens with the proxy tokenizer right after
the pipeline returns, so the guard compares apples-to-apples. The
recount cost is a few ms on a 50K-token request and is dwarfed by
upstream call latency.
Verification
* 80 targeted tests across test_compression_cache,
test_compression_observability, test_proxy_anthropic_cache_stability,
test_proxy_intelligent_context pass.
* make ci-precheck clean.
* End-to-end real-API run against api.anthropic.com via local proxy:
- Turn 1 fresh: 14987 → 14371 (4.1%) on a 3-tool-round payload;
smart_crusher and diff strategies fired with non-zero savings.
- Turn 2 (turn 1 history + 1 new tool_result): 23161 → 21928 (5.3%);
only the new tool_result compressed; older turns marked
router:protected:user_message; Anthropic returned
cache_creation_input_tokens > 0 confirming the prefix was not
busted.
Two new regression tests in test_compression_observability lock down
the inner ContentRouter observer wiring so a future copy of Bug 1
fails the suite the day it lands.
2026-04-30 12:59:19 -07:00
|
|
|
"""First-time content should NOT be deferred — there is no
|
|
|
|
|
prefix-cache entry to preserve, so compression carries no bust
|
|
|
|
|
cost. Issue #327: prior behavior deferred first-sight, which
|
|
|
|
|
marked every fresh tool_result as stable and disabled
|
|
|
|
|
compression for typical Claude Code workloads.
|
|
|
|
|
"""
|
2026-04-07 17:10:20 -07:00
|
|
|
h = CompressionCache.content_hash("brand new content")
|
fix(proxy): restore Anthropic compression on token mode (issue #327)
Three bugs combined to drive end-to-end compression on the Anthropic
backend to ~0% in token mode (the default). User report #327 saw a
~9× drop in dashboard savings from one day to the next on Claude
Code traffic; the dashboard headline was technically correct but the
underlying compression genuinely was not running. After this change
the same Claude Code-shape multi-turn conversation goes from
14987 → 14371 tokens at the request boundary on turn 1 and only
recompresses the freshest tool_result on subsequent turns, with the
prior turns frozen byte-identical to preserve the upstream prefix
cache.
Bug 1 — IntelligentContextManager inner ContentRouter has no observer
PR #302 (commit cf979958, 2026-04-28) wired CompressionObserver onto
the outer ContentRouter in proxy/server.py and onto SmartCrusher.
The inner ContentRouter constructed lazily inside
IntelligentContextManager._get_content_router (added Jan 18, 2026
in 57b2de5 alongside the COMPRESS_FIRST strategy) was missed. That
inner router handles the bulk of Claude Code's tool_result-block
compression, so per-strategy counters surfaced by PR #314 in v0.15.0
showed compressions_by_strategy={"text": 6} while
summary.compression.total_tokens_removed=1.3M — math-impossible.
Fix: add observer= parameter to IntelligentContextManager.__init__,
forward it to the inner ContentRouter at intelligent_context.py:525,
and pass observer=self.metrics from proxy/server.py.
Bug 2 — TTL deferral marks every fresh tool_result as stable
should_defer_compression in compression_cache.py returned True on
first-sight (added 2026-04-07 in commit 22dad13 with the intent of
batching first-time compressions near the 5-min cache TTL boundary
to trade many small busts for one). The token-mode walker at
anthropic.py:766-787 walks every message past frozen_message_count,
calls should_defer_compression on each fresh tool_result, gets True,
and advances ttl_frozen += 1 — every iteration. Result:
frozen_message_count grows to len(messages), the pipeline freezes
the entire request, and nothing reaches a real compressor.
The defer-first-sight rationale assumes recurring content within
TTL. Real Claude Code traffic produces unique content per turn, so
"defer until next sight" defers forever. Compressing fresh content
on first sight does not bust any prefix cache because Anthropic has
not cached that byte position yet — it's a cache write either way.
Fix: should_defer_compression returns False on first-sight (record
the timestamp; compress now). Subsequent sightings within TTL still
defer (batch window preserved for genuinely repeating content).
Updated tests in test_compression_cache.py to assert the corrected
semantics and verify _first_seen is recorded on first call.
Bug 3 — cross-tokenizer comparison in token-mode inflation guard
anthropic.py:634 sets original_tokens = tokenizer.count_messages(...)
using the proxy-side EstimatingTokenCounter. The token-mode branch
at line 816 set optimized_tokens = result.tokens_after from
pipeline, which uses the provider-side AnthropicProvider tiktoken
estimator. The two tokenizers disagree by ~25% on the same payload.
The inflation guard at line 901
(if optimized_tokens > original_tokens: revert to originals) treats
those two numbers as comparable. After a real 12% compression the
provider-tokenizer figure was still higher than the proxy-tokenizer
baseline, so the guard fired, optimized_messages was reset to the
original input, transforms_applied was emptied, and tokens_saved
went to 0. The dashboard showed no compression even when the
pipeline successfully compressed.
Fix: recount optimized_tokens with the proxy tokenizer right after
the pipeline returns, so the guard compares apples-to-apples. The
recount cost is a few ms on a 50K-token request and is dwarfed by
upstream call latency.
Verification
* 80 targeted tests across test_compression_cache,
test_compression_observability, test_proxy_anthropic_cache_stability,
test_proxy_intelligent_context pass.
* make ci-precheck clean.
* End-to-end real-API run against api.anthropic.com via local proxy:
- Turn 1 fresh: 14987 → 14371 (4.1%) on a 3-tool-round payload;
smart_crusher and diff strategies fired with non-zero savings.
- Turn 2 (turn 1 history + 1 new tool_result): 23161 → 21928 (5.3%);
only the new tool_result compressed; older turns marked
router:protected:user_message; Anthropic returned
cache_creation_input_tokens > 0 confirming the prefix was not
busted.
Two new regression tests in test_compression_observability lock down
the inner ContentRouter observer wiring so a future copy of Bug 1
fails the suite the day it lands.
2026-04-30 12:59:19 -07:00
|
|
|
assert cache.should_defer_compression(h, ttl_seconds=300, batch_window=30) is False
|
|
|
|
|
# Subsequent sightings within TTL should defer (batch window).
|
2026-04-07 17:10:20 -07:00
|
|
|
assert cache.should_defer_compression(h, ttl_seconds=300, batch_window=30) is True
|
|
|
|
|
|
fix(proxy): restore Anthropic compression on token mode (issue #327)
Three bugs combined to drive end-to-end compression on the Anthropic
backend to ~0% in token mode (the default). User report #327 saw a
~9× drop in dashboard savings from one day to the next on Claude
Code traffic; the dashboard headline was technically correct but the
underlying compression genuinely was not running. After this change
the same Claude Code-shape multi-turn conversation goes from
14987 → 14371 tokens at the request boundary on turn 1 and only
recompresses the freshest tool_result on subsequent turns, with the
prior turns frozen byte-identical to preserve the upstream prefix
cache.
Bug 1 — IntelligentContextManager inner ContentRouter has no observer
PR #302 (commit cf979958, 2026-04-28) wired CompressionObserver onto
the outer ContentRouter in proxy/server.py and onto SmartCrusher.
The inner ContentRouter constructed lazily inside
IntelligentContextManager._get_content_router (added Jan 18, 2026
in 57b2de5 alongside the COMPRESS_FIRST strategy) was missed. That
inner router handles the bulk of Claude Code's tool_result-block
compression, so per-strategy counters surfaced by PR #314 in v0.15.0
showed compressions_by_strategy={"text": 6} while
summary.compression.total_tokens_removed=1.3M — math-impossible.
Fix: add observer= parameter to IntelligentContextManager.__init__,
forward it to the inner ContentRouter at intelligent_context.py:525,
and pass observer=self.metrics from proxy/server.py.
Bug 2 — TTL deferral marks every fresh tool_result as stable
should_defer_compression in compression_cache.py returned True on
first-sight (added 2026-04-07 in commit 22dad13 with the intent of
batching first-time compressions near the 5-min cache TTL boundary
to trade many small busts for one). The token-mode walker at
anthropic.py:766-787 walks every message past frozen_message_count,
calls should_defer_compression on each fresh tool_result, gets True,
and advances ttl_frozen += 1 — every iteration. Result:
frozen_message_count grows to len(messages), the pipeline freezes
the entire request, and nothing reaches a real compressor.
The defer-first-sight rationale assumes recurring content within
TTL. Real Claude Code traffic produces unique content per turn, so
"defer until next sight" defers forever. Compressing fresh content
on first sight does not bust any prefix cache because Anthropic has
not cached that byte position yet — it's a cache write either way.
Fix: should_defer_compression returns False on first-sight (record
the timestamp; compress now). Subsequent sightings within TTL still
defer (batch window preserved for genuinely repeating content).
Updated tests in test_compression_cache.py to assert the corrected
semantics and verify _first_seen is recorded on first call.
Bug 3 — cross-tokenizer comparison in token-mode inflation guard
anthropic.py:634 sets original_tokens = tokenizer.count_messages(...)
using the proxy-side EstimatingTokenCounter. The token-mode branch
at line 816 set optimized_tokens = result.tokens_after from
pipeline, which uses the provider-side AnthropicProvider tiktoken
estimator. The two tokenizers disagree by ~25% on the same payload.
The inflation guard at line 901
(if optimized_tokens > original_tokens: revert to originals) treats
those two numbers as comparable. After a real 12% compression the
provider-tokenizer figure was still higher than the proxy-tokenizer
baseline, so the guard fired, optimized_messages was reset to the
original input, transforms_applied was emptied, and tokens_saved
went to 0. The dashboard showed no compression even when the
pipeline successfully compressed.
Fix: recount optimized_tokens with the proxy tokenizer right after
the pipeline returns, so the guard compares apples-to-apples. The
recount cost is a few ms on a 50K-token request and is dwarfed by
upstream call latency.
Verification
* 80 targeted tests across test_compression_cache,
test_compression_observability, test_proxy_anthropic_cache_stability,
test_proxy_intelligent_context pass.
* make ci-precheck clean.
* End-to-end real-API run against api.anthropic.com via local proxy:
- Turn 1 fresh: 14987 → 14371 (4.1%) on a 3-tool-round payload;
smart_crusher and diff strategies fired with non-zero savings.
- Turn 2 (turn 1 history + 1 new tool_result): 23161 → 21928 (5.3%);
only the new tool_result compressed; older turns marked
router:protected:user_message; Anthropic returned
cache_creation_input_tokens > 0 confirming the prefix was not
busted.
Two new regression tests in test_compression_observability lock down
the inner ContentRouter observer wiring so a future copy of Bug 1
fails the suite the day it lands.
2026-04-30 12:59:19 -07:00
|
|
|
def test_should_defer_compression_records_first_seen(self, cache: CompressionCache) -> None:
|
|
|
|
|
"""First-sight call must record the timestamp so subsequent
|
|
|
|
|
in-window calls can defer. Without this the deferral pathway
|
|
|
|
|
for genuinely-repeated content stops working."""
|
|
|
|
|
h = CompressionCache.content_hash("seen-twice content")
|
|
|
|
|
cache.should_defer_compression(h) # first sight
|
|
|
|
|
assert h in cache._first_seen
|
|
|
|
|
|
2026-04-07 17:10:20 -07:00
|
|
|
def test_should_defer_compression_near_ttl(self, cache: CompressionCache) -> None:
|
|
|
|
|
"""Content near TTL boundary should NOT be deferred."""
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
h = CompressionCache.content_hash("old content")
|
|
|
|
|
# Backdate first_seen to simulate age near TTL
|
|
|
|
|
cache._first_seen[h] = time.time() - 280 # 280s old, TTL=300, window=30
|
|
|
|
|
assert cache.should_defer_compression(h, ttl_seconds=300, batch_window=30) is False
|
|
|
|
|
|
2026-03-19 09:52:33 -07:00
|
|
|
|
|
|
|
|
class TestCompressionCacheApplyAndUpdate:
|
|
|
|
|
def test_apply_cached_swaps_tool_results(self, cache: CompressionCache) -> None:
|
|
|
|
|
original_content = "big tool output"
|
|
|
|
|
h = CompressionCache.content_hash(original_content)
|
|
|
|
|
cache.store_compressed(h, "small output", tokens_saved=5)
|
|
|
|
|
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "hi"},
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": original_content}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
result = cache.apply_cached(messages)
|
|
|
|
|
assert result[1]["content"][0]["content"] == "small output"
|
|
|
|
|
|
|
|
|
|
def test_apply_cached_preserves_uncached_messages(self, cache: CompressionCache) -> None:
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "hello"},
|
|
|
|
|
{"role": "assistant", "content": "world"},
|
|
|
|
|
]
|
|
|
|
|
result = cache.apply_cached(messages)
|
|
|
|
|
assert result[0] is messages[0]
|
|
|
|
|
assert result[1] is messages[1]
|
|
|
|
|
|
|
|
|
|
def test_apply_cached_never_adds_messages(self, cache: CompressionCache) -> None:
|
|
|
|
|
# Store something in cache that doesn't correspond to any message
|
|
|
|
|
cache.store_compressed("orphan_hash", "orphan_value", tokens_saved=1)
|
|
|
|
|
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "hello"},
|
|
|
|
|
{"role": "assistant", "content": "hi"},
|
|
|
|
|
]
|
|
|
|
|
result = cache.apply_cached(messages)
|
|
|
|
|
assert len(result) == len(messages)
|
|
|
|
|
|
|
|
|
|
def test_update_from_result_caches_changes(self, cache: CompressionCache) -> None:
|
|
|
|
|
originals = [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": "original output"}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
compressed = [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": "compressed output"}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
cache.update_from_result(originals, compressed)
|
|
|
|
|
|
|
|
|
|
h = CompressionCache.content_hash("original output")
|
|
|
|
|
assert cache.get_compressed(h) == "compressed output"
|
|
|
|
|
|
|
|
|
|
def test_update_from_result_ignores_unchanged(self, cache: CompressionCache) -> None:
|
|
|
|
|
originals = [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": "same content"}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
compressed = [
|
|
|
|
|
{
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [
|
|
|
|
|
{"type": "tool_result", "tool_use_id": "t1", "content": "same content"}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
cache.update_from_result(originals, compressed)
|
|
|
|
|
h = CompressionCache.content_hash("same content")
|
|
|
|
|
assert cache.get_compressed(h) is None
|
|
|
|
|
|
|
|
|
|
def test_apply_does_not_modify_original_messages(self, cache: CompressionCache) -> None:
|
|
|
|
|
original_content = "big tool output"
|
|
|
|
|
h = CompressionCache.content_hash(original_content)
|
|
|
|
|
cache.store_compressed(h, "small output", tokens_saved=5)
|
|
|
|
|
|
|
|
|
|
msg = {
|
|
|
|
|
"role": "user",
|
|
|
|
|
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": original_content}],
|
|
|
|
|
}
|
|
|
|
|
messages = [msg]
|
|
|
|
|
cache.apply_cached(messages)
|
|
|
|
|
|
|
|
|
|
# Original must be untouched
|
|
|
|
|
assert msg["content"][0]["content"] == original_content
|
|
|
|
|
|
|
|
|
|
def test_openai_format_tool_result(self, cache: CompressionCache) -> None:
|
|
|
|
|
original_content = "openai tool output"
|
|
|
|
|
h = CompressionCache.content_hash(original_content)
|
|
|
|
|
cache.store_compressed(h, "compressed openai", tokens_saved=4)
|
|
|
|
|
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "tool", "tool_call_id": "tc1", "content": original_content},
|
|
|
|
|
]
|
|
|
|
|
result = cache.apply_cached(messages)
|
|
|
|
|
assert result[0]["content"] == "compressed openai"
|
|
|
|
|
# Original untouched
|
|
|
|
|
assert messages[0]["content"] == original_content
|