wiki-speedrun / scripts /test_llm_models.py
jwlutz's picture
Initial commit: Wikipedia Speedrun benchmark with Flask dashboard
0e61be5
Raw
History Blame Contribute Delete
11.3 kB
#!/usr/bin/env python3
"""
Parallel test of LLM models on Wikipedia speedrun.
Tests multiple models simultaneously on the same problem to compare:
- Response time
- Link choice quality
- Token usage
Usage:
python scripts/test_llm_models.py
python scripts/test_llm_models.py --models "gpt-4o-mini,claude-3-haiku" --start "Cat" --target "Dog"
python scripts/test_llm_models.py --free-only
python scripts/test_llm_models.py --full-game --models "gemini-2.0-flash-exp:free"
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
# Fix Windows console encoding
if sys.platform == "win32":
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from dotenv import load_dotenv
load_dotenv(project_root / ".env")
import requests
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
# Models to test
FREE_MODELS = [
"google/gemini-2.0-flash-exp:free",
"mistralai/mistral-small-3.1-24b-instruct:free",
"meta-llama/llama-3.3-70b-instruct:free",
"deepseek/deepseek-r1-0528:free",
]
BUDGET_MODELS = [
"mistralai/mistral-small-3.1-24b-instruct",
"openai/gpt-5-nano",
"meta-llama/llama-3.2-3b-instruct",
]
VALUE_MODELS = [
"google/gemini-2.0-flash-001",
"openai/gpt-4o-mini",
"deepseek/deepseek-v3.2",
]
FAST_MODELS = [
"anthropic/claude-3-haiku",
"anthropic/claude-3.5-haiku",
"google/gemini-3-flash-preview",
]
SMART_MODELS = [
"anthropic/claude-haiku-4.5",
"openai/gpt-5",
]
# Sample prompt for testing
SAMPLE_LINKS = [
"United States",
"Agriculture",
"South America",
"Food",
"Vegetable",
"Plant",
"Europe",
"History",
"World War II",
"International trade",
"Science",
"Economy",
"Government",
"Politics",
"Democratic Party (United States)",
"President of the United States",
"Washington, D.C.",
"American",
"English language",
"Human",
]
@dataclass
class TestResult:
model: str
success: bool
response: str = ""
parsed_link: str = ""
elapsed_seconds: float = 0.0
prompt_tokens: int = 0
completion_tokens: int = 0
error: str = ""
def build_prompt(current: str, target: str, links: list[str]) -> str:
"""Build test prompt."""
links_str = "\n".join(f"- {link}" for link in links)
return f"""You are playing Wikipedia Speedrun. Navigate from one article to another by clicking links only.
Current page: "{current}"
Target page: "{target}"
Available links on this page ({len(links)} total):
{links_str}
Which link should you click to reach "{target}"?
Reply with ONLY the exact link name from the list above, nothing else."""
def test_model(model: str, prompt: str, timeout: int = 60) -> TestResult:
"""Test a single model."""
start_time = time.time()
try:
response = requests.post(
f"{OPENROUTER_BASE_URL}/chat/completions",
headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/jacklutz/wiki_speedrun",
"X-Title": "Wikipedia Speedrun Benchmark",
},
json={
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.0,
"max_tokens": 100,
},
timeout=timeout,
)
elapsed = time.time() - start_time
if response.status_code != 200:
return TestResult(
model=model,
success=False,
elapsed_seconds=elapsed,
error=f"HTTP {response.status_code}: {response.text[:100]}",
)
data = response.json()
content = data.get("choices", [{}])[0].get("message", {}).get("content", "")
usage = data.get("usage", {})
# Parse link
parsed = content.strip().strip('"').strip("'")
if "\n" in parsed:
parsed = parsed.split("\n")[0].strip()
return TestResult(
model=model,
success=True,
response=content,
parsed_link=parsed,
elapsed_seconds=round(elapsed, 3),
prompt_tokens=usage.get("prompt_tokens", 0),
completion_tokens=usage.get("completion_tokens", 0),
)
except requests.exceptions.Timeout:
return TestResult(
model=model,
success=False,
elapsed_seconds=60,
error="Timeout",
)
except Exception as e:
return TestResult(
model=model,
success=False,
elapsed_seconds=time.time() - start_time,
error=str(e)[:100],
)
def run_full_game(model: str, start: str, target: str, max_steps: int = 25) -> dict:
"""Run a full game with a model."""
from src.agents import get_agent
from src.game import GameEngine
agent = get_agent("llm", model=model)
with GameEngine(visualize=False) as engine:
start_time = time.time()
result = engine.run(
agent=agent,
start=start,
target=target,
max_steps=max_steps,
)
elapsed = time.time() - start_time
stats = agent.get_stats()
return {
"model": model,
"won": result.won,
"clicks": result.total_clicks,
"path": result.path,
"elapsed_seconds": round(elapsed, 2),
"total_tokens": stats.get("total_tokens", 0),
"api_time": stats.get("total_time_seconds", 0),
}
def main():
parser = argparse.ArgumentParser(description="Test LLM models for Wikipedia speedrun")
parser.add_argument(
"--models",
type=str,
default=None,
help="Comma-separated list of models to test (default: free models)",
)
parser.add_argument(
"--free-only",
action="store_true",
help="Only test free models",
)
parser.add_argument(
"--all",
action="store_true",
help="Test all model tiers",
)
parser.add_argument(
"--start",
type=str,
default="Potato",
help="Start article for test",
)
parser.add_argument(
"--target",
type=str,
default="Barack Obama",
help="Target article for test",
)
parser.add_argument(
"--full-game",
action="store_true",
help="Run full games instead of single prompts",
)
parser.add_argument(
"--parallel",
type=int,
default=5,
help="Number of parallel requests (default: 5)",
)
parser.add_argument(
"--output",
type=str,
default=None,
help="Save results to JSON file",
)
args = parser.parse_args()
if not OPENROUTER_API_KEY:
print("ERROR: OPENROUTER_API_KEY not set in .env")
return 1
# Determine models to test
if args.models:
models = [m.strip() for m in args.models.split(",")]
elif args.all:
models = FREE_MODELS + BUDGET_MODELS + VALUE_MODELS + FAST_MODELS + SMART_MODELS
elif args.free_only:
models = FREE_MODELS
else:
models = FREE_MODELS # Default to free
print("=" * 80)
print("LLM Model Comparison for Wikipedia Speedrun")
print("=" * 80)
print(f"Testing {len(models)} models")
print(f"Problem: {args.start}{args.target}")
print(f"Mode: {'Full game' if args.full_game else 'Single prompt'}")
print(f"Parallel: {args.parallel}")
print("=" * 80)
if args.full_game:
# Run full games (sequential to avoid overwhelming Wikipedia)
print("\nRunning full games (sequential)...")
results = []
for model in models:
print(f"\n Testing {model}...")
try:
result = run_full_game(model, args.start, args.target)
results.append(result)
status = "WIN" if result["won"] else "LOST"
print(f" {status} in {result['clicks']} clicks, {result['elapsed_seconds']}s")
except Exception as e:
print(f" ERROR: {e}")
results.append({"model": model, "error": str(e)})
else:
# Single prompt test (parallel)
prompt = build_prompt(args.start, args.target, SAMPLE_LINKS)
print(f"\nTesting with sample prompt (parallel={args.parallel})...")
results = []
with ThreadPoolExecutor(max_workers=args.parallel) as executor:
futures = {
executor.submit(test_model, model, prompt): model
for model in models
}
for future in as_completed(futures):
result = future.result()
results.append(result)
if result.success:
print(f" {result.model:<50} {result.elapsed_seconds:>6.2f}s → {result.parsed_link}")
else:
print(f" {result.model:<50} FAILED: {result.error[:30]}")
# Summary
print("\n" + "=" * 80)
print("SUMMARY")
print("=" * 80)
if args.full_game:
print(f"{'Model':<50} {'Result':>8} {'Clicks':>8} {'Time':>10}")
print("-" * 80)
for r in sorted(results, key=lambda x: (not x.get("won", False), x.get("clicks", 999))):
if "error" in r:
print(f"{r['model']:<50} {'ERROR':>8}")
else:
status = "WIN" if r["won"] else "LOST"
print(f"{r['model']:<50} {status:>8} {r['clicks']:>8} {r['elapsed_seconds']:>9.2f}s")
else:
print(f"{'Model':<50} {'Time':>8} {'Tokens':>8} {'Choice'}")
print("-" * 80)
for r in sorted(results, key=lambda x: x.elapsed_seconds if x.success else 999):
if r.success:
tokens = r.prompt_tokens + r.completion_tokens
print(f"{r.model:<50} {r.elapsed_seconds:>7.2f}s {tokens:>8} {r.parsed_link}")
else:
print(f"{r.model:<50} {'FAILED':>8} {r.error[:30]}")
# Save results
if args.output:
output_path = Path(args.output)
with open(output_path, "w") as f:
if args.full_game:
json.dump(results, f, indent=2)
else:
json.dump(
[
{
"model": r.model,
"success": r.success,
"elapsed_seconds": r.elapsed_seconds,
"parsed_link": r.parsed_link,
"tokens": r.prompt_tokens + r.completion_tokens,
"error": r.error,
}
for r in results
],
f,
indent=2,
)
print(f"\nResults saved to {output_path}")
return 0
if __name__ == "__main__":
sys.exit(main())