|
12 | 12 | Includes progress tracking with ETA via tqdm or custom callbacks. |
13 | 13 | """ |
14 | 14 |
|
| 15 | +import sys |
15 | 16 | import time |
16 | 17 | from collections.abc import Callable |
17 | 18 | from concurrent.futures import ThreadPoolExecutor, as_completed |
|
20 | 21 |
|
21 | 22 | from benchmarks.base import Benchmark, BenchmarkResult, BenchmarkSample, SampleResult |
22 | 23 |
|
| 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 | + |
23 | 65 | # Type alias for progress callback |
24 | 66 | ProgressCallback = Callable[[int, int, "SampleResult | None", "ProgressStats"], None] |
25 | 67 |
|
@@ -321,6 +363,18 @@ def _run_sequential( |
321 | 363 | results.append(sample_result) |
322 | 364 | self._update_progress(sample_result, stats, progress_mode, pbar) |
323 | 365 |
|
| 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 | + |
324 | 378 | finally: |
325 | 379 | if pbar is not None: |
326 | 380 | pbar.close() |
@@ -354,6 +408,7 @@ def _run_parallel( |
354 | 408 | results: dict[str, SampleResult] = {} |
355 | 409 | lock = threading.Lock() |
356 | 410 | pbar = None |
| 411 | + fatal_error_found: tuple[str, str] | None = None # (error, suggestion) |
357 | 412 |
|
358 | 413 | try: |
359 | 414 | if progress_mode == "tqdm": |
@@ -393,10 +448,23 @@ def _run_parallel( |
393 | 448 | results[sample.id] = sample_result |
394 | 449 | self._update_progress(sample_result, stats, progress_mode, pbar) |
395 | 450 |
|
| 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 | + |
396 | 457 | finally: |
397 | 458 | if pbar is not None: |
398 | 459 | pbar.close() |
399 | 460 |
|
| 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 | + |
400 | 468 | # Return results in original sample order |
401 | 469 | return [results[sample.id] for sample in samples] |
402 | 470 |
|
|
0 commit comments