Skip to content

Commit 8ac5113

Browse files
committed
feat: add backend:model spec syntax for multi-model comparison
Add unified model specification format using colon separator: - --models backend:model [backend:model ...] - Examples: openai:gpt-4o, anthropic:claude-sonnet-4-20250514 - Model-only format defaults to openai (e.g., "gpt-4o" -> "openai:gpt-4o") Supports comparing multiple models in a single run: python -m benchmarks.cli run --benchmark niah \ --models openai:gpt-4o anthropic:claude-sonnet-4-20250514 Legacy --backend and --model args still work for backwards compatibility.
1 parent fa0eb39 commit 8ac5113

2 files changed

Lines changed: 139 additions & 49 deletions

File tree

‎benchmarks/cli.py‎

Lines changed: 108 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,22 @@
33
CLI for running RLM benchmarks.
44
55
Usage:
6-
# Run benchmarks
7-
python -m benchmarks.cli run --benchmark oolong --num-samples 10
6+
# Run benchmarks with model spec (backend:model format)
7+
python -m benchmarks.cli run --benchmark niah --models openai:gpt-4o
8+
python -m benchmarks.cli run --benchmark niah --models openai:gpt-4o anthropic:claude-sonnet-4-20250514
9+
10+
# Run with methods comparison
811
python -m benchmarks.cli run --benchmark niah --methods rlm direct
912
1013
# Query stored results
1114
python -m benchmarks.cli history --benchmark oolong-toy_dnd --limit 10
1215
python -m benchmarks.cli compare --benchmark niah-100k --group-by method
13-
14-
# Legacy (no subcommand defaults to 'run')
15-
python -m benchmarks.cli --benchmark oolong --num-samples 10
1616
"""
1717

1818
import argparse
1919
import json
2020
import sys
21+
from dataclasses import dataclass
2122
from datetime import datetime
2223

2324
from benchmarks.base import BenchmarkResult
@@ -28,6 +29,37 @@
2829
from benchmarks.tasks.oolong import OolongBenchmark, OolongPairsBenchmark
2930

3031

32+
@dataclass
33+
class ModelSpec:
34+
"""Parsed model specification."""
35+
36+
backend: str
37+
model: str
38+
39+
def __str__(self) -> str:
40+
return f"{self.backend}:{self.model}"
41+
42+
43+
def parse_model_spec(spec: str) -> ModelSpec:
44+
"""Parse a backend:model specification string.
45+
46+
Formats supported:
47+
- "backend:model" -> ModelSpec(backend, model)
48+
- "model" -> ModelSpec("openai", model) # default backend
49+
50+
Examples:
51+
- "openai:gpt-4o" -> ModelSpec("openai", "gpt-4o")
52+
- "anthropic:claude-sonnet-4-20250514" -> ModelSpec("anthropic", "claude-sonnet-4-20250514")
53+
- "gpt-4o" -> ModelSpec("openai", "gpt-4o")
54+
"""
55+
if ":" in spec:
56+
parts = spec.split(":", 1)
57+
return ModelSpec(backend=parts[0], model=parts[1])
58+
else:
59+
# Default to openai backend
60+
return ModelSpec(backend="openai", model=spec)
61+
62+
3163
def get_benchmark(args: argparse.Namespace):
3264
"""Instantiate benchmark from CLI arguments."""
3365
name = args.benchmark.lower()
@@ -278,17 +310,25 @@ def main():
278310
default=None,
279311
help="Output file path for results (JSON)",
280312
)
313+
run_parser.add_argument(
314+
"--models",
315+
nargs="+",
316+
default=None,
317+
help="Model specs as backend:model (e.g., openai:gpt-4o anthropic:claude-sonnet-4-20250514). "
318+
"Can specify multiple for comparison. Default: openai/gpt-4o-mini",
319+
)
320+
# Legacy args for backwards compatibility
281321
run_parser.add_argument(
282322
"--backend",
283323
type=str,
284-
default="openai",
285-
help="LLM backend (default: openai)",
324+
default=None,
325+
help="(Legacy) LLM backend. Prefer --models backend:model syntax.",
286326
)
287327
run_parser.add_argument(
288328
"--model",
289329
type=str,
290-
default="gpt-5-mini",
291-
help="Model name (default: gpt-5-mini)",
330+
default=None,
331+
help="(Legacy) Model name. Prefer --models backend:model syntax.",
292332
)
293333
run_parser.add_argument(
294334
"--environment",
@@ -389,62 +429,81 @@ def main():
389429
return 1
390430

391431

432+
def get_model_specs(args: argparse.Namespace) -> list[ModelSpec]:
433+
"""Get model specifications from args, handling legacy and new formats."""
434+
if args.models:
435+
# New format: --models backend:model [backend:model ...]
436+
return [parse_model_spec(spec) for spec in args.models]
437+
elif args.backend or args.model:
438+
# Legacy format: --backend X --model Y
439+
backend = args.backend or "openai"
440+
model = args.model or "gpt-4o-mini"
441+
return [ModelSpec(backend=backend, model=model)]
442+
else:
443+
# Default
444+
return [ModelSpec(backend="openai", model="gpt-4o-mini")]
445+
446+
392447
def cmd_run(args: argparse.Namespace) -> int:
393448
"""Run benchmarks (main command)."""
394-
runner = BenchmarkRunner(
395-
backend=args.backend,
396-
model=args.model,
397-
environment=args.environment,
398-
max_iterations=args.max_iterations,
399-
verbose=args.verbose,
400-
log_dir=args.log_dir,
401-
max_workers=args.max_workers,
402-
progress=args.progress,
403-
)
449+
model_specs = get_model_specs(args)
404450

405451
if args.benchmark == "all":
406452
benchmarks = get_all_benchmarks(args)
407453
else:
408454
benchmarks = [get_benchmark(args)]
409455

410456
all_results = {}
457+
store = ResultsStore(args.results_dir)
411458

412-
for benchmark in benchmarks:
413-
print(f"\n{'=' * 60}")
414-
print(f"Running: {benchmark.name}")
415-
print(f"Description: {benchmark.description}")
416-
print(f"{'=' * 60}")
417-
418-
results = compare_methods(
419-
benchmark=benchmark,
420-
runner=runner,
421-
methods=args.methods,
422-
num_samples=args.num_samples,
423-
seed=args.seed,
459+
for model_spec in model_specs:
460+
print(f"\n{'=' * 70}")
461+
print(f"Model: {model_spec}")
462+
print(f"{'=' * 70}")
463+
464+
runner = BenchmarkRunner(
465+
backend=model_spec.backend,
466+
model=model_spec.model,
467+
environment=args.environment,
468+
max_iterations=args.max_iterations,
469+
verbose=args.verbose,
470+
log_dir=args.log_dir,
471+
max_workers=args.max_workers,
472+
progress=args.progress,
424473
)
425474

426-
for method, result in results.items():
427-
key = f"{benchmark.name}/{method}"
428-
all_results[key] = result
475+
for benchmark in benchmarks:
476+
print(f"\nRunning: {benchmark.name}")
477+
print(f"Description: {benchmark.description}")
478+
479+
results = compare_methods(
480+
benchmark=benchmark,
481+
runner=runner,
482+
methods=args.methods,
483+
num_samples=args.num_samples,
484+
seed=args.seed,
485+
)
486+
487+
for method, result in results.items():
488+
key = f"{model_spec}/{benchmark.name}/{method}"
489+
all_results[key] = result
490+
491+
# Save to results store
492+
config = ExperimentConfig(
493+
backend=model_spec.backend,
494+
model=model_spec.model,
495+
environment=args.environment,
496+
max_iterations=args.max_iterations,
497+
num_samples=args.num_samples,
498+
seed=args.seed,
499+
method=method,
500+
)
501+
exp_id = store.save(result, config)
502+
print(f" Saved as experiment {exp_id}")
429503

430504
# Print summary
431505
print_summary(all_results)
432506

433-
# Save to results store
434-
store = ResultsStore(args.results_dir)
435-
config = ExperimentConfig(
436-
backend=args.backend,
437-
model=args.model,
438-
environment=args.environment,
439-
max_iterations=args.max_iterations,
440-
num_samples=args.num_samples,
441-
seed=args.seed,
442-
)
443-
444-
for key, result in all_results.items():
445-
exp_id = store.save(result, config)
446-
print(f"Saved {key} as experiment {exp_id}")
447-
448507
if args.output:
449508
save_results(all_results, args.output)
450509

‎tests/benchmarks/test_benchmarks.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,37 @@ def test_simple_progress_mode(self, capsys):
446446
assert "Acc:" in captured.out
447447

448448

449+
class TestModelSpec:
450+
"""Tests for model specification parsing."""
451+
452+
def test_parse_model_spec_with_backend(self):
453+
"""Test parsing backend:model format."""
454+
from benchmarks.cli import parse_model_spec
455+
456+
spec = parse_model_spec("openai:gpt-4o")
457+
assert spec.backend == "openai"
458+
assert spec.model == "gpt-4o"
459+
460+
spec = parse_model_spec("anthropic:claude-sonnet-4-20250514")
461+
assert spec.backend == "anthropic"
462+
assert spec.model == "claude-sonnet-4-20250514"
463+
464+
def test_parse_model_spec_model_only(self):
465+
"""Test parsing model-only format defaults to openai."""
466+
from benchmarks.cli import parse_model_spec
467+
468+
spec = parse_model_spec("gpt-4o")
469+
assert spec.backend == "openai"
470+
assert spec.model == "gpt-4o"
471+
472+
def test_model_spec_str(self):
473+
"""Test ModelSpec string representation."""
474+
from benchmarks.cli import ModelSpec
475+
476+
spec = ModelSpec(backend="anthropic", model="claude-sonnet-4-20250514")
477+
assert str(spec) == "anthropic:claude-sonnet-4-20250514"
478+
479+
449480
class TestBenchmarkIntegration:
450481
"""Integration tests for benchmark framework."""
451482

0 commit comments

Comments
 (0)