Files
chopratejas--headroom/benchmarks/claude_session_mode_benchmark.py
wehub-resource-sync 0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:20 +08:00

2056 lines
80 KiB
Python

#!/usr/bin/env python3
"""Replay real Claude Code sessions through baseline/token/cache simulations."""
from __future__ import annotations
import argparse
import concurrent.futures
import copy
import json
import logging
import os
from collections import Counter
from dataclasses import asdict, dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
from headroom.cache.compression_cache import CompressionCache
from headroom.cache.prefix_tracker import PrefixCacheTracker
from headroom.pricing.litellm_pricing import get_model_pricing
from headroom.proxy.handlers.anthropic import AnthropicHandlerMixin
from headroom.proxy.models import ProxyConfig
from headroom.proxy.server import HeadroomProxy
from headroom.tokenizers import get_tokenizer
from headroom.utils import extract_user_query
try:
from headroom.proxy.modes import PROXY_MODE_CACHE, PROXY_MODE_TOKEN
except ImportError:
PROXY_MODE_CACHE = "cache"
PROXY_MODE_TOKEN = "token"
DEFAULT_ROOT = Path.home() / ".claude" / "projects"
DEFAULT_OUTPUT_DIR = Path("benchmark_results")
DEFAULT_CACHE_TTL_MINUTES = 5
OUTPUT_MD = "claude_session_mode_simulation.md"
OUTPUT_JSON = "claude_session_mode_simulation.json"
OUTPUT_HTML = "claude_session_mode_simulation.html"
CHECKPOINT_DIRNAME = "checkpoints"
@dataclass
class ReplayTurn:
session_id: str
project_key: str
decoded_project_path: str
request_id: str
model: str
timestamp: datetime
input_messages: list[dict[str, Any]]
assistant_message: dict[str, Any]
output_tokens: int
observed_input_tokens: int = 0
observed_cache_read_tokens: int = 0
observed_cache_write_tokens: int = 0
@dataclass
class SessionReplay:
session_id: str
project_key: str
decoded_project_path: str
turns: list[ReplayTurn] = field(default_factory=list)
@dataclass
class TurnMetrics:
session_id: str
request_id: str
model: str
timestamp: str
raw_input_tokens: int
forwarded_input_tokens: int
cache_read_tokens: int
cache_write_tokens: int
regular_input_tokens: int
output_tokens: int
paid_input_cost_usd: float
cache_read_cost_usd: float
cache_write_cost_usd: float
paid_output_cost_usd: float
total_cost_usd: float
@dataclass
class ModeSummary:
mode: str
sessions: int = 0
requests: int = 0
raw_input_tokens: int = 0
forwarded_input_tokens: int = 0
cache_read_tokens: int = 0
cache_write_tokens: int = 0
regular_input_tokens: int = 0
output_tokens: int = 0
paid_input_cost_usd: float = 0.0
cache_read_cost_usd: float = 0.0
cache_write_cost_usd: float = 0.0
paid_output_cost_usd: float = 0.0
total_cost_usd: float = 0.0
cache_eligible_turns: int = 0
cache_bust_turns: int = 0
ttl_expiry_turns: int = 0
rewrite_turns: int = 0
stable_replay_rewrite_turns: int = 0
busting_rewrite_turns: int = 0
non_cache_eligible_rewrite_turns: int = 0
retroactive_rewrite_turns: int = 0
latest_turn_only_rewrite_turns: int = 0
turns: list[TurnMetrics] = field(default_factory=list)
@property
def raw_tokens(self) -> int:
return self.raw_input_tokens + self.output_tokens
@property
def cache_tokens(self) -> int:
return self.cache_read_tokens + self.cache_write_tokens
@property
def prompt_window_with_cache(self) -> int:
return self.forwarded_input_tokens
@property
def prompt_window_without_cache_reads(self) -> int:
return self.forwarded_input_tokens - self.cache_read_tokens
@property
def no_cache_total_cost_usd(self) -> float:
return (
self.paid_input_cost_usd + (self.cache_read_cost_usd * 10.0) + self.paid_output_cost_usd
)
@property
def no_cache_paid_input_tokens(self) -> int:
return self.forwarded_input_tokens
@dataclass
class DatasetSummary:
projects: int
sessions: int
requests: int
models: dict[str, int]
decoded_project_paths: int
sampled_requests: int = 0
sampling_note: str = ""
IMPACT_DIRECTION = {
"forwarded_input_tokens": "lower",
"cache_read_tokens": "higher",
"cache_write_tokens": "lower",
"regular_input_tokens": "lower",
"output_tokens": "same",
"total_cost_usd": "lower",
"no_cache_total_cost_usd": "lower",
"prompt_window_with_cache": "lower",
"prompt_window_without_cache_reads": "lower",
"cache_bust_turns": "lower",
"ttl_expiry_turns": "lower",
"rewrite_turns": "lower",
"stable_replay_rewrite_turns": "lower",
"busting_rewrite_turns": "lower",
"non_cache_eligible_rewrite_turns": "lower",
"retroactive_rewrite_turns": "lower",
"latest_turn_only_rewrite_turns": "lower",
}
@dataclass
class ObservedSummary:
sessions: int = 0
requests: int = 0
input_tokens: int = 0
cache_read_tokens: int = 0
cache_write_tokens: int = 0
output_tokens: int = 0
total_cost_usd: float = 0.0
cache_read_cost_usd: float = 0.0
cache_write_cost_usd: float = 0.0
paid_input_cost_usd: float = 0.0
paid_output_cost_usd: float = 0.0
healthy_growth_turns: int = 0
broken_prefix_turns: int = 0
resume_like_resets: int = 0
@property
def raw_tokens(self) -> int:
return (
self.input_tokens
+ self.cache_read_tokens
+ self.cache_write_tokens
+ self.output_tokens
)
@property
def cache_ratio_pct(self) -> float:
total = self.input_tokens + self.cache_read_tokens + self.cache_write_tokens
if total <= 0:
return 0.0
return self.cache_read_tokens / total * 100.0
def _update_dataset_with_replay(
dataset: DatasetSummary | None, replay: SessionReplay
) -> DatasetSummary:
if dataset is None:
dataset = DatasetSummary(
projects=0,
sessions=0,
requests=0,
models={},
decoded_project_paths=0,
)
projects = {replay.project_key}
project_paths = {replay.decoded_project_path}
model_counts = Counter(dataset.models)
requests = dataset.requests
for turn in replay.turns:
model_counts[turn.model] += 1
requests += 1
return DatasetSummary(
projects=dataset.projects + len(projects),
sessions=dataset.sessions + 1,
requests=requests,
models=dict(sorted(model_counts.items())),
decoded_project_paths=dataset.decoded_project_paths + len(project_paths),
)
def _turn_metrics_from_dict(data: dict[str, Any]) -> TurnMetrics:
return TurnMetrics(**data)
def _mode_summary_from_dict(data: dict[str, Any]) -> ModeSummary:
turns = [_turn_metrics_from_dict(turn) for turn in data.get("turns", [])]
summary = ModeSummary(
mode=data["mode"],
sessions=data.get("sessions", 0),
requests=data.get("requests", 0),
raw_input_tokens=data.get("raw_input_tokens", 0),
forwarded_input_tokens=data.get("forwarded_input_tokens", 0),
cache_read_tokens=data.get("cache_read_tokens", 0),
cache_write_tokens=data.get("cache_write_tokens", 0),
regular_input_tokens=data.get("regular_input_tokens", 0),
output_tokens=data.get("output_tokens", 0),
paid_input_cost_usd=data.get("paid_input_cost_usd", 0.0),
cache_read_cost_usd=data.get("cache_read_cost_usd", 0.0),
cache_write_cost_usd=data.get("cache_write_cost_usd", 0.0),
paid_output_cost_usd=data.get("paid_output_cost_usd", 0.0),
total_cost_usd=data.get("total_cost_usd", 0.0),
cache_eligible_turns=data.get("cache_eligible_turns", 0),
cache_bust_turns=data.get("cache_bust_turns", 0),
ttl_expiry_turns=data.get("ttl_expiry_turns", 0),
rewrite_turns=data.get("rewrite_turns", 0),
stable_replay_rewrite_turns=data.get("stable_replay_rewrite_turns", 0),
busting_rewrite_turns=data.get("busting_rewrite_turns", 0),
non_cache_eligible_rewrite_turns=data.get("non_cache_eligible_rewrite_turns", 0),
retroactive_rewrite_turns=data.get("retroactive_rewrite_turns", 0),
latest_turn_only_rewrite_turns=data.get("latest_turn_only_rewrite_turns", 0),
turns=turns,
)
return summary
def decode_project_key(project_key: str) -> str:
"""Decode Claude's project directory encoding back to a local path-ish string."""
if "--" not in project_key:
return project_key.replace("-", "\\")
drive, remainder = project_key.split("--", 1)
return drive + ":\\" + remainder.replace("-", "\\")
def _parse_timestamp(value: str | None) -> datetime:
if not value:
return datetime.min.replace(tzinfo=timezone.utc)
if value.endswith("Z"):
value = value[:-1] + "+00:00"
return datetime.fromisoformat(value).astimezone(timezone.utc)
def _canonical_block_key(block: Any) -> str:
return json.dumps(block, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def _assistant_blocks_from_content(content: Any) -> list[dict[str, Any]]:
if isinstance(content, str):
return [{"type": "text", "text": content}] if content else []
if isinstance(content, list):
return [block for block in content if isinstance(block, dict)]
return []
def _messages_have_images(messages: list[dict[str, Any]]) -> bool:
for message in messages:
content = message.get("content")
if not isinstance(content, list):
continue
for block in content:
if isinstance(block, dict) and block.get("type") == "image":
return True
return False
def _finalize_group(
group: dict[str, Any] | None,
pending_messages: list[dict[str, Any]],
turns: list[ReplayTurn],
*,
session_id: str,
project_key: str,
decoded_project_path: str,
) -> None:
if not group:
return
assistant_message = {
"role": "assistant",
"content": group["blocks"] if group["blocks"] else "",
}
turns.append(
ReplayTurn(
session_id=session_id,
project_key=project_key,
decoded_project_path=decoded_project_path,
request_id=group["request_id"],
model=group["model"],
timestamp=group["timestamp"],
input_messages=copy.deepcopy(pending_messages),
assistant_message=assistant_message,
output_tokens=group["output_tokens"],
observed_input_tokens=group["observed_input_tokens"],
observed_cache_read_tokens=group["observed_cache_read_tokens"],
observed_cache_write_tokens=group["observed_cache_write_tokens"],
)
)
def load_session_replay(session_file: Path) -> SessionReplay | None:
"""Load a top-level Claude session transcript into replayable request turns."""
project_key = session_file.parent.name
decoded_project_path = decode_project_key(project_key)
session_id = session_file.stem
pending_messages: list[dict[str, Any]] = []
turns: list[ReplayTurn] = []
current_group: dict[str, Any] | None = None
try:
with session_file.open("r", encoding="utf-8") as handle:
for raw_line in handle:
line = raw_line.strip()
if not line:
continue
try:
event = json.loads(line)
except json.JSONDecodeError:
continue
event_type = event.get("type")
message = event.get("message")
if (
event_type == "user"
and isinstance(message, dict)
and message.get("role") == "user"
):
_finalize_group(
current_group,
pending_messages,
turns,
session_id=session_id,
project_key=project_key,
decoded_project_path=decoded_project_path,
)
current_group = None
pending_messages.clear()
pending_messages.append(copy.deepcopy(message))
continue
if (
event_type == "assistant"
and isinstance(message, dict)
and message.get("role") == "assistant"
and event.get("requestId")
):
request_id = str(event["requestId"])
usage = message.get("usage") or {}
timestamp = _parse_timestamp(event.get("timestamp"))
blocks = _assistant_blocks_from_content(message.get("content"))
if current_group is None or current_group["request_id"] != request_id:
had_group = current_group is not None
_finalize_group(
current_group,
pending_messages,
turns,
session_id=session_id,
project_key=project_key,
decoded_project_path=decoded_project_path,
)
if had_group:
pending_messages.clear()
current_group = {
"request_id": request_id,
"model": str(message.get("model", "unknown")),
"timestamp": timestamp,
"blocks": [],
"seen": set(),
"output_tokens": 0,
"observed_input_tokens": 0,
"observed_cache_read_tokens": 0,
"observed_cache_write_tokens": 0,
}
for block in blocks:
key = _canonical_block_key(block)
if key not in current_group["seen"]:
current_group["seen"].add(key)
current_group["blocks"].append(copy.deepcopy(block))
current_group["output_tokens"] = max(
current_group["output_tokens"],
int(usage.get("output_tokens", 0) or 0),
)
current_group["observed_input_tokens"] = max(
current_group["observed_input_tokens"],
int(usage.get("input_tokens", 0) or 0),
)
current_group["observed_cache_read_tokens"] = max(
current_group["observed_cache_read_tokens"],
int(usage.get("cache_read_input_tokens", 0) or 0),
)
current_group["observed_cache_write_tokens"] = max(
current_group["observed_cache_write_tokens"],
int(usage.get("cache_creation_input_tokens", 0) or 0),
)
except OSError:
return None
_finalize_group(
current_group,
pending_messages,
turns,
session_id=session_id,
project_key=project_key,
decoded_project_path=decoded_project_path,
)
if not turns:
return None
return SessionReplay(
session_id=session_id,
project_key=project_key,
decoded_project_path=decoded_project_path,
turns=turns,
)
def trim_replay_to_recent_turns(
replay: SessionReplay, recent_turns: int | None = None
) -> SessionReplay:
if recent_turns is None or recent_turns <= 0 or len(replay.turns) <= recent_turns:
return replay
return SessionReplay(
session_id=replay.session_id,
project_key=replay.project_key,
decoded_project_path=replay.decoded_project_path,
turns=replay.turns[-recent_turns:],
)
def resolve_checkpoint_dir(
base_dir: Path,
*,
recent_turns_per_session: int | None = None,
cache_ttl_minutes: int = DEFAULT_CACHE_TTL_MINUTES,
) -> Path:
suffix_parts = ["v5", f"ttl_{cache_ttl_minutes}m"]
if recent_turns_per_session:
suffix_parts.append(f"recent_{recent_turns_per_session}")
else:
suffix_parts.append("full")
return base_dir / "__".join(suffix_parts)
def discover_session_files(root: Path) -> list[Path]:
if not root.exists():
return []
files: list[Path] = []
for project_dir in sorted(p for p in root.iterdir() if p.is_dir()):
files.extend(
sorted(p for p in project_dir.iterdir() if p.is_file() and p.suffix == ".jsonl")
)
return files
def load_replays(root: Path, max_sessions: int | None = None) -> list[SessionReplay]:
replays: list[SessionReplay] = []
session_files = discover_session_files(root)
total = len(session_files)
for index, session_file in enumerate(session_files, start=1):
if index == 1 or index % 10 == 0 or index == total:
print(f"[load] session={index}/{total} file={session_file.name}", flush=True)
replay = load_session_replay(session_file)
if replay is not None:
replays.append(replay)
if max_sessions is not None and len(replays) >= max_sessions:
break
return replays
def select_session_files(root: Path, max_sessions: int | None = None) -> list[Path]:
session_files = discover_session_files(root)
if max_sessions is not None:
session_files = session_files[:max_sessions]
return session_files
def build_dataset_and_observed_from_files(
session_files: list[Path],
*,
cache_write_multiplier: float = 1.25,
recent_turns_per_session: int | None = None,
) -> tuple[DatasetSummary, ObservedSummary]:
model_counts: Counter[str] = Counter()
project_keys: set[str] = set()
decoded_project_paths: set[str] = set()
requests = 0
observed = ObservedSummary()
total = len(session_files)
for index, session_file in enumerate(session_files, start=1):
if index == 1 or index % 10 == 0 or index == total:
print(f"[load] session={index}/{total} file={session_file.name}", flush=True)
replay = load_session_replay(session_file)
if replay is None:
continue
replay = trim_replay_to_recent_turns(replay, recent_turns_per_session)
project_keys.add(replay.project_key)
decoded_project_paths.add(replay.decoded_project_path)
observed.sessions += 1
for turn in replay.turns:
model_counts[turn.model] += 1
requests += 1
rates = _resolve_model_rates(turn.model, cache_write_multiplier=cache_write_multiplier)
observed.requests += 1
observed.input_tokens += turn.observed_input_tokens
observed.cache_read_tokens += turn.observed_cache_read_tokens
observed.cache_write_tokens += turn.observed_cache_write_tokens
observed.output_tokens += turn.output_tokens
observed.paid_input_cost_usd += turn.observed_input_tokens * rates["input"]
observed.cache_read_cost_usd += turn.observed_cache_read_tokens * rates["cache_read"]
observed.cache_write_cost_usd += turn.observed_cache_write_tokens * rates["cache_write"]
observed.paid_output_cost_usd += turn.output_tokens * rates["output"]
prev_read = 0
prev_write = 0
for turn in replay.turns:
read = turn.observed_cache_read_tokens
write = turn.observed_cache_write_tokens
if read > prev_read and write <= prev_write:
observed.healthy_growth_turns += 1
if read == prev_read and write > prev_write:
observed.broken_prefix_turns += 1
if read < prev_read and write > 0:
observed.resume_like_resets += 1
prev_read = read
prev_write = write
observed.total_cost_usd = (
observed.paid_input_cost_usd
+ observed.cache_read_cost_usd
+ observed.cache_write_cost_usd
+ observed.paid_output_cost_usd
)
dataset = DatasetSummary(
projects=len(project_keys),
sessions=observed.sessions,
requests=requests,
models=dict(sorted(model_counts.items())),
decoded_project_paths=len(decoded_project_paths),
sampled_requests=requests,
sampling_note=(
f"Most recent {recent_turns_per_session} turns per session"
if recent_turns_per_session
else "Full replayable session history"
),
)
return dataset, observed
def summarize_dataset(replays: list[SessionReplay]) -> DatasetSummary:
model_counts: Counter[str] = Counter()
project_paths: set[str] = set()
requests = 0
for replay in replays:
project_paths.add(replay.decoded_project_path)
for turn in replay.turns:
model_counts[turn.model] += 1
requests += 1
return DatasetSummary(
projects=len({r.project_key for r in replays}),
sessions=len(replays),
requests=requests,
models=dict(sorted(model_counts.items())),
decoded_project_paths=len(project_paths),
)
def summarize_observed_usage(
replays: list[SessionReplay], *, cache_write_multiplier: float = 1.25
) -> ObservedSummary:
summary = ObservedSummary(sessions=len(replays))
for replay in replays:
prev_read = 0
prev_write = 0
for turn in replay.turns:
rates = _resolve_model_rates(turn.model, cache_write_multiplier=cache_write_multiplier)
summary.requests += 1
summary.input_tokens += turn.observed_input_tokens
summary.cache_read_tokens += turn.observed_cache_read_tokens
summary.cache_write_tokens += turn.observed_cache_write_tokens
summary.output_tokens += turn.output_tokens
summary.paid_input_cost_usd += turn.observed_input_tokens * rates["input"]
summary.cache_read_cost_usd += turn.observed_cache_read_tokens * rates["cache_read"]
summary.cache_write_cost_usd += turn.observed_cache_write_tokens * rates["cache_write"]
summary.paid_output_cost_usd += turn.output_tokens * rates["output"]
read = turn.observed_cache_read_tokens
write = turn.observed_cache_write_tokens
if read > prev_read and write <= prev_write:
summary.healthy_growth_turns += 1
if read == prev_read and write > prev_write:
summary.broken_prefix_turns += 1
if read < prev_read and write > 0:
summary.resume_like_resets += 1
prev_read = read
prev_write = write
summary.total_cost_usd = (
summary.paid_input_cost_usd
+ summary.cache_read_cost_usd
+ summary.cache_write_cost_usd
+ summary.paid_output_cost_usd
)
return summary
def _common_prefix_tokens(
prev: list[dict[str, Any]],
curr: list[dict[str, Any]],
tokenizer: Any,
) -> int:
common = 0
for a, b in zip(prev, curr):
if a != b:
break
common += tokenizer.count_message(b)
return common
def _rewrite_scope(
original_messages: list[dict[str, Any]],
forwarded_messages: list[dict[str, Any]],
*,
stable_prefix_message_count: int,
) -> tuple[bool, bool]:
if original_messages == forwarded_messages:
return False, False
stable_count = min(
stable_prefix_message_count,
len(original_messages),
len(forwarded_messages),
)
retroactive = False
if len(forwarded_messages) < stable_prefix_message_count:
retroactive = True
elif stable_count > 0 and forwarded_messages[:stable_count] != original_messages[:stable_count]:
retroactive = True
return True, retroactive
def _extract_cache_stable_delta(
current_messages: list[dict[str, Any]],
previous_original_messages: list[dict[str, Any]] | None,
previous_forwarded_messages: list[dict[str, Any]] | None,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]] | None:
if previous_original_messages is None or previous_forwarded_messages is None:
return None
if len(current_messages) < len(previous_original_messages):
return None
stable_count = len(previous_original_messages)
if current_messages[:stable_count] != previous_original_messages:
return None
return (
copy.deepcopy(previous_forwarded_messages),
copy.deepcopy(current_messages[stable_count:]),
)
def _extract_cache_stable_last_message_suffix(
current_messages: list[dict[str, Any]],
previous_original_messages: list[dict[str, Any]] | None,
previous_forwarded_messages: list[dict[str, Any]] | None,
) -> tuple[list[dict[str, Any]], dict[str, Any], list[dict[str, Any]]] | None:
if not previous_original_messages or previous_forwarded_messages is None:
return None
if (
len(current_messages) != len(previous_original_messages)
or len(previous_forwarded_messages) != len(previous_original_messages)
or not current_messages
):
return None
prefix_len = len(current_messages) - 1
if prefix_len > 0 and current_messages[:prefix_len] != previous_original_messages[:prefix_len]:
return None
current_last = current_messages[-1]
previous_original_last = previous_original_messages[-1]
previous_forwarded_last = previous_forwarded_messages[-1]
if current_last.get("role") != previous_original_last.get("role") or current_last.get(
"role"
) != previous_forwarded_last.get("role"):
return None
current_content = current_last.get("content")
previous_original_content = previous_original_last.get("content")
previous_forwarded_content = previous_forwarded_last.get("content")
if (
isinstance(current_content, str)
and isinstance(previous_original_content, str)
and isinstance(previous_forwarded_content, str)
and current_content.startswith(previous_original_content)
):
suffix = current_content[len(previous_original_content) :]
delta_messages = []
if suffix:
delta_messages = [{**copy.deepcopy(current_last), "content": suffix}]
return (
copy.deepcopy(previous_forwarded_messages[:-1]),
copy.deepcopy(previous_forwarded_last),
delta_messages,
)
if (
isinstance(current_content, list)
and isinstance(previous_original_content, list)
and isinstance(previous_forwarded_content, list)
and len(current_content) >= len(previous_original_content)
and current_content[: len(previous_original_content)] == previous_original_content
):
delta_blocks = copy.deepcopy(current_content[len(previous_original_content) :])
delta_messages = []
if delta_blocks:
delta_messages = [{**copy.deepcopy(current_last), "content": delta_blocks}]
return (
copy.deepcopy(previous_forwarded_messages[:-1]),
copy.deepcopy(previous_forwarded_last),
delta_messages,
)
return None
def _merge_appended_message_delta(
previous_forwarded_message: dict[str, Any],
delta_forwarded_message: dict[str, Any] | None,
) -> dict[str, Any] | None:
if delta_forwarded_message is None:
return copy.deepcopy(previous_forwarded_message)
if previous_forwarded_message.get("role") != delta_forwarded_message.get("role"):
return None
previous_content = previous_forwarded_message.get("content")
delta_content = delta_forwarded_message.get("content")
if isinstance(previous_content, str) and isinstance(delta_content, str):
return {
**copy.deepcopy(previous_forwarded_message),
"content": previous_content + delta_content,
}
if isinstance(previous_content, list) and isinstance(delta_content, list):
return {
**copy.deepcopy(previous_forwarded_message),
"content": copy.deepcopy(previous_content) + copy.deepcopy(delta_content),
}
return None
def _make_proxy(mode: str) -> HeadroomProxy:
cfg = ProxyConfig(
mode=mode,
optimize=True,
image_optimize=True,
smart_routing=False,
code_aware_enabled=False,
read_lifecycle=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
log_requests=False,
ccr_inject_tool=False,
ccr_handle_responses=False,
ccr_context_tracking=False,
)
return HeadroomProxy(cfg)
def _apply_mode_to_messages(
proxy: HeadroomProxy | None,
mode: str,
messages: list[dict[str, Any]],
*,
model: str,
prefix_tracker: PrefixCacheTracker | None,
comp_cache: CompressionCache | None,
previous_original_messages: list[dict[str, Any]] | None = None,
previous_forwarded_messages: list[dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
if mode == "baseline":
return copy.deepcopy(messages)
assert proxy is not None
assert prefix_tracker is not None
if mode == PROXY_MODE_CACHE:
supports_delta_replay = hasattr(
AnthropicHandlerMixin, "_extract_cache_stable_last_message_suffix"
)
if not supports_delta_replay:
frozen_message_count = prefix_tracker.get_frozen_message_count()
context_limit = proxy.anthropic_provider.get_context_limit(model)
result = proxy.anthropic_pipeline.apply(
messages=copy.deepcopy(messages),
model=model,
model_limit=context_limit,
context=extract_user_query(messages),
frozen_message_count=frozen_message_count,
)
if hasattr(AnthropicHandlerMixin, "_restore_frozen_prefix"):
result.messages, _ = AnthropicHandlerMixin._restore_frozen_prefix(
messages,
result.messages,
frozen_message_count=frozen_message_count,
)
return result.messages
delta = _extract_cache_stable_delta(
messages,
previous_original_messages,
previous_forwarded_messages,
)
if delta is not None:
stable_forwarded_prefix, delta_messages = delta
if not delta_messages:
return stable_forwarded_prefix
context_limit = proxy.anthropic_provider.get_context_limit(model)
result = proxy.anthropic_pipeline.apply(
messages=delta_messages,
model=model,
model_limit=context_limit,
context=extract_user_query(delta_messages),
frozen_message_count=0,
)
return stable_forwarded_prefix + result.messages
return copy.deepcopy(messages)
frozen_message_count = prefix_tracker.get_frozen_message_count()
working_messages = copy.deepcopy(messages)
if proxy.config.image_optimize and working_messages and _messages_have_images(working_messages):
from headroom.proxy.helpers import _get_image_compressor
compressor = _get_image_compressor()
if compressor and compressor.has_images(working_messages):
if mode == PROXY_MODE_CACHE:
working_messages = (
AnthropicHandlerMixin._compress_latest_user_turn_images_cache_safe(
working_messages,
frozen_message_count=frozen_message_count,
compressor=compressor,
)
)
else:
working_messages = compressor.compress(working_messages, provider="anthropic")
if mode == PROXY_MODE_TOKEN and comp_cache is not None:
working_messages = comp_cache.apply_cached(working_messages)
cache_frozen_count = comp_cache.compute_frozen_count(messages)
frozen_message_count = min(frozen_message_count, cache_frozen_count)
context_limit = proxy.anthropic_provider.get_context_limit(model)
result = proxy.anthropic_pipeline.apply(
messages=working_messages,
model=model,
model_limit=context_limit,
context=extract_user_query(working_messages),
frozen_message_count=frozen_message_count,
)
forwarded = result.messages
if mode == PROXY_MODE_TOKEN and comp_cache is not None and forwarded != working_messages:
comp_cache.update_from_result(messages, forwarded)
if mode == PROXY_MODE_CACHE:
forwarded, _ = AnthropicHandlerMixin._restore_frozen_prefix(
messages,
forwarded,
frozen_message_count=frozen_message_count,
)
return forwarded
@dataclass
class _PendingTurn:
summary: ModeSummary
turn: ReplayTurn
tokenizer: Any
raw_input_tokens: int
request_messages: list[dict[str, Any]]
forwarded: list[dict[str, Any]]
rewrite: bool
retroactive_rewrite: bool
def _cache_gap_within_ttl(
current_ts: datetime,
previous_ts: datetime | None,
*,
ttl: timedelta,
) -> bool:
if previous_ts is None:
return False
return current_ts - previous_ts <= ttl
def _resolve_model_rates(model: str, *, cache_write_multiplier: float) -> dict[str, float]:
pricing = get_model_pricing(model)
if pricing is None:
if "opus" in model:
input_per_1m = 15.0
output_per_1m = 75.0
elif "haiku" in model:
input_per_1m = 1.0
output_per_1m = 5.0
else:
input_per_1m = 3.0
output_per_1m = 15.0
else:
input_per_1m = pricing.input_cost_per_1m
output_per_1m = pricing.output_cost_per_1m
return {
"input": input_per_1m / 1_000_000,
"output": output_per_1m / 1_000_000,
"cache_read": (input_per_1m * 0.10) / 1_000_000,
"cache_write": (input_per_1m * cache_write_multiplier) / 1_000_000,
}
def _apply_turn_metrics(
summary: ModeSummary,
turn: ReplayTurn,
*,
raw_input_tokens: int,
tokenizer: Any,
forwarded: list[dict[str, Any]],
previous_forwarded: list[dict[str, Any]],
previous_timestamp: datetime | None,
next_forwarded: list[dict[str, Any]] | None,
next_timestamp: datetime | None,
ttl: timedelta,
cache_write_multiplier: float,
) -> None:
forwarded_input_tokens = tokenizer.count_messages(forwarded)
read_tokens = 0
cache_eligible = _cache_gap_within_ttl(turn.timestamp, previous_timestamp, ttl=ttl)
if cache_eligible:
read_tokens = _common_prefix_tokens(previous_forwarded, forwarded, tokenizer)
summary.cache_eligible_turns += 1
prefix_preserved = (
len(forwarded) >= len(previous_forwarded)
and forwarded[: len(previous_forwarded)] == previous_forwarded
)
if previous_forwarded and not prefix_preserved:
summary.cache_bust_turns += 1
elif previous_timestamp is not None:
summary.ttl_expiry_turns += 1
write_tokens = 0
if next_forwarded is not None and _cache_gap_within_ttl(
next_timestamp, turn.timestamp, ttl=ttl
):
next_common = _common_prefix_tokens(forwarded, next_forwarded, tokenizer)
write_tokens = max(0, next_common - read_tokens)
regular_input_tokens = max(0, forwarded_input_tokens - read_tokens - write_tokens)
rates = _resolve_model_rates(turn.model, cache_write_multiplier=cache_write_multiplier)
paid_input_cost_usd = regular_input_tokens * rates["input"]
cache_read_cost_usd = read_tokens * rates["cache_read"]
cache_write_cost_usd = write_tokens * rates["cache_write"]
paid_output_cost_usd = turn.output_tokens * rates["output"]
total_cost_usd = (
paid_input_cost_usd + cache_read_cost_usd + cache_write_cost_usd + paid_output_cost_usd
)
summary.requests += 1
summary.raw_input_tokens += raw_input_tokens
summary.forwarded_input_tokens += forwarded_input_tokens
summary.cache_read_tokens += read_tokens
summary.cache_write_tokens += write_tokens
summary.regular_input_tokens += regular_input_tokens
summary.output_tokens += turn.output_tokens
summary.paid_input_cost_usd += paid_input_cost_usd
summary.cache_read_cost_usd += cache_read_cost_usd
summary.cache_write_cost_usd += cache_write_cost_usd
summary.paid_output_cost_usd += paid_output_cost_usd
summary.total_cost_usd += total_cost_usd
def _merge_mode_summary(target: ModeSummary, source: ModeSummary) -> None:
target.sessions += source.sessions
target.requests += source.requests
target.raw_input_tokens += source.raw_input_tokens
target.forwarded_input_tokens += source.forwarded_input_tokens
target.cache_read_tokens += source.cache_read_tokens
target.cache_write_tokens += source.cache_write_tokens
target.regular_input_tokens += source.regular_input_tokens
target.output_tokens += source.output_tokens
target.paid_input_cost_usd += source.paid_input_cost_usd
target.cache_read_cost_usd += source.cache_read_cost_usd
target.cache_write_cost_usd += source.cache_write_cost_usd
target.paid_output_cost_usd += source.paid_output_cost_usd
target.total_cost_usd += source.total_cost_usd
target.cache_eligible_turns += source.cache_eligible_turns
target.cache_bust_turns += source.cache_bust_turns
target.ttl_expiry_turns += source.ttl_expiry_turns
target.rewrite_turns += source.rewrite_turns
target.stable_replay_rewrite_turns += source.stable_replay_rewrite_turns
target.busting_rewrite_turns += source.busting_rewrite_turns
target.non_cache_eligible_rewrite_turns += source.non_cache_eligible_rewrite_turns
target.retroactive_rewrite_turns += source.retroactive_rewrite_turns
target.latest_turn_only_rewrite_turns += source.latest_turn_only_rewrite_turns
def _disable_headroom_benchmark_logging() -> None:
logging.raiseExceptions = False
for logger_name in (
"headroom",
"headroom.cache",
"headroom.cache.compression_cache",
"headroom.proxy",
"headroom.transforms",
):
logger = logging.getLogger(logger_name)
logger.handlers.clear()
logger.propagate = False
logger.setLevel(logging.CRITICAL)
def _checkpoint_path(checkpoint_dir: Path, mode: str, replay: SessionReplay) -> Path:
return checkpoint_dir / f"{mode}--{replay.session_id}.json"
def _checkpoint_path_for_session_id(checkpoint_dir: Path, mode: str, session_id: str) -> Path:
return checkpoint_dir / f"{mode}--{session_id}.json"
def _load_checkpoint(checkpoint_dir: Path, mode: str, replay: SessionReplay) -> ModeSummary | None:
path = _checkpoint_path(checkpoint_dir, mode, replay)
if not path.exists():
return None
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
return _mode_summary_from_dict(payload)
def _load_checkpoint_by_session_id(
checkpoint_dir: Path, mode: str, session_id: str
) -> ModeSummary | None:
path = _checkpoint_path_for_session_id(checkpoint_dir, mode, session_id)
if not path.exists():
return None
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return None
return _mode_summary_from_dict(payload)
def _write_checkpoint(
checkpoint_dir: Path,
mode: str,
replay: SessionReplay,
summary: ModeSummary,
) -> None:
checkpoint_dir.mkdir(parents=True, exist_ok=True)
path = _checkpoint_path(checkpoint_dir, mode, replay)
payload = asdict(summary)
payload["turns"] = []
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
def _write_checkpoint_by_session_id(
checkpoint_dir: Path, mode: str, session_id: str, summary: ModeSummary
) -> None:
checkpoint_dir.mkdir(parents=True, exist_ok=True)
path = _checkpoint_path_for_session_id(checkpoint_dir, mode, session_id)
payload = asdict(summary)
payload["turns"] = []
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
def _update_prefix_tracker(
prefix_tracker: PrefixCacheTracker,
*,
cache_read_tokens: int,
cache_write_tokens: int,
messages: list[dict[str, Any]],
message_token_counts: list[int],
original_messages: list[dict[str, Any]] | None = None,
) -> None:
try:
prefix_tracker.update_from_response(
cache_read_tokens=cache_read_tokens,
cache_write_tokens=cache_write_tokens,
messages=messages,
message_token_counts=message_token_counts,
original_messages=original_messages,
)
except TypeError:
prefix_tracker.update_from_response(
cache_read_tokens=cache_read_tokens,
cache_write_tokens=cache_write_tokens,
messages=messages,
message_token_counts=message_token_counts,
)
def _simulate_single_replay_mode(
replay: SessionReplay,
mode: str,
cache_ttl_minutes: int,
cache_write_multiplier: float,
) -> ModeSummary:
_disable_headroom_benchmark_logging()
summary = ModeSummary(mode=mode, sessions=1)
ttl = timedelta(minutes=cache_ttl_minutes)
proxy = None if mode == "baseline" else _make_proxy(mode)
pending: _PendingTurn | None = None
conversation: list[dict[str, Any]] = []
conversation_token_total = 0
previous_forwarded: list[dict[str, Any]] = []
previous_original_context: list[dict[str, Any]] | None = None
previous_forwarded_context: list[dict[str, Any]] | None = None
previous_timestamp: datetime | None = None
prefix_tracker = None if mode == "baseline" else PrefixCacheTracker("anthropic")
comp_cache = CompressionCache() if mode == PROXY_MODE_TOKEN else None
for turn in replay.turns:
tokenizer = get_tokenizer(turn.model)
turn_input_token_total = sum(tokenizer.count_message(msg) for msg in turn.input_messages)
prior_context_message_count = len(conversation)
conversation.extend(turn.input_messages)
raw_input_tokens = conversation_token_total + turn_input_token_total
forwarded = _apply_mode_to_messages(
proxy,
mode,
conversation,
model=turn.model,
prefix_tracker=prefix_tracker,
comp_cache=comp_cache,
previous_original_messages=previous_original_context,
previous_forwarded_messages=previous_forwarded_context,
)
rewrite, retroactive_rewrite = _rewrite_scope(
conversation,
forwarded,
stable_prefix_message_count=prior_context_message_count,
)
if rewrite:
summary.rewrite_turns += 1
if retroactive_rewrite:
summary.retroactive_rewrite_turns += 1
else:
summary.latest_turn_only_rewrite_turns += 1
prior_forwarded_for_rewrite = (
pending.forwarded if pending is not None else previous_forwarded
)
prior_timestamp_for_rewrite = (
pending.turn.timestamp if pending is not None else previous_timestamp
)
if (
prior_timestamp_for_rewrite is not None
and _cache_gap_within_ttl(turn.timestamp, prior_timestamp_for_rewrite, ttl=ttl)
and prior_forwarded_for_rewrite
):
prefix_preserved = (
len(forwarded) >= len(prior_forwarded_for_rewrite)
and forwarded[: len(prior_forwarded_for_rewrite)] == prior_forwarded_for_rewrite
)
if prefix_preserved:
summary.stable_replay_rewrite_turns += 1
else:
summary.busting_rewrite_turns += 1
else:
summary.non_cache_eligible_rewrite_turns += 1
if pending is not None:
_apply_turn_metrics(
pending.summary,
pending.turn,
raw_input_tokens=pending.raw_input_tokens,
tokenizer=pending.tokenizer,
forwarded=pending.forwarded,
previous_forwarded=previous_forwarded,
previous_timestamp=previous_timestamp,
next_forwarded=forwarded,
next_timestamp=turn.timestamp,
ttl=ttl,
cache_write_multiplier=cache_write_multiplier,
)
previous_forwarded = copy.deepcopy(pending.forwarded)
previous_timestamp = pending.turn.timestamp
if prefix_tracker is not None:
_update_prefix_tracker(
prefix_tracker,
cache_read_tokens=0,
cache_write_tokens=0,
messages=forwarded,
message_token_counts=[tokenizer.count_message(msg) for msg in forwarded],
original_messages=conversation,
)
pending = _PendingTurn(
summary=summary,
turn=turn,
tokenizer=tokenizer,
raw_input_tokens=raw_input_tokens,
request_messages=copy.deepcopy(conversation),
forwarded=forwarded,
rewrite=rewrite,
retroactive_rewrite=retroactive_rewrite,
)
conversation.append(turn.assistant_message)
conversation_token_total = raw_input_tokens + tokenizer.count_message(
turn.assistant_message
)
previous_original_context = copy.deepcopy(conversation)
previous_forwarded_context = copy.deepcopy(forwarded) + [
copy.deepcopy(turn.assistant_message)
]
if pending is not None:
_apply_turn_metrics(
pending.summary,
pending.turn,
raw_input_tokens=pending.raw_input_tokens,
tokenizer=pending.tokenizer,
forwarded=pending.forwarded,
previous_forwarded=previous_forwarded,
previous_timestamp=previous_timestamp,
next_forwarded=None,
next_timestamp=None,
ttl=ttl,
cache_write_multiplier=cache_write_multiplier,
)
return summary
def _simulate_single_session_file_mode(
session_file: Path,
mode: str,
cache_ttl_minutes: int,
cache_write_multiplier: float,
recent_turns_per_session: int | None = None,
) -> tuple[str, ModeSummary]:
replay = load_session_replay(session_file)
if replay is None:
return session_file.stem, ModeSummary(mode=mode)
replay = trim_replay_to_recent_turns(replay, recent_turns_per_session)
return replay.session_id, _simulate_single_replay_mode(
replay,
mode,
cache_ttl_minutes,
cache_write_multiplier,
)
def simulate_replays(
replays: list[SessionReplay],
*,
cache_ttl_minutes: int = DEFAULT_CACHE_TTL_MINUTES,
cache_write_multiplier: float = 1.25,
workers: int = 1,
checkpoint_dir: Path | None = None,
) -> tuple[DatasetSummary, dict[str, ModeSummary]]:
dataset = summarize_dataset(replays)
summaries = {
"baseline": ModeSummary(mode="baseline"),
PROXY_MODE_TOKEN: ModeSummary(mode=PROXY_MODE_TOKEN),
PROXY_MODE_CACHE: ModeSummary(mode=PROXY_MODE_CACHE),
}
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
print(f"[simulate] mode={mode} sessions={len(replays)}", flush=True)
worker_count = workers if workers > 0 else max(1, min(8, os.cpu_count() or 1))
if worker_count > 1 and len(replays) > 1:
with concurrent.futures.ProcessPoolExecutor(max_workers=worker_count) as executor:
future_map: dict[concurrent.futures.Future[ModeSummary], SessionReplay] = {}
completed = 0
for replay in replays:
cached = (
_load_checkpoint(checkpoint_dir, mode, replay)
if checkpoint_dir is not None
else None
)
if cached is not None:
completed += 1
_merge_mode_summary(summaries[mode], cached)
if completed == 1 or completed % 10 == 0 or completed == len(replays):
print(
f"[simulate] mode={mode} completed={completed}/{len(replays)}",
flush=True,
)
continue
future = executor.submit(
_simulate_single_replay_mode,
replay,
mode,
cache_ttl_minutes,
cache_write_multiplier,
)
future_map[future] = replay
for future in concurrent.futures.as_completed(future_map):
replay = future_map[future]
partial = future.result()
if checkpoint_dir is not None:
_write_checkpoint(checkpoint_dir, mode, replay, partial)
completed += 1
if completed == 1 or completed % 10 == 0 or completed == len(replays):
print(
f"[simulate] mode={mode} completed={completed}/{len(replays)}",
flush=True,
)
_merge_mode_summary(summaries[mode], partial)
else:
for index, replay in enumerate(replays, start=1):
cached = (
_load_checkpoint(checkpoint_dir, mode, replay)
if checkpoint_dir is not None
else None
)
if cached is not None:
_merge_mode_summary(summaries[mode], cached)
continue
if index == 1 or index % 10 == 0 or index == len(replays):
print(
f"[simulate] mode={mode} session={index}/{len(replays)} "
f"requests={len(replay.turns)}",
flush=True,
)
partial = _simulate_single_replay_mode(
replay,
mode,
cache_ttl_minutes,
cache_write_multiplier,
)
if checkpoint_dir is not None:
_write_checkpoint(checkpoint_dir, mode, replay, partial)
_merge_mode_summary(summaries[mode], partial)
return dataset, summaries
def simulate_session_files(
session_files: list[Path],
dataset: DatasetSummary,
*,
cache_ttl_minutes: int = DEFAULT_CACHE_TTL_MINUTES,
cache_write_multiplier: float = 1.25,
workers: int = 1,
checkpoint_dir: Path | None = None,
recent_turns_per_session: int | None = None,
) -> dict[str, ModeSummary]:
summaries = {
"baseline": ModeSummary(mode="baseline"),
PROXY_MODE_TOKEN: ModeSummary(mode=PROXY_MODE_TOKEN),
PROXY_MODE_CACHE: ModeSummary(mode=PROXY_MODE_CACHE),
}
total = len(session_files)
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
print(f"[simulate] mode={mode} sessions={total}", flush=True)
worker_count = workers if workers > 0 else 1
if worker_count > 1 and total > 1:
with concurrent.futures.ProcessPoolExecutor(
max_workers=worker_count,
initializer=_disable_headroom_benchmark_logging,
) as executor:
future_map: dict[concurrent.futures.Future[tuple[str, ModeSummary]], str] = {}
completed = 0
for session_file in session_files:
session_id = session_file.stem
cached = (
_load_checkpoint_by_session_id(checkpoint_dir, mode, session_id)
if checkpoint_dir is not None
else None
)
if cached is not None:
completed += 1
_merge_mode_summary(summaries[mode], cached)
if completed == 1 or completed % 10 == 0 or completed == total:
print(
f"[simulate] mode={mode} completed={completed}/{total}",
flush=True,
)
continue
future = executor.submit(
_simulate_single_session_file_mode,
session_file,
mode,
cache_ttl_minutes,
cache_write_multiplier,
recent_turns_per_session,
)
future_map[future] = session_id
for future in concurrent.futures.as_completed(future_map):
session_id, partial = future.result()
if checkpoint_dir is not None:
_write_checkpoint_by_session_id(checkpoint_dir, mode, session_id, partial)
completed += 1
if completed == 1 or completed % 10 == 0 or completed == total:
print(
f"[simulate] mode={mode} completed={completed}/{total}",
flush=True,
)
_merge_mode_summary(summaries[mode], partial)
else:
for index, session_file in enumerate(session_files, start=1):
session_id = session_file.stem
cached = (
_load_checkpoint_by_session_id(checkpoint_dir, mode, session_id)
if checkpoint_dir is not None
else None
)
if cached is not None:
_merge_mode_summary(summaries[mode], cached)
if index == 1 or index % 10 == 0 or index == total:
print(
f"[simulate] mode={mode} completed={index}/{total}",
flush=True,
)
continue
replay = load_session_replay(session_file)
if replay is None:
continue
replay = trim_replay_to_recent_turns(replay, recent_turns_per_session)
if index == 1 or index % 10 == 0 or index == total:
print(
f"[simulate] mode={mode} session={index}/{total} "
f"requests={len(replay.turns)}",
flush=True,
)
partial = _simulate_single_replay_mode(
replay,
mode,
cache_ttl_minutes,
cache_write_multiplier,
)
if checkpoint_dir is not None:
_write_checkpoint_by_session_id(checkpoint_dir, mode, session_id, partial)
_merge_mode_summary(summaries[mode], partial)
return summaries
def determine_winners(summaries: dict[str, ModeSummary]) -> dict[str, str]:
return {
"total_cost": min(summaries.values(), key=lambda s: s.total_cost_usd).mode,
"no_cache_total_cost": min(
summaries.values(), key=lambda s: s.no_cache_total_cost_usd
).mode,
"window_with_cache": min(summaries.values(), key=lambda s: s.prompt_window_with_cache).mode,
"window_without_cache_reads": min(
summaries.values(), key=lambda s: s.prompt_window_without_cache_reads
).mode,
}
def _metric_value(summary: ModeSummary, field: str) -> float:
value = getattr(summary, field)
return float(value)
def classify_metric_impact(
baseline: ModeSummary,
candidate: ModeSummary,
field: str,
) -> dict[str, float | str]:
baseline_value = _metric_value(baseline, field)
candidate_value = _metric_value(candidate, field)
delta = candidate_value - baseline_value
direction = IMPACT_DIRECTION[field]
tolerance = 1e-9
if abs(delta) <= tolerance:
impact = "no_change"
elif direction == "lower":
impact = "assist" if delta < 0 else "harm"
elif direction == "higher":
impact = "assist" if delta > 0 else "harm"
else:
impact = "harm" if abs(delta) > tolerance else "no_change"
return {
"baseline": baseline_value,
"candidate": candidate_value,
"delta": delta,
"impact": impact,
"direction": direction,
}
def summarize_mode_impact_vs_baseline(
summaries: dict[str, ModeSummary],
) -> dict[str, dict[str, dict[str, float | str]]]:
baseline = summaries["baseline"]
result: dict[str, dict[str, dict[str, float | str]]] = {}
for mode in (PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
candidate = summaries[mode]
result[mode] = {
field: classify_metric_impact(baseline, candidate, field) for field in IMPACT_DIRECTION
}
return result
def format_currency(value: float) -> str:
return f"${value:,.2f}"
def print_console_report(dataset: DatasetSummary, summaries: dict[str, ModeSummary]) -> None:
winners = determine_winners(summaries)
impacts = summarize_mode_impact_vs_baseline(summaries)
print("Claude session mode simulation")
print(
f"Dataset: {dataset.projects} projects, {dataset.sessions} sessions, "
f"{dataset.requests} requests"
)
print(f"Sampling: {dataset.sampling_note}")
print()
print(
"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"
)
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
summary = summaries[mode]
print(
f"{mode:<9} {summary.raw_tokens:>11,} {summary.cache_tokens:>12,} "
f"{summary.cache_read_tokens:>11,} {summary.cache_write_tokens:>12,} "
f"{summary.regular_input_tokens:>10,} {summary.output_tokens:>12,} "
f"{summary.cache_bust_turns:>7,} {summary.ttl_expiry_turns:>9,} "
f"{summary.rewrite_turns:>9,} {summary.stable_replay_rewrite_turns:>10,} "
f"{summary.busting_rewrite_turns:>8,} {summary.non_cache_eligible_rewrite_turns:>12,} "
f"{summary.retroactive_rewrite_turns:>10,} "
f"{format_currency(summary.total_cost_usd):>11} "
f"{format_currency(summary.no_cache_total_cost_usd):>11}"
)
print()
print(f"Winner by total cost: {winners['total_cost']}")
print(f"Winner by total cost with no cache help: {winners['no_cache_total_cost']}")
print(f"Winner if cache tokens count against window: {winners['window_with_cache']}")
print(
"Winner if cache read tokens do not count against window: "
f"{winners['window_without_cache_reads']}"
)
print()
print("Impact vs baseline")
for mode in (PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
impact = impacts[mode]
print(
f"{mode}: total_cost={impact['total_cost_usd']['impact']} "
f"({format_currency(impact['total_cost_usd']['delta'])}), "
f"cache_read={impact['cache_read_tokens']['impact']} "
f"({int(impact['cache_read_tokens']['delta']):,}), "
f"cache_write={impact['cache_write_tokens']['impact']} "
f"({int(impact['cache_write_tokens']['delta']):,}), "
f"paid_input={impact['regular_input_tokens']['impact']} "
f"({int(impact['regular_input_tokens']['delta']):,}), "
f"rewrite={impact['rewrite_turns']['impact']} "
f"({int(impact['rewrite_turns']['delta']):,}), "
f"stable_rw={impact['stable_replay_rewrite_turns']['impact']} "
f"({int(impact['stable_replay_rewrite_turns']['delta']):,}), "
f"bust_rw={impact['busting_rewrite_turns']['impact']} "
f"({int(impact['busting_rewrite_turns']['delta']):,}), "
f"noncache_rw={impact['non_cache_eligible_rewrite_turns']['impact']} "
f"({int(impact['non_cache_eligible_rewrite_turns']['delta']):,}), "
f"retro_rw={impact['retroactive_rewrite_turns']['impact']} "
f"({int(impact['retroactive_rewrite_turns']['delta']):,}), "
f"window={impact['prompt_window_with_cache']['impact']} "
f"({int(impact['prompt_window_with_cache']['delta']):,})"
)
def print_observed_console_report(observed: ObservedSummary) -> None:
print()
print("Observed Claude session usage")
print(
f"requests={observed.requests:,} cache_ratio={observed.cache_ratio_pct:.1f}% "
f"broken_prefix_turns={observed.broken_prefix_turns:,} "
f"resume_like_resets={observed.resume_like_resets:,}"
)
print(
f"input={observed.input_tokens:,} cache_read={observed.cache_read_tokens:,} "
f"cache_write={observed.cache_write_tokens:,} output={observed.output_tokens:,} "
f"total_cost={format_currency(observed.total_cost_usd)}"
)
def build_report_markdown(
dataset: DatasetSummary,
observed: ObservedSummary,
summaries: dict[str, ModeSummary],
) -> str:
winners = determine_winners(summaries)
impacts = summarize_mode_impact_vs_baseline(summaries)
model_lines = "\n".join(f"- `{model}`: {count}" for model, count in dataset.models.items())
rows = []
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
summary = summaries[mode]
rows.append(
"| "
+ " | ".join(
[
summary.mode,
f"{summary.raw_tokens:,}",
f"{summary.cache_tokens:,}",
f"{summary.cache_read_tokens:,}",
f"{summary.cache_write_tokens:,}",
f"{summary.regular_input_tokens:,}",
f"{summary.output_tokens:,}",
format_currency(summary.paid_input_cost_usd),
format_currency(summary.cache_read_cost_usd),
format_currency(summary.cache_write_cost_usd),
format_currency(summary.paid_output_cost_usd),
format_currency(summary.total_cost_usd),
format_currency(summary.no_cache_total_cost_usd),
f"{summary.cache_bust_turns:,}",
f"{summary.ttl_expiry_turns:,}",
f"{summary.rewrite_turns:,}",
f"{summary.stable_replay_rewrite_turns:,}",
f"{summary.busting_rewrite_turns:,}",
f"{summary.non_cache_eligible_rewrite_turns:,}",
f"{summary.retroactive_rewrite_turns:,}",
f"{summary.latest_turn_only_rewrite_turns:,}",
f"{summary.prompt_window_with_cache:,}",
f"{summary.prompt_window_without_cache_reads:,}",
]
)
+ " |"
)
impact_rows = []
for mode in (PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
for metric_key, label in (
("total_cost_usd", "Total Cost"),
("cache_read_tokens", "Cache Read Tokens"),
("cache_write_tokens", "Cache Write Tokens"),
("regular_input_tokens", "Paid Input Tokens"),
("output_tokens", "Paid Output Tokens"),
("prompt_window_with_cache", "Window With Cache"),
("prompt_window_without_cache_reads", "Window Without Cache Reads"),
("cache_bust_turns", "Cache Bust Turns"),
("rewrite_turns", "Rewrite Turns"),
("stable_replay_rewrite_turns", "Stable Replay Rewrite Turns"),
("busting_rewrite_turns", "Busting Rewrite Turns"),
("non_cache_eligible_rewrite_turns", "Non-Cache-Eligible Rewrite Turns"),
("retroactive_rewrite_turns", "Retroactive Rewrite Turns"),
("latest_turn_only_rewrite_turns", "Latest-Turn-Only Rewrite Turns"),
):
impact = impacts[mode][metric_key]
delta = impact["delta"]
delta_text = format_currency(delta) if "cost" in metric_key else f"{int(delta):,}"
impact_rows.append(
f"| {mode} | {label} | {impact['impact']} | {delta_text} | {impact['direction']} |"
)
return "\n".join(
[
"# Claude Session Mode Simulation",
"",
"## Dataset",
"",
f"- Projects: {dataset.projects}",
f"- Sessions: {dataset.sessions}",
f"- Requests: {dataset.requests}",
f"- Sampled requests: {dataset.sampled_requests}",
f"- Distinct decoded project paths: {dataset.decoded_project_paths}",
f"- Sampling: {dataset.sampling_note}",
"- Models:",
model_lines or "- None",
"",
"## Assumptions",
"",
"- Uses top-level session `.jsonl` files under `~/.claude/projects`.",
"- Replays only transcript-visible messages. Hidden system/tool schemas from Claude Code are not available in local transcript files and are therefore excluded.",
"- Simulates Anthropic prompt caching with a 5 minute TTL.",
"- Estimates cache read cost as 10% of base input price and cache write/store cost as 125% of base input price.",
"- Holds recorded output token counts constant across baseline/token/cache so comparisons isolate input-side behavior.",
"",
"## Observed",
"",
f"- Requests with observed usage: {observed.requests:,}",
f"- Cache ratio: {observed.cache_ratio_pct:.1f}%",
f"- Healthy growth turns: {observed.healthy_growth_turns:,}",
f"- Broken prefix turns: {observed.broken_prefix_turns:,}",
f"- Resume-like resets: {observed.resume_like_resets:,}",
f"- Observed total cost: {format_currency(observed.total_cost_usd)}",
"",
"## Summary",
"",
"| 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) |",
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
*rows,
"",
"## Impact vs Baseline",
"",
"| Mode | Metric | Classification | Delta | Better Direction |",
"| --- | --- | --- | ---: | --- |",
*impact_rows,
"",
"## Winners",
"",
f"- Total cost winner: `{winners['total_cost']}`",
f"- No-cache total cost winner: `{winners['no_cache_total_cost']}`",
f"- Window winner if cache tokens count: `{winners['window_with_cache']}`",
"- Window winner if cache read tokens do not count: "
f"`{winners['window_without_cache_reads']}`",
]
)
def build_report_html(
dataset: DatasetSummary,
observed: ObservedSummary,
summaries: dict[str, ModeSummary],
) -> str:
winners = determine_winners(summaries)
impacts = summarize_mode_impact_vs_baseline(summaries)
model_items = "".join(
f"<li><code>{model}</code><span>{count:,}</span></li>"
for model, count in dataset.models.items()
)
summary_rows = []
for mode in ("baseline", PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
summary = summaries[mode]
summary_rows.append(
"<tr>"
f"<td><span class='badge'>{summary.mode}</span></td>"
f"<td>{summary.raw_tokens:,}</td>"
f"<td>{summary.cache_tokens:,}</td>"
f"<td>{summary.cache_read_tokens:,}</td>"
f"<td>{summary.cache_write_tokens:,}</td>"
f"<td>{summary.regular_input_tokens:,}</td>"
f"<td>{summary.output_tokens:,}</td>"
f"<td>{summary.cache_bust_turns:,}</td>"
f"<td>{summary.ttl_expiry_turns:,}</td>"
f"<td>{summary.rewrite_turns:,}</td>"
f"<td>{summary.stable_replay_rewrite_turns:,}</td>"
f"<td>{summary.busting_rewrite_turns:,}</td>"
f"<td>{summary.non_cache_eligible_rewrite_turns:,}</td>"
f"<td>{summary.retroactive_rewrite_turns:,}</td>"
f"<td>{summary.latest_turn_only_rewrite_turns:,}</td>"
f"<td>{format_currency(summary.total_cost_usd)}</td>"
f"<td>{format_currency(summary.no_cache_total_cost_usd)}</td>"
f"<td>{summary.prompt_window_with_cache:,}</td>"
f"<td>{summary.prompt_window_without_cache_reads:,}</td>"
"</tr>"
)
impact_rows = []
for mode in (PROXY_MODE_TOKEN, PROXY_MODE_CACHE):
for metric_key, label in (
("total_cost_usd", "Total Cost"),
("cache_read_tokens", "Cache Read Tokens"),
("cache_write_tokens", "Cache Write Tokens"),
("regular_input_tokens", "Paid Input Tokens"),
("output_tokens", "Paid Output Tokens"),
("prompt_window_with_cache", "Window With Cache"),
("prompt_window_without_cache_reads", "Window Without Cache Reads"),
("cache_bust_turns", "Cache Bust Turns"),
("rewrite_turns", "Rewrite Turns"),
("stable_replay_rewrite_turns", "Stable Replay Rewrite Turns"),
("busting_rewrite_turns", "Busting Rewrite Turns"),
("non_cache_eligible_rewrite_turns", "Non-Cache-Eligible Rewrite Turns"),
("retroactive_rewrite_turns", "Retroactive Rewrite Turns"),
("latest_turn_only_rewrite_turns", "Latest-Turn-Only Rewrite Turns"),
):
impact = impacts[mode][metric_key]
delta = impact["delta"]
delta_text = format_currency(delta) if "cost" in metric_key else f"{int(delta):,}"
impact_rows.append(
"<tr>"
f"<td><span class='badge'>{mode}</span></td>"
f"<td>{label}</td>"
f"<td>{impact['impact']}</td>"
f"<td>{delta_text}</td>"
f"<td>{impact['direction']}</td>"
"</tr>"
)
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Claude Session Mode Simulation</title>
<style>
:root {{
--bg: #fafaf9;
--fg: #0f172a;
--muted: #64748b;
--card: rgba(255,255,255,0.88);
--border: #e2e8f0;
--accent: #0f766e;
--accent-soft: #ccfbf1;
--warn: #b45309;
--bad: #b91c1c;
--shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
--radius: 18px;
--font: "Geist", "Segoe UI", system-ui, sans-serif;
}}
* {{ box-sizing: border-box; }}
body {{
margin: 0;
font-family: var(--font);
color: var(--fg);
background:
radial-gradient(circle at top left, #dbeafe 0%, transparent 35%),
radial-gradient(circle at top right, #ccfbf1 0%, transparent 30%),
linear-gradient(180deg, #f8fafc 0%, #f8fafc 100%);
}}
.shell {{ max-width: 1280px; margin: 0 auto; padding: 40px 20px 64px; }}
.hero {{
background: linear-gradient(135deg, rgba(255,255,255,0.92), rgba(248,250,252,0.86));
border: 1px solid rgba(226,232,240,0.9);
box-shadow: var(--shadow);
border-radius: 28px;
padding: 28px;
backdrop-filter: blur(12px);
}}
h1, h2 {{ margin: 0 0 12px; letter-spacing: -0.03em; }}
p {{ margin: 0; color: var(--muted); line-height: 1.55; }}
.grid {{ display: grid; gap: 16px; margin-top: 20px; }}
.grid.cards {{ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }}
.card {{
background: var(--card);
border: 1px solid rgba(226,232,240,0.95);
border-radius: var(--radius);
padding: 18px;
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}}
.eyebrow {{ color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }}
.value {{ font-size: 28px; font-weight: 700; margin-top: 10px; }}
.subtle {{ color: var(--muted); font-size: 14px; margin-top: 6px; }}
.section {{ margin-top: 22px; }}
.table-wrap {{ overflow-x: auto; }}
table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
th, td {{ text-align: left; padding: 12px 14px; border-bottom: 1px solid var(--border); white-space: nowrap; }}
th {{ color: var(--muted); font-weight: 600; background: rgba(248,250,252,0.8); }}
.badge {{
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 10px; border-radius: 999px;
background: var(--accent-soft); color: var(--accent); font-weight: 600; font-size: 12px;
}}
ul.models {{ list-style: none; padding: 0; margin: 0; }}
ul.models li {{ display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border); }}
.winner-list div {{ margin-top: 10px; font-size: 15px; }}
.good {{ color: var(--accent); }}
.warn {{ color: var(--warn); }}
.bad {{ color: var(--bad); }}
code {{ font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: .95em; }}
@media (max-width: 720px) {{
.shell {{ padding: 20px 12px 40px; }}
.hero {{ padding: 20px; border-radius: 22px; }}
.value {{ font-size: 24px; }}
}}
</style>
</head>
<body>
<div class="shell">
<section class="hero">
<div class="eyebrow">Local Claude Cache Analysis</div>
<h1>Claude Session Mode Simulation</h1>
<p>Observed usage is read directly from <code>~/.claude/projects</code>. Baseline, token, and cache are replayed locally through Headroom without making API calls.</p>
<div class="grid cards">
<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>
<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>
<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>
<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>
</div>
</section>
<section class="section grid" style="grid-template-columns: 1.1fr .9fr;">
<div class="card">
<h2>Winners</h2>
<div class="winner-list">
<div><span class="eyebrow">Total cost</span><br><span class="badge">{winners["total_cost"]}</span></div>
<div><span class="eyebrow">No-cache total cost</span><br><span class="badge">{winners["no_cache_total_cost"]}</span></div>
<div><span class="eyebrow">Window if cache counts</span><br><span class="badge">{winners["window_with_cache"]}</span></div>
<div><span class="eyebrow">Window if cache reads do not count</span><br><span class="badge">{winners["window_without_cache_reads"]}</span></div>
</div>
</div>
<div class="card">
<h2>Models</h2>
<ul class="models">{model_items}</ul>
</div>
</section>
<section class="section card">
<h2>Observed Diagnostics</h2>
<div class="grid cards">
<div><div class="eyebrow">Healthy Growth Turns</div><div class="value good">{observed.healthy_growth_turns:,}</div></div>
<div><div class="eyebrow">Broken Prefix Turns</div><div class="value bad">{observed.broken_prefix_turns:,}</div></div>
<div><div class="eyebrow">Resume-like Resets</div><div class="value warn">{observed.resume_like_resets:,}</div></div>
</div>
</section>
<section class="section card">
<h2>Mode Summary</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<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>
</tr>
</thead>
<tbody>
{"".join(summary_rows)}
</tbody>
</table>
</div>
</section>
<section class="section card">
<h2>Impact vs Baseline</h2>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Mode</th><th>Metric</th><th>Classification</th><th>Delta</th><th>Better Direction</th>
</tr>
</thead>
<tbody>
{"".join(impact_rows)}
</tbody>
</table>
</div>
</section>
</div>
</body>
</html>"""
def write_report(
output_dir: Path,
dataset: DatasetSummary,
observed: ObservedSummary,
summaries: dict[str, ModeSummary],
) -> tuple[Path, Path, Path]:
output_dir.mkdir(parents=True, exist_ok=True)
md_path = output_dir / OUTPUT_MD
json_path = output_dir / OUTPUT_JSON
html_path = output_dir / OUTPUT_HTML
md_path.write_text(build_report_markdown(dataset, observed, summaries), encoding="utf-8")
html_path.write_text(build_report_html(dataset, observed, summaries), encoding="utf-8")
payload = {
"dataset": asdict(dataset),
"observed": asdict(observed),
"summaries": {mode: asdict(summary) for mode, summary in summaries.items()},
"winners": determine_winners(summaries),
"impact_vs_baseline": summarize_mode_impact_vs_baseline(summaries),
}
json_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
return md_path, json_path, html_path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR)
parser.add_argument("--max-sessions", type=int, default=None)
parser.add_argument(
"--recent-turns-per-session",
type=int,
default=None,
help="Limit each replay to its most recent N turns for broader, faster sampling.",
)
parser.add_argument("--cache-ttl-minutes", type=int, default=DEFAULT_CACHE_TTL_MINUTES)
parser.add_argument(
"--cache-write-multiplier",
type=float,
default=1.25,
help="Multiplier over base input price used for cache writes/store cost.",
)
parser.add_argument(
"--workers",
type=int,
default=1,
help="Worker processes to use. Higher values use more memory.",
)
parser.add_argument(
"--checkpoint-dir",
type=Path,
default=DEFAULT_OUTPUT_DIR / CHECKPOINT_DIRNAME,
help="Directory for resumable per-session checkpoints.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
logging.getLogger("headroom.transforms").setLevel(logging.WARNING)
logging.getLogger("headroom.proxy").setLevel(logging.WARNING)
checkpoint_dir = resolve_checkpoint_dir(
args.checkpoint_dir,
recent_turns_per_session=args.recent_turns_per_session,
cache_ttl_minutes=args.cache_ttl_minutes,
)
session_files = select_session_files(args.root, max_sessions=args.max_sessions)
if not session_files:
print(f"No Claude session replays found under {args.root}")
return 1
dataset, observed = build_dataset_and_observed_from_files(
session_files,
cache_write_multiplier=args.cache_write_multiplier,
recent_turns_per_session=args.recent_turns_per_session,
)
print(
f"[load] loaded {dataset.sessions} sessions from {args.root}"
+ (f" (max_sessions={args.max_sessions})" if args.max_sessions is not None else ""),
flush=True,
)
summaries = simulate_session_files(
session_files,
dataset,
cache_ttl_minutes=args.cache_ttl_minutes,
cache_write_multiplier=args.cache_write_multiplier,
workers=args.workers,
checkpoint_dir=checkpoint_dir,
recent_turns_per_session=args.recent_turns_per_session,
)
md_path, json_path, html_path = write_report(args.output_dir, dataset, observed, summaries)
print_observed_console_report(observed)
print_console_report(dataset, summaries)
print()
print(f"Markdown report: {md_path}")
print(f"JSON report: {json_path}")
print(f"HTML report: {html_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())