Spaces:
Running
Running
| #!/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", | |
| ] | |
| 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()) | |