Skip to content

Commit 57fe93e

Browse files
authored
Merge pull request #19 from ShaneIsley/claude/rlm-testing-framework-byfLB
Claude/rlm testing framework byf lb
2 parents 166ccfc + 8ac5113 commit 57fe93e

3 files changed

Lines changed: 160 additions & 51 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

‎rlm/core/lm_handler.py‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,16 @@ def handle(self):
3939

4040
socket_send(self.connection, response.to_dict())
4141

42+
except (BrokenPipeError, ConnectionResetError):
43+
# Client disconnected - this is expected, silently ignore
44+
pass
4245
except Exception as e:
43-
response = LMResponse.error_response(str(e))
44-
socket_send(self.connection, response.to_dict())
46+
try:
47+
response = LMResponse.error_response(str(e))
48+
socket_send(self.connection, response.to_dict())
49+
except (BrokenPipeError, ConnectionResetError):
50+
# Client disconnected while we tried to send error
51+
pass
4552

4653
def _handle_single(self, request: LMRequest, handler: "LMHandler") -> LMResponse:
4754
"""Handle a single prompt request."""
@@ -98,6 +105,18 @@ class ThreadingLMServer(ThreadingTCPServer):
98105
daemon_threads = True
99106
allow_reuse_address = True
100107

108+
def handle_error(self, request, client_address):
109+
"""Suppress expected errors when clients disconnect."""
110+
import sys
111+
112+
exc_type, exc_value, _ = sys.exc_info()
113+
# BrokenPipeError and ConnectionResetError are expected when
114+
# subprocess environments disconnect after getting their response
115+
if exc_type in (BrokenPipeError, ConnectionResetError):
116+
return # Silently ignore
117+
# For other errors, use default behavior (print to stderr)
118+
super().handle_error(request, client_address)
119+
101120

102121
class LMHandler:
103122
"""

‎tests/benchmarks/test_benchmarks.py‎

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

452452

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

0 commit comments

Comments
 (0)