fix: retries are never happening in eval runner#1548
Conversation
EvalRunner's retry classifier (_is_retryable_error) checks isinstance against litellm error types, but the model adapter wraps every provider exception in KilnRunError before it reaches the retry loop — so the checks never matched and rate-limited/transient eval items failed on their first attempt instead of using AsyncJobRunner's max_retries=2. Tests didn't catch it because they raise bare litellm errors instead of wrapped ones. The classifier now unwraps KilnRunError (recursively, guarding a contract-violating None original) and classifies the underlying error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JNMsZVT2b9PuANGAHkPyb1
WalkthroughAdds KilnRunError unwrapping in eval retry classification, uses the underlying exception text for RetryableError messages, and expands tests for wrapped, nested, and non-retryable failures. ChangesRetry error unwrapping
Estimated code review effort: 2 (Simple) | ~12 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📊 Coverage ReportOverall Coverage: 92% Diff: origin/main...HEAD
Summary
|
There was a problem hiding this comment.
Code Review
This pull request introduces a helper function _unwrap_kiln_run_error to extract the underlying exception from a wrapped KilnRunError and integrates it into _is_retryable_error to ensure transient errors (like rate limits) are correctly classified for retries. Unit tests are also added to verify this behavior. Feedback suggests unwrapping the exception in run_job as well, to ensure that logged messages and raised RetryableErrors contain the detailed underlying error message rather than the generic KilnRunError text, aligning with the docstring's intent.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…apper text Addresses review: run_job re-raised transient failures as RetryableError(str(e)) where e is the KilnRunError wrapper, whose message is genericized user-facing text — so logs after retry exhaustion lost the provider detail. Use the shared unwrap so the underlying error's message is kept (same as #1547). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JNMsZVT2b9PuANGAHkPyb1
What
Extracted root-cause fix from #1547 (as discussed there), targeting
maindirectly since evals on main are affected today.EvalRunner.run_jobclassifies transient failures with_is_retryable_errorand re-raises them asRetryableErrorsoAsyncJobRunnerretries them (max_retries=2). But the classifier checksisinstance(e, litellm.RateLimitError)etc., while the model adapter wraps every provider exception inKilnRunError(base_adapter.py) before it reaches the retry loop. The wrapper never matched, so no transient eval error was ever retried on a real run — rate-limited items failed on their first attempt. Tests didn't catch it because they raise bare litellm errors instead of wrapped ones.Fix
_is_retryable_errornow unwrapsKilnRunErrorvia a small_unwrap_kiln_run_errorhelper and classifies the underlying error. The helper walks nested wrappers (not currently possible, defense-in-depth) and guards a contract-violatingNoneoriginal.That's the whole change — no new retry knobs or backoff behavior; the runner's existing
max_retries=2/ 1s delay simply works now. The fuller retry work (configurable schedule for background jobs, jittered exponential backoff) stays in #1547.Tests
🤖 Generated with Claude Code
https://claude.ai/code/session_01JNMsZVT2b9PuANGAHkPyb1