Skip to content

fix(litellm): fail fast on non-transient errors - #314

Merged
hydropix merged 1 commit into
mainfrom
fix/litellm-fail-fast
Oct 23, 2026
Merged

hydropix merged 1 commit into
mainfrom
fix/litellm-fail-fast

Conversation

@hydropix

Copy link
Copy Markdown
Owner

Summary

The LiteLLM provider retried every error that was not a pooled 429. An invalid key, an unknown model or a malformed request was retried MAX_TRANSLATION_ATTEMPTS times with a 2s sleep each before returning None. The HTTP providers (OpenAI, Gemini, Mistral) already fail fast on non-retryable 4xx; this brings LiteLLM in line.

  • Non-transient LiteLLM exceptions (AuthenticationError, PermissionDeniedError, NotFoundError, BadRequestError and its subclasses such as ContentPolicyViolationError / UnsupportedParamsError, UnprocessableEntityError, BudgetExceededError) return None on the first attempt. Classification is by qualname across the MRO because litellm is an optional lazy import.
  • Any other exception carrying a non-retryable status_code (for example the generic APIError with a 402) also fails fast, reusing is_retryable_http_status().
  • ContextWindowExceededError is always raised as ContextOverflowError, even when its message matches none of the overflow keywords, so the chunking layer can still shrink and retry. It is not swallowed by the fail-fast path even though it derives from BadRequestError.
  • Unchanged: KeyPool rotation on 429 via handle_rate_limit(), exponential backoff for _TRANSIENT_EXCEPTIONS, and the plain 2s retry for unclassified exceptions.

LiteLLM stays CLI-only; nothing in the web UI changes.

Backlog note: item 6 of 3.5 in docs/BACKLOG.md (key pool peek() without acquire()) was already fixed by #273, so it is not touched here.

Tests

New cases in tests/unit/test_litellm_provider.py use fake litellm.exceptions.* classes that mirror the real hierarchy:

  • each non-transient class gives up after exactly one call
  • an auth error in a multi-key pool does not rotate keys
  • APIError with 402 fails fast, with 502 is retried
  • APIConnectionError and Timeout are still retried up to the limit
  • ContextWindowExceededError with a keyword-free message raises ContextOverflowError

8 of the new cases fail against the previous litellm.py.

The attempt-count assertions now read MAX_TRANSLATION_ATTEMPTS from the provider module instead of src.config. Another test reloads src.config, which made the existing test_rate_limit_without_pool_keeps_plain_backoff fail in a full-suite run (2 vs 3).

Full suite: 2617 passed, 6 failed. All 6 failures are in tests/characterization/test_progress_baseline.py (stale goldens, already failing on main, unrelated to LiteLLM).

🤖 Generated with Claude Code

Every exception other than a pooled 429 went through the same retry loop,
so an invalid key, an unknown model or a malformed request was retried
MAX_TRANSLATION_ATTEMPTS times with a 2s sleep each before giving up.

The provider now returns None on the first non-transient LiteLLM error,
matching the HTTP providers' handling of non-retryable 4xx:

- AuthenticationError, PermissionDeniedError, NotFoundError,
  BadRequestError (and its subclasses), UnprocessableEntityError and
  BudgetExceededError are matched by qualname across the MRO, since
  litellm is a lazy import and its classes cannot be referenced at load.
- Any other exception carrying a non-retryable `status_code` (e.g. the
  generic APIError with a 402) also fails fast via
  is_retryable_http_status().
- ContextWindowExceededError is always surfaced as ContextOverflowError,
  even when its message matches none of the overflow keywords, so the
  chunking layer can still shrink and retry.

The KeyPool 429 rotation, the backoff on _TRANSIENT_EXCEPTIONS and the
plain retry for unclassified errors are unchanged.

Tests: the attempt-count assertions now read the constant bound in the
provider module. Another test reloads src.config, which made
test_rate_limit_without_pool_keeps_plain_backoff fail in a full run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hydropix

Copy link
Copy Markdown
Owner Author

Reviewed and merging. Checking the MRO by qualname is the right call while litellm stays a lazy import, and putting the transient classes first means a 429 or a timeout can never be mistaken for a client error. Raising ContextWindowExceededError as ContextOverflowError before the fail-fast check keeps the chunk-shrinking retry working, which was the one thing I wanted to be sure of. The LiteLLM tests pass locally (25 passed) and the Docker build is green. This will go out with the next release.

@hydropix
hydropix merged commit 5c774a1 into main Oct 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant