Skip to content

Commit 0a3739c

Browse files
authored
Merge pull request #22 from ShaneIsley/claude/rlm-testing-framework-byfLB
feat: add fatal error detection with actionable suggestions
2 parents b323c63 + 5177e49 commit 0a3739c

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

‎benchmarks/runner.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
Includes progress tracking with ETA via tqdm or custom callbacks.
1313
"""
1414

15+
import sys
1516
import time
1617
from collections.abc import Callable
1718
from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -20,6 +21,47 @@
2021

2122
from benchmarks.base import Benchmark, BenchmarkResult, BenchmarkSample, SampleResult
2223

24+
25+
# Error patterns that indicate fatal configuration issues (not worth retrying)
26+
FATAL_ERROR_PATTERNS = [
27+
# Authentication/Authorization
28+
("insufficient_quota", "API quota exceeded. Add credits at https://platform.openai.com/account/billing"),
29+
("invalid_api_key", "Invalid API key. Check your API key configuration."),
30+
("invalid api key", "Invalid API key. Check your API key configuration."),
31+
("incorrect api key", "Invalid API key. Check your API key configuration."),
32+
("authentication", "Authentication failed. Verify your API key is correct."),
33+
("unauthorized", "Unauthorized. Check your API key and permissions."),
34+
("api_key", "API key issue. Check your API key configuration."),
35+
# Model issues
36+
("model_not_found", "Model not found. Verify the model name is correct."),
37+
("does not exist", "Model does not exist. Check available models for your account."),
38+
("no such model", "Model not found. Check available models for your account."),
39+
# Rate limiting (without retry-after)
40+
("rate_limit", "Rate limited. Consider reducing --max-workers or adding delays."),
41+
]
42+
43+
44+
class FatalBenchmarkError(Exception):
45+
"""Raised when a benchmark encounters an unrecoverable error."""
46+
47+
def __init__(self, message: str, suggestion: str):
48+
self.message = message
49+
self.suggestion = suggestion
50+
super().__init__(f"{message}\n\nSuggestion: {suggestion}")
51+
52+
53+
def classify_error(error_str: str) -> tuple[bool, str]:
54+
"""Classify an error as fatal or transient.
55+
56+
Returns:
57+
(is_fatal, suggestion) - If fatal, includes actionable suggestion.
58+
"""
59+
error_lower = error_str.lower()
60+
for pattern, suggestion in FATAL_ERROR_PATTERNS:
61+
if pattern in error_lower:
62+
return True, suggestion
63+
return False, ""
64+
2365
# Type alias for progress callback
2466
ProgressCallback = Callable[[int, int, "SampleResult | None", "ProgressStats"], None]
2567

@@ -321,6 +363,18 @@ def _run_sequential(
321363
results.append(sample_result)
322364
self._update_progress(sample_result, stats, progress_mode, pbar)
323365

366+
# Check for fatal errors on first sample - abort early
367+
if sample_result.error and stats.completed == 1:
368+
is_fatal, suggestion = classify_error(sample_result.error)
369+
if is_fatal:
370+
if pbar:
371+
pbar.close()
372+
pbar = None
373+
print(f"\n❌ Fatal error detected: {sample_result.error}", file=sys.stderr)
374+
print(f"\n💡 {suggestion}", file=sys.stderr)
375+
print("\nAborting benchmark - fix the issue above and retry.", file=sys.stderr)
376+
return results
377+
324378
finally:
325379
if pbar is not None:
326380
pbar.close()
@@ -354,6 +408,7 @@ def _run_parallel(
354408
results: dict[str, SampleResult] = {}
355409
lock = threading.Lock()
356410
pbar = None
411+
fatal_error_found: tuple[str, str] | None = None # (error, suggestion)
357412

358413
try:
359414
if progress_mode == "tqdm":
@@ -393,10 +448,23 @@ def _run_parallel(
393448
results[sample.id] = sample_result
394449
self._update_progress(sample_result, stats, progress_mode, pbar)
395450

451+
# Track first fatal error (for reporting after completion)
452+
if sample_result.error and fatal_error_found is None:
453+
is_fatal, suggestion = classify_error(sample_result.error)
454+
if is_fatal:
455+
fatal_error_found = (sample_result.error, suggestion)
456+
396457
finally:
397458
if pbar is not None:
398459
pbar.close()
399460

461+
# Report fatal errors after parallel run completes
462+
if fatal_error_found:
463+
error, suggestion = fatal_error_found
464+
print(f"\n❌ Fatal error detected: {error}", file=sys.stderr)
465+
print(f"\n💡 {suggestion}", file=sys.stderr)
466+
print("\nFix the issue above before running more benchmarks.", file=sys.stderr)
467+
400468
# Return results in original sample order
401469
return [results[sample.id] for sample in samples]
402470

‎tests/benchmarks/test_benchmarks.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,54 @@ def __init__(self, **kwargs):
568568
assert captured_kwargs.get("backend_kwargs") == {"model_name": "gpt-4o-test"}
569569

570570

571+
class TestErrorClassification:
572+
"""Tests for error classification and fatal error handling."""
573+
574+
def test_classify_quota_error(self):
575+
"""Test that quota errors are classified as fatal."""
576+
from benchmarks.runner import classify_error
577+
578+
error = "Error code: 429 - {'error': {'code': 'insufficient_quota'}}"
579+
is_fatal, suggestion = classify_error(error)
580+
assert is_fatal
581+
assert "quota" in suggestion.lower() or "billing" in suggestion.lower()
582+
583+
def test_classify_auth_error(self):
584+
"""Test that auth errors are classified as fatal."""
585+
from benchmarks.runner import classify_error
586+
587+
error = "Invalid API key provided"
588+
is_fatal, suggestion = classify_error(error)
589+
assert is_fatal
590+
assert "key" in suggestion.lower()
591+
592+
def test_classify_model_error(self):
593+
"""Test that model not found errors are classified as fatal."""
594+
from benchmarks.runner import classify_error
595+
596+
error = "The model 'gpt-99' does not exist"
597+
is_fatal, suggestion = classify_error(error)
598+
assert is_fatal
599+
assert "model" in suggestion.lower()
600+
601+
def test_classify_transient_error(self):
602+
"""Test that transient errors are not classified as fatal."""
603+
from benchmarks.runner import classify_error
604+
605+
error = "Connection timeout after 30 seconds"
606+
is_fatal, suggestion = classify_error(error)
607+
assert not is_fatal
608+
assert suggestion == ""
609+
610+
def test_classify_generic_error(self):
611+
"""Test that unknown errors are not classified as fatal."""
612+
from benchmarks.runner import classify_error
613+
614+
error = "Something unexpected happened"
615+
is_fatal, suggestion = classify_error(error)
616+
assert not is_fatal
617+
618+
571619
class TestBenchmarkIntegration:
572620
"""Integration tests for benchmark framework."""
573621

0 commit comments

Comments
 (0)