JerrettDavis commited on
Commit
52bd5e8
·
1 Parent(s): 29d84b6

Harden cache validation reporting and TTL analysis

Browse files
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 = ["v4", f"ttl_{cache_ttl_minutes}m"]
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.retroactive_rewrite_turns:>10,} "
 
 
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
- base_count = len(self._encoding.encode(text))
 
 
 
 
 
 
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 == "v4__ttl_5m__full"
342
  assert (
343
- resolve_checkpoint_dir(base, recent_turns_per_session=200).name == "v4__ttl_5m__recent_200"
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