JerrettDavis commited on
Commit
4489e43
·
1 Parent(s): e0b7611

Improve Claude cache simulation accounting

Browse files
benchmarks/claude_session_mode_benchmark.py CHANGED
@@ -93,6 +93,9 @@ class ModeSummary:
93
  cache_write_cost_usd: float = 0.0
94
  paid_output_cost_usd: float = 0.0
95
  total_cost_usd: float = 0.0
 
 
 
96
  turns: list[TurnMetrics] = field(default_factory=list)
97
 
98
  @property
@@ -111,6 +114,16 @@ class ModeSummary:
111
  def prompt_window_without_cache_reads(self) -> int:
112
  return self.forwarded_input_tokens - self.cache_read_tokens
113
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  @dataclass
116
  class DatasetSummary:
@@ -119,6 +132,8 @@ class DatasetSummary:
119
  requests: int
120
  models: dict[str, int]
121
  decoded_project_paths: int
 
 
122
 
123
 
124
  @dataclass
@@ -203,6 +218,9 @@ def _mode_summary_from_dict(data: dict[str, Any]) -> ModeSummary:
203
  cache_write_cost_usd=data.get("cache_write_cost_usd", 0.0),
204
  paid_output_cost_usd=data.get("paid_output_cost_usd", 0.0),
205
  total_cost_usd=data.get("total_cost_usd", 0.0),
 
 
 
206
  turns=turns,
207
  )
208
  return summary
@@ -289,44 +307,24 @@ def load_session_replay(session_file: Path) -> SessionReplay | None:
289
  turns: list[ReplayTurn] = []
290
  current_group: dict[str, Any] | None = None
291
 
292
- with session_file.open("r", encoding="utf-8") as handle:
293
- for raw_line in handle:
294
- line = raw_line.strip()
295
- if not line:
296
- continue
297
- try:
298
- event = json.loads(line)
299
- except json.JSONDecodeError:
300
- continue
301
- event_type = event.get("type")
302
- message = event.get("message")
303
-
304
- if event_type == "user" and isinstance(message, dict) and message.get("role") == "user":
305
- _finalize_group(
306
- current_group,
307
- pending_messages,
308
- turns,
309
- session_id=session_id,
310
- project_key=project_key,
311
- decoded_project_path=decoded_project_path,
312
- )
313
- current_group = None
314
- pending_messages.clear()
315
- pending_messages.append(copy.deepcopy(message))
316
- continue
317
-
318
- if (
319
- event_type == "assistant"
320
- and isinstance(message, dict)
321
- and message.get("role") == "assistant"
322
- and event.get("requestId")
323
- ):
324
- request_id = str(event["requestId"])
325
- usage = message.get("usage") or {}
326
- timestamp = _parse_timestamp(event.get("timestamp"))
327
- blocks = _assistant_blocks_from_content(message.get("content"))
328
- if current_group is None or current_group["request_id"] != request_id:
329
- had_group = current_group is not None
330
  _finalize_group(
331
  current_group,
332
  pending_messages,
@@ -335,40 +333,67 @@ def load_session_replay(session_file: Path) -> SessionReplay | None:
335
  project_key=project_key,
336
  decoded_project_path=decoded_project_path,
337
  )
338
- if had_group:
339
- pending_messages.clear()
340
- current_group = {
341
- "request_id": request_id,
342
- "model": str(message.get("model", "unknown")),
343
- "timestamp": timestamp,
344
- "blocks": [],
345
- "seen": set(),
346
- "output_tokens": 0,
347
- "observed_input_tokens": 0,
348
- "observed_cache_read_tokens": 0,
349
- "observed_cache_write_tokens": 0,
350
- }
351
- for block in blocks:
352
- key = _canonical_block_key(block)
353
- if key not in current_group["seen"]:
354
- current_group["seen"].add(key)
355
- current_group["blocks"].append(copy.deepcopy(block))
356
- current_group["output_tokens"] = max(
357
- current_group["output_tokens"],
358
- int(usage.get("output_tokens", 0) or 0),
359
- )
360
- current_group["observed_input_tokens"] = max(
361
- current_group["observed_input_tokens"],
362
- int(usage.get("input_tokens", 0) or 0),
363
- )
364
- current_group["observed_cache_read_tokens"] = max(
365
- current_group["observed_cache_read_tokens"],
366
- int(usage.get("cache_read_input_tokens", 0) or 0),
367
- )
368
- current_group["observed_cache_write_tokens"] = max(
369
- current_group["observed_cache_write_tokens"],
370
- int(usage.get("cache_creation_input_tokens", 0) or 0),
371
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
  _finalize_group(
374
  current_group,
@@ -389,6 +414,33 @@ def load_session_replay(session_file: Path) -> SessionReplay | None:
389
  )
390
 
391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  def discover_session_files(root: Path) -> list[Path]:
393
  if not root.exists():
394
  return []
@@ -423,7 +475,10 @@ def select_session_files(root: Path, max_sessions: int | None = None) -> list[Pa
423
 
424
 
425
  def build_dataset_and_observed_from_files(
426
- session_files: list[Path], *, cache_write_multiplier: float = 1.25
 
 
 
427
  ) -> tuple[DatasetSummary, ObservedSummary]:
428
  model_counts: Counter[str] = Counter()
429
  project_keys: set[str] = set()
@@ -438,6 +493,7 @@ def build_dataset_and_observed_from_files(
438
  replay = load_session_replay(session_file)
439
  if replay is None:
440
  continue
 
441
  project_keys.add(replay.project_key)
442
  decoded_project_paths.add(replay.decoded_project_path)
443
  observed.sessions += 1
@@ -481,6 +537,12 @@ def build_dataset_and_observed_from_files(
481
  requests=requests,
482
  models=dict(sorted(model_counts.items())),
483
  decoded_project_paths=len(decoded_project_paths),
 
 
 
 
 
 
484
  )
485
  return dataset, observed
486
 
@@ -700,8 +762,18 @@ def _apply_turn_metrics(
700
  forwarded_input_tokens = tokenizer.count_messages(forwarded)
701
 
702
  read_tokens = 0
703
- if _cache_gap_within_ttl(turn.timestamp, previous_timestamp, ttl=ttl):
 
704
  read_tokens = _common_prefix_tokens(previous_forwarded, forwarded, tokenizer)
 
 
 
 
 
 
 
 
 
705
 
706
  write_tokens = 0
707
  if next_forwarded is not None and _cache_gap_within_ttl(
@@ -748,6 +820,9 @@ def _merge_mode_summary(target: ModeSummary, source: ModeSummary) -> None:
748
  target.cache_write_cost_usd += source.cache_write_cost_usd
749
  target.paid_output_cost_usd += source.paid_output_cost_usd
750
  target.total_cost_usd += source.total_cost_usd
 
 
 
751
 
752
 
753
  def _disable_headroom_benchmark_logging() -> None:
@@ -885,8 +960,8 @@ def _simulate_single_replay_mode(
885
  forwarded=forwarded,
886
  )
887
  conversation.append(turn.assistant_message)
888
- conversation_token_total = (
889
- raw_input_tokens + tokenizer.count_message(turn.assistant_message)
890
  )
891
 
892
  if pending is not None:
@@ -912,10 +987,12 @@ def _simulate_single_session_file_mode(
912
  mode: str,
913
  cache_ttl_minutes: int,
914
  cache_write_multiplier: float,
 
915
  ) -> tuple[str, ModeSummary]:
916
  replay = load_session_replay(session_file)
917
  if replay is None:
918
  return session_file.stem, ModeSummary(mode=mode)
 
919
  return replay.session_id, _simulate_single_replay_mode(
920
  replay,
921
  mode,
@@ -1018,6 +1095,7 @@ def simulate_session_files(
1018
  cache_write_multiplier: float = 1.25,
1019
  workers: int = 1,
1020
  checkpoint_dir: Path | None = None,
 
1021
  ) -> dict[str, ModeSummary]:
1022
  summaries = {
1023
  "baseline": ModeSummary(mode="baseline"),
@@ -1058,6 +1136,7 @@ def simulate_session_files(
1058
  mode,
1059
  cache_ttl_minutes,
1060
  cache_write_multiplier,
 
1061
  )
1062
  future_map[future] = session_id
1063
  for future in concurrent.futures.as_completed(future_map):
@@ -1090,6 +1169,7 @@ def simulate_session_files(
1090
  replay = load_session_replay(session_file)
1091
  if replay is None:
1092
  continue
 
1093
  if index == 1 or index % 10 == 0 or index == total:
1094
  print(
1095
  f"[simulate] mode={mode} session={index}/{total} "
@@ -1112,6 +1192,9 @@ def simulate_session_files(
1112
  def determine_winners(summaries: dict[str, ModeSummary]) -> dict[str, str]:
1113
  return {
1114
  "total_cost": min(summaries.values(), key=lambda s: s.total_cost_usd).mode,
 
 
 
1115
  "window_with_cache": min(summaries.values(), key=lambda s: s.prompt_window_with_cache).mode,
1116
  "window_without_cache_reads": min(
1117
  summaries.values(), key=lambda s: s.prompt_window_without_cache_reads
@@ -1130,9 +1213,10 @@ def print_console_report(dataset: DatasetSummary, summaries: dict[str, ModeSumma
1130
  f"Dataset: {dataset.projects} projects, {dataset.sessions} sessions, "
1131
  f"{dataset.requests} requests"
1132
  )
 
1133
  print()
1134
  print(
1135
- "mode raw_tok cache_tok cache_read cache_write paid_in paid_out total_cost"
1136
  )
1137
  for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
1138
  summary = summaries[mode]
@@ -1140,10 +1224,13 @@ def print_console_report(dataset: DatasetSummary, summaries: dict[str, ModeSumma
1140
  f"{mode:<9} {summary.raw_tokens:>11,} {summary.cache_tokens:>12,} "
1141
  f"{summary.cache_read_tokens:>11,} {summary.cache_write_tokens:>12,} "
1142
  f"{summary.regular_input_tokens:>10,} {summary.output_tokens:>12,} "
1143
- f"{format_currency(summary.total_cost_usd):>11}"
 
 
1144
  )
1145
  print()
1146
  print(f"Winner by total cost: {winners['total_cost']}")
 
1147
  print(f"Winner if cache tokens count against window: {winners['window_with_cache']}")
1148
  print(
1149
  "Winner if cache read tokens do not count against window: "
@@ -1192,6 +1279,9 @@ def build_report_markdown(
1192
  format_currency(summary.cache_write_cost_usd),
1193
  format_currency(summary.paid_output_cost_usd),
1194
  format_currency(summary.total_cost_usd),
 
 
 
1195
  f"{summary.prompt_window_with_cache:,}",
1196
  f"{summary.prompt_window_without_cache_reads:,}",
1197
  ]
@@ -1207,7 +1297,9 @@ def build_report_markdown(
1207
  f"- Projects: {dataset.projects}",
1208
  f"- Sessions: {dataset.sessions}",
1209
  f"- Requests: {dataset.requests}",
 
1210
  f"- Distinct decoded project paths: {dataset.decoded_project_paths}",
 
1211
  "- Models:",
1212
  model_lines or "- None",
1213
  "",
@@ -1230,13 +1322,14 @@ def build_report_markdown(
1230
  "",
1231
  "## Summary",
1232
  "",
1233
- "| 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 | Window Tokens (Cache Counted) | Window Tokens (Cache Reads Excluded) |",
1234
- "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
1235
  *rows,
1236
  "",
1237
  "## Winners",
1238
  "",
1239
  f"- Total cost winner: `{winners['total_cost']}`",
 
1240
  f"- Window winner if cache tokens count: `{winners['window_with_cache']}`",
1241
  "- Window winner if cache read tokens do not count: "
1242
  f"`{winners['window_without_cache_reads']}`",
@@ -1266,7 +1359,10 @@ def build_report_html(
1266
  f"<td>{summary.cache_write_tokens:,}</td>"
1267
  f"<td>{summary.regular_input_tokens:,}</td>"
1268
  f"<td>{summary.output_tokens:,}</td>"
 
 
1269
  f"<td>{format_currency(summary.total_cost_usd)}</td>"
 
1270
  f"<td>{summary.prompt_window_with_cache:,}</td>"
1271
  f"<td>{summary.prompt_window_without_cache_reads:,}</td>"
1272
  "</tr>"
@@ -1360,7 +1456,7 @@ def build_report_html(
1360
  <div class="card"><div class="eyebrow">Projects</div><div class="value">{dataset.projects:,}</div><div class="subtle">{dataset.sessions:,} sessions / {dataset.requests:,} requests</div></div>
1361
  <div class="card"><div class="eyebrow">Observed Cache Ratio</div><div class="value">{observed.cache_ratio_pct:.1f}%</div><div class="subtle">read / (read + write + input)</div></div>
1362
  <div class="card"><div class="eyebrow">Observed Total Cost</div><div class="value">{format_currency(observed.total_cost_usd)}</div><div class="subtle">{observed.cache_read_tokens:,} read / {observed.cache_write_tokens:,} write</div></div>
1363
- <div class="card"><div class="eyebrow">Broken Prefix Turns</div><div class="value">{observed.broken_prefix_turns:,}</div><div class="subtle">CR stuck while CC grows</div></div>
1364
  </div>
1365
  </section>
1366
  <section class="section grid" style="grid-template-columns: 1.1fr .9fr;">
@@ -1368,6 +1464,7 @@ def build_report_html(
1368
  <h2>Winners</h2>
1369
  <div class="winner-list">
1370
  <div><span class="eyebrow">Total cost</span><br><span class="badge">{winners["total_cost"]}</span></div>
 
1371
  <div><span class="eyebrow">Window if cache counts</span><br><span class="badge">{winners["window_with_cache"]}</span></div>
1372
  <div><span class="eyebrow">Window if cache reads do not count</span><br><span class="badge">{winners["window_without_cache_reads"]}</span></div>
1373
  </div>
@@ -1391,7 +1488,7 @@ def build_report_html(
1391
  <table>
1392
  <thead>
1393
  <tr>
1394
- <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>Total Cost</th><th>Window With Cache</th><th>Window Without Cache Reads</th>
1395
  </tr>
1396
  </thead>
1397
  <tbody>
@@ -1432,6 +1529,12 @@ def parse_args() -> argparse.Namespace:
1432
  parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
1433
  parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
1434
  parser.add_argument("--max-sessions", type=int, default=None)
 
 
 
 
 
 
1435
  parser.add_argument("--cache-ttl-minutes", type=int, default=DEFAULT_CACHE_TTL_MINUTES)
1436
  parser.add_argument(
1437
  "--cache-write-multiplier",
@@ -1458,6 +1561,11 @@ def main() -> int:
1458
  args = parse_args()
1459
  logging.getLogger("headroom.transforms").setLevel(logging.WARNING)
1460
  logging.getLogger("headroom.proxy").setLevel(logging.WARNING)
 
 
 
 
 
1461
  session_files = select_session_files(args.root, max_sessions=args.max_sessions)
1462
  if not session_files:
1463
  print(f"No Claude session replays found under {args.root}")
@@ -1465,6 +1573,7 @@ def main() -> int:
1465
  dataset, observed = build_dataset_and_observed_from_files(
1466
  session_files,
1467
  cache_write_multiplier=args.cache_write_multiplier,
 
1468
  )
1469
  print(
1470
  f"[load] loaded {dataset.sessions} sessions from {args.root}"
@@ -1477,7 +1586,8 @@ def main() -> int:
1477
  cache_ttl_minutes=args.cache_ttl_minutes,
1478
  cache_write_multiplier=args.cache_write_multiplier,
1479
  workers=args.workers,
1480
- checkpoint_dir=args.checkpoint_dir,
 
1481
  )
1482
  md_path, json_path, html_path = write_report(args.output_dir, dataset, observed, summaries)
1483
  print_observed_console_report(observed)
 
93
  cache_write_cost_usd: float = 0.0
94
  paid_output_cost_usd: float = 0.0
95
  total_cost_usd: float = 0.0
96
+ cache_eligible_turns: int = 0
97
+ cache_bust_turns: int = 0
98
+ ttl_expiry_turns: int = 0
99
  turns: list[TurnMetrics] = field(default_factory=list)
100
 
101
  @property
 
114
  def prompt_window_without_cache_reads(self) -> int:
115
  return self.forwarded_input_tokens - self.cache_read_tokens
116
 
117
+ @property
118
+ def no_cache_total_cost_usd(self) -> float:
119
+ return (
120
+ self.paid_input_cost_usd + (self.cache_read_cost_usd * 10.0) + self.paid_output_cost_usd
121
+ )
122
+
123
+ @property
124
+ def no_cache_paid_input_tokens(self) -> int:
125
+ return self.forwarded_input_tokens
126
+
127
 
128
  @dataclass
129
  class DatasetSummary:
 
132
  requests: int
133
  models: dict[str, int]
134
  decoded_project_paths: int
135
+ sampled_requests: int = 0
136
+ sampling_note: str = ""
137
 
138
 
139
  @dataclass
 
218
  cache_write_cost_usd=data.get("cache_write_cost_usd", 0.0),
219
  paid_output_cost_usd=data.get("paid_output_cost_usd", 0.0),
220
  total_cost_usd=data.get("total_cost_usd", 0.0),
221
+ cache_eligible_turns=data.get("cache_eligible_turns", 0),
222
+ cache_bust_turns=data.get("cache_bust_turns", 0),
223
+ ttl_expiry_turns=data.get("ttl_expiry_turns", 0),
224
  turns=turns,
225
  )
226
  return summary
 
307
  turns: list[ReplayTurn] = []
308
  current_group: dict[str, Any] | None = None
309
 
310
+ try:
311
+ with session_file.open("r", encoding="utf-8") as handle:
312
+ for raw_line in handle:
313
+ line = raw_line.strip()
314
+ if not line:
315
+ continue
316
+ try:
317
+ event = json.loads(line)
318
+ except json.JSONDecodeError:
319
+ continue
320
+ event_type = event.get("type")
321
+ message = event.get("message")
322
+
323
+ if (
324
+ event_type == "user"
325
+ and isinstance(message, dict)
326
+ and message.get("role") == "user"
327
+ ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  _finalize_group(
329
  current_group,
330
  pending_messages,
 
333
  project_key=project_key,
334
  decoded_project_path=decoded_project_path,
335
  )
336
+ current_group = None
337
+ pending_messages.clear()
338
+ pending_messages.append(copy.deepcopy(message))
339
+ continue
340
+
341
+ if (
342
+ event_type == "assistant"
343
+ and isinstance(message, dict)
344
+ and message.get("role") == "assistant"
345
+ and event.get("requestId")
346
+ ):
347
+ request_id = str(event["requestId"])
348
+ usage = message.get("usage") or {}
349
+ timestamp = _parse_timestamp(event.get("timestamp"))
350
+ blocks = _assistant_blocks_from_content(message.get("content"))
351
+ if current_group is None or current_group["request_id"] != request_id:
352
+ had_group = current_group is not None
353
+ _finalize_group(
354
+ current_group,
355
+ pending_messages,
356
+ turns,
357
+ session_id=session_id,
358
+ project_key=project_key,
359
+ decoded_project_path=decoded_project_path,
360
+ )
361
+ if had_group:
362
+ pending_messages.clear()
363
+ current_group = {
364
+ "request_id": request_id,
365
+ "model": str(message.get("model", "unknown")),
366
+ "timestamp": timestamp,
367
+ "blocks": [],
368
+ "seen": set(),
369
+ "output_tokens": 0,
370
+ "observed_input_tokens": 0,
371
+ "observed_cache_read_tokens": 0,
372
+ "observed_cache_write_tokens": 0,
373
+ }
374
+ for block in blocks:
375
+ key = _canonical_block_key(block)
376
+ if key not in current_group["seen"]:
377
+ current_group["seen"].add(key)
378
+ current_group["blocks"].append(copy.deepcopy(block))
379
+ current_group["output_tokens"] = max(
380
+ current_group["output_tokens"],
381
+ int(usage.get("output_tokens", 0) or 0),
382
+ )
383
+ current_group["observed_input_tokens"] = max(
384
+ current_group["observed_input_tokens"],
385
+ int(usage.get("input_tokens", 0) or 0),
386
+ )
387
+ current_group["observed_cache_read_tokens"] = max(
388
+ current_group["observed_cache_read_tokens"],
389
+ int(usage.get("cache_read_input_tokens", 0) or 0),
390
+ )
391
+ current_group["observed_cache_write_tokens"] = max(
392
+ current_group["observed_cache_write_tokens"],
393
+ int(usage.get("cache_creation_input_tokens", 0) or 0),
394
+ )
395
+ except OSError:
396
+ return None
397
 
398
  _finalize_group(
399
  current_group,
 
414
  )
415
 
416
 
417
+ def trim_replay_to_recent_turns(
418
+ replay: SessionReplay, recent_turns: int | None = None
419
+ ) -> SessionReplay:
420
+ if recent_turns is None or recent_turns <= 0 or len(replay.turns) <= recent_turns:
421
+ return replay
422
+ return SessionReplay(
423
+ session_id=replay.session_id,
424
+ project_key=replay.project_key,
425
+ decoded_project_path=replay.decoded_project_path,
426
+ turns=replay.turns[-recent_turns:],
427
+ )
428
+
429
+
430
+ def resolve_checkpoint_dir(
431
+ base_dir: Path,
432
+ *,
433
+ recent_turns_per_session: int | None = None,
434
+ cache_ttl_minutes: int = DEFAULT_CACHE_TTL_MINUTES,
435
+ ) -> Path:
436
+ suffix_parts = ["v2", f"ttl_{cache_ttl_minutes}m"]
437
+ if recent_turns_per_session:
438
+ suffix_parts.append(f"recent_{recent_turns_per_session}")
439
+ else:
440
+ suffix_parts.append("full")
441
+ return base_dir / "__".join(suffix_parts)
442
+
443
+
444
  def discover_session_files(root: Path) -> list[Path]:
445
  if not root.exists():
446
  return []
 
475
 
476
 
477
  def build_dataset_and_observed_from_files(
478
+ session_files: list[Path],
479
+ *,
480
+ cache_write_multiplier: float = 1.25,
481
+ recent_turns_per_session: int | None = None,
482
  ) -> tuple[DatasetSummary, ObservedSummary]:
483
  model_counts: Counter[str] = Counter()
484
  project_keys: set[str] = set()
 
493
  replay = load_session_replay(session_file)
494
  if replay is None:
495
  continue
496
+ replay = trim_replay_to_recent_turns(replay, recent_turns_per_session)
497
  project_keys.add(replay.project_key)
498
  decoded_project_paths.add(replay.decoded_project_path)
499
  observed.sessions += 1
 
537
  requests=requests,
538
  models=dict(sorted(model_counts.items())),
539
  decoded_project_paths=len(decoded_project_paths),
540
+ sampled_requests=requests,
541
+ sampling_note=(
542
+ f"Most recent {recent_turns_per_session} turns per session"
543
+ if recent_turns_per_session
544
+ else "Full replayable session history"
545
+ ),
546
  )
547
  return dataset, observed
548
 
 
762
  forwarded_input_tokens = tokenizer.count_messages(forwarded)
763
 
764
  read_tokens = 0
765
+ cache_eligible = _cache_gap_within_ttl(turn.timestamp, previous_timestamp, ttl=ttl)
766
+ if cache_eligible:
767
  read_tokens = _common_prefix_tokens(previous_forwarded, forwarded, tokenizer)
768
+ summary.cache_eligible_turns += 1
769
+ prefix_preserved = (
770
+ len(forwarded) >= len(previous_forwarded)
771
+ and forwarded[: len(previous_forwarded)] == previous_forwarded
772
+ )
773
+ if previous_forwarded and not prefix_preserved:
774
+ summary.cache_bust_turns += 1
775
+ elif previous_timestamp is not None:
776
+ summary.ttl_expiry_turns += 1
777
 
778
  write_tokens = 0
779
  if next_forwarded is not None and _cache_gap_within_ttl(
 
820
  target.cache_write_cost_usd += source.cache_write_cost_usd
821
  target.paid_output_cost_usd += source.paid_output_cost_usd
822
  target.total_cost_usd += source.total_cost_usd
823
+ target.cache_eligible_turns += source.cache_eligible_turns
824
+ target.cache_bust_turns += source.cache_bust_turns
825
+ target.ttl_expiry_turns += source.ttl_expiry_turns
826
 
827
 
828
  def _disable_headroom_benchmark_logging() -> None:
 
960
  forwarded=forwarded,
961
  )
962
  conversation.append(turn.assistant_message)
963
+ conversation_token_total = raw_input_tokens + tokenizer.count_message(
964
+ turn.assistant_message
965
  )
966
 
967
  if pending is not None:
 
987
  mode: str,
988
  cache_ttl_minutes: int,
989
  cache_write_multiplier: float,
990
+ recent_turns_per_session: int | None = None,
991
  ) -> tuple[str, ModeSummary]:
992
  replay = load_session_replay(session_file)
993
  if replay is None:
994
  return session_file.stem, ModeSummary(mode=mode)
995
+ replay = trim_replay_to_recent_turns(replay, recent_turns_per_session)
996
  return replay.session_id, _simulate_single_replay_mode(
997
  replay,
998
  mode,
 
1095
  cache_write_multiplier: float = 1.25,
1096
  workers: int = 1,
1097
  checkpoint_dir: Path | None = None,
1098
+ recent_turns_per_session: int | None = None,
1099
  ) -> dict[str, ModeSummary]:
1100
  summaries = {
1101
  "baseline": ModeSummary(mode="baseline"),
 
1136
  mode,
1137
  cache_ttl_minutes,
1138
  cache_write_multiplier,
1139
+ recent_turns_per_session,
1140
  )
1141
  future_map[future] = session_id
1142
  for future in concurrent.futures.as_completed(future_map):
 
1169
  replay = load_session_replay(session_file)
1170
  if replay is None:
1171
  continue
1172
+ replay = trim_replay_to_recent_turns(replay, recent_turns_per_session)
1173
  if index == 1 or index % 10 == 0 or index == total:
1174
  print(
1175
  f"[simulate] mode={mode} session={index}/{total} "
 
1192
  def determine_winners(summaries: dict[str, ModeSummary]) -> dict[str, str]:
1193
  return {
1194
  "total_cost": min(summaries.values(), key=lambda s: s.total_cost_usd).mode,
1195
+ "no_cache_total_cost": min(
1196
+ summaries.values(), key=lambda s: s.no_cache_total_cost_usd
1197
+ ).mode,
1198
  "window_with_cache": min(summaries.values(), key=lambda s: s.prompt_window_with_cache).mode,
1199
  "window_without_cache_reads": min(
1200
  summaries.values(), key=lambda s: s.prompt_window_without_cache_reads
 
1213
  f"Dataset: {dataset.projects} projects, {dataset.sessions} sessions, "
1214
  f"{dataset.requests} requests"
1215
  )
1216
+ print(f"Sampling: {dataset.sampling_note}")
1217
  print()
1218
  print(
1219
+ "mode raw_tok cache_tok cache_read cache_write paid_in paid_out busts ttl_exp total_cost no_cache"
1220
  )
1221
  for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
1222
  summary = summaries[mode]
 
1224
  f"{mode:<9} {summary.raw_tokens:>11,} {summary.cache_tokens:>12,} "
1225
  f"{summary.cache_read_tokens:>11,} {summary.cache_write_tokens:>12,} "
1226
  f"{summary.regular_input_tokens:>10,} {summary.output_tokens:>12,} "
1227
+ f"{summary.cache_bust_turns:>7,} {summary.ttl_expiry_turns:>9,} "
1228
+ f"{format_currency(summary.total_cost_usd):>11} "
1229
+ f"{format_currency(summary.no_cache_total_cost_usd):>11}"
1230
  )
1231
  print()
1232
  print(f"Winner by total cost: {winners['total_cost']}")
1233
+ print(f"Winner by total cost with no cache help: {winners['no_cache_total_cost']}")
1234
  print(f"Winner if cache tokens count against window: {winners['window_with_cache']}")
1235
  print(
1236
  "Winner if cache read tokens do not count against window: "
 
1279
  format_currency(summary.cache_write_cost_usd),
1280
  format_currency(summary.paid_output_cost_usd),
1281
  format_currency(summary.total_cost_usd),
1282
+ format_currency(summary.no_cache_total_cost_usd),
1283
+ f"{summary.cache_bust_turns:,}",
1284
+ f"{summary.ttl_expiry_turns:,}",
1285
  f"{summary.prompt_window_with_cache:,}",
1286
  f"{summary.prompt_window_without_cache_reads:,}",
1287
  ]
 
1297
  f"- Projects: {dataset.projects}",
1298
  f"- Sessions: {dataset.sessions}",
1299
  f"- Requests: {dataset.requests}",
1300
+ f"- Sampled requests: {dataset.sampled_requests}",
1301
  f"- Distinct decoded project paths: {dataset.decoded_project_paths}",
1302
+ f"- Sampling: {dataset.sampling_note}",
1303
  "- Models:",
1304
  model_lines or "- None",
1305
  "",
 
1322
  "",
1323
  "## Summary",
1324
  "",
1325
+ "| 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 | Window Tokens (Cache Counted) | Window Tokens (Cache Reads Excluded) |",
1326
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
1327
  *rows,
1328
  "",
1329
  "## Winners",
1330
  "",
1331
  f"- Total cost winner: `{winners['total_cost']}`",
1332
+ f"- No-cache total cost winner: `{winners['no_cache_total_cost']}`",
1333
  f"- Window winner if cache tokens count: `{winners['window_with_cache']}`",
1334
  "- Window winner if cache read tokens do not count: "
1335
  f"`{winners['window_without_cache_reads']}`",
 
1359
  f"<td>{summary.cache_write_tokens:,}</td>"
1360
  f"<td>{summary.regular_input_tokens:,}</td>"
1361
  f"<td>{summary.output_tokens:,}</td>"
1362
+ f"<td>{summary.cache_bust_turns:,}</td>"
1363
+ f"<td>{summary.ttl_expiry_turns:,}</td>"
1364
  f"<td>{format_currency(summary.total_cost_usd)}</td>"
1365
+ f"<td>{format_currency(summary.no_cache_total_cost_usd)}</td>"
1366
  f"<td>{summary.prompt_window_with_cache:,}</td>"
1367
  f"<td>{summary.prompt_window_without_cache_reads:,}</td>"
1368
  "</tr>"
 
1456
  <div class="card"><div class="eyebrow">Projects</div><div class="value">{dataset.projects:,}</div><div class="subtle">{dataset.sessions:,} sessions / {dataset.requests:,} requests</div></div>
1457
  <div class="card"><div class="eyebrow">Observed Cache Ratio</div><div class="value">{observed.cache_ratio_pct:.1f}%</div><div class="subtle">read / (read + write + input)</div></div>
1458
  <div class="card"><div class="eyebrow">Observed Total Cost</div><div class="value">{format_currency(observed.total_cost_usd)}</div><div class="subtle">{observed.cache_read_tokens:,} read / {observed.cache_write_tokens:,} write</div></div>
1459
+ <div class="card"><div class="eyebrow">Broken Prefix Turns</div><div class="value">{observed.broken_prefix_turns:,}</div><div class="subtle">{dataset.sampling_note}</div></div>
1460
  </div>
1461
  </section>
1462
  <section class="section grid" style="grid-template-columns: 1.1fr .9fr;">
 
1464
  <h2>Winners</h2>
1465
  <div class="winner-list">
1466
  <div><span class="eyebrow">Total cost</span><br><span class="badge">{winners["total_cost"]}</span></div>
1467
+ <div><span class="eyebrow">No-cache total cost</span><br><span class="badge">{winners["no_cache_total_cost"]}</span></div>
1468
  <div><span class="eyebrow">Window if cache counts</span><br><span class="badge">{winners["window_with_cache"]}</span></div>
1469
  <div><span class="eyebrow">Window if cache reads do not count</span><br><span class="badge">{winners["window_without_cache_reads"]}</span></div>
1470
  </div>
 
1488
  <table>
1489
  <thead>
1490
  <tr>
1491
+ <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>Total Cost</th><th>No-Cache Cost</th><th>Window With Cache</th><th>Window Without Cache Reads</th>
1492
  </tr>
1493
  </thead>
1494
  <tbody>
 
1529
  parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
1530
  parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
1531
  parser.add_argument("--max-sessions", type=int, default=None)
1532
+ parser.add_argument(
1533
+ "--recent-turns-per-session",
1534
+ type=int,
1535
+ default=None,
1536
+ help="Limit each replay to its most recent N turns for broader, faster sampling.",
1537
+ )
1538
  parser.add_argument("--cache-ttl-minutes", type=int, default=DEFAULT_CACHE_TTL_MINUTES)
1539
  parser.add_argument(
1540
  "--cache-write-multiplier",
 
1561
  args = parse_args()
1562
  logging.getLogger("headroom.transforms").setLevel(logging.WARNING)
1563
  logging.getLogger("headroom.proxy").setLevel(logging.WARNING)
1564
+ checkpoint_dir = resolve_checkpoint_dir(
1565
+ args.checkpoint_dir,
1566
+ recent_turns_per_session=args.recent_turns_per_session,
1567
+ cache_ttl_minutes=args.cache_ttl_minutes,
1568
+ )
1569
  session_files = select_session_files(args.root, max_sessions=args.max_sessions)
1570
  if not session_files:
1571
  print(f"No Claude session replays found under {args.root}")
 
1573
  dataset, observed = build_dataset_and_observed_from_files(
1574
  session_files,
1575
  cache_write_multiplier=args.cache_write_multiplier,
1576
+ recent_turns_per_session=args.recent_turns_per_session,
1577
  )
1578
  print(
1579
  f"[load] loaded {dataset.sessions} sessions from {args.root}"
 
1586
  cache_ttl_minutes=args.cache_ttl_minutes,
1587
  cache_write_multiplier=args.cache_write_multiplier,
1588
  workers=args.workers,
1589
+ checkpoint_dir=checkpoint_dir,
1590
+ recent_turns_per_session=args.recent_turns_per_session,
1591
  )
1592
  md_path, json_path, html_path = write_report(args.output_dir, dataset, observed, summaries)
1593
  print_observed_console_report(observed)
tests/test_claude_session_mode_benchmark.py CHANGED
@@ -13,11 +13,14 @@ from benchmarks.claude_session_mode_benchmark import (
13
  ReplayTurn,
14
  SessionReplay,
15
  _write_checkpoint_by_session_id,
 
16
  decode_project_key,
17
  determine_winners,
18
  load_session_replay,
 
19
  simulate_replays,
20
  summarize_observed_usage,
 
21
  )
22
 
23
 
@@ -141,6 +144,9 @@ def test_simulation_and_winner_logic() -> None:
141
  <= summaries["baseline"].forwarded_input_tokens
142
  )
143
  assert summaries[PROXY_MODE_CACHE].cache_read_tokens >= 0
 
 
 
144
 
145
  winners = determine_winners(summaries)
146
  assert winners["total_cost"] in {"baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE}
@@ -220,3 +226,111 @@ def test_checkpoint_write_omits_per_turn_payload(tmp_path: Path) -> None:
220
 
221
  payload = json.loads((tmp_path / f"{PROXY_MODE_TOKEN}--session-1.json").read_text())
222
  assert payload["turns"] == []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ReplayTurn,
14
  SessionReplay,
15
  _write_checkpoint_by_session_id,
16
+ build_dataset_and_observed_from_files,
17
  decode_project_key,
18
  determine_winners,
19
  load_session_replay,
20
+ resolve_checkpoint_dir,
21
  simulate_replays,
22
  summarize_observed_usage,
23
+ trim_replay_to_recent_turns,
24
  )
25
 
26
 
 
144
  <= summaries["baseline"].forwarded_input_tokens
145
  )
146
  assert summaries[PROXY_MODE_CACHE].cache_read_tokens >= 0
147
+ assert summaries["baseline"].cache_bust_turns == 0
148
+ assert summaries[PROXY_MODE_CACHE].cache_bust_turns == 0
149
+ assert summaries[PROXY_MODE_TOKEN].cache_bust_turns >= 0
150
 
151
  winners = determine_winners(summaries)
152
  assert winners["total_cost"] in {"baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE}
 
226
 
227
  payload = json.loads((tmp_path / f"{PROXY_MODE_TOKEN}--session-1.json").read_text())
228
  assert payload["turns"] == []
229
+
230
+
231
+ def test_trim_replay_to_recent_turns_keeps_latest_slice() -> None:
232
+ replay = SessionReplay(
233
+ session_id="s1",
234
+ project_key="C--git-demo",
235
+ decoded_project_path=r"C:\git\demo",
236
+ turns=[
237
+ ReplayTurn(
238
+ session_id="s1",
239
+ project_key="C--git-demo",
240
+ decoded_project_path=r"C:\git\demo",
241
+ request_id=f"r{i}",
242
+ model="claude-sonnet-4-6",
243
+ timestamp=datetime.fromisoformat(f"2026-03-13T01:0{i}:00+00:00"),
244
+ input_messages=[{"role": "user", "content": str(i)}],
245
+ assistant_message={"role": "assistant", "content": str(i)},
246
+ output_tokens=i,
247
+ )
248
+ for i in range(4)
249
+ ],
250
+ )
251
+
252
+ trimmed = trim_replay_to_recent_turns(replay, 2)
253
+
254
+ assert [turn.request_id for turn in trimmed.turns] == ["r2", "r3"]
255
+
256
+
257
+ def test_build_dataset_and_observed_from_files_applies_recent_turn_sampling(
258
+ tmp_path: Path,
259
+ ) -> None:
260
+ project_dir = tmp_path / "C--git-BetBlocker"
261
+ project_dir.mkdir()
262
+ session_file = project_dir / "sess-1.jsonl"
263
+ lines = []
264
+ for i in range(3):
265
+ lines.append(
266
+ {
267
+ "type": "user",
268
+ "message": {"role": "user", "content": f"Hello {i}"},
269
+ "timestamp": f"2026-03-13T01:0{i}:00Z",
270
+ }
271
+ )
272
+ lines.append(
273
+ {
274
+ "type": "assistant",
275
+ "requestId": f"req-{i}",
276
+ "timestamp": f"2026-03-13T01:0{i}:01Z",
277
+ "message": {
278
+ "role": "assistant",
279
+ "model": "claude-sonnet-4-6",
280
+ "content": [{"type": "text", "text": f"Hi {i}"}],
281
+ "usage": {
282
+ "output_tokens": 3,
283
+ "input_tokens": 10,
284
+ "cache_read_input_tokens": 20,
285
+ "cache_creation_input_tokens": 5,
286
+ },
287
+ },
288
+ }
289
+ )
290
+ session_file.write_text("\n".join(json.dumps(line) for line in lines), encoding="utf-8")
291
+
292
+ dataset, observed = build_dataset_and_observed_from_files(
293
+ [session_file],
294
+ recent_turns_per_session=2,
295
+ )
296
+
297
+ assert dataset.requests == 2
298
+ assert dataset.sampled_requests == 2
299
+ assert dataset.sampling_note == "Most recent 2 turns per session"
300
+ assert observed.requests == 2
301
+
302
+
303
+ def test_determine_winners_includes_no_cache_counterfactual() -> None:
304
+ summaries = {
305
+ "baseline": ModeSummary(
306
+ mode="baseline",
307
+ paid_input_cost_usd=1.0,
308
+ cache_read_cost_usd=0.2,
309
+ paid_output_cost_usd=0.5,
310
+ ),
311
+ PROXY_MODE_TOKEN: ModeSummary(
312
+ mode=PROXY_MODE_TOKEN,
313
+ paid_input_cost_usd=0.8,
314
+ cache_read_cost_usd=0.1,
315
+ paid_output_cost_usd=0.5,
316
+ ),
317
+ PROXY_MODE_CACHE: ModeSummary(
318
+ mode=PROXY_MODE_CACHE,
319
+ paid_input_cost_usd=0.7,
320
+ cache_read_cost_usd=0.3,
321
+ paid_output_cost_usd=0.5,
322
+ ),
323
+ }
324
+
325
+ winners = determine_winners(summaries)
326
+
327
+ assert winners["no_cache_total_cost"] == PROXY_MODE_TOKEN
328
+
329
+
330
+ def test_resolve_checkpoint_dir_namespaces_sampling_mode() -> None:
331
+ base = Path("benchmark_results") / "checkpoints"
332
+
333
+ assert resolve_checkpoint_dir(base).name == "v2__ttl_5m__full"
334
+ assert (
335
+ resolve_checkpoint_dir(base, recent_turns_per_session=200).name == "v2__ttl_5m__recent_200"
336
+ )