Spaces:
Build error
Build error
Commit ·
52bd5e8
1
Parent(s): 29d84b6
Harden cache validation reporting and TTL analysis
Browse files- benchmarks/cache_bust_trace_report.py +356 -0
- benchmarks/cache_validation_bundle.py +697 -0
- benchmarks/claude_session_mode_benchmark.py +61 -6
- benchmarks/synthetic_long_cache_suite_report.py +428 -0
- benchmarks/synthetic_token_cache_bust_report.py +370 -0
- docs/benchmarks.md +44 -0
- headroom/providers/anthropic.py +7 -1
- tests/test_claude_session_mode_benchmark.py +7 -2
- tests/test_providers/test_anthropic.py +5 -0
benchmarks/cache_bust_trace_report.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Trace and report concrete cache-busting turns from local Claude session replays."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
import subprocess
|
| 9 |
+
import sys
|
| 10 |
+
import tempfile
|
| 11 |
+
from dataclasses import asdict, dataclass
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
if __package__ in {None, ""}:
|
| 16 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
DEFAULT_OUTPUT_DIR = Path("benchmark_results") / "cache_bust_trace"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class BustEvent:
|
| 24 |
+
branch: str
|
| 25 |
+
mode: str
|
| 26 |
+
session_id: str
|
| 27 |
+
project: str
|
| 28 |
+
request_id: str
|
| 29 |
+
timestamp: str
|
| 30 |
+
first_diff_index: int | None
|
| 31 |
+
prev_len: int
|
| 32 |
+
curr_len: int
|
| 33 |
+
prev_msg: dict[str, Any] | None
|
| 34 |
+
curr_msg: dict[str, Any] | None
|
| 35 |
+
prev_tail: list[dict[str, Any]]
|
| 36 |
+
curr_tail: list[dict[str, Any]]
|
| 37 |
+
retroactive_rewrite: bool
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _run_git(args: list[str], cwd: Path) -> str:
|
| 41 |
+
completed = subprocess.run(
|
| 42 |
+
["git", *args],
|
| 43 |
+
cwd=cwd,
|
| 44 |
+
check=True,
|
| 45 |
+
capture_output=True,
|
| 46 |
+
text=True,
|
| 47 |
+
)
|
| 48 |
+
return completed.stdout.strip()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _ref_slug(ref: str) -> str:
|
| 52 |
+
return "".join(ch if ch.isalnum() else "-" for ch in ref).strip("-").lower() or "ref"
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _first_diff_index(prev: list[dict[str, Any]], curr: list[dict[str, Any]]) -> int | None:
|
| 56 |
+
for i, (a, b) in enumerate(zip(prev, curr)):
|
| 57 |
+
if a != b:
|
| 58 |
+
return i
|
| 59 |
+
if len(prev) != len(curr):
|
| 60 |
+
return min(len(prev), len(curr))
|
| 61 |
+
return None
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _trace_branch(
|
| 65 |
+
repo_root: Path,
|
| 66 |
+
ref: str,
|
| 67 |
+
label: str,
|
| 68 |
+
*,
|
| 69 |
+
recent_turns_per_session: int,
|
| 70 |
+
max_events_per_mode: int = 10,
|
| 71 |
+
) -> list[BustEvent]:
|
| 72 |
+
worktree_root = Path(tempfile.mkdtemp(prefix="headroom-bust-trace-"))
|
| 73 |
+
worktree_dir = worktree_root / _ref_slug(label)
|
| 74 |
+
_run_git(["worktree", "add", "--detach", str(worktree_dir), ref], repo_root)
|
| 75 |
+
try:
|
| 76 |
+
env = os.environ.copy()
|
| 77 |
+
env["PYTHONPATH"] = str(worktree_dir)
|
| 78 |
+
code = """
|
| 79 |
+
import copy, json
|
| 80 |
+
from datetime import timedelta
|
| 81 |
+
from pathlib import Path
|
| 82 |
+
import importlib.util
|
| 83 |
+
import os
|
| 84 |
+
import sys
|
| 85 |
+
|
| 86 |
+
module_path = Path(os.environ['BUST_TRACE_SCRIPT'])
|
| 87 |
+
spec = importlib.util.spec_from_file_location('branch_benchmark', module_path)
|
| 88 |
+
mod = importlib.util.module_from_spec(spec)
|
| 89 |
+
assert spec and spec.loader
|
| 90 |
+
sys.modules[spec.name] = mod
|
| 91 |
+
spec.loader.exec_module(mod)
|
| 92 |
+
|
| 93 |
+
PROXY_MODE_CACHE = mod.PROXY_MODE_CACHE
|
| 94 |
+
PROXY_MODE_TOKEN = mod.PROXY_MODE_TOKEN
|
| 95 |
+
PrefixCacheTracker = mod.PrefixCacheTracker
|
| 96 |
+
_apply_mode_to_messages = mod._apply_mode_to_messages
|
| 97 |
+
_cache_gap_within_ttl = mod._cache_gap_within_ttl
|
| 98 |
+
_rewrite_scope = mod._rewrite_scope
|
| 99 |
+
get_tokenizer = mod.get_tokenizer
|
| 100 |
+
load_session_replay = mod.load_session_replay
|
| 101 |
+
select_session_files = mod.select_session_files
|
| 102 |
+
trim_replay_to_recent_turns = mod.trim_replay_to_recent_turns
|
| 103 |
+
_make_proxy = mod._make_proxy
|
| 104 |
+
from headroom.cache.compression_cache import CompressionCache
|
| 105 |
+
|
| 106 |
+
ROOT = Path.home() / '.claude' / 'projects'
|
| 107 |
+
TTL = timedelta(minutes=5)
|
| 108 |
+
recent_turns_per_session = int(__import__('os').environ['BUST_TRACE_RECENT'])
|
| 109 |
+
max_events_per_mode = int(__import__('os').environ['BUST_TRACE_MAX'])
|
| 110 |
+
|
| 111 |
+
def first_diff_index(prev, curr):
|
| 112 |
+
for i, (a, b) in enumerate(zip(prev, curr)):
|
| 113 |
+
if a != b:
|
| 114 |
+
return i
|
| 115 |
+
if len(prev) != len(curr):
|
| 116 |
+
return min(len(prev), len(curr))
|
| 117 |
+
return None
|
| 118 |
+
|
| 119 |
+
def trace_mode(mode):
|
| 120 |
+
proxy = _make_proxy(mode)
|
| 121 |
+
session_files = select_session_files(ROOT)
|
| 122 |
+
events = []
|
| 123 |
+
for session_file in session_files:
|
| 124 |
+
replay = load_session_replay(session_file)
|
| 125 |
+
if replay is None:
|
| 126 |
+
continue
|
| 127 |
+
replay = trim_replay_to_recent_turns(replay, recent_turns_per_session)
|
| 128 |
+
prefix_tracker = PrefixCacheTracker('anthropic')
|
| 129 |
+
comp_cache = CompressionCache() if mode == PROXY_MODE_TOKEN else None
|
| 130 |
+
conversation = []
|
| 131 |
+
conversation_token_total = 0
|
| 132 |
+
previous_forwarded = []
|
| 133 |
+
previous_original_context = None
|
| 134 |
+
previous_forwarded_context = None
|
| 135 |
+
previous_timestamp = None
|
| 136 |
+
pending = None
|
| 137 |
+
for turn in replay.turns:
|
| 138 |
+
tokenizer = get_tokenizer(turn.model)
|
| 139 |
+
turn_input_token_total = sum(tokenizer.count_message(msg) for msg in turn.input_messages)
|
| 140 |
+
prior_context_message_count = len(conversation)
|
| 141 |
+
conversation.extend(turn.input_messages)
|
| 142 |
+
raw_input_tokens = conversation_token_total + turn_input_token_total
|
| 143 |
+
forwarded = _apply_mode_to_messages(
|
| 144 |
+
proxy, mode, conversation,
|
| 145 |
+
model=turn.model, prefix_tracker=prefix_tracker, comp_cache=comp_cache,
|
| 146 |
+
previous_original_messages=previous_original_context,
|
| 147 |
+
previous_forwarded_messages=previous_forwarded_context,
|
| 148 |
+
)
|
| 149 |
+
if pending is not None:
|
| 150 |
+
eligible = _cache_gap_within_ttl(pending.turn.timestamp, previous_timestamp, ttl=TTL)
|
| 151 |
+
if eligible and previous_forwarded:
|
| 152 |
+
prefix_preserved = (
|
| 153 |
+
len(pending.forwarded) >= len(previous_forwarded)
|
| 154 |
+
and pending.forwarded[: len(previous_forwarded)] == previous_forwarded
|
| 155 |
+
)
|
| 156 |
+
if not prefix_preserved:
|
| 157 |
+
idx = first_diff_index(previous_forwarded, pending.forwarded)
|
| 158 |
+
_, retro = _rewrite_scope(
|
| 159 |
+
pending.request_messages,
|
| 160 |
+
pending.forwarded,
|
| 161 |
+
stable_prefix_message_count=max(len(previous_forwarded) - 1, 0),
|
| 162 |
+
)
|
| 163 |
+
events.append({
|
| 164 |
+
'mode': mode,
|
| 165 |
+
'session_id': replay.session_id,
|
| 166 |
+
'project': replay.decoded_project_path,
|
| 167 |
+
'request_id': pending.turn.request_id,
|
| 168 |
+
'timestamp': pending.turn.timestamp.isoformat(),
|
| 169 |
+
'first_diff_index': idx,
|
| 170 |
+
'prev_len': len(previous_forwarded),
|
| 171 |
+
'curr_len': len(pending.forwarded),
|
| 172 |
+
'prev_msg': previous_forwarded[idx] if idx is not None and idx < len(previous_forwarded) else None,
|
| 173 |
+
'curr_msg': pending.forwarded[idx] if idx is not None and idx < len(pending.forwarded) else None,
|
| 174 |
+
'prev_tail': previous_forwarded_context[-4:] if previous_forwarded_context else [],
|
| 175 |
+
'curr_tail': pending.request_messages[-4:],
|
| 176 |
+
'retroactive_rewrite': retro,
|
| 177 |
+
})
|
| 178 |
+
if len(events) >= max_events_per_mode:
|
| 179 |
+
return events
|
| 180 |
+
previous_forwarded = copy.deepcopy(pending.forwarded)
|
| 181 |
+
previous_timestamp = pending.turn.timestamp
|
| 182 |
+
try:
|
| 183 |
+
prefix_tracker.update_from_response(
|
| 184 |
+
cache_read_tokens=0,
|
| 185 |
+
cache_write_tokens=0,
|
| 186 |
+
messages=forwarded,
|
| 187 |
+
message_token_counts=[tokenizer.count_message(msg) for msg in forwarded],
|
| 188 |
+
original_messages=conversation,
|
| 189 |
+
)
|
| 190 |
+
except TypeError:
|
| 191 |
+
prefix_tracker.update_from_response(
|
| 192 |
+
cache_read_tokens=0,
|
| 193 |
+
cache_write_tokens=0,
|
| 194 |
+
messages=forwarded,
|
| 195 |
+
message_token_counts=[tokenizer.count_message(msg) for msg in forwarded],
|
| 196 |
+
)
|
| 197 |
+
class Pending: pass
|
| 198 |
+
pending = Pending()
|
| 199 |
+
pending.turn = turn
|
| 200 |
+
pending.request_messages = copy.deepcopy(conversation)
|
| 201 |
+
pending.forwarded = forwarded
|
| 202 |
+
conversation.append(turn.assistant_message)
|
| 203 |
+
conversation_token_total = raw_input_tokens + tokenizer.count_message(turn.assistant_message)
|
| 204 |
+
previous_original_context = copy.deepcopy(conversation)
|
| 205 |
+
previous_forwarded_context = copy.deepcopy(forwarded) + [copy.deepcopy(turn.assistant_message)]
|
| 206 |
+
return events
|
| 207 |
+
|
| 208 |
+
print(json.dumps({
|
| 209 |
+
'token': trace_mode(PROXY_MODE_TOKEN),
|
| 210 |
+
'cache': trace_mode(PROXY_MODE_CACHE),
|
| 211 |
+
}, indent=2))
|
| 212 |
+
"""
|
| 213 |
+
env["BUST_TRACE_RECENT"] = str(recent_turns_per_session)
|
| 214 |
+
env["BUST_TRACE_MAX"] = str(max_events_per_mode)
|
| 215 |
+
script_path = worktree_dir / "benchmarks" / "claude_session_mode_benchmark.py"
|
| 216 |
+
if not script_path.exists():
|
| 217 |
+
script_path = repo_root / "benchmarks" / "claude_session_mode_benchmark.py"
|
| 218 |
+
env["BUST_TRACE_SCRIPT"] = str(script_path)
|
| 219 |
+
completed = subprocess.run(
|
| 220 |
+
[sys.executable, "-c", code],
|
| 221 |
+
cwd=worktree_dir,
|
| 222 |
+
check=True,
|
| 223 |
+
capture_output=True,
|
| 224 |
+
text=True,
|
| 225 |
+
env=env,
|
| 226 |
+
)
|
| 227 |
+
payload = json.loads(completed.stdout)
|
| 228 |
+
events: list[BustEvent] = []
|
| 229 |
+
for mode in ("token", "cache"):
|
| 230 |
+
for item in payload[mode]:
|
| 231 |
+
events.append(BustEvent(branch=label, **item))
|
| 232 |
+
return events
|
| 233 |
+
except subprocess.CalledProcessError as exc:
|
| 234 |
+
raise RuntimeError(
|
| 235 |
+
f"trace failed for {label} ({ref})\nSTDOUT:\n{exc.stdout}\nSTDERR:\n{exc.stderr}"
|
| 236 |
+
) from exc
|
| 237 |
+
finally:
|
| 238 |
+
subprocess.run(
|
| 239 |
+
["git", "worktree", "remove", "--force", str(worktree_dir)],
|
| 240 |
+
cwd=repo_root,
|
| 241 |
+
check=True,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _render_markdown(events: list[BustEvent], recent_turns_per_session: int) -> str:
|
| 246 |
+
lines = [
|
| 247 |
+
"# Cache Bust Trace Report",
|
| 248 |
+
"",
|
| 249 |
+
f"- Sampling: most recent {recent_turns_per_session} turns per session",
|
| 250 |
+
"",
|
| 251 |
+
]
|
| 252 |
+
for branch in ("main", "pr"):
|
| 253 |
+
lines.append(f"## {branch}")
|
| 254 |
+
lines.append("")
|
| 255 |
+
branch_events = [e for e in events if e.branch == branch]
|
| 256 |
+
for mode in ("token", "cache"):
|
| 257 |
+
lines.append(f"### {mode}")
|
| 258 |
+
mode_events = [e for e in branch_events if e.mode == mode]
|
| 259 |
+
if not mode_events:
|
| 260 |
+
lines.append("")
|
| 261 |
+
lines.append("- No bust events captured.")
|
| 262 |
+
lines.append("")
|
| 263 |
+
continue
|
| 264 |
+
for event in mode_events:
|
| 265 |
+
lines.append("")
|
| 266 |
+
lines.append(
|
| 267 |
+
f"- `{event.project}` `{event.session_id}` `{event.request_id}` "
|
| 268 |
+
f"{event.timestamp} diff_index={event.first_diff_index} "
|
| 269 |
+
f"retroactive={event.retroactive_rewrite}"
|
| 270 |
+
)
|
| 271 |
+
lines.append("")
|
| 272 |
+
return "\n".join(lines)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def _render_html(events: list[BustEvent], recent_turns_per_session: int) -> str:
|
| 276 |
+
sections = []
|
| 277 |
+
for branch in ("main", "pr"):
|
| 278 |
+
rows = []
|
| 279 |
+
branch_events = [e for e in events if e.branch == branch]
|
| 280 |
+
for mode in ("token", "cache"):
|
| 281 |
+
mode_events = [e for e in branch_events if e.mode == mode]
|
| 282 |
+
if not mode_events:
|
| 283 |
+
rows.append(
|
| 284 |
+
f"<tr><td>{mode}</td><td colspan='6'>No bust events captured.</td></tr>"
|
| 285 |
+
)
|
| 286 |
+
continue
|
| 287 |
+
for event in mode_events:
|
| 288 |
+
rows.append(
|
| 289 |
+
"<tr>"
|
| 290 |
+
f"<td>{mode}</td>"
|
| 291 |
+
f"<td>{event.project}</td>"
|
| 292 |
+
f"<td>{event.session_id}</td>"
|
| 293 |
+
f"<td>{event.request_id}</td>"
|
| 294 |
+
f"<td>{event.timestamp}</td>"
|
| 295 |
+
f"<td>{event.first_diff_index}</td>"
|
| 296 |
+
f"<td>{event.retroactive_rewrite}</td>"
|
| 297 |
+
"</tr>"
|
| 298 |
+
)
|
| 299 |
+
sections.append(
|
| 300 |
+
f"<section><h2>{branch}</h2><table><thead><tr>"
|
| 301 |
+
"<th>Mode</th><th>Project</th><th>Session</th><th>Request</th>"
|
| 302 |
+
"<th>Timestamp</th><th>First Diff</th><th>Retroactive</th>"
|
| 303 |
+
f"</tr></thead><tbody>{''.join(rows)}</tbody></table></section>"
|
| 304 |
+
)
|
| 305 |
+
return f"""<!doctype html>
|
| 306 |
+
<html lang="en">
|
| 307 |
+
<head>
|
| 308 |
+
<meta charset="utf-8">
|
| 309 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 310 |
+
<title>Cache Bust Trace Report</title>
|
| 311 |
+
<style>
|
| 312 |
+
body {{ font-family: 'Segoe UI', system-ui, sans-serif; margin: 0; background: #f8fafc; color: #0f172a; }}
|
| 313 |
+
.shell {{ max-width: 1280px; margin: 0 auto; padding: 32px 16px 48px; }}
|
| 314 |
+
h1, h2 {{ letter-spacing: -0.02em; }}
|
| 315 |
+
section {{ background: white; border: 1px solid #e2e8f0; border-radius: 16px; padding: 20px; margin-top: 16px; }}
|
| 316 |
+
table {{ width: 100%; border-collapse: collapse; }}
|
| 317 |
+
th, td {{ padding: 10px 12px; border-bottom: 1px solid #e2e8f0; text-align: left; white-space: nowrap; }}
|
| 318 |
+
th {{ background: #f1f5f9; }}
|
| 319 |
+
</style>
|
| 320 |
+
</head>
|
| 321 |
+
<body>
|
| 322 |
+
<div class="shell">
|
| 323 |
+
<h1>Cache Bust Trace Report</h1>
|
| 324 |
+
<p>Most recent {recent_turns_per_session} turns per session.</p>
|
| 325 |
+
{''.join(sections)}
|
| 326 |
+
</div>
|
| 327 |
+
</body>
|
| 328 |
+
</html>"""
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def main() -> int:
|
| 332 |
+
repo_root = Path(__file__).resolve().parents[1]
|
| 333 |
+
output_dir = DEFAULT_OUTPUT_DIR
|
| 334 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 335 |
+
recent_turns_per_session = 200
|
| 336 |
+
events = _trace_branch(
|
| 337 |
+
repo_root, "upstream/main", "main", recent_turns_per_session=recent_turns_per_session
|
| 338 |
+
)
|
| 339 |
+
events.extend(
|
| 340 |
+
_trace_branch(repo_root, "HEAD", "pr", recent_turns_per_session=recent_turns_per_session)
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
md_path = output_dir / "cache_bust_trace.md"
|
| 344 |
+
json_path = output_dir / "cache_bust_trace.json"
|
| 345 |
+
html_path = output_dir / "cache_bust_trace.html"
|
| 346 |
+
md_path.write_text(_render_markdown(events, recent_turns_per_session), encoding="utf-8")
|
| 347 |
+
json_path.write_text(json.dumps([asdict(event) for event in events], indent=2), encoding="utf-8")
|
| 348 |
+
html_path.write_text(_render_html(events, recent_turns_per_session), encoding="utf-8")
|
| 349 |
+
print(md_path)
|
| 350 |
+
print(json_path)
|
| 351 |
+
print(html_path)
|
| 352 |
+
return 0
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
if __name__ == "__main__":
|
| 356 |
+
raise SystemExit(main())
|
benchmarks/cache_validation_bundle.py
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate a reproducible local cache-validation report bundle."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import copy
|
| 8 |
+
import hashlib
|
| 9 |
+
import html
|
| 10 |
+
import json
|
| 11 |
+
import logging
|
| 12 |
+
import platform
|
| 13 |
+
import subprocess
|
| 14 |
+
import sys
|
| 15 |
+
from dataclasses import asdict
|
| 16 |
+
from datetime import timedelta
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
if __package__ in {None, ""}:
|
| 21 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 22 |
+
|
| 23 |
+
import benchmarks.claude_session_mode_benchmark as real_bench
|
| 24 |
+
import benchmarks.synthetic_long_cache_suite_report as long_suite
|
| 25 |
+
import benchmarks.synthetic_token_cache_bust_report as token_bust
|
| 26 |
+
from benchmarks.claude_session_mode_benchmark import (
|
| 27 |
+
PROXY_MODE_CACHE,
|
| 28 |
+
PROXY_MODE_TOKEN,
|
| 29 |
+
_apply_mode_to_messages,
|
| 30 |
+
_cache_gap_within_ttl,
|
| 31 |
+
_rewrite_scope,
|
| 32 |
+
build_dataset_and_observed_from_files,
|
| 33 |
+
determine_winners,
|
| 34 |
+
format_currency,
|
| 35 |
+
get_tokenizer,
|
| 36 |
+
load_session_replay,
|
| 37 |
+
resolve_checkpoint_dir,
|
| 38 |
+
select_session_files,
|
| 39 |
+
simulate_session_files,
|
| 40 |
+
trim_replay_to_recent_turns,
|
| 41 |
+
write_report,
|
| 42 |
+
)
|
| 43 |
+
from headroom.cache.compression_cache import CompressionCache
|
| 44 |
+
from headroom.cache.prefix_tracker import PrefixCacheTracker
|
| 45 |
+
|
| 46 |
+
DEFAULT_OUTPUT_DIR = Path("benchmark_results") / "cache_validation_bundle"
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _excerpt_content(content: Any, *, max_chars: int) -> str:
|
| 50 |
+
if isinstance(content, str):
|
| 51 |
+
text = content.replace("\n", " ")
|
| 52 |
+
return text[:max_chars] + ("..." if len(text) > max_chars else "")
|
| 53 |
+
if isinstance(content, list):
|
| 54 |
+
parts = []
|
| 55 |
+
for block in content[:4]:
|
| 56 |
+
if isinstance(block, dict):
|
| 57 |
+
btype = str(block.get("type", "unknown"))
|
| 58 |
+
bcontent = block.get("content", "")
|
| 59 |
+
if isinstance(bcontent, str):
|
| 60 |
+
bcontent = bcontent.replace("\n", " ")
|
| 61 |
+
bcontent = bcontent[:max_chars] + ("..." if len(bcontent) > max_chars else "")
|
| 62 |
+
parts.append(f"[{btype}] {bcontent}")
|
| 63 |
+
else:
|
| 64 |
+
parts.append(str(block)[:max_chars])
|
| 65 |
+
return " | ".join(parts)
|
| 66 |
+
return str(content)[:max_chars]
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _message_preview(msg: dict[str, Any], *, max_chars: int) -> dict[str, str]:
|
| 70 |
+
return {
|
| 71 |
+
"role": str(msg.get("role")),
|
| 72 |
+
"content_excerpt": _excerpt_content(msg.get("content"), max_chars=max_chars),
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _stable_hash(value: str) -> str:
|
| 77 |
+
return hashlib.sha256(value.encode("utf-8")).hexdigest()[:12]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _redact_text(value: str, *, prefix: str) -> str:
|
| 81 |
+
return f"{prefix}-{_stable_hash(value)}"
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _redact_path(value: str) -> str:
|
| 85 |
+
path = Path(value)
|
| 86 |
+
suffix = path.suffix
|
| 87 |
+
return f"path-{_stable_hash(value)}{suffix}"
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _git_output(args: list[str], cwd: Path) -> str | None:
|
| 91 |
+
try:
|
| 92 |
+
completed = subprocess.run(
|
| 93 |
+
["git", *args],
|
| 94 |
+
cwd=cwd,
|
| 95 |
+
check=True,
|
| 96 |
+
capture_output=True,
|
| 97 |
+
text=True,
|
| 98 |
+
)
|
| 99 |
+
return completed.stdout.strip()
|
| 100 |
+
except Exception:
|
| 101 |
+
return None
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _runtime_metadata(repo_root: Path) -> dict[str, Any]:
|
| 105 |
+
return {
|
| 106 |
+
"git_sha": _git_output(["rev-parse", "HEAD"], repo_root),
|
| 107 |
+
"git_dirty": bool(_git_output(["status", "--porcelain"], repo_root)),
|
| 108 |
+
"python_version": sys.version,
|
| 109 |
+
"platform": platform.platform(),
|
| 110 |
+
"implementation": platform.python_implementation(),
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _corpus_fingerprint(
|
| 115 |
+
*,
|
| 116 |
+
root: Path,
|
| 117 |
+
session_files: list[Path],
|
| 118 |
+
max_sessions: int | None,
|
| 119 |
+
recent_turns_per_session: int | None,
|
| 120 |
+
cache_ttl_minutes: int,
|
| 121 |
+
) -> dict[str, Any]:
|
| 122 |
+
normalized_files = [str(p.resolve()) for p in session_files]
|
| 123 |
+
payload = {
|
| 124 |
+
"root": str(root.resolve()),
|
| 125 |
+
"session_files": normalized_files,
|
| 126 |
+
"max_sessions": max_sessions,
|
| 127 |
+
"recent_turns_per_session": recent_turns_per_session,
|
| 128 |
+
"cache_ttl_minutes": cache_ttl_minutes,
|
| 129 |
+
}
|
| 130 |
+
digest = hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
|
| 131 |
+
return {
|
| 132 |
+
"root": str(root.resolve()),
|
| 133 |
+
"session_file_count": len(session_files),
|
| 134 |
+
"session_files_sha256": digest,
|
| 135 |
+
"max_sessions": max_sessions,
|
| 136 |
+
"recent_turns_per_session": recent_turns_per_session,
|
| 137 |
+
"cache_ttl_minutes": cache_ttl_minutes,
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def _collect_real_processed_events(
|
| 142 |
+
*,
|
| 143 |
+
root: Path,
|
| 144 |
+
recent_turns_per_session: int | None,
|
| 145 |
+
max_events_per_mode: int,
|
| 146 |
+
ttl_minutes: int,
|
| 147 |
+
max_chars: int,
|
| 148 |
+
include_content: bool,
|
| 149 |
+
) -> dict[str, Any]:
|
| 150 |
+
ttl = timedelta(minutes=ttl_minutes)
|
| 151 |
+
events: list[dict[str, Any]] = []
|
| 152 |
+
session_files = select_session_files(root)
|
| 153 |
+
for mode in (PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 154 |
+
proxy = real_bench._make_proxy(mode)
|
| 155 |
+
collected = 0
|
| 156 |
+
for session_file in session_files:
|
| 157 |
+
replay = load_session_replay(session_file)
|
| 158 |
+
if replay is None:
|
| 159 |
+
continue
|
| 160 |
+
replay = trim_replay_to_recent_turns(replay, recent_turns_per_session)
|
| 161 |
+
prefix_tracker = PrefixCacheTracker("anthropic")
|
| 162 |
+
comp_cache = CompressionCache() if mode == PROXY_MODE_TOKEN else None
|
| 163 |
+
conversation: list[dict[str, Any]] = []
|
| 164 |
+
previous_original_context: list[dict[str, Any]] | None = None
|
| 165 |
+
previous_forwarded_context: list[dict[str, Any]] | None = None
|
| 166 |
+
previous_forwarded: list[dict[str, Any]] = []
|
| 167 |
+
previous_timestamp = None
|
| 168 |
+
pending = None
|
| 169 |
+
for turn in replay.turns:
|
| 170 |
+
tokenizer = get_tokenizer(turn.model)
|
| 171 |
+
prior_context_message_count = len(conversation)
|
| 172 |
+
conversation.extend(turn.input_messages)
|
| 173 |
+
forwarded = _apply_mode_to_messages(
|
| 174 |
+
proxy,
|
| 175 |
+
mode,
|
| 176 |
+
conversation,
|
| 177 |
+
model=turn.model,
|
| 178 |
+
prefix_tracker=prefix_tracker,
|
| 179 |
+
comp_cache=comp_cache,
|
| 180 |
+
previous_original_messages=previous_original_context,
|
| 181 |
+
previous_forwarded_messages=previous_forwarded_context,
|
| 182 |
+
)
|
| 183 |
+
rewrite, retro = _rewrite_scope(
|
| 184 |
+
conversation,
|
| 185 |
+
forwarded,
|
| 186 |
+
stable_prefix_message_count=prior_context_message_count,
|
| 187 |
+
)
|
| 188 |
+
if rewrite:
|
| 189 |
+
prior_forwarded = pending.forwarded if pending is not None else previous_forwarded
|
| 190 |
+
prior_ts = pending.turn.timestamp if pending is not None else previous_timestamp
|
| 191 |
+
eligible = bool(
|
| 192 |
+
prior_ts is not None
|
| 193 |
+
and _cache_gap_within_ttl(turn.timestamp, prior_ts, ttl=ttl)
|
| 194 |
+
and prior_forwarded
|
| 195 |
+
)
|
| 196 |
+
prefix_preserved = None
|
| 197 |
+
first_diff_index = None
|
| 198 |
+
if eligible:
|
| 199 |
+
prefix_preserved = (
|
| 200 |
+
len(forwarded) >= len(prior_forwarded)
|
| 201 |
+
and forwarded[: len(prior_forwarded)] == prior_forwarded
|
| 202 |
+
)
|
| 203 |
+
if not prefix_preserved:
|
| 204 |
+
for idx, (a, b) in enumerate(zip(prior_forwarded, forwarded)):
|
| 205 |
+
if a != b:
|
| 206 |
+
first_diff_index = idx
|
| 207 |
+
break
|
| 208 |
+
if first_diff_index is None:
|
| 209 |
+
first_diff_index = min(len(prior_forwarded), len(forwarded))
|
| 210 |
+
events.append(
|
| 211 |
+
{
|
| 212 |
+
"mode": mode,
|
| 213 |
+
"session_id": replay.session_id if include_content else _redact_text(replay.session_id, prefix="session"),
|
| 214 |
+
"project": replay.decoded_project_path if include_content else _redact_path(replay.decoded_project_path),
|
| 215 |
+
"request_id": turn.request_id if include_content else _redact_text(turn.request_id, prefix="request"),
|
| 216 |
+
"timestamp": turn.timestamp.isoformat(),
|
| 217 |
+
"cache_eligible": eligible,
|
| 218 |
+
"prefix_preserved": prefix_preserved,
|
| 219 |
+
"retroactive_rewrite": retro,
|
| 220 |
+
"first_diff_index": first_diff_index,
|
| 221 |
+
"original_tail": [
|
| 222 |
+
_message_preview(m, max_chars=max_chars) if include_content else {
|
| 223 |
+
"role": str(m.get("role")),
|
| 224 |
+
"content_excerpt": "[redacted]",
|
| 225 |
+
}
|
| 226 |
+
for m in conversation[max(0, len(conversation) - 4) :]
|
| 227 |
+
],
|
| 228 |
+
"forwarded_tail": [
|
| 229 |
+
_message_preview(m, max_chars=max_chars) if include_content else {
|
| 230 |
+
"role": str(m.get("role")),
|
| 231 |
+
"content_excerpt": "[redacted]",
|
| 232 |
+
}
|
| 233 |
+
for m in forwarded[max(0, len(forwarded) - 4) :]
|
| 234 |
+
],
|
| 235 |
+
}
|
| 236 |
+
)
|
| 237 |
+
collected += 1
|
| 238 |
+
if collected >= max_events_per_mode:
|
| 239 |
+
break
|
| 240 |
+
if pending is not None:
|
| 241 |
+
previous_forwarded = copy.deepcopy(pending.forwarded)
|
| 242 |
+
previous_timestamp = pending.turn.timestamp
|
| 243 |
+
real_bench._update_prefix_tracker(
|
| 244 |
+
prefix_tracker,
|
| 245 |
+
cache_read_tokens=0,
|
| 246 |
+
cache_write_tokens=0,
|
| 247 |
+
messages=forwarded,
|
| 248 |
+
message_token_counts=[tokenizer.count_message(msg) for msg in forwarded],
|
| 249 |
+
original_messages=conversation,
|
| 250 |
+
)
|
| 251 |
+
class Pending:
|
| 252 |
+
pass
|
| 253 |
+
|
| 254 |
+
pending = Pending()
|
| 255 |
+
pending.turn = turn
|
| 256 |
+
pending.forwarded = forwarded
|
| 257 |
+
conversation.append(turn.assistant_message)
|
| 258 |
+
previous_original_context = copy.deepcopy(conversation)
|
| 259 |
+
previous_forwarded_context = copy.deepcopy(forwarded) + [
|
| 260 |
+
copy.deepcopy(turn.assistant_message)
|
| 261 |
+
]
|
| 262 |
+
if collected >= max_events_per_mode:
|
| 263 |
+
break
|
| 264 |
+
return {"events": events}
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def _write_processed_event_reports(output_dir: Path, payload: dict[str, Any]) -> tuple[Path, Path, Path]:
|
| 268 |
+
out_dir = output_dir / "real_processed"
|
| 269 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 270 |
+
json_path = out_dir / "real_processed_rewrite_report.json"
|
| 271 |
+
md_path = out_dir / "real_processed_rewrite_report.md"
|
| 272 |
+
html_path = out_dir / "real_processed_rewrite_report.html"
|
| 273 |
+
json_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
| 274 |
+
|
| 275 |
+
md = [
|
| 276 |
+
"# Real Processed Rewrite Report",
|
| 277 |
+
"",
|
| 278 |
+
"Local-only report from real Claude transcript replays. Do not commit.",
|
| 279 |
+
"",
|
| 280 |
+
]
|
| 281 |
+
for mode in (PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 282 |
+
mode_events = [e for e in payload["events"] if e["mode"] == mode]
|
| 283 |
+
md.extend([f"## `{mode}`", ""])
|
| 284 |
+
if not mode_events:
|
| 285 |
+
md.extend(["No rewrite events captured.", ""])
|
| 286 |
+
continue
|
| 287 |
+
for i, e in enumerate(mode_events, start=1):
|
| 288 |
+
md.extend(
|
| 289 |
+
[
|
| 290 |
+
f"### Event {i}",
|
| 291 |
+
"",
|
| 292 |
+
f"- session: `{e['session_id']}`",
|
| 293 |
+
f"- request: `{e['request_id']}`",
|
| 294 |
+
f"- cache eligible: `{e['cache_eligible']}`",
|
| 295 |
+
f"- prefix preserved: `{e['prefix_preserved']}`",
|
| 296 |
+
f"- retroactive rewrite: `{e['retroactive_rewrite']}`",
|
| 297 |
+
f"- first diff index: `{e['first_diff_index']}`",
|
| 298 |
+
"",
|
| 299 |
+
"**Original Tail**",
|
| 300 |
+
"",
|
| 301 |
+
]
|
| 302 |
+
)
|
| 303 |
+
for msg in e["original_tail"]:
|
| 304 |
+
md.append(f"- `{msg['role']}`: {msg['content_excerpt']}")
|
| 305 |
+
md.extend(["", "**Forwarded Tail**", ""])
|
| 306 |
+
for msg in e["forwarded_tail"]:
|
| 307 |
+
md.append(f"- `{msg['role']}`: {msg['content_excerpt']}")
|
| 308 |
+
md.extend(["", ""])
|
| 309 |
+
md_path.write_text("\n".join(md), encoding="utf-8")
|
| 310 |
+
|
| 311 |
+
sections = []
|
| 312 |
+
for mode in (PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 313 |
+
mode_events = [e for e in payload["events"] if e["mode"] == mode]
|
| 314 |
+
cards = []
|
| 315 |
+
for i, e in enumerate(mode_events, start=1):
|
| 316 |
+
orig = "".join(
|
| 317 |
+
f"<li><code>{html.escape(str(m['role']))}</code>: "
|
| 318 |
+
f"{html.escape(str(m['content_excerpt']))}</li>"
|
| 319 |
+
for m in e["original_tail"]
|
| 320 |
+
)
|
| 321 |
+
fwd = "".join(
|
| 322 |
+
f"<li><code>{html.escape(str(m['role']))}</code>: "
|
| 323 |
+
f"{html.escape(str(m['content_excerpt']))}</li>"
|
| 324 |
+
for m in e["forwarded_tail"]
|
| 325 |
+
)
|
| 326 |
+
cards.append(
|
| 327 |
+
"<div class='event'>"
|
| 328 |
+
f"<h3>Event {i}</h3>"
|
| 329 |
+
f"<p><strong>session</strong>: <code>{html.escape(e['session_id'])}</code><br>"
|
| 330 |
+
f"<strong>request</strong>: <code>{html.escape(e['request_id'])}</code><br>"
|
| 331 |
+
f"<strong>cache eligible</strong>: <code>{e['cache_eligible']}</code><br>"
|
| 332 |
+
f"<strong>prefix preserved</strong>: <code>{e['prefix_preserved']}</code><br>"
|
| 333 |
+
f"<strong>retroactive rewrite</strong>: <code>{e['retroactive_rewrite']}</code><br>"
|
| 334 |
+
f"<strong>first diff index</strong>: <code>{e['first_diff_index']}</code></p>"
|
| 335 |
+
f"<div class='cols'><div><h4>Original Tail</h4><ul>{orig}</ul></div>"
|
| 336 |
+
f"<div><h4>Forwarded Tail</h4><ul>{fwd}</ul></div></div>"
|
| 337 |
+
"</div>"
|
| 338 |
+
)
|
| 339 |
+
sections.append(
|
| 340 |
+
f"<section class='card'><h2>{html.escape(mode)}</h2>"
|
| 341 |
+
+ ("".join(cards) if cards else "<p>No rewrite events captured.</p>")
|
| 342 |
+
+ "</section>"
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
html_doc = (
|
| 346 |
+
"<!doctype html><html><head><meta charset='utf-8'>"
|
| 347 |
+
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
|
| 348 |
+
"<title>Real Processed Rewrite Report</title>"
|
| 349 |
+
"<style>"
|
| 350 |
+
"body{font-family:ui-sans-serif,system-ui,sans-serif;max-width:1200px;margin:40px auto;padding:0 20px;line-height:1.55;color:#111827;background:#f8fafc}"
|
| 351 |
+
".card,.event{background:white;border:1px solid #cbd5e1;border-radius:16px;padding:20px;margin:18px 0;box-shadow:0 8px 24px rgba(15,23,42,.06)}"
|
| 352 |
+
".cols{display:grid;grid-template-columns:1fr 1fr;gap:20px} code{background:#e5e7eb;padding:1px 4px;border-radius:4px} ul{padding-left:20px}"
|
| 353 |
+
"</style></head><body>"
|
| 354 |
+
"<h1>Real Processed Rewrite Report</h1>"
|
| 355 |
+
"<div class='card'><p>Local-only report from real Claude transcript replays. Do not commit.</p></div>"
|
| 356 |
+
+ "".join(sections)
|
| 357 |
+
+ "</body></html>"
|
| 358 |
+
)
|
| 359 |
+
html_path.write_text(html_doc, encoding="utf-8")
|
| 360 |
+
return md_path, json_path, html_path
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def _write_index(
|
| 364 |
+
output_dir: Path,
|
| 365 |
+
*,
|
| 366 |
+
args: argparse.Namespace,
|
| 367 |
+
dataset: dict[str, Any],
|
| 368 |
+
observed: dict[str, Any],
|
| 369 |
+
summaries: dict[str, Any],
|
| 370 |
+
winners: dict[str, str],
|
| 371 |
+
metadata: dict[str, Any],
|
| 372 |
+
corpus: dict[str, Any],
|
| 373 |
+
processed_paths: tuple[Path, Path, Path],
|
| 374 |
+
token_bust_paths: tuple[Path, Path, Path],
|
| 375 |
+
long_suite_paths: tuple[Path, Path, Path],
|
| 376 |
+
) -> tuple[Path, Path]:
|
| 377 |
+
md_path = output_dir / "index.md"
|
| 378 |
+
html_path = output_dir / "index.html"
|
| 379 |
+
md_lines = [
|
| 380 |
+
"# Cache Validation Bundle",
|
| 381 |
+
"",
|
| 382 |
+
"This bundle is reproducible on another machine with local Claude transcript data in `~/.claude/projects`.",
|
| 383 |
+
"",
|
| 384 |
+
"## Configuration",
|
| 385 |
+
"",
|
| 386 |
+
f"- root: `{args.root}`",
|
| 387 |
+
f"- output dir: `{args.output_dir}`",
|
| 388 |
+
f"- recent turns per session: `{args.recent_turns_per_session}`",
|
| 389 |
+
f"- workers: `{args.workers}`",
|
| 390 |
+
f"- cache TTL minutes: `{args.cache_ttl_minutes}`",
|
| 391 |
+
f"- cache write multiplier: `{args.cache_write_multiplier}`",
|
| 392 |
+
f"- max real processed events per mode: `{args.max_real_events_per_mode}`",
|
| 393 |
+
f"- include transcript content: `{args.include_content}`",
|
| 394 |
+
"",
|
| 395 |
+
"## Reproducibility",
|
| 396 |
+
"",
|
| 397 |
+
f"- git sha: `{metadata['git_sha']}`",
|
| 398 |
+
f"- git dirty: `{metadata['git_dirty']}`",
|
| 399 |
+
f"- python: `{metadata['implementation']}`",
|
| 400 |
+
f"- platform: `{metadata['platform']}`",
|
| 401 |
+
f"- corpus session file count: `{corpus['session_file_count']}`",
|
| 402 |
+
f"- corpus fingerprint: `{corpus['session_files_sha256']}`",
|
| 403 |
+
"",
|
| 404 |
+
"## Real Corpus Summary",
|
| 405 |
+
"",
|
| 406 |
+
f"- projects: `{dataset['projects']}`",
|
| 407 |
+
f"- sessions: `{dataset['sessions']}`",
|
| 408 |
+
f"- requests: `{dataset['requests']}`",
|
| 409 |
+
f"- observed total cost: `{format_currency(observed['total_cost_usd'])}`",
|
| 410 |
+
f"- winner by total cost: `{winners['total_cost']}`",
|
| 411 |
+
"",
|
| 412 |
+
"| Mode | Total Cost | Cache Busts | Busting Rewrites | Stable Replay Rewrites | Rewrites | Retroactive Rewrites | TTL Expiry | Forwarded Tokens |",
|
| 413 |
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
| 414 |
+
]
|
| 415 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 416 |
+
summary = summaries[mode]
|
| 417 |
+
md_lines.append(
|
| 418 |
+
f"| `{mode}` | {format_currency(summary['total_cost_usd'])} | {summary['cache_bust_turns']} | "
|
| 419 |
+
f"{summary['busting_rewrite_turns']} | {summary['stable_replay_rewrite_turns']} | "
|
| 420 |
+
f"{summary['rewrite_turns']} | {summary['retroactive_rewrite_turns']} | "
|
| 421 |
+
f"{summary['ttl_expiry_turns']} | {summary['forwarded_input_tokens']:,} |"
|
| 422 |
+
)
|
| 423 |
+
md_lines.extend(
|
| 424 |
+
[
|
| 425 |
+
"",
|
| 426 |
+
"## Interpretation",
|
| 427 |
+
"",
|
| 428 |
+
"- `cache_bust_turns` and `busting_rewrite_turns` are the hard-failure metrics for Anthropic prefix caching.",
|
| 429 |
+
"- `stable_replay_rewrite_turns` indicates replay of previously-forwarded bytes that still preserves cache prefix stability.",
|
| 430 |
+
"- `retroactive_rewrite_turns` is descriptive only; it does not imply a cache break by itself.",
|
| 431 |
+
"- `ttl_expiry_turns` is workload timing context, not compression correctness.",
|
| 432 |
+
"",
|
| 433 |
+
"## Artifacts",
|
| 434 |
+
"",
|
| 435 |
+
f"- real corpus summary markdown: [real/{real_bench.OUTPUT_MD}](real/{real_bench.OUTPUT_MD})",
|
| 436 |
+
f"- real corpus summary html: [real/{real_bench.OUTPUT_HTML}](real/{real_bench.OUTPUT_HTML})",
|
| 437 |
+
f"- real processed markdown: [real_processed/{processed_paths[0].name}](real_processed/{processed_paths[0].name})",
|
| 438 |
+
f"- real processed html: [real_processed/{processed_paths[2].name}](real_processed/{processed_paths[2].name})",
|
| 439 |
+
f"- synthetic token bust markdown: [synthetic_token_bust/{token_bust_paths[0].name}](synthetic_token_bust/{token_bust_paths[0].name})",
|
| 440 |
+
f"- synthetic token bust html: [synthetic_token_bust/{token_bust_paths[2].name}](synthetic_token_bust/{token_bust_paths[2].name})",
|
| 441 |
+
f"- synthetic long suite markdown: [synthetic_long_suite/{long_suite_paths[0].name}](synthetic_long_suite/{long_suite_paths[0].name})",
|
| 442 |
+
f"- synthetic long suite html: [synthetic_long_suite/{long_suite_paths[2].name}](synthetic_long_suite/{long_suite_paths[2].name})",
|
| 443 |
+
]
|
| 444 |
+
)
|
| 445 |
+
md_path.write_text("\n".join(md_lines), encoding="utf-8")
|
| 446 |
+
|
| 447 |
+
rows = []
|
| 448 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 449 |
+
summary = summaries[mode]
|
| 450 |
+
rows.append(
|
| 451 |
+
"<tr>"
|
| 452 |
+
f"<td><code>{html.escape(mode)}</code></td>"
|
| 453 |
+
f"<td>{html.escape(format_currency(summary['total_cost_usd']))}</td>"
|
| 454 |
+
f"<td>{summary['cache_bust_turns']}</td>"
|
| 455 |
+
f"<td>{summary['busting_rewrite_turns']}</td>"
|
| 456 |
+
f"<td>{summary['stable_replay_rewrite_turns']}</td>"
|
| 457 |
+
f"<td>{summary['rewrite_turns']}</td>"
|
| 458 |
+
f"<td>{summary['retroactive_rewrite_turns']}</td>"
|
| 459 |
+
f"<td>{summary['ttl_expiry_turns']}</td>"
|
| 460 |
+
f"<td>{summary['forwarded_input_tokens']:,}</td>"
|
| 461 |
+
"</tr>"
|
| 462 |
+
)
|
| 463 |
+
html_doc = (
|
| 464 |
+
"<!doctype html><html><head><meta charset='utf-8'>"
|
| 465 |
+
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
|
| 466 |
+
"<title>Cache Validation Bundle</title>"
|
| 467 |
+
"<style>"
|
| 468 |
+
"body{font-family:ui-sans-serif,system-ui,sans-serif;max-width:1200px;margin:40px auto;padding:0 20px;line-height:1.55;color:#111827;background:#f8fafc}"
|
| 469 |
+
".card{background:white;border:1px solid #cbd5e1;border-radius:16px;padding:24px;margin:18px 0;box-shadow:0 8px 24px rgba(15,23,42,.06)}"
|
| 470 |
+
"table{border-collapse:collapse;width:100%;margin:16px 0;background:white}"
|
| 471 |
+
"th,td{border:1px solid #cbd5e1;padding:10px;text-align:left}th{background:#e2e8f0}"
|
| 472 |
+
"code{background:#e5e7eb;padding:1px 4px;border-radius:4px}"
|
| 473 |
+
"</style></head><body>"
|
| 474 |
+
"<h1>Cache Validation Bundle</h1>"
|
| 475 |
+
"<div class='card'>"
|
| 476 |
+
f"<p><strong>root</strong>: <code>{html.escape(str(args.root))}</code><br>"
|
| 477 |
+
f"<strong>recent turns per session</strong>: <code>{html.escape(str(args.recent_turns_per_session))}</code><br>"
|
| 478 |
+
f"<strong>workers</strong>: <code>{args.workers}</code><br>"
|
| 479 |
+
f"<strong>cache TTL minutes</strong>: <code>{args.cache_ttl_minutes}</code><br>"
|
| 480 |
+
f"<strong>include transcript content</strong>: <code>{args.include_content}</code></p>"
|
| 481 |
+
"</div>"
|
| 482 |
+
"<div class='card'><h2>Reproducibility</h2>"
|
| 483 |
+
f"<p><strong>git sha</strong>: <code>{html.escape(str(metadata['git_sha']))}</code><br>"
|
| 484 |
+
f"<strong>git dirty</strong>: <code>{metadata['git_dirty']}</code><br>"
|
| 485 |
+
f"<strong>python</strong>: <code>{html.escape(str(metadata['implementation']))}</code><br>"
|
| 486 |
+
f"<strong>platform</strong>: <code>{html.escape(str(metadata['platform']))}</code><br>"
|
| 487 |
+
f"<strong>corpus session file count</strong>: <code>{corpus['session_file_count']}</code><br>"
|
| 488 |
+
f"<strong>corpus fingerprint</strong>: <code>{html.escape(str(corpus['session_files_sha256']))}</code></p>"
|
| 489 |
+
"</div>"
|
| 490 |
+
"<div class='card'><h2>Real Corpus Summary</h2>"
|
| 491 |
+
f"<p>projects: <code>{dataset['projects']}</code><br>"
|
| 492 |
+
f"sessions: <code>{dataset['sessions']}</code><br>"
|
| 493 |
+
f"requests: <code>{dataset['requests']}</code><br>"
|
| 494 |
+
f"observed total cost: <code>{html.escape(format_currency(observed['total_cost_usd']))}</code><br>"
|
| 495 |
+
f"winner by total cost: <code>{html.escape(winners['total_cost'])}</code></p>"
|
| 496 |
+
"<table><thead><tr><th>Mode</th><th>Total Cost</th><th>Cache Busts</th><th>Busting Rewrites</th>"
|
| 497 |
+
"<th>Stable Replay Rewrites</th><th>Rewrites</th>"
|
| 498 |
+
"<th>Retroactive Rewrites</th><th>TTL Expiry</th><th>Forwarded Tokens</th></tr></thead><tbody>"
|
| 499 |
+
+ "".join(rows)
|
| 500 |
+
+ "</tbody></table>"
|
| 501 |
+
"<p><strong>Interpretation</strong>: <code>cache_bust_turns</code> and "
|
| 502 |
+
"<code>busting_rewrite_turns</code> are the hard-failure metrics. "
|
| 503 |
+
"<code>stable_replay_rewrite_turns</code> is acceptable stable replay. "
|
| 504 |
+
"<code>retroactive_rewrite_turns</code> is descriptive only. "
|
| 505 |
+
"<code>ttl_expiry_turns</code> is workload timing context.</p></div>"
|
| 506 |
+
"<div class='card'><h2>Artifacts</h2><ul>"
|
| 507 |
+
f"<li><a href='real/{real_bench.OUTPUT_HTML}'>Real corpus summary HTML</a></li>"
|
| 508 |
+
f"<li><a href='real/{real_bench.OUTPUT_MD}'>Real corpus summary Markdown</a></li>"
|
| 509 |
+
f"<li><a href='real_processed/{processed_paths[2].name}'>Real processed rewrite HTML</a></li>"
|
| 510 |
+
f"<li><a href='real_processed/{processed_paths[0].name}'>Real processed rewrite Markdown</a></li>"
|
| 511 |
+
f"<li><a href='synthetic_token_bust/{token_bust_paths[2].name}'>Synthetic token-bust HTML</a></li>"
|
| 512 |
+
f"<li><a href='synthetic_token_bust/{token_bust_paths[0].name}'>Synthetic token-bust Markdown</a></li>"
|
| 513 |
+
f"<li><a href='synthetic_long_suite/{long_suite_paths[2].name}'>Synthetic long suite HTML</a></li>"
|
| 514 |
+
f"<li><a href='synthetic_long_suite/{long_suite_paths[0].name}'>Synthetic long suite Markdown</a></li>"
|
| 515 |
+
"</ul></div></body></html>"
|
| 516 |
+
)
|
| 517 |
+
html_path.write_text(html_doc, encoding="utf-8")
|
| 518 |
+
return md_path, html_path
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
def parse_args() -> argparse.Namespace:
|
| 522 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 523 |
+
parser.add_argument("--root", type=Path, default=real_bench.DEFAULT_ROOT)
|
| 524 |
+
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
|
| 525 |
+
parser.add_argument("--recent-turns-per-session", type=int, default=None)
|
| 526 |
+
parser.add_argument("--workers", type=int, default=1)
|
| 527 |
+
parser.add_argument("--cache-ttl-minutes", type=int, default=real_bench.DEFAULT_CACHE_TTL_MINUTES)
|
| 528 |
+
parser.add_argument("--cache-write-multiplier", type=float, default=1.25)
|
| 529 |
+
parser.add_argument("--max-sessions", type=int, default=None)
|
| 530 |
+
parser.add_argument("--max-real-events-per-mode", type=int, default=8)
|
| 531 |
+
parser.add_argument("--content-excerpt-chars", type=int, default=220)
|
| 532 |
+
parser.add_argument(
|
| 533 |
+
"--include-content",
|
| 534 |
+
action="store_true",
|
| 535 |
+
help="Include real transcript-derived content excerpts in the processed event reports.",
|
| 536 |
+
)
|
| 537 |
+
parser.add_argument(
|
| 538 |
+
"--checkpoint-dir",
|
| 539 |
+
type=Path,
|
| 540 |
+
default=real_bench.DEFAULT_OUTPUT_DIR / real_bench.CHECKPOINT_DIRNAME,
|
| 541 |
+
)
|
| 542 |
+
return parser.parse_args()
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def main() -> int:
|
| 546 |
+
args = parse_args()
|
| 547 |
+
output_dir = args.output_dir
|
| 548 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 549 |
+
|
| 550 |
+
logging.getLogger("headroom.transforms").setLevel(logging.WARNING)
|
| 551 |
+
logging.getLogger("headroom.proxy").setLevel(logging.WARNING)
|
| 552 |
+
|
| 553 |
+
session_files = select_session_files(args.root, max_sessions=args.max_sessions)
|
| 554 |
+
if not session_files:
|
| 555 |
+
print(f"No Claude session replays found under {args.root}")
|
| 556 |
+
return 1
|
| 557 |
+
|
| 558 |
+
repo_root = Path(__file__).resolve().parents[1]
|
| 559 |
+
metadata = _runtime_metadata(repo_root)
|
| 560 |
+
corpus = _corpus_fingerprint(
|
| 561 |
+
root=args.root,
|
| 562 |
+
session_files=session_files,
|
| 563 |
+
max_sessions=args.max_sessions,
|
| 564 |
+
recent_turns_per_session=args.recent_turns_per_session,
|
| 565 |
+
cache_ttl_minutes=args.cache_ttl_minutes,
|
| 566 |
+
)
|
| 567 |
+
dataset, observed = build_dataset_and_observed_from_files(
|
| 568 |
+
session_files,
|
| 569 |
+
cache_write_multiplier=args.cache_write_multiplier,
|
| 570 |
+
recent_turns_per_session=args.recent_turns_per_session,
|
| 571 |
+
)
|
| 572 |
+
checkpoint_base = output_dir / "checkpoints" / corpus["session_files_sha256"]
|
| 573 |
+
checkpoint_dir = resolve_checkpoint_dir(
|
| 574 |
+
checkpoint_base,
|
| 575 |
+
recent_turns_per_session=args.recent_turns_per_session,
|
| 576 |
+
cache_ttl_minutes=args.cache_ttl_minutes,
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
real_output_dir = output_dir / "real"
|
| 580 |
+
summaries = simulate_session_files(
|
| 581 |
+
session_files,
|
| 582 |
+
dataset,
|
| 583 |
+
cache_ttl_minutes=args.cache_ttl_minutes,
|
| 584 |
+
cache_write_multiplier=args.cache_write_multiplier,
|
| 585 |
+
workers=args.workers,
|
| 586 |
+
checkpoint_dir=checkpoint_dir,
|
| 587 |
+
recent_turns_per_session=args.recent_turns_per_session,
|
| 588 |
+
)
|
| 589 |
+
real_md, real_json, real_html = write_report(real_output_dir, dataset, observed, summaries)
|
| 590 |
+
|
| 591 |
+
processed_payload = _collect_real_processed_events(
|
| 592 |
+
root=args.root,
|
| 593 |
+
recent_turns_per_session=args.recent_turns_per_session,
|
| 594 |
+
max_events_per_mode=args.max_real_events_per_mode,
|
| 595 |
+
ttl_minutes=args.cache_ttl_minutes,
|
| 596 |
+
max_chars=args.content_excerpt_chars,
|
| 597 |
+
include_content=args.include_content,
|
| 598 |
+
)
|
| 599 |
+
processed_paths = _write_processed_event_reports(output_dir, processed_payload)
|
| 600 |
+
|
| 601 |
+
token_bust.OUTPUT_DIR = output_dir / "synthetic_token_bust"
|
| 602 |
+
token_bust_replay = token_bust._build_replay()
|
| 603 |
+
original_make_proxy = token_bust.bench._make_proxy
|
| 604 |
+
token_bust.bench._make_proxy = lambda mode: token_bust._FakeProxy()
|
| 605 |
+
try:
|
| 606 |
+
_, token_bust_summaries = token_bust.simulate_replays(
|
| 607 |
+
[token_bust_replay], cache_ttl_minutes=token_bust.TTL_MINUTES if hasattr(token_bust, "TTL_MINUTES") else 5
|
| 608 |
+
)
|
| 609 |
+
token_bust_events = token_bust._build_bust_events(token_bust_replay)
|
| 610 |
+
finally:
|
| 611 |
+
token_bust.bench._make_proxy = original_make_proxy
|
| 612 |
+
token_bust_paths = token_bust._write_report(
|
| 613 |
+
token_bust_replay,
|
| 614 |
+
token_bust_summaries,
|
| 615 |
+
determine_winners(token_bust_summaries),
|
| 616 |
+
token_bust_events,
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
long_suite.OUTPUT_DIR = output_dir / "synthetic_long_suite"
|
| 620 |
+
per_scenario, aggregate = long_suite._run_suite()
|
| 621 |
+
long_suite_paths = long_suite._write_report(per_scenario, aggregate)
|
| 622 |
+
|
| 623 |
+
bundle_payload = {
|
| 624 |
+
"config": {
|
| 625 |
+
"root": _redact_path(str(args.root.resolve())),
|
| 626 |
+
"output_dir": _redact_path(str(output_dir.resolve())),
|
| 627 |
+
"recent_turns_per_session": args.recent_turns_per_session,
|
| 628 |
+
"workers": args.workers,
|
| 629 |
+
"cache_ttl_minutes": args.cache_ttl_minutes,
|
| 630 |
+
"cache_write_multiplier": args.cache_write_multiplier,
|
| 631 |
+
"max_sessions": args.max_sessions,
|
| 632 |
+
"max_real_events_per_mode": args.max_real_events_per_mode,
|
| 633 |
+
"content_excerpt_chars": args.content_excerpt_chars,
|
| 634 |
+
"include_content": args.include_content,
|
| 635 |
+
"checkpoint_dir": _redact_path(str(checkpoint_dir.resolve())),
|
| 636 |
+
},
|
| 637 |
+
"runtime": metadata,
|
| 638 |
+
"corpus": corpus,
|
| 639 |
+
"real": {
|
| 640 |
+
"dataset": asdict(dataset),
|
| 641 |
+
"observed": asdict(observed),
|
| 642 |
+
"summaries": {mode: asdict(summary) for mode, summary in summaries.items()},
|
| 643 |
+
"winners": determine_winners(summaries),
|
| 644 |
+
"paths": {
|
| 645 |
+
"markdown": str(real_md),
|
| 646 |
+
"json": str(real_json),
|
| 647 |
+
"html": str(real_html),
|
| 648 |
+
},
|
| 649 |
+
},
|
| 650 |
+
"processed_real": {
|
| 651 |
+
"events": processed_payload["events"],
|
| 652 |
+
"paths": {
|
| 653 |
+
"markdown": str(processed_paths[0]),
|
| 654 |
+
"json": str(processed_paths[1]),
|
| 655 |
+
"html": str(processed_paths[2]),
|
| 656 |
+
},
|
| 657 |
+
},
|
| 658 |
+
"synthetic_token_bust": {
|
| 659 |
+
"paths": {
|
| 660 |
+
"markdown": str(token_bust_paths[0]),
|
| 661 |
+
"json": str(token_bust_paths[1]),
|
| 662 |
+
"html": str(token_bust_paths[2]),
|
| 663 |
+
}
|
| 664 |
+
},
|
| 665 |
+
"synthetic_long_suite": {
|
| 666 |
+
"paths": {
|
| 667 |
+
"markdown": str(long_suite_paths[0]),
|
| 668 |
+
"json": str(long_suite_paths[1]),
|
| 669 |
+
"html": str(long_suite_paths[2]),
|
| 670 |
+
}
|
| 671 |
+
},
|
| 672 |
+
}
|
| 673 |
+
manifest_path = output_dir / "bundle_manifest.json"
|
| 674 |
+
manifest_path.write_text(json.dumps(bundle_payload, indent=2), encoding="utf-8")
|
| 675 |
+
|
| 676 |
+
index_md, index_html = _write_index(
|
| 677 |
+
output_dir,
|
| 678 |
+
args=args,
|
| 679 |
+
dataset=asdict(dataset),
|
| 680 |
+
observed=asdict(observed),
|
| 681 |
+
summaries={mode: asdict(summary) for mode, summary in summaries.items()},
|
| 682 |
+
winners=determine_winners(summaries),
|
| 683 |
+
metadata=metadata,
|
| 684 |
+
corpus=corpus,
|
| 685 |
+
processed_paths=processed_paths,
|
| 686 |
+
token_bust_paths=token_bust_paths,
|
| 687 |
+
long_suite_paths=long_suite_paths,
|
| 688 |
+
)
|
| 689 |
+
|
| 690 |
+
print(f"Index markdown: {index_md}")
|
| 691 |
+
print(f"Index html: {index_html}")
|
| 692 |
+
print(f"Manifest: {manifest_path}")
|
| 693 |
+
return 0
|
| 694 |
+
|
| 695 |
+
|
| 696 |
+
if __name__ == "__main__":
|
| 697 |
+
raise SystemExit(main())
|
benchmarks/claude_session_mode_benchmark.py
CHANGED
|
@@ -102,6 +102,9 @@ class ModeSummary:
|
|
| 102 |
cache_bust_turns: int = 0
|
| 103 |
ttl_expiry_turns: int = 0
|
| 104 |
rewrite_turns: int = 0
|
|
|
|
|
|
|
|
|
|
| 105 |
retroactive_rewrite_turns: int = 0
|
| 106 |
latest_turn_only_rewrite_turns: int = 0
|
| 107 |
turns: list[TurnMetrics] = field(default_factory=list)
|
|
@@ -157,6 +160,9 @@ IMPACT_DIRECTION = {
|
|
| 157 |
"cache_bust_turns": "lower",
|
| 158 |
"ttl_expiry_turns": "lower",
|
| 159 |
"rewrite_turns": "lower",
|
|
|
|
|
|
|
|
|
|
| 160 |
"retroactive_rewrite_turns": "lower",
|
| 161 |
"latest_turn_only_rewrite_turns": "lower",
|
| 162 |
}
|
|
@@ -248,6 +254,9 @@ def _mode_summary_from_dict(data: dict[str, Any]) -> ModeSummary:
|
|
| 248 |
cache_bust_turns=data.get("cache_bust_turns", 0),
|
| 249 |
ttl_expiry_turns=data.get("ttl_expiry_turns", 0),
|
| 250 |
rewrite_turns=data.get("rewrite_turns", 0),
|
|
|
|
|
|
|
|
|
|
| 251 |
retroactive_rewrite_turns=data.get("retroactive_rewrite_turns", 0),
|
| 252 |
latest_turn_only_rewrite_turns=data.get("latest_turn_only_rewrite_turns", 0),
|
| 253 |
turns=turns,
|
|
@@ -462,7 +471,7 @@ def resolve_checkpoint_dir(
|
|
| 462 |
recent_turns_per_session: int | None = None,
|
| 463 |
cache_ttl_minutes: int = DEFAULT_CACHE_TTL_MINUTES,
|
| 464 |
) -> Path:
|
| 465 |
-
suffix_parts = ["
|
| 466 |
if recent_turns_per_session:
|
| 467 |
suffix_parts.append(f"recent_{recent_turns_per_session}")
|
| 468 |
else:
|
|
@@ -906,6 +915,8 @@ class _PendingTurn:
|
|
| 906 |
raw_input_tokens: int
|
| 907 |
request_messages: list[dict[str, Any]]
|
| 908 |
forwarded: list[dict[str, Any]]
|
|
|
|
|
|
|
| 909 |
|
| 910 |
|
| 911 |
def _cache_gap_within_ttl(
|
|
@@ -1021,6 +1032,9 @@ def _merge_mode_summary(target: ModeSummary, source: ModeSummary) -> None:
|
|
| 1021 |
target.cache_bust_turns += source.cache_bust_turns
|
| 1022 |
target.ttl_expiry_turns += source.ttl_expiry_turns
|
| 1023 |
target.rewrite_turns += source.rewrite_turns
|
|
|
|
|
|
|
|
|
|
| 1024 |
target.retroactive_rewrite_turns += source.retroactive_rewrite_turns
|
| 1025 |
target.latest_turn_only_rewrite_turns += source.latest_turn_only_rewrite_turns
|
| 1026 |
|
|
@@ -1169,6 +1183,25 @@ def _simulate_single_replay_mode(
|
|
| 1169 |
summary.retroactive_rewrite_turns += 1
|
| 1170 |
else:
|
| 1171 |
summary.latest_turn_only_rewrite_turns += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1172 |
if pending is not None:
|
| 1173 |
_apply_turn_metrics(
|
| 1174 |
pending.summary,
|
|
@@ -1203,6 +1236,8 @@ def _simulate_single_replay_mode(
|
|
| 1203 |
raw_input_tokens=raw_input_tokens,
|
| 1204 |
request_messages=copy.deepcopy(conversation),
|
| 1205 |
forwarded=forwarded,
|
|
|
|
|
|
|
| 1206 |
)
|
| 1207 |
conversation.append(turn.assistant_message)
|
| 1208 |
conversation_token_total = raw_input_tokens + tokenizer.count_message(
|
|
@@ -1513,7 +1548,7 @@ def print_console_report(dataset: DatasetSummary, summaries: dict[str, ModeSumma
|
|
| 1513 |
print(f"Sampling: {dataset.sampling_note}")
|
| 1514 |
print()
|
| 1515 |
print(
|
| 1516 |
-
"mode raw_tok cache_tok cache_read cache_write paid_in paid_out busts ttl_exp rewrite retro_rw total_cost no_cache"
|
| 1517 |
)
|
| 1518 |
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 1519 |
summary = summaries[mode]
|
|
@@ -1522,7 +1557,9 @@ def print_console_report(dataset: DatasetSummary, summaries: dict[str, ModeSumma
|
|
| 1522 |
f"{summary.cache_read_tokens:>11,} {summary.cache_write_tokens:>12,} "
|
| 1523 |
f"{summary.regular_input_tokens:>10,} {summary.output_tokens:>12,} "
|
| 1524 |
f"{summary.cache_bust_turns:>7,} {summary.ttl_expiry_turns:>9,} "
|
| 1525 |
-
f"{summary.rewrite_turns:>9,} {summary.
|
|
|
|
|
|
|
| 1526 |
f"{format_currency(summary.total_cost_usd):>11} "
|
| 1527 |
f"{format_currency(summary.no_cache_total_cost_usd):>11}"
|
| 1528 |
)
|
|
@@ -1549,6 +1586,12 @@ def print_console_report(dataset: DatasetSummary, summaries: dict[str, ModeSumma
|
|
| 1549 |
f"({int(impact['regular_input_tokens']['delta']):,}), "
|
| 1550 |
f"rewrite={impact['rewrite_turns']['impact']} "
|
| 1551 |
f"({int(impact['rewrite_turns']['delta']):,}), "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1552 |
f"retro_rw={impact['retroactive_rewrite_turns']['impact']} "
|
| 1553 |
f"({int(impact['retroactive_rewrite_turns']['delta']):,}), "
|
| 1554 |
f"window={impact['prompt_window_with_cache']['impact']} "
|
|
@@ -1602,6 +1645,9 @@ def build_report_markdown(
|
|
| 1602 |
f"{summary.cache_bust_turns:,}",
|
| 1603 |
f"{summary.ttl_expiry_turns:,}",
|
| 1604 |
f"{summary.rewrite_turns:,}",
|
|
|
|
|
|
|
|
|
|
| 1605 |
f"{summary.retroactive_rewrite_turns:,}",
|
| 1606 |
f"{summary.latest_turn_only_rewrite_turns:,}",
|
| 1607 |
f"{summary.prompt_window_with_cache:,}",
|
|
@@ -1622,6 +1668,9 @@ def build_report_markdown(
|
|
| 1622 |
("prompt_window_without_cache_reads", "Window Without Cache Reads"),
|
| 1623 |
("cache_bust_turns", "Cache Bust Turns"),
|
| 1624 |
("rewrite_turns", "Rewrite Turns"),
|
|
|
|
|
|
|
|
|
|
| 1625 |
("retroactive_rewrite_turns", "Retroactive Rewrite Turns"),
|
| 1626 |
("latest_turn_only_rewrite_turns", "Latest-Turn-Only Rewrite Turns"),
|
| 1627 |
):
|
|
@@ -1665,8 +1714,8 @@ def build_report_markdown(
|
|
| 1665 |
"",
|
| 1666 |
"## Summary",
|
| 1667 |
"",
|
| 1668 |
-
"| Mode | Raw Tokens | Cache Tokens | Cache Read | Cache Write | Paid Input Tokens | Paid Output Tokens | Paid Input Cost | Cache Read Cost | Cache Write Cost | Paid Output Cost | Total Cost | No-Cache Total Cost | Cache Bust Turns | TTL Expiry Turns | Rewrite Turns | Retroactive Rewrite Turns | Latest-Turn-Only Rewrite Turns | Window Tokens (Cache Counted) | Window Tokens (Cache Reads Excluded) |",
|
| 1669 |
-
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
| 1670 |
*rows,
|
| 1671 |
"",
|
| 1672 |
"## Impact vs Baseline",
|
|
@@ -1712,6 +1761,9 @@ def build_report_html(
|
|
| 1712 |
f"<td>{summary.cache_bust_turns:,}</td>"
|
| 1713 |
f"<td>{summary.ttl_expiry_turns:,}</td>"
|
| 1714 |
f"<td>{summary.rewrite_turns:,}</td>"
|
|
|
|
|
|
|
|
|
|
| 1715 |
f"<td>{summary.retroactive_rewrite_turns:,}</td>"
|
| 1716 |
f"<td>{summary.latest_turn_only_rewrite_turns:,}</td>"
|
| 1717 |
f"<td>{format_currency(summary.total_cost_usd)}</td>"
|
|
@@ -1732,6 +1784,9 @@ def build_report_html(
|
|
| 1732 |
("prompt_window_without_cache_reads", "Window Without Cache Reads"),
|
| 1733 |
("cache_bust_turns", "Cache Bust Turns"),
|
| 1734 |
("rewrite_turns", "Rewrite Turns"),
|
|
|
|
|
|
|
|
|
|
| 1735 |
("retroactive_rewrite_turns", "Retroactive Rewrite Turns"),
|
| 1736 |
("latest_turn_only_rewrite_turns", "Latest-Turn-Only Rewrite Turns"),
|
| 1737 |
):
|
|
@@ -1868,7 +1923,7 @@ def build_report_html(
|
|
| 1868 |
<table>
|
| 1869 |
<thead>
|
| 1870 |
<tr>
|
| 1871 |
-
<th>Mode</th><th>Raw Tokens</th><th>Cache Tokens</th><th>Cache Read</th><th>Cache Write</th><th>Paid Input</th><th>Paid Output</th><th>Cache Busts</th><th>TTL Expiry</th><th>Rewrite Turns</th><th>Retroactive Rewrites</th><th>Latest-Turn-Only Rewrites</th><th>Total Cost</th><th>No-Cache Cost</th><th>Window With Cache</th><th>Window Without Cache Reads</th>
|
| 1872 |
</tr>
|
| 1873 |
</thead>
|
| 1874 |
<tbody>
|
|
|
|
| 102 |
cache_bust_turns: int = 0
|
| 103 |
ttl_expiry_turns: int = 0
|
| 104 |
rewrite_turns: int = 0
|
| 105 |
+
stable_replay_rewrite_turns: int = 0
|
| 106 |
+
busting_rewrite_turns: int = 0
|
| 107 |
+
non_cache_eligible_rewrite_turns: int = 0
|
| 108 |
retroactive_rewrite_turns: int = 0
|
| 109 |
latest_turn_only_rewrite_turns: int = 0
|
| 110 |
turns: list[TurnMetrics] = field(default_factory=list)
|
|
|
|
| 160 |
"cache_bust_turns": "lower",
|
| 161 |
"ttl_expiry_turns": "lower",
|
| 162 |
"rewrite_turns": "lower",
|
| 163 |
+
"stable_replay_rewrite_turns": "lower",
|
| 164 |
+
"busting_rewrite_turns": "lower",
|
| 165 |
+
"non_cache_eligible_rewrite_turns": "lower",
|
| 166 |
"retroactive_rewrite_turns": "lower",
|
| 167 |
"latest_turn_only_rewrite_turns": "lower",
|
| 168 |
}
|
|
|
|
| 254 |
cache_bust_turns=data.get("cache_bust_turns", 0),
|
| 255 |
ttl_expiry_turns=data.get("ttl_expiry_turns", 0),
|
| 256 |
rewrite_turns=data.get("rewrite_turns", 0),
|
| 257 |
+
stable_replay_rewrite_turns=data.get("stable_replay_rewrite_turns", 0),
|
| 258 |
+
busting_rewrite_turns=data.get("busting_rewrite_turns", 0),
|
| 259 |
+
non_cache_eligible_rewrite_turns=data.get("non_cache_eligible_rewrite_turns", 0),
|
| 260 |
retroactive_rewrite_turns=data.get("retroactive_rewrite_turns", 0),
|
| 261 |
latest_turn_only_rewrite_turns=data.get("latest_turn_only_rewrite_turns", 0),
|
| 262 |
turns=turns,
|
|
|
|
| 471 |
recent_turns_per_session: int | None = None,
|
| 472 |
cache_ttl_minutes: int = DEFAULT_CACHE_TTL_MINUTES,
|
| 473 |
) -> Path:
|
| 474 |
+
suffix_parts = ["v5", f"ttl_{cache_ttl_minutes}m"]
|
| 475 |
if recent_turns_per_session:
|
| 476 |
suffix_parts.append(f"recent_{recent_turns_per_session}")
|
| 477 |
else:
|
|
|
|
| 915 |
raw_input_tokens: int
|
| 916 |
request_messages: list[dict[str, Any]]
|
| 917 |
forwarded: list[dict[str, Any]]
|
| 918 |
+
rewrite: bool
|
| 919 |
+
retroactive_rewrite: bool
|
| 920 |
|
| 921 |
|
| 922 |
def _cache_gap_within_ttl(
|
|
|
|
| 1032 |
target.cache_bust_turns += source.cache_bust_turns
|
| 1033 |
target.ttl_expiry_turns += source.ttl_expiry_turns
|
| 1034 |
target.rewrite_turns += source.rewrite_turns
|
| 1035 |
+
target.stable_replay_rewrite_turns += source.stable_replay_rewrite_turns
|
| 1036 |
+
target.busting_rewrite_turns += source.busting_rewrite_turns
|
| 1037 |
+
target.non_cache_eligible_rewrite_turns += source.non_cache_eligible_rewrite_turns
|
| 1038 |
target.retroactive_rewrite_turns += source.retroactive_rewrite_turns
|
| 1039 |
target.latest_turn_only_rewrite_turns += source.latest_turn_only_rewrite_turns
|
| 1040 |
|
|
|
|
| 1183 |
summary.retroactive_rewrite_turns += 1
|
| 1184 |
else:
|
| 1185 |
summary.latest_turn_only_rewrite_turns += 1
|
| 1186 |
+
prior_forwarded_for_rewrite = pending.forwarded if pending is not None else previous_forwarded
|
| 1187 |
+
prior_timestamp_for_rewrite = (
|
| 1188 |
+
pending.turn.timestamp if pending is not None else previous_timestamp
|
| 1189 |
+
)
|
| 1190 |
+
if (
|
| 1191 |
+
prior_timestamp_for_rewrite is not None
|
| 1192 |
+
and _cache_gap_within_ttl(turn.timestamp, prior_timestamp_for_rewrite, ttl=ttl)
|
| 1193 |
+
and prior_forwarded_for_rewrite
|
| 1194 |
+
):
|
| 1195 |
+
prefix_preserved = (
|
| 1196 |
+
len(forwarded) >= len(prior_forwarded_for_rewrite)
|
| 1197 |
+
and forwarded[: len(prior_forwarded_for_rewrite)] == prior_forwarded_for_rewrite
|
| 1198 |
+
)
|
| 1199 |
+
if prefix_preserved:
|
| 1200 |
+
summary.stable_replay_rewrite_turns += 1
|
| 1201 |
+
else:
|
| 1202 |
+
summary.busting_rewrite_turns += 1
|
| 1203 |
+
else:
|
| 1204 |
+
summary.non_cache_eligible_rewrite_turns += 1
|
| 1205 |
if pending is not None:
|
| 1206 |
_apply_turn_metrics(
|
| 1207 |
pending.summary,
|
|
|
|
| 1236 |
raw_input_tokens=raw_input_tokens,
|
| 1237 |
request_messages=copy.deepcopy(conversation),
|
| 1238 |
forwarded=forwarded,
|
| 1239 |
+
rewrite=rewrite,
|
| 1240 |
+
retroactive_rewrite=retroactive_rewrite,
|
| 1241 |
)
|
| 1242 |
conversation.append(turn.assistant_message)
|
| 1243 |
conversation_token_total = raw_input_tokens + tokenizer.count_message(
|
|
|
|
| 1548 |
print(f"Sampling: {dataset.sampling_note}")
|
| 1549 |
print()
|
| 1550 |
print(
|
| 1551 |
+
"mode raw_tok cache_tok cache_read cache_write paid_in paid_out busts ttl_exp rewrite stable_rw bust_rw noncache_rw retro_rw total_cost no_cache"
|
| 1552 |
)
|
| 1553 |
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 1554 |
summary = summaries[mode]
|
|
|
|
| 1557 |
f"{summary.cache_read_tokens:>11,} {summary.cache_write_tokens:>12,} "
|
| 1558 |
f"{summary.regular_input_tokens:>10,} {summary.output_tokens:>12,} "
|
| 1559 |
f"{summary.cache_bust_turns:>7,} {summary.ttl_expiry_turns:>9,} "
|
| 1560 |
+
f"{summary.rewrite_turns:>9,} {summary.stable_replay_rewrite_turns:>10,} "
|
| 1561 |
+
f"{summary.busting_rewrite_turns:>8,} {summary.non_cache_eligible_rewrite_turns:>12,} "
|
| 1562 |
+
f"{summary.retroactive_rewrite_turns:>10,} "
|
| 1563 |
f"{format_currency(summary.total_cost_usd):>11} "
|
| 1564 |
f"{format_currency(summary.no_cache_total_cost_usd):>11}"
|
| 1565 |
)
|
|
|
|
| 1586 |
f"({int(impact['regular_input_tokens']['delta']):,}), "
|
| 1587 |
f"rewrite={impact['rewrite_turns']['impact']} "
|
| 1588 |
f"({int(impact['rewrite_turns']['delta']):,}), "
|
| 1589 |
+
f"stable_rw={impact['stable_replay_rewrite_turns']['impact']} "
|
| 1590 |
+
f"({int(impact['stable_replay_rewrite_turns']['delta']):,}), "
|
| 1591 |
+
f"bust_rw={impact['busting_rewrite_turns']['impact']} "
|
| 1592 |
+
f"({int(impact['busting_rewrite_turns']['delta']):,}), "
|
| 1593 |
+
f"noncache_rw={impact['non_cache_eligible_rewrite_turns']['impact']} "
|
| 1594 |
+
f"({int(impact['non_cache_eligible_rewrite_turns']['delta']):,}), "
|
| 1595 |
f"retro_rw={impact['retroactive_rewrite_turns']['impact']} "
|
| 1596 |
f"({int(impact['retroactive_rewrite_turns']['delta']):,}), "
|
| 1597 |
f"window={impact['prompt_window_with_cache']['impact']} "
|
|
|
|
| 1645 |
f"{summary.cache_bust_turns:,}",
|
| 1646 |
f"{summary.ttl_expiry_turns:,}",
|
| 1647 |
f"{summary.rewrite_turns:,}",
|
| 1648 |
+
f"{summary.stable_replay_rewrite_turns:,}",
|
| 1649 |
+
f"{summary.busting_rewrite_turns:,}",
|
| 1650 |
+
f"{summary.non_cache_eligible_rewrite_turns:,}",
|
| 1651 |
f"{summary.retroactive_rewrite_turns:,}",
|
| 1652 |
f"{summary.latest_turn_only_rewrite_turns:,}",
|
| 1653 |
f"{summary.prompt_window_with_cache:,}",
|
|
|
|
| 1668 |
("prompt_window_without_cache_reads", "Window Without Cache Reads"),
|
| 1669 |
("cache_bust_turns", "Cache Bust Turns"),
|
| 1670 |
("rewrite_turns", "Rewrite Turns"),
|
| 1671 |
+
("stable_replay_rewrite_turns", "Stable Replay Rewrite Turns"),
|
| 1672 |
+
("busting_rewrite_turns", "Busting Rewrite Turns"),
|
| 1673 |
+
("non_cache_eligible_rewrite_turns", "Non-Cache-Eligible Rewrite Turns"),
|
| 1674 |
("retroactive_rewrite_turns", "Retroactive Rewrite Turns"),
|
| 1675 |
("latest_turn_only_rewrite_turns", "Latest-Turn-Only Rewrite Turns"),
|
| 1676 |
):
|
|
|
|
| 1714 |
"",
|
| 1715 |
"## Summary",
|
| 1716 |
"",
|
| 1717 |
+
"| Mode | Raw Tokens | Cache Tokens | Cache Read | Cache Write | Paid Input Tokens | Paid Output Tokens | Paid Input Cost | Cache Read Cost | Cache Write Cost | Paid Output Cost | Total Cost | No-Cache Total Cost | Cache Bust Turns | TTL Expiry Turns | Rewrite Turns | Stable Replay Rewrite Turns | Busting Rewrite Turns | Non-Cache-Eligible Rewrite Turns | Retroactive Rewrite Turns | Latest-Turn-Only Rewrite Turns | Window Tokens (Cache Counted) | Window Tokens (Cache Reads Excluded) |",
|
| 1718 |
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
| 1719 |
*rows,
|
| 1720 |
"",
|
| 1721 |
"## Impact vs Baseline",
|
|
|
|
| 1761 |
f"<td>{summary.cache_bust_turns:,}</td>"
|
| 1762 |
f"<td>{summary.ttl_expiry_turns:,}</td>"
|
| 1763 |
f"<td>{summary.rewrite_turns:,}</td>"
|
| 1764 |
+
f"<td>{summary.stable_replay_rewrite_turns:,}</td>"
|
| 1765 |
+
f"<td>{summary.busting_rewrite_turns:,}</td>"
|
| 1766 |
+
f"<td>{summary.non_cache_eligible_rewrite_turns:,}</td>"
|
| 1767 |
f"<td>{summary.retroactive_rewrite_turns:,}</td>"
|
| 1768 |
f"<td>{summary.latest_turn_only_rewrite_turns:,}</td>"
|
| 1769 |
f"<td>{format_currency(summary.total_cost_usd)}</td>"
|
|
|
|
| 1784 |
("prompt_window_without_cache_reads", "Window Without Cache Reads"),
|
| 1785 |
("cache_bust_turns", "Cache Bust Turns"),
|
| 1786 |
("rewrite_turns", "Rewrite Turns"),
|
| 1787 |
+
("stable_replay_rewrite_turns", "Stable Replay Rewrite Turns"),
|
| 1788 |
+
("busting_rewrite_turns", "Busting Rewrite Turns"),
|
| 1789 |
+
("non_cache_eligible_rewrite_turns", "Non-Cache-Eligible Rewrite Turns"),
|
| 1790 |
("retroactive_rewrite_turns", "Retroactive Rewrite Turns"),
|
| 1791 |
("latest_turn_only_rewrite_turns", "Latest-Turn-Only Rewrite Turns"),
|
| 1792 |
):
|
|
|
|
| 1923 |
<table>
|
| 1924 |
<thead>
|
| 1925 |
<tr>
|
| 1926 |
+
<th>Mode</th><th>Raw Tokens</th><th>Cache Tokens</th><th>Cache Read</th><th>Cache Write</th><th>Paid Input</th><th>Paid Output</th><th>Cache Busts</th><th>TTL Expiry</th><th>Rewrite Turns</th><th>Stable Replay Rewrites</th><th>Busting Rewrites</th><th>Non-Cache-Eligible Rewrites</th><th>Retroactive Rewrites</th><th>Latest-Turn-Only Rewrites</th><th>Total Cost</th><th>No-Cache Cost</th><th>Window With Cache</th><th>Window Without Cache Reads</th>
|
| 1927 |
</tr>
|
| 1928 |
</thead>
|
| 1929 |
<tbody>
|
benchmarks/synthetic_long_cache_suite_report.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run a long deterministic synthetic suite for cache and rewrite behavior."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import copy
|
| 7 |
+
import html
|
| 8 |
+
import json
|
| 9 |
+
import sys
|
| 10 |
+
from datetime import datetime, timedelta, timezone
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from types import SimpleNamespace
|
| 13 |
+
|
| 14 |
+
if __package__ in {None, ""}:
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 16 |
+
|
| 17 |
+
import benchmarks.claude_session_mode_benchmark as bench
|
| 18 |
+
from benchmarks.claude_session_mode_benchmark import (
|
| 19 |
+
PROXY_MODE_CACHE,
|
| 20 |
+
PROXY_MODE_TOKEN,
|
| 21 |
+
ReplayTurn,
|
| 22 |
+
SessionReplay,
|
| 23 |
+
determine_winners,
|
| 24 |
+
format_currency,
|
| 25 |
+
simulate_replays,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
OUTPUT_DIR = Path("benchmark_results") / "synthetic_long_cache_suite"
|
| 29 |
+
MODEL = "claude-sonnet-4-6"
|
| 30 |
+
TTL_MINUTES = 5
|
| 31 |
+
TURNS_PER_SCENARIO = 400
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class _FakeProvider:
|
| 35 |
+
@staticmethod
|
| 36 |
+
def get_context_limit(model: str) -> int:
|
| 37 |
+
return 200_000
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class _HistoryPressurePipeline:
|
| 41 |
+
@staticmethod
|
| 42 |
+
def apply(messages, **kwargs): # noqa: ANN001
|
| 43 |
+
rewritten = []
|
| 44 |
+
total = len(messages)
|
| 45 |
+
# Leave the latest two messages untouched; rewrite older tool results.
|
| 46 |
+
# Token mode reprocesses full history, so prior-turn tool results become
|
| 47 |
+
# compressed on later turns and can bust prefix cache. Cache mode only
|
| 48 |
+
# processes the newly-appended delta, so it does not revisit older turns.
|
| 49 |
+
protected_start = max(total - 2, 0)
|
| 50 |
+
for index, message in enumerate(messages):
|
| 51 |
+
content = message.get("content")
|
| 52 |
+
if (
|
| 53 |
+
index < protected_start
|
| 54 |
+
and isinstance(content, list)
|
| 55 |
+
and any(
|
| 56 |
+
isinstance(block, dict) and block.get("type") == "tool_result"
|
| 57 |
+
for block in content
|
| 58 |
+
)
|
| 59 |
+
):
|
| 60 |
+
new_blocks = []
|
| 61 |
+
for block in content:
|
| 62 |
+
if isinstance(block, dict) and block.get("type") == "tool_result":
|
| 63 |
+
new_blocks.append({**block, "content": "[compressed-older-tool-result]"})
|
| 64 |
+
else:
|
| 65 |
+
new_blocks.append(copy.deepcopy(block))
|
| 66 |
+
rewritten.append({**message, "content": new_blocks})
|
| 67 |
+
else:
|
| 68 |
+
rewritten.append(copy.deepcopy(message))
|
| 69 |
+
return SimpleNamespace(messages=rewritten)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class _FakeProxy:
|
| 73 |
+
def __init__(self) -> None:
|
| 74 |
+
self.config = SimpleNamespace(image_optimize=False)
|
| 75 |
+
self.anthropic_provider = _FakeProvider()
|
| 76 |
+
self.anthropic_pipeline = _HistoryPressurePipeline()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _tool_result_payload(turn_number: int, scenario: str) -> str:
|
| 80 |
+
return (f"{scenario}-tool-output-{turn_number} " * 80).strip()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _build_stable_append_only() -> SessionReplay:
|
| 84 |
+
base = datetime(2026, 3, 13, 1, 0, tzinfo=timezone.utc)
|
| 85 |
+
turns: list[ReplayTurn] = []
|
| 86 |
+
for index in range(TURNS_PER_SCENARIO):
|
| 87 |
+
turns.append(
|
| 88 |
+
ReplayTurn(
|
| 89 |
+
session_id="stable-append-only",
|
| 90 |
+
project_key="C--git-synthetic",
|
| 91 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 92 |
+
request_id=f"stable-{index + 1:04d}",
|
| 93 |
+
model=MODEL,
|
| 94 |
+
timestamp=base + timedelta(minutes=index * 2),
|
| 95 |
+
input_messages=[
|
| 96 |
+
{
|
| 97 |
+
"role": "user",
|
| 98 |
+
"content": f"Stable append-only turn {index + 1}. Summarize and continue.",
|
| 99 |
+
}
|
| 100 |
+
],
|
| 101 |
+
assistant_message={"role": "assistant", "content": f"ok stable {index + 1}"},
|
| 102 |
+
output_tokens=12,
|
| 103 |
+
)
|
| 104 |
+
)
|
| 105 |
+
return SessionReplay(
|
| 106 |
+
session_id="stable-append-only",
|
| 107 |
+
project_key="C--git-synthetic",
|
| 108 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 109 |
+
turns=turns,
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _build_token_rewrite_pressure() -> SessionReplay:
|
| 114 |
+
base = datetime(2026, 3, 14, 1, 0, tzinfo=timezone.utc)
|
| 115 |
+
turns: list[ReplayTurn] = []
|
| 116 |
+
for index in range(TURNS_PER_SCENARIO):
|
| 117 |
+
turn_no = index + 1
|
| 118 |
+
turns.append(
|
| 119 |
+
ReplayTurn(
|
| 120 |
+
session_id="token-rewrite-pressure",
|
| 121 |
+
project_key="C--git-synthetic",
|
| 122 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 123 |
+
request_id=f"rewrite-{turn_no:04d}",
|
| 124 |
+
model=MODEL,
|
| 125 |
+
timestamp=base + timedelta(minutes=index * 2),
|
| 126 |
+
input_messages=[
|
| 127 |
+
{"role": "user", "content": f"Inspect tool output for turn {turn_no}."},
|
| 128 |
+
{
|
| 129 |
+
"role": "user",
|
| 130 |
+
"content": [
|
| 131 |
+
{
|
| 132 |
+
"type": "tool_result",
|
| 133 |
+
"tool_use_id": f"tool-{turn_no}",
|
| 134 |
+
"content": _tool_result_payload(turn_no, "rewrite"),
|
| 135 |
+
}
|
| 136 |
+
],
|
| 137 |
+
},
|
| 138 |
+
],
|
| 139 |
+
assistant_message={"role": "assistant", "content": f"ok rewrite {turn_no}"},
|
| 140 |
+
output_tokens=14,
|
| 141 |
+
)
|
| 142 |
+
)
|
| 143 |
+
return SessionReplay(
|
| 144 |
+
session_id="token-rewrite-pressure",
|
| 145 |
+
project_key="C--git-synthetic",
|
| 146 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 147 |
+
turns=turns,
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _build_ttl_resets() -> SessionReplay:
|
| 152 |
+
base = datetime(2026, 3, 15, 1, 0, tzinfo=timezone.utc)
|
| 153 |
+
turns: list[ReplayTurn] = []
|
| 154 |
+
for index in range(TURNS_PER_SCENARIO):
|
| 155 |
+
turns.append(
|
| 156 |
+
ReplayTurn(
|
| 157 |
+
session_id="ttl-resets",
|
| 158 |
+
project_key="C--git-synthetic",
|
| 159 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 160 |
+
request_id=f"ttl-{index + 1:04d}",
|
| 161 |
+
model=MODEL,
|
| 162 |
+
timestamp=base + timedelta(minutes=index * 7),
|
| 163 |
+
input_messages=[
|
| 164 |
+
{
|
| 165 |
+
"role": "user",
|
| 166 |
+
"content": f"TTL reset turn {index + 1}. Continue the thread.",
|
| 167 |
+
}
|
| 168 |
+
],
|
| 169 |
+
assistant_message={"role": "assistant", "content": f"ok ttl {index + 1}"},
|
| 170 |
+
output_tokens=12,
|
| 171 |
+
)
|
| 172 |
+
)
|
| 173 |
+
return SessionReplay(
|
| 174 |
+
session_id="ttl-resets",
|
| 175 |
+
project_key="C--git-synthetic",
|
| 176 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 177 |
+
turns=turns,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _build_suite() -> list[SessionReplay]:
|
| 182 |
+
return [
|
| 183 |
+
_build_stable_append_only(),
|
| 184 |
+
_build_token_rewrite_pressure(),
|
| 185 |
+
_build_ttl_resets(),
|
| 186 |
+
]
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _scenario_label(session_id: str) -> str:
|
| 190 |
+
return session_id.replace("-", " ").title()
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _run_suite() -> tuple[dict[str, dict[str, bench.ModeSummary]], dict[str, bench.ModeSummary]]:
|
| 194 |
+
original_make_proxy = bench._make_proxy
|
| 195 |
+
bench._make_proxy = lambda mode: _FakeProxy()
|
| 196 |
+
try:
|
| 197 |
+
per_scenario: dict[str, dict[str, bench.ModeSummary]] = {}
|
| 198 |
+
suite = _build_suite()
|
| 199 |
+
for replay in suite:
|
| 200 |
+
_, summaries = simulate_replays([replay], cache_ttl_minutes=TTL_MINUTES)
|
| 201 |
+
per_scenario[replay.session_id] = summaries
|
| 202 |
+
_, aggregate = simulate_replays(suite, cache_ttl_minutes=TTL_MINUTES)
|
| 203 |
+
finally:
|
| 204 |
+
bench._make_proxy = original_make_proxy
|
| 205 |
+
return per_scenario, aggregate
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def _summary_payload(summary: bench.ModeSummary) -> dict[str, int | float | str]:
|
| 209 |
+
return {
|
| 210 |
+
"total_cost_usd": summary.total_cost_usd,
|
| 211 |
+
"no_cache_total_cost_usd": summary.no_cache_total_cost_usd,
|
| 212 |
+
"forwarded_input_tokens": summary.forwarded_input_tokens,
|
| 213 |
+
"cache_bust_turns": summary.cache_bust_turns,
|
| 214 |
+
"ttl_expiry_turns": summary.ttl_expiry_turns,
|
| 215 |
+
"rewrite_turns": summary.rewrite_turns,
|
| 216 |
+
"stable_replay_rewrite_turns": summary.stable_replay_rewrite_turns,
|
| 217 |
+
"busting_rewrite_turns": summary.busting_rewrite_turns,
|
| 218 |
+
"non_cache_eligible_rewrite_turns": summary.non_cache_eligible_rewrite_turns,
|
| 219 |
+
"retroactive_rewrite_turns": summary.retroactive_rewrite_turns,
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def _write_report(
|
| 224 |
+
per_scenario: dict[str, dict[str, bench.ModeSummary]],
|
| 225 |
+
aggregate: dict[str, bench.ModeSummary],
|
| 226 |
+
) -> tuple[Path, Path, Path]:
|
| 227 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 228 |
+
payload = {
|
| 229 |
+
"turns_per_scenario": TURNS_PER_SCENARIO,
|
| 230 |
+
"total_turns": TURNS_PER_SCENARIO * len(per_scenario),
|
| 231 |
+
"ttl_minutes": TTL_MINUTES,
|
| 232 |
+
"scenarios": {
|
| 233 |
+
session_id: {
|
| 234 |
+
mode: _summary_payload(summary)
|
| 235 |
+
for mode, summary in summaries.items()
|
| 236 |
+
}
|
| 237 |
+
for session_id, summaries in per_scenario.items()
|
| 238 |
+
},
|
| 239 |
+
"aggregate": {mode: _summary_payload(summary) for mode, summary in aggregate.items()},
|
| 240 |
+
"aggregate_winners": determine_winners(aggregate),
|
| 241 |
+
}
|
| 242 |
+
json_path = OUTPUT_DIR / "synthetic_long_cache_suite.json"
|
| 243 |
+
md_path = OUTPUT_DIR / "synthetic_long_cache_suite.md"
|
| 244 |
+
html_path = OUTPUT_DIR / "synthetic_long_cache_suite.html"
|
| 245 |
+
json_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
| 246 |
+
|
| 247 |
+
md_lines = [
|
| 248 |
+
"# Synthetic Long Cache Suite",
|
| 249 |
+
"",
|
| 250 |
+
f"- Turns per scenario: `{TURNS_PER_SCENARIO}`",
|
| 251 |
+
f"- Total turns: `{TURNS_PER_SCENARIO * len(per_scenario)}`",
|
| 252 |
+
f"- Cache TTL: `{TTL_MINUTES}` minutes",
|
| 253 |
+
"",
|
| 254 |
+
"## Scenarios",
|
| 255 |
+
"",
|
| 256 |
+
"1. `stable-append-only`: append-only conversation, no rewrite pressure",
|
| 257 |
+
"2. `token-rewrite-pressure`: each turn adds a tool result; older tool results become compressible later",
|
| 258 |
+
"3. `ttl-resets`: append-only conversation with >TTL gaps to force normal cache expiry",
|
| 259 |
+
"",
|
| 260 |
+
]
|
| 261 |
+
|
| 262 |
+
for session_id, summaries in per_scenario.items():
|
| 263 |
+
winners = determine_winners(summaries)
|
| 264 |
+
md_lines.extend(
|
| 265 |
+
[
|
| 266 |
+
f"## {_scenario_label(session_id)}",
|
| 267 |
+
"",
|
| 268 |
+
"| Mode | Cost | Forwarded Tokens | Cache Busts | TTL Expiry | Rewrites | Stable Replay Rewrites | Busting Rewrites | Retroactive Rewrites |",
|
| 269 |
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
| 270 |
+
]
|
| 271 |
+
)
|
| 272 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 273 |
+
summary = summaries[mode]
|
| 274 |
+
md_lines.append(
|
| 275 |
+
f"| `{mode}` | {format_currency(summary.total_cost_usd)} | "
|
| 276 |
+
f"{summary.forwarded_input_tokens:,} | {summary.cache_bust_turns} | "
|
| 277 |
+
f"{summary.ttl_expiry_turns} | {summary.rewrite_turns} | "
|
| 278 |
+
f"{summary.stable_replay_rewrite_turns} | {summary.busting_rewrite_turns} | "
|
| 279 |
+
f"{summary.retroactive_rewrite_turns} |"
|
| 280 |
+
)
|
| 281 |
+
md_lines.extend(
|
| 282 |
+
[
|
| 283 |
+
"",
|
| 284 |
+
f"- total cost winner: `{winners['total_cost']}`",
|
| 285 |
+
f"- no-cache total cost winner: `{winners['no_cache_total_cost']}`",
|
| 286 |
+
f"- window winner with cache counted: `{winners['window_with_cache']}`",
|
| 287 |
+
"",
|
| 288 |
+
]
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
aggregate_winners = determine_winners(aggregate)
|
| 292 |
+
md_lines.extend(
|
| 293 |
+
[
|
| 294 |
+
"## Aggregate",
|
| 295 |
+
"",
|
| 296 |
+
"| Mode | Cost | Forwarded Tokens | Cache Busts | TTL Expiry | Rewrites | Stable Replay Rewrites | Busting Rewrites | Retroactive Rewrites |",
|
| 297 |
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
| 298 |
+
]
|
| 299 |
+
)
|
| 300 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 301 |
+
summary = aggregate[mode]
|
| 302 |
+
md_lines.append(
|
| 303 |
+
f"| `{mode}` | {format_currency(summary.total_cost_usd)} | "
|
| 304 |
+
f"{summary.forwarded_input_tokens:,} | {summary.cache_bust_turns} | "
|
| 305 |
+
f"{summary.ttl_expiry_turns} | {summary.rewrite_turns} | "
|
| 306 |
+
f"{summary.stable_replay_rewrite_turns} | {summary.busting_rewrite_turns} | "
|
| 307 |
+
f"{summary.retroactive_rewrite_turns} |"
|
| 308 |
+
)
|
| 309 |
+
md_lines.extend(
|
| 310 |
+
[
|
| 311 |
+
"",
|
| 312 |
+
f"- total cost winner: `{aggregate_winners['total_cost']}`",
|
| 313 |
+
f"- no-cache total cost winner: `{aggregate_winners['no_cache_total_cost']}`",
|
| 314 |
+
f"- window winner if cache tokens count: `{aggregate_winners['window_with_cache']}`",
|
| 315 |
+
f"- window winner if cache read tokens do not count: `{aggregate_winners['window_without_cache_reads']}`",
|
| 316 |
+
]
|
| 317 |
+
)
|
| 318 |
+
md_path.write_text("\n".join(md_lines), encoding="utf-8")
|
| 319 |
+
|
| 320 |
+
scenario_sections: list[str] = []
|
| 321 |
+
for session_id, summaries in per_scenario.items():
|
| 322 |
+
rows = []
|
| 323 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 324 |
+
summary = summaries[mode]
|
| 325 |
+
rows.append(
|
| 326 |
+
"<tr>"
|
| 327 |
+
f"<td><code>{html.escape(mode)}</code></td>"
|
| 328 |
+
f"<td>{html.escape(format_currency(summary.total_cost_usd))}</td>"
|
| 329 |
+
f"<td>{summary.forwarded_input_tokens:,}</td>"
|
| 330 |
+
f"<td>{summary.cache_bust_turns}</td>"
|
| 331 |
+
f"<td>{summary.ttl_expiry_turns}</td>"
|
| 332 |
+
f"<td>{summary.rewrite_turns}</td>"
|
| 333 |
+
f"<td>{summary.stable_replay_rewrite_turns}</td>"
|
| 334 |
+
f"<td>{summary.busting_rewrite_turns}</td>"
|
| 335 |
+
f"<td>{summary.retroactive_rewrite_turns}</td>"
|
| 336 |
+
"</tr>"
|
| 337 |
+
)
|
| 338 |
+
scenario_sections.append(
|
| 339 |
+
"<section class='card'>"
|
| 340 |
+
f"<h2>{html.escape(_scenario_label(session_id))}</h2>"
|
| 341 |
+
"<table><thead><tr><th>Mode</th><th>Cost</th><th>Forwarded Tokens</th><th>Cache Busts</th>"
|
| 342 |
+
"<th>TTL Expiry</th><th>Rewrites</th><th>Stable Replay Rewrites</th>"
|
| 343 |
+
"<th>Busting Rewrites</th><th>Retroactive Rewrites</th></tr></thead><tbody>"
|
| 344 |
+
+ "".join(rows)
|
| 345 |
+
+ "</tbody></table></section>"
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
aggregate_rows = []
|
| 349 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 350 |
+
summary = aggregate[mode]
|
| 351 |
+
aggregate_rows.append(
|
| 352 |
+
"<tr>"
|
| 353 |
+
f"<td><code>{html.escape(mode)}</code></td>"
|
| 354 |
+
f"<td>{html.escape(format_currency(summary.total_cost_usd))}</td>"
|
| 355 |
+
f"<td>{summary.forwarded_input_tokens:,}</td>"
|
| 356 |
+
f"<td>{summary.cache_bust_turns}</td>"
|
| 357 |
+
f"<td>{summary.ttl_expiry_turns}</td>"
|
| 358 |
+
f"<td>{summary.rewrite_turns}</td>"
|
| 359 |
+
f"<td>{summary.stable_replay_rewrite_turns}</td>"
|
| 360 |
+
f"<td>{summary.busting_rewrite_turns}</td>"
|
| 361 |
+
f"<td>{summary.retroactive_rewrite_turns}</td>"
|
| 362 |
+
"</tr>"
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
html_doc = (
|
| 366 |
+
"<!doctype html><html><head><meta charset='utf-8'>"
|
| 367 |
+
"<meta name='viewport' content='width=device-width, initial-scale=1'>"
|
| 368 |
+
"<title>Synthetic Long Cache Suite</title>"
|
| 369 |
+
"<style>"
|
| 370 |
+
"body{font-family:ui-sans-serif,system-ui,sans-serif;max-width:1200px;margin:40px auto;padding:0 20px;line-height:1.55;color:#111827;background:#f8fafc}"
|
| 371 |
+
"h1,h2{letter-spacing:-0.02em}"
|
| 372 |
+
"code{background:#e5e7eb;padding:1px 4px;border-radius:4px}"
|
| 373 |
+
"table{border-collapse:collapse;width:100%;margin:16px 0;background:white}"
|
| 374 |
+
"th,td{border:1px solid #cbd5e1;padding:10px;text-align:left}"
|
| 375 |
+
"th{background:#e2e8f0}"
|
| 376 |
+
".card{background:white;border:1px solid #cbd5e1;border-radius:16px;padding:24px;margin:18px 0;box-shadow:0 8px 24px rgba(15,23,42,.06)}"
|
| 377 |
+
"</style></head><body>"
|
| 378 |
+
"<h1>Synthetic Long Cache Suite</h1>"
|
| 379 |
+
f"<div class='card'><p>Total turns: <code>{TURNS_PER_SCENARIO * len(per_scenario)}</code><br>"
|
| 380 |
+
f"Turns per scenario: <code>{TURNS_PER_SCENARIO}</code><br>"
|
| 381 |
+
f"Cache TTL: <code>{TTL_MINUTES}</code> minutes</p></div>"
|
| 382 |
+
+ "".join(scenario_sections)
|
| 383 |
+
+ "<section class='card'><h2>Aggregate</h2>"
|
| 384 |
+
"<table><thead><tr><th>Mode</th><th>Cost</th><th>Forwarded Tokens</th><th>Cache Busts</th>"
|
| 385 |
+
"<th>TTL Expiry</th><th>Rewrites</th><th>Stable Replay Rewrites</th>"
|
| 386 |
+
"<th>Busting Rewrites</th><th>Retroactive Rewrites</th></tr></thead><tbody>"
|
| 387 |
+
+ "".join(aggregate_rows)
|
| 388 |
+
+ "</tbody></table></section></body></html>"
|
| 389 |
+
)
|
| 390 |
+
html_path.write_text(html_doc, encoding="utf-8")
|
| 391 |
+
return md_path, json_path, html_path
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
def main() -> int:
|
| 395 |
+
per_scenario, aggregate = _run_suite()
|
| 396 |
+
md_path, json_path, html_path = _write_report(per_scenario, aggregate)
|
| 397 |
+
print("Synthetic long cache suite")
|
| 398 |
+
print(f"turns_per_scenario={TURNS_PER_SCENARIO}")
|
| 399 |
+
print(f"total_turns={TURNS_PER_SCENARIO * len(per_scenario)}")
|
| 400 |
+
for session_id, summaries in per_scenario.items():
|
| 401 |
+
print(f"scenario={session_id}")
|
| 402 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 403 |
+
summary = summaries[mode]
|
| 404 |
+
print(
|
| 405 |
+
f" {mode}: cost={format_currency(summary.total_cost_usd)} "
|
| 406 |
+
f"busts={summary.cache_bust_turns} ttl={summary.ttl_expiry_turns} "
|
| 407 |
+
f"rewrites={summary.rewrite_turns} stable_rw={summary.stable_replay_rewrite_turns} "
|
| 408 |
+
f"bust_rw={summary.busting_rewrite_turns} "
|
| 409 |
+
f"forwarded={summary.forwarded_input_tokens}"
|
| 410 |
+
)
|
| 411 |
+
print("aggregate")
|
| 412 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 413 |
+
summary = aggregate[mode]
|
| 414 |
+
print(
|
| 415 |
+
f" {mode}: cost={format_currency(summary.total_cost_usd)} "
|
| 416 |
+
f"busts={summary.cache_bust_turns} ttl={summary.ttl_expiry_turns} "
|
| 417 |
+
f"rewrites={summary.rewrite_turns} stable_rw={summary.stable_replay_rewrite_turns} "
|
| 418 |
+
f"bust_rw={summary.busting_rewrite_turns} "
|
| 419 |
+
f"forwarded={summary.forwarded_input_tokens}"
|
| 420 |
+
)
|
| 421 |
+
print(f"Markdown report: {md_path}")
|
| 422 |
+
print(f"JSON report: {json_path}")
|
| 423 |
+
print(f"HTML report: {html_path}")
|
| 424 |
+
return 0
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
if __name__ == "__main__":
|
| 428 |
+
raise SystemExit(main())
|
benchmarks/synthetic_token_cache_bust_report.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run a deterministic synthetic replay that forces token-mode cache busts."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import copy
|
| 7 |
+
import html
|
| 8 |
+
import json
|
| 9 |
+
import sys
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from types import SimpleNamespace
|
| 13 |
+
|
| 14 |
+
if __package__ in {None, ""}:
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 16 |
+
|
| 17 |
+
import benchmarks.claude_session_mode_benchmark as bench
|
| 18 |
+
from benchmarks.claude_session_mode_benchmark import (
|
| 19 |
+
PROXY_MODE_CACHE,
|
| 20 |
+
PROXY_MODE_TOKEN,
|
| 21 |
+
ReplayTurn,
|
| 22 |
+
SessionReplay,
|
| 23 |
+
_apply_mode_to_messages,
|
| 24 |
+
_cache_gap_within_ttl,
|
| 25 |
+
determine_winners,
|
| 26 |
+
format_currency,
|
| 27 |
+
get_tokenizer,
|
| 28 |
+
simulate_replays,
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
OUTPUT_DIR = Path("benchmark_results") / "synthetic_token_cache_bust"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class _FakeProvider:
|
| 35 |
+
@staticmethod
|
| 36 |
+
def get_context_limit(model: str) -> int:
|
| 37 |
+
return 200_000
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class _FakePipeline:
|
| 41 |
+
@staticmethod
|
| 42 |
+
def apply(messages, **kwargs): # noqa: ANN001
|
| 43 |
+
rewritten = []
|
| 44 |
+
should_rewrite_history = len(messages) > 2
|
| 45 |
+
for message in messages:
|
| 46 |
+
content = message.get("content")
|
| 47 |
+
if (
|
| 48 |
+
should_rewrite_history
|
| 49 |
+
and isinstance(content, list)
|
| 50 |
+
and any(
|
| 51 |
+
isinstance(block, dict) and block.get("type") == "tool_result"
|
| 52 |
+
for block in content
|
| 53 |
+
)
|
| 54 |
+
):
|
| 55 |
+
new_blocks = []
|
| 56 |
+
for block in content:
|
| 57 |
+
if isinstance(block, dict) and block.get("type") == "tool_result":
|
| 58 |
+
new_blocks.append({**block, "content": "[compressed-tool-result]"})
|
| 59 |
+
else:
|
| 60 |
+
new_blocks.append(block)
|
| 61 |
+
rewritten.append({**message, "content": new_blocks})
|
| 62 |
+
else:
|
| 63 |
+
rewritten.append(copy.deepcopy(message))
|
| 64 |
+
return SimpleNamespace(messages=rewritten)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class _FakeProxy:
|
| 68 |
+
def __init__(self) -> None:
|
| 69 |
+
self.config = SimpleNamespace(image_optimize=False)
|
| 70 |
+
self.anthropic_provider = _FakeProvider()
|
| 71 |
+
self.anthropic_pipeline = _FakePipeline()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _build_replay() -> SessionReplay:
|
| 75 |
+
return SessionReplay(
|
| 76 |
+
session_id="token-cache-bust",
|
| 77 |
+
project_key="C--git-synthetic",
|
| 78 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 79 |
+
turns=[
|
| 80 |
+
ReplayTurn(
|
| 81 |
+
session_id="token-cache-bust",
|
| 82 |
+
project_key="C--git-synthetic",
|
| 83 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 84 |
+
request_id="r1",
|
| 85 |
+
model="claude-sonnet-4-6",
|
| 86 |
+
timestamp=datetime.fromisoformat("2026-03-13T01:00:00+00:00"),
|
| 87 |
+
input_messages=[
|
| 88 |
+
{"role": "user", "content": "Summarize this tool output"},
|
| 89 |
+
{
|
| 90 |
+
"role": "user",
|
| 91 |
+
"content": [
|
| 92 |
+
{
|
| 93 |
+
"type": "tool_result",
|
| 94 |
+
"tool_use_id": "tool-1",
|
| 95 |
+
"content": "X" * 800,
|
| 96 |
+
}
|
| 97 |
+
],
|
| 98 |
+
},
|
| 99 |
+
],
|
| 100 |
+
assistant_message={"role": "assistant", "content": "ok"},
|
| 101 |
+
output_tokens=10,
|
| 102 |
+
),
|
| 103 |
+
ReplayTurn(
|
| 104 |
+
session_id="token-cache-bust",
|
| 105 |
+
project_key="C--git-synthetic",
|
| 106 |
+
decoded_project_path=r"C:\git\synthetic",
|
| 107 |
+
request_id="r2",
|
| 108 |
+
model="claude-sonnet-4-6",
|
| 109 |
+
timestamp=datetime.fromisoformat("2026-03-13T01:02:00+00:00"),
|
| 110 |
+
input_messages=[{"role": "user", "content": "What changed?"}],
|
| 111 |
+
assistant_message={"role": "assistant", "content": "done"},
|
| 112 |
+
output_tokens=12,
|
| 113 |
+
),
|
| 114 |
+
],
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _build_bust_events(replay: SessionReplay) -> dict[str, list[dict[str, object]]]:
|
| 119 |
+
events: dict[str, list[dict[str, object]]] = {
|
| 120 |
+
"baseline": [],
|
| 121 |
+
PROXY_MODE_TOKEN: [],
|
| 122 |
+
PROXY_MODE_CACHE: [],
|
| 123 |
+
}
|
| 124 |
+
ttl_minutes = 5
|
| 125 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 126 |
+
proxy = None if mode == "baseline" else _FakeProxy()
|
| 127 |
+
prefix_tracker = None if mode == "baseline" else bench.PrefixCacheTracker("anthropic")
|
| 128 |
+
comp_cache = bench.CompressionCache() if mode == PROXY_MODE_TOKEN else None
|
| 129 |
+
conversation: list[dict[str, object]] = []
|
| 130 |
+
previous_original: list[dict[str, object]] | None = None
|
| 131 |
+
previous_forwarded_context: list[dict[str, object]] | None = None
|
| 132 |
+
previous_forwarded_request: list[dict[str, object]] | None = None
|
| 133 |
+
previous_request_id: str | None = None
|
| 134 |
+
previous_timestamp: datetime | None = None
|
| 135 |
+
|
| 136 |
+
for turn in replay.turns:
|
| 137 |
+
conversation.extend(copy.deepcopy(turn.input_messages))
|
| 138 |
+
forwarded = _apply_mode_to_messages(
|
| 139 |
+
proxy,
|
| 140 |
+
mode,
|
| 141 |
+
conversation,
|
| 142 |
+
model=turn.model,
|
| 143 |
+
prefix_tracker=prefix_tracker,
|
| 144 |
+
comp_cache=comp_cache,
|
| 145 |
+
previous_original_messages=previous_original,
|
| 146 |
+
previous_forwarded_messages=previous_forwarded_context,
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
if (
|
| 150 |
+
previous_forwarded_request is not None
|
| 151 |
+
and _cache_gap_within_ttl(
|
| 152 |
+
turn.timestamp,
|
| 153 |
+
previous_timestamp,
|
| 154 |
+
ttl=bench.timedelta(minutes=ttl_minutes),
|
| 155 |
+
)
|
| 156 |
+
):
|
| 157 |
+
prefix_preserved = (
|
| 158 |
+
len(forwarded) >= len(previous_forwarded_request)
|
| 159 |
+
and forwarded[: len(previous_forwarded_request)] == previous_forwarded_request
|
| 160 |
+
)
|
| 161 |
+
if not prefix_preserved:
|
| 162 |
+
divergent_index = next(
|
| 163 |
+
(
|
| 164 |
+
idx
|
| 165 |
+
for idx, (prev_msg, curr_msg) in enumerate(
|
| 166 |
+
zip(previous_forwarded_request, forwarded, strict=False)
|
| 167 |
+
)
|
| 168 |
+
if prev_msg != curr_msg
|
| 169 |
+
),
|
| 170 |
+
min(len(previous_forwarded_request), len(forwarded)),
|
| 171 |
+
)
|
| 172 |
+
events[mode].append(
|
| 173 |
+
{
|
| 174 |
+
"request_id": turn.request_id,
|
| 175 |
+
"previous_request_id": previous_request_id,
|
| 176 |
+
"divergent_index": divergent_index,
|
| 177 |
+
"previous_forwarded": previous_forwarded_request,
|
| 178 |
+
"current_forwarded": forwarded,
|
| 179 |
+
}
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
tokenizer = get_tokenizer(turn.model)
|
| 183 |
+
if prefix_tracker is not None:
|
| 184 |
+
bench._update_prefix_tracker(
|
| 185 |
+
prefix_tracker,
|
| 186 |
+
cache_read_tokens=0,
|
| 187 |
+
cache_write_tokens=0,
|
| 188 |
+
messages=forwarded,
|
| 189 |
+
message_token_counts=[tokenizer.count_message(msg) for msg in forwarded],
|
| 190 |
+
original_messages=conversation,
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
conversation.append(copy.deepcopy(turn.assistant_message))
|
| 194 |
+
previous_original = copy.deepcopy(conversation)
|
| 195 |
+
previous_forwarded_context = copy.deepcopy(forwarded) + [copy.deepcopy(turn.assistant_message)]
|
| 196 |
+
previous_forwarded_request = copy.deepcopy(forwarded)
|
| 197 |
+
previous_request_id = turn.request_id
|
| 198 |
+
previous_timestamp = turn.timestamp
|
| 199 |
+
|
| 200 |
+
return events
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def _write_report(
|
| 204 |
+
replay: SessionReplay,
|
| 205 |
+
summaries: dict[str, bench.ModeSummary],
|
| 206 |
+
winners: dict[str, str],
|
| 207 |
+
events: dict[str, list[dict[str, object]]],
|
| 208 |
+
) -> tuple[Path, Path, Path]:
|
| 209 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 210 |
+
payload = {
|
| 211 |
+
"session_id": replay.session_id,
|
| 212 |
+
"requests": len(replay.turns),
|
| 213 |
+
"summaries": {
|
| 214 |
+
mode: {
|
| 215 |
+
"total_cost_usd": summary.total_cost_usd,
|
| 216 |
+
"cache_bust_turns": summary.cache_bust_turns,
|
| 217 |
+
"rewrite_turns": summary.rewrite_turns,
|
| 218 |
+
"retroactive_rewrite_turns": summary.retroactive_rewrite_turns,
|
| 219 |
+
"forwarded_input_tokens": summary.forwarded_input_tokens,
|
| 220 |
+
}
|
| 221 |
+
for mode, summary in summaries.items()
|
| 222 |
+
},
|
| 223 |
+
"winners": winners,
|
| 224 |
+
"events": events,
|
| 225 |
+
}
|
| 226 |
+
json_path = OUTPUT_DIR / "synthetic_token_cache_bust.json"
|
| 227 |
+
md_path = OUTPUT_DIR / "synthetic_token_cache_bust.md"
|
| 228 |
+
html_path = OUTPUT_DIR / "synthetic_token_cache_bust.html"
|
| 229 |
+
json_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
| 230 |
+
|
| 231 |
+
md_lines = [
|
| 232 |
+
"# Synthetic Token Cache Bust Report",
|
| 233 |
+
"",
|
| 234 |
+
f"Session: `{replay.session_id}`",
|
| 235 |
+
f"Requests: `{len(replay.turns)}`",
|
| 236 |
+
"",
|
| 237 |
+
"## Summary",
|
| 238 |
+
"",
|
| 239 |
+
"| Mode | Cost | Cache Busts | Rewrites | Retroactive Rewrites | Forwarded Tokens |",
|
| 240 |
+
"| --- | ---: | ---: | ---: | ---: | ---: |",
|
| 241 |
+
]
|
| 242 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 243 |
+
summary = summaries[mode]
|
| 244 |
+
md_lines.append(
|
| 245 |
+
f"| `{mode}` | {format_currency(summary.total_cost_usd)} | "
|
| 246 |
+
f"{summary.cache_bust_turns} | {summary.rewrite_turns} | "
|
| 247 |
+
f"{summary.retroactive_rewrite_turns} | {summary.forwarded_input_tokens} |"
|
| 248 |
+
)
|
| 249 |
+
md_lines.extend(
|
| 250 |
+
[
|
| 251 |
+
"",
|
| 252 |
+
"## Winners",
|
| 253 |
+
"",
|
| 254 |
+
f"- total cost: `{winners['total_cost']}`",
|
| 255 |
+
f"- no-cache total cost: `{winners['no_cache_total_cost']}`",
|
| 256 |
+
f"- window with cache counted: `{winners['window_with_cache']}`",
|
| 257 |
+
f"- window without cache reads: `{winners['window_without_cache_reads']}`",
|
| 258 |
+
"",
|
| 259 |
+
"## Cache Bust Events",
|
| 260 |
+
"",
|
| 261 |
+
]
|
| 262 |
+
)
|
| 263 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 264 |
+
md_lines.append(f"### `{mode}`")
|
| 265 |
+
if not events[mode]:
|
| 266 |
+
md_lines.append("")
|
| 267 |
+
md_lines.append("- none")
|
| 268 |
+
md_lines.append("")
|
| 269 |
+
continue
|
| 270 |
+
md_lines.append("")
|
| 271 |
+
for event in events[mode]:
|
| 272 |
+
md_lines.append(
|
| 273 |
+
f"- request `{event['request_id']}` diverged from `{event['previous_request_id']}` "
|
| 274 |
+
f"at message index `{event['divergent_index']}`"
|
| 275 |
+
)
|
| 276 |
+
md_lines.append("")
|
| 277 |
+
md_path.write_text("\n".join(md_lines), encoding="utf-8")
|
| 278 |
+
|
| 279 |
+
rows = []
|
| 280 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 281 |
+
summary = summaries[mode]
|
| 282 |
+
rows.append(
|
| 283 |
+
"<tr>"
|
| 284 |
+
f"<td>{html.escape(mode)}</td>"
|
| 285 |
+
f"<td>{html.escape(format_currency(summary.total_cost_usd))}</td>"
|
| 286 |
+
f"<td>{summary.cache_bust_turns}</td>"
|
| 287 |
+
f"<td>{summary.rewrite_turns}</td>"
|
| 288 |
+
f"<td>{summary.retroactive_rewrite_turns}</td>"
|
| 289 |
+
f"<td>{summary.forwarded_input_tokens}</td>"
|
| 290 |
+
"</tr>"
|
| 291 |
+
)
|
| 292 |
+
event_sections = []
|
| 293 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 294 |
+
section = [f"<h2>{html.escape(mode)}</h2>"]
|
| 295 |
+
if not events[mode]:
|
| 296 |
+
section.append("<p>none</p>")
|
| 297 |
+
else:
|
| 298 |
+
section.append("<ul>")
|
| 299 |
+
for event in events[mode]:
|
| 300 |
+
section.append(
|
| 301 |
+
"<li>"
|
| 302 |
+
f"request <code>{html.escape(str(event['request_id']))}</code> diverged from "
|
| 303 |
+
f"<code>{html.escape(str(event['previous_request_id']))}</code> at message index "
|
| 304 |
+
f"<code>{event['divergent_index']}</code>"
|
| 305 |
+
"</li>"
|
| 306 |
+
)
|
| 307 |
+
section.append("</ul>")
|
| 308 |
+
event_sections.append("".join(section))
|
| 309 |
+
|
| 310 |
+
html_doc = (
|
| 311 |
+
"<!doctype html><html><head><meta charset='utf-8'>"
|
| 312 |
+
"<title>Synthetic Token Cache Bust Report</title>"
|
| 313 |
+
"<style>"
|
| 314 |
+
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:32px;line-height:1.5;}"
|
| 315 |
+
"table{border-collapse:collapse;width:100%;margin:16px 0;}"
|
| 316 |
+
"th,td{border:1px solid #d0d7de;padding:8px 10px;text-align:left;}"
|
| 317 |
+
"th{background:#f6f8fa;}"
|
| 318 |
+
"code{background:#f6f8fa;padding:1px 4px;border-radius:4px;}"
|
| 319 |
+
"</style></head><body>"
|
| 320 |
+
"<h1>Synthetic Token Cache Bust Report</h1>"
|
| 321 |
+
f"<p>Session: <code>{html.escape(replay.session_id)}</code><br>Requests: <code>{len(replay.turns)}</code></p>"
|
| 322 |
+
"<table><thead><tr><th>Mode</th><th>Cost</th><th>Cache Busts</th><th>Rewrites</th>"
|
| 323 |
+
"<th>Retroactive Rewrites</th><th>Forwarded Tokens</th></tr></thead><tbody>"
|
| 324 |
+
+ "".join(rows)
|
| 325 |
+
+ "</tbody></table>"
|
| 326 |
+
"<h2>Winners</h2><ul>"
|
| 327 |
+
f"<li>total cost: <code>{html.escape(winners['total_cost'])}</code></li>"
|
| 328 |
+
f"<li>no-cache total cost: <code>{html.escape(winners['no_cache_total_cost'])}</code></li>"
|
| 329 |
+
f"<li>window with cache counted: <code>{html.escape(winners['window_with_cache'])}</code></li>"
|
| 330 |
+
f"<li>window without cache reads: <code>{html.escape(winners['window_without_cache_reads'])}</code></li>"
|
| 331 |
+
"</ul><h2>Cache Bust Events</h2>"
|
| 332 |
+
+ "".join(event_sections)
|
| 333 |
+
+ "</body></html>"
|
| 334 |
+
)
|
| 335 |
+
html_path.write_text(html_doc, encoding="utf-8")
|
| 336 |
+
return md_path, json_path, html_path
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def main() -> int:
|
| 340 |
+
original_make_proxy = bench._make_proxy
|
| 341 |
+
bench._make_proxy = lambda mode: _FakeProxy()
|
| 342 |
+
try:
|
| 343 |
+
replay = _build_replay()
|
| 344 |
+
dataset, summaries = simulate_replays([replay], cache_ttl_minutes=5)
|
| 345 |
+
events = _build_bust_events(replay)
|
| 346 |
+
finally:
|
| 347 |
+
bench._make_proxy = original_make_proxy
|
| 348 |
+
|
| 349 |
+
winners = determine_winners(summaries)
|
| 350 |
+
md_path, json_path, html_path = _write_report(replay, summaries, winners, events)
|
| 351 |
+
print("Synthetic token-cache-bust replay")
|
| 352 |
+
print(f"requests={dataset.requests}")
|
| 353 |
+
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
|
| 354 |
+
summary = summaries[mode]
|
| 355 |
+
print(
|
| 356 |
+
f"{mode}: cost={format_currency(summary.total_cost_usd)} "
|
| 357 |
+
f"busts={summary.cache_bust_turns} "
|
| 358 |
+
f"rewrites={summary.rewrite_turns} "
|
| 359 |
+
f"retro_rw={summary.retroactive_rewrite_turns} "
|
| 360 |
+
f"forwarded={summary.forwarded_input_tokens}"
|
| 361 |
+
)
|
| 362 |
+
print(f"winner_total_cost={winners['total_cost']}")
|
| 363 |
+
print(f"Markdown report: {md_path}")
|
| 364 |
+
print(f"JSON report: {json_path}")
|
| 365 |
+
print(f"HTML report: {html_path}")
|
| 366 |
+
return 0
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
if __name__ == "__main__":
|
| 370 |
+
raise SystemExit(main())
|
docs/benchmarks.md
CHANGED
|
@@ -228,6 +228,50 @@ This benchmark compares `token` vs `cache` proxy modes on the same synthetic con
|
|
| 228 |
|
| 229 |
Use it when you want a clean PR-vs-`main` comparison on the same transcript slice.
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
The Claude session benchmark replays local transcript data from `~/.claude/projects`
|
| 232 |
through `baseline`, `token`, and `cache` modes. It estimates raw tokens, cache
|
| 233 |
read/write tokens, paid input/output costs, and prompt-window winners under two
|
|
|
|
| 228 |
|
| 229 |
Use it when you want a clean PR-vs-`main` comparison on the same transcript slice.
|
| 230 |
|
| 231 |
+
For a deterministic cache-busting proof case, run:
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
python benchmarks/synthetic_token_cache_bust_report.py
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
That synthetic replay forces `token` mode to retroactively rewrite a prior tool result on the second turn while `cache` mode remains stable. Use it to verify the simulator can distinguish:
|
| 238 |
+
|
| 239 |
+
- `token`: history rewrite + cache bust
|
| 240 |
+
- `cache`: no rewrite + no bust
|
| 241 |
+
|
| 242 |
+
For a reproducible local report bundle that combines:
|
| 243 |
+
|
| 244 |
+
- full real-session replay summaries
|
| 245 |
+
- local-only processed real input/output excerpts
|
| 246 |
+
- synthetic token-bust proof
|
| 247 |
+
- synthetic long-form stress tests
|
| 248 |
+
|
| 249 |
+
run:
|
| 250 |
+
|
| 251 |
+
```bash
|
| 252 |
+
python benchmarks/cache_validation_bundle.py --workers 1 --output-dir benchmark_results/cache_validation_bundle_full
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
Notes:
|
| 256 |
+
|
| 257 |
+
- By default the bundle is redaction-safe for sharing:
|
| 258 |
+
- real processed reports redact transcript-derived content excerpts
|
| 259 |
+
- manifest paths are redacted
|
| 260 |
+
- To include local processed content excerpts for private review on your own machine:
|
| 261 |
+
|
| 262 |
+
```bash
|
| 263 |
+
python benchmarks/cache_validation_bundle.py --workers 1 --include-content
|
| 264 |
+
```
|
| 265 |
+
|
| 266 |
+
- The bundle writes:
|
| 267 |
+
- `index.html` / `index.md`: top-level summary and links
|
| 268 |
+
- `bundle_manifest.json`: runtime metadata + corpus fingerprint
|
| 269 |
+
- `real/`: full real-session replay reports
|
| 270 |
+
- `real_processed/`: processed before/after excerpts from real transcripts
|
| 271 |
+
- `synthetic_token_bust/`: minimal explicit cache-bust proof
|
| 272 |
+
- `synthetic_long_suite/`: long deterministic rewrite/TTL scenarios
|
| 273 |
+
- Checkpoints are scoped under the bundle output directory and fingerprinted by the selected corpus so stale runs do not contaminate new results.
|
| 274 |
+
|
| 275 |
The Claude session benchmark replays local transcript data from `~/.claude/projects`
|
| 276 |
through `baseline`, `token`, and `cache` modes. It estimates raw tokens, cache
|
| 277 |
read/write tokens, paid input/output costs, and prompt-window winners under two
|
headroom/providers/anthropic.py
CHANGED
|
@@ -250,7 +250,13 @@ class AnthropicTokenCounter(TokenCounter):
|
|
| 250 |
|
| 251 |
if self._encoding:
|
| 252 |
# tiktoken with ~1.1x multiplier for Claude
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
return int(base_count * 1.1)
|
| 255 |
|
| 256 |
# Character-based fallback
|
|
|
|
| 250 |
|
| 251 |
if self._encoding:
|
| 252 |
# tiktoken with ~1.1x multiplier for Claude
|
| 253 |
+
try:
|
| 254 |
+
base_count = len(self._encoding.encode(text))
|
| 255 |
+
except ValueError:
|
| 256 |
+
# Real tool output can legitimately contain strings that look like
|
| 257 |
+
# tiktoken special tokens (for example FIM markers in code spans).
|
| 258 |
+
# Treat them as ordinary text for estimation instead of failing.
|
| 259 |
+
base_count = len(self._encoding.encode(text, disallowed_special=()))
|
| 260 |
return int(base_count * 1.1)
|
| 261 |
|
| 262 |
# Character-based fallback
|
tests/test_claude_session_mode_benchmark.py
CHANGED
|
@@ -338,9 +338,9 @@ def test_determine_winners_includes_no_cache_counterfactual() -> None:
|
|
| 338 |
def test_resolve_checkpoint_dir_namespaces_sampling_mode() -> None:
|
| 339 |
base = Path("benchmark_results") / "checkpoints"
|
| 340 |
|
| 341 |
-
assert resolve_checkpoint_dir(base).name == "
|
| 342 |
assert (
|
| 343 |
-
resolve_checkpoint_dir(base, recent_turns_per_session=200).name == "
|
| 344 |
)
|
| 345 |
|
| 346 |
|
|
@@ -519,6 +519,11 @@ def test_synthetic_token_mode_busts_cache_while_cache_mode_stays_stable(monkeypa
|
|
| 519 |
|
| 520 |
assert token.cache_bust_turns == 1
|
| 521 |
assert token.rewrite_turns >= 1
|
|
|
|
|
|
|
|
|
|
| 522 |
assert token.retroactive_rewrite_turns >= 1
|
| 523 |
assert cache.cache_bust_turns == 0
|
|
|
|
|
|
|
| 524 |
assert cache.retroactive_rewrite_turns == 0
|
|
|
|
| 338 |
def test_resolve_checkpoint_dir_namespaces_sampling_mode() -> None:
|
| 339 |
base = Path("benchmark_results") / "checkpoints"
|
| 340 |
|
| 341 |
+
assert resolve_checkpoint_dir(base).name == "v5__ttl_5m__full"
|
| 342 |
assert (
|
| 343 |
+
resolve_checkpoint_dir(base, recent_turns_per_session=200).name == "v5__ttl_5m__recent_200"
|
| 344 |
)
|
| 345 |
|
| 346 |
|
|
|
|
| 519 |
|
| 520 |
assert token.cache_bust_turns == 1
|
| 521 |
assert token.rewrite_turns >= 1
|
| 522 |
+
assert token.busting_rewrite_turns >= 1
|
| 523 |
+
assert token.non_cache_eligible_rewrite_turns == 0
|
| 524 |
+
assert token.stable_replay_rewrite_turns == 0
|
| 525 |
assert token.retroactive_rewrite_turns >= 1
|
| 526 |
assert cache.cache_bust_turns == 0
|
| 527 |
+
assert cache.busting_rewrite_turns == 0
|
| 528 |
+
assert cache.non_cache_eligible_rewrite_turns == 0
|
| 529 |
assert cache.retroactive_rewrite_turns == 0
|
tests/test_providers/test_anthropic.py
CHANGED
|
@@ -22,6 +22,11 @@ class TestAnthropicTokenCounting:
|
|
| 22 |
count = counter.count_messages(messages)
|
| 23 |
assert count > 0
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
class TestAnthropicModelLimits:
|
| 27 |
@pytest.fixture
|
|
|
|
| 22 |
count = counter.count_messages(messages)
|
| 23 |
assert count > 0
|
| 24 |
|
| 25 |
+
def test_count_text_allows_literal_special_tokens(self, anthropic_provider):
|
| 26 |
+
counter = anthropic_provider.get_token_counter("claude-3-5-sonnet-20241022")
|
| 27 |
+
count = counter.count_text("prefix <|fim_suffix|> suffix")
|
| 28 |
+
assert count > 0
|
| 29 |
+
|
| 30 |
|
| 31 |
class TestAnthropicModelLimits:
|
| 32 |
@pytest.fixture
|