Spaces:
Sleeping
Sleeping
Download scripts/benchmark_models.py from misack/war_gaming: direct link, hf CLI and curl.
- Browser
- Download file 9.55 kB
-
https://huggingface.co/spaces/misack/war_gaming/resolve/main/scripts/benchmark_models.py
- Command line
-
hf download hf://spaces/misack/war_gaming/scripts/benchmark_models.py
-
curl -L -o benchmark_models.py https://huggingface.co/spaces/misack/war_gaming/resolve/main/scripts/benchmark_models.py
9.55 kB
| """Benchmark every registered model by running a fixed one-turn game. | |
| For each model in `wargame.config.MODEL_REGISTRY` (or a subset via | |
| `--models`), this script: | |
| 1. Creates a fresh competitor_disruption game pinned to that model | |
| 2. Submits a canned player action | |
| 3. Advances one turn (deliberations + adjudication + world update + next briefing) | |
| 4. Captures per-model metrics: wall time, total LLM calls, validation retries, | |
| input/output tokens, Turn 1 briefing headline, the first verdict narrative | |
| 5. Writes a markdown report with a summary table + per-model detail blocks | |
| Opt-in — makes real API calls. Not wired into pytest. Expected runtime ~2 min | |
| per Anthropic model, similar for the others. | |
| Usage: | |
| uv run python scripts/benchmark_models.py | |
| uv run python scripts/benchmark_models.py --out benchmarks/custom.md | |
| uv run python scripts/benchmark_models.py --models claude-opus-4-7,gpt-5.4 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| import time | |
| import traceback | |
| from dataclasses import dataclass, field | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| # Add project root to sys.path so `wargame` is importable when run as a script. | |
| _ROOT = Path(__file__).resolve().parent.parent | |
| if str(_ROOT) not in sys.path: | |
| sys.path.insert(0, str(_ROOT)) | |
| from wargame.agents.base import cache_stats, reset_cache_stats # noqa: E402 | |
| from wargame.config import MODEL_REGISTRY # noqa: E402 | |
| from wargame.engine import loop as engine_loop # noqa: E402 | |
| from wargame.engine.state import ActionSubmission, Anchor, AnchorKind # noqa: E402 | |
| CANNED_ACTION = ActionSubmission( | |
| actor="player", | |
| intent=( | |
| "Announce an EU-made heat-pump bundle with a 10-year service guarantee " | |
| "and match Aergis on installer payouts." | |
| ), | |
| justification=( | |
| "Stabilize the installer channel before Aergis locks in DACH direct " | |
| "sales, while framing the move around EU-made provenance to pressure " | |
| "Aergis's Foreign Subsidies exposure." | |
| ), | |
| anchors=[ | |
| Anchor( | |
| kind=AnchorKind.PRIOR_ANALOGUE, | |
| detail=( | |
| "Viessmann's 2023 installer-loyalty program stabilized channel " | |
| "defection within two quarters." | |
| ), | |
| ), | |
| Anchor( | |
| kind=AnchorKind.STATED_CAPABILITY, | |
| detail=( | |
| "Service arm already runs 8-year service contracts; extension " | |
| "to 10 is a margin decision, not a capability gap." | |
| ), | |
| ), | |
| Anchor( | |
| kind=AnchorKind.RESOURCE_COMMITTED, | |
| detail=( | |
| "EUR 12M service-reserve tranche earmarked for warranty expansion " | |
| "in the FY24 budget." | |
| ), | |
| ), | |
| ], | |
| ) | |
| class ModelResult: | |
| model_id: str | |
| display_name: str | |
| provider: str | |
| ok: bool = False | |
| error: str | None = None | |
| total_seconds: float = 0.0 | |
| new_game_seconds: float = 0.0 | |
| advance_seconds: float = 0.0 | |
| llm_calls: int = 0 | |
| validation_retries: int = 0 | |
| input_tokens: int = 0 | |
| output_tokens: int = 0 | |
| cache_read_tokens: int = 0 | |
| cache_write_tokens: int = 0 | |
| turn1_headline: str = "" | |
| first_verdict: str = "" | |
| notes: list[str] = field(default_factory=list) | |
| def mean_call_ms(self) -> float: | |
| return (self.total_seconds / self.llm_calls * 1000.0) if self.llm_calls else 0.0 | |
| def benchmark_model(model_id: str) -> ModelResult: | |
| spec = MODEL_REGISTRY[model_id] | |
| result = ModelResult( | |
| model_id=model_id, | |
| display_name=spec.display_name, | |
| provider=spec.provider.value, | |
| ) | |
| reset_cache_stats() | |
| t_start = time.perf_counter() | |
| try: | |
| t0 = time.perf_counter() | |
| game = engine_loop.new_game( | |
| scenario_id="competitor_disruption", | |
| model=model_id, | |
| ) | |
| result.new_game_seconds = time.perf_counter() - t0 | |
| engine_loop.act(game, CANNED_ACTION) | |
| t1 = time.perf_counter() | |
| engine_loop.advance(game, generate_next_briefing=True) | |
| result.advance_seconds = time.perf_counter() - t1 | |
| briefing = game.turns[0].briefing | |
| if briefing: | |
| result.turn1_headline = briefing.headline | |
| verdicts = game.turns[0].verdicts | |
| if verdicts: | |
| v = verdicts[0] | |
| result.first_verdict = ( | |
| f"{v.action_actor} → {v.outcome.value} " | |
| f"(plausibility={v.plausibility}): {v.effect_narrative}" | |
| ) | |
| result.ok = True | |
| except Exception as exc: # noqa: BLE001 | |
| result.error = f"{type(exc).__name__}: {exc}" | |
| result.notes.append(traceback.format_exc(limit=3)) | |
| finally: | |
| result.total_seconds = time.perf_counter() - t_start | |
| stats = cache_stats() | |
| result.llm_calls = stats["calls"] | |
| result.validation_retries = stats["validation_retries"] | |
| result.input_tokens = stats["uncached_input_tokens"] | |
| result.output_tokens = stats["output_tokens"] | |
| result.cache_read_tokens = stats["cache_read_tokens"] | |
| result.cache_write_tokens = stats["cache_write_tokens"] | |
| return result | |
| def write_report(results: list[ModelResult], out_path: Path) -> None: | |
| out_path.parent.mkdir(parents=True, exist_ok=True) | |
| lines: list[str] = [] | |
| lines.append("# Model Benchmark — competitor_disruption, one-turn run") | |
| lines.append("") | |
| lines.append( | |
| f"Generated: **{datetime.now(tz=timezone.utc).isoformat(timespec='seconds')}**" | |
| ) | |
| lines.append("") | |
| lines.append("Each model ran a fresh game: Turn 1 briefing, canned player action, full") | |
| lines.append("deliberation + adjudication + world update + Turn 2 briefing. Token counts") | |
| lines.append("are **uncached** input; cache reads are shown separately.") | |
| lines.append("") | |
| lines.append("## Summary") | |
| lines.append("") | |
| lines.append( | |
| "| Model | Provider | Status | Total (s) | New-game (s) | Advance (s) | LLM calls | Retries | Mean call (ms) | In tok | Out tok | Cache read |" | |
| ) | |
| lines.append( | |
| "|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|" | |
| ) | |
| for r in results: | |
| status = "ok" if r.ok else f"FAIL ({r.error[:40] if r.error else '?'})" | |
| lines.append( | |
| f"| `{r.model_id}` | {r.provider} | {status} | {r.total_seconds:.1f} | " | |
| f"{r.new_game_seconds:.1f} | {r.advance_seconds:.1f} | {r.llm_calls} | " | |
| f"{r.validation_retries} | {r.mean_call_ms:.0f} | {r.input_tokens} | " | |
| f"{r.output_tokens} | {r.cache_read_tokens} |" | |
| ) | |
| lines.append("") | |
| lines.append("## Per-model detail") | |
| lines.append("") | |
| for r in results: | |
| lines.append(f"### `{r.model_id}` — {r.display_name}") | |
| lines.append("") | |
| if not r.ok: | |
| lines.append(f"**Failed:** `{r.error}`") | |
| if r.notes: | |
| lines.append("") | |
| lines.append("```") | |
| lines.append(r.notes[0].strip()) | |
| lines.append("```") | |
| lines.append("") | |
| continue | |
| lines.append(f"- Provider: `{r.provider}`") | |
| lines.append(f"- Total wall time: **{r.total_seconds:.1f}s** " | |
| f"(new_game {r.new_game_seconds:.1f}s + advance {r.advance_seconds:.1f}s)") | |
| lines.append(f"- LLM calls: **{r.llm_calls}**, validation retries: **{r.validation_retries}**") | |
| lines.append(f"- Mean call latency: **{r.mean_call_ms:.0f} ms**") | |
| lines.append(f"- Tokens: {r.input_tokens} in / {r.output_tokens} out " | |
| f"(cache read {r.cache_read_tokens}, write {r.cache_write_tokens})") | |
| lines.append("") | |
| if r.turn1_headline: | |
| lines.append(f"**Turn 1 headline:** {r.turn1_headline}") | |
| lines.append("") | |
| if r.first_verdict: | |
| lines.append(f"**First verdict:** {r.first_verdict}") | |
| lines.append("") | |
| out_path.write_text("\n".join(lines), encoding="utf-8") | |
| def main() -> int: | |
| ap = argparse.ArgumentParser(description=__doc__) | |
| ap.add_argument( | |
| "--out", | |
| type=Path, | |
| default=None, | |
| help="Output markdown path. Defaults to benchmarks/<timestamp>.md.", | |
| ) | |
| ap.add_argument( | |
| "--models", | |
| type=str, | |
| default=None, | |
| help="Comma-separated model ids. Defaults to every registered model.", | |
| ) | |
| args = ap.parse_args() | |
| if args.models: | |
| requested = [m.strip() for m in args.models.split(",") if m.strip()] | |
| else: | |
| requested = list(MODEL_REGISTRY.keys()) | |
| unknown = [m for m in requested if m not in MODEL_REGISTRY] | |
| if unknown: | |
| print(f"Unknown models: {', '.join(unknown)}", file=sys.stderr) | |
| return 2 | |
| if args.out is None: | |
| ts = datetime.now(tz=timezone.utc).strftime("%Y%m%dT%H%M%SZ") | |
| args.out = Path("benchmarks") / f"{ts}.md" | |
| results: list[ModelResult] = [] | |
| for model_id in requested: | |
| print(f"[benchmark] {model_id}...", flush=True) | |
| r = benchmark_model(model_id) | |
| status = "ok" if r.ok else "FAIL" | |
| print( | |
| f" -> {status} in {r.total_seconds:.1f}s, " | |
| f"{r.llm_calls} calls, {r.validation_retries} retries", | |
| flush=True, | |
| ) | |
| if not r.ok: | |
| print(f" error: {r.error}", flush=True) | |
| results.append(r) | |
| write_report(results, args.out) | |
| print(f"[benchmark] wrote {args.out}") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |