Spaces:
Build error
Build error
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 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 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 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
}
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
if
|
| 354 |
-
current_group
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 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],
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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"{
|
|
|
|
|
|
|
| 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">
|
| 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=
|
|
|
|
| 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 |
+
)
|