refactor(exceptions): extract exception_type provider dispatch so basedpyright can analyze it#30802
Conversation
…edpyright can analyze it CustomStreamWrapper aside, exception_type is one of three functions in litellm/ that basedpyright refuses to analyze, emitting "Code is too complex to analyze" and skipping the entire ~2,280-line body. Every type error on the error-mapping path was therefore invisible and unguarded. This extracts the ~20-branch per-provider dispatch into 18 typed helper functions (one per provider family), leaving the common setup, the timeout/litellm_proxy special cases, the generic catch-all and the except handler in exception_type. The function drops well under the complexity ceiling and basedpyright now type-checks it and every helper. The dynamic provider exception is modelled with a small _ProviderHTTPException Protocol so the helpers can read status_code/message/response without Any, keeping the per-rule basedpyright budget green on the unchanged baseline (no ceiling moves). The per-branch exception_mapping_worked = True assignments become moot once each branch lives in its own helper: the except handler already re-raises litellm exceptions through its isinstance loop regardless of the flag, and the three raise original_exception sites already ran with the flag False, so the propagated exception is identical in every case. Extracting the blocks surfaced a latent bug: two branches (replicate's "ModelError" and cohere's "CohereConnectionError") key on type(original_exception).__name__, which the original code held in a local named exception_type that shadowed the function. Threaded that class-name value into the helpers; without it the name resolves to the module-level function and the branch silently mismaps (or raises TypeError) to a generic APIConnectionError. Added two regression tests that map a real provider-shaped exception through exception_type and assert the correct litellm exception; both fail if the class-name threading is broken and pass once it is restored.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR refactors
Confidence Score: 5/5Safe to merge. The refactoring faithfully preserves all original exception-mapping branches, the latent class-name bug is fixed and regression-tested, and the except handler correctly attaches litellm_response_headers on re-raised mapped exceptions via the isinstance loop just as before. The extraction is mechanical and complete: every provider branch maps to exactly one helper, the class-name string is threaded into every call site, and the except handler's setattr+isinstance path for already-mapped exceptions works identically to the old exception_mapping_worked=True path. The three new tests are well-targeted and would catch the most likely regression vectors. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/litellm_core_utils/exception_mapping_utils.py | ~2,000 lines of provider dispatch extracted into 18 typed helpers; exception_type class-name string correctly threaded into every helper; except-handler's isinstance loop and setattr for litellm_response_headers preserved correctly in both old and new paths; no new bugs introduced |
| tests/test_litellm/litellm_core_utils/test_exception_mapping_utils.py | Three new mock-only regression tests added: replicate ModelError→BadRequestError, cohere CohereConnectionError→RateLimitError, replicate 422→UnprocessableEntityError; no real network calls; new test classes are local stubs that don't conflict with production imports |
Reviews (2): Last reviewed commit: "refactor(exceptions): drop unreachable d..." | Re-trigger Greptile
The replicate status-code ladder carried two identical `status_code == 422` elif branches. Python's elif chain exits on the first match, so the second branch could never run; it was dead code faithfully carried over from the pre-refactor function. Removing it leaves the reachable branch that maps a replicate 422 to UnprocessableEntityError untouched. Adds a regression test pinning that mapping so the surviving branch can't be dropped or broken silently.
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit a795bd0. Configure here.
…edpyright can analyze it (BerriAI#30802)
Relevant issues
Second of three PRs making basedpyright actually analyze the codebase's hottest functions. Across
litellm/, basedpyright bails out with "Code is too complex to analyze" on exactly three functions, skipping their entire bodies:CustomStreamWrapper.chunk_creator(done in #30793),exception_type(this PR), andcompletion(litellm/main.py, still to come). Each is scoped on its own.Linear ticket
N/A
Pre-Submission checklist
exception_typeclass-name local that two provider branches key on), plus one pinning the replicate 422 mapping after the dead-branch cleanup below; each fails when its target is broken and passes once it is restored, and the existing mapped suite exercises the dispatch across providersmake test-unitexception_typeand its helpers in one module, plus the mapped test fileWhat and why
exception_typepacked a ~20-branch per-providerif/elifdispatch, each branch heavy with nested status-code and error-string checks, into a single ~2,280-line body. That pushed it past basedpyright's code-flow complexity ceiling, so basedpyright emitted "Code is too complex to analyze" and skipped the whole function. Every type error on one of the hottest error-mapping paths was invisible and unguarded.This extracts the dispatch into 18 typed helper functions, one per provider family, and leaves the common setup, the timeout and
litellm_proxyspecial cases, the generic catch-all and theexcepthandler inexception_type. The function drops well under the ceiling and basedpyright now type-checks it and every helper.The dynamic provider exception is modelled with a small
_ProviderHTTPExceptionProtocol so the helpers readstatus_code/message/responsewithoutAny, which keeps the per-rule basedpyright budget green on the unchanged baseline; no ceiling moves. The per-branchexception_mapping_worked = Trueassignments become moot once each branch lives in its own helper: theexcepthandler already re-raises litellm exceptions through itsisinstanceloop regardless of the flag, and the threeraise original_exceptionsites already ran with the flagFalse, so the propagated exception is identical in every case.Extracting the blocks surfaced a latent bug. Two branches, replicate's
"ModelError"and cohere's"CohereConnectionError", key ontype(original_exception).__name__, which the original code held in a local namedexception_typethat shadowed the function itself. That class-name value is now threaded into the helpers; without it the bare name resolves to the module-level function and the branch silently mismaps (or raisesTypeError) into a genericAPIConnectionError. The two new regression tests cover exactly these paths.Extraction also surfaced a dead branch in the replicate status-code ladder: two identical
status_code == 422elifbranches, where the second can never run because theelifchain exits on the first match. It was faithfully carried over from the pre-refactor function. This drops the unreachable duplicate and leaves the reachable branch that maps a replicate 422 toUnprocessableEntityErroruntouched; since the removed branch was dead, there is no behavior change. A third regression test pins that 422 mapping so the surviving branch can't be dropped or broken silently.Screenshots / Proof of Fix
Before, on
litellm_internal_staging, basedpyright refuses to analyze the function:After this PR the bailout is gone and the per-rule budget gate passes on the unchanged budget:
Behavioral proof against a live proxy hitting real provider APIs and spending real money. The refactored dispatch routes by provider, so this exercises two different helpers on real upstream errors plus a billed success, and confirms each maps to the correct litellm exception.
_map_anthropic_exception_map_openai_exceptionThe regression tests are also mutation-checked: passing the class-name as
""to the helpers (simulating broken threading) makes the replicate and cohere tests fail, and restoring the threading makes them pass; disabling the surviving 422 branch makes the new replicate 422 test fail.Type
🧹 Refactoring
Changes
Extracts the per-provider dispatch out of
exception_typeinto 18 typed helper functions plus a_ProviderHTTPExceptionProtocol for the dynamic exception shape, restoring basedpyright analysis of the function and its helpers with no budget ceiling moved. Threads the exception class-name into the helpers so the replicate and cohere class-name branches keep working, drops the unreachable duplicate replicate 422 branch, and adds three regression tests for those paths. No behavior change to the propagated exceptions.Note
Medium Risk
Large mechanical move on the central LLM error-mapping path across many providers; behavior is intended to be unchanged but any wiring mistake would misclassify errors for all routed requests.
Overview
Refactors
exception_typeso basedpyright can type-check it: the giant per-providerif/elifladder moves into 18_map_*_exceptionhelpers, with a_ProviderHTTPExceptionProtocol and a cast on the incoming provider error.exception_typekeeps shared setup (debug prints,error_str,extra_information), timeout / proxy edge cases, and the generic fallback; each provider branch is now a single helper call.The refactor threads the original exception’s class name (
type(original_exception).__name__) into helpers as theexception_typeargument so replicateModelErrorand cohereCohereConnectionErrorbranches still match on the SDK type name (previously a local shadowed the function name and could break silently). It also removes a duplicate unreachable replicate422branch and adds three regression tests for those paths.Reviewed by Cursor Bugbot for commit a795bd0. Bugbot is set up for automated code reviews on this repo. Configure here.