Skip to content

refactor(exceptions): extract exception_type provider dispatch so basedpyright can analyze it#30802

Merged
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_refactor_exception_type_complexity
Jun 22, 2026
Merged

refactor(exceptions): extract exception_type provider dispatch so basedpyright can analyze it#30802
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_refactor_exception_type_complexity

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

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), and completion (litellm/main.py, still to come). Each is scoped on its own.

Linear ticket

N/A

Pre-Submission checklist

  • I have added meaningful tests: two regression tests for the latent bug this refactor surfaced (the exception_type class-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 providers
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only restructures exception_type and its helpers in one module, plus the mapped test file
  • I have requested a Greptile review and received a Confidence Score of at least 4/5

What and why

exception_type packed a ~20-branch per-provider if/elif dispatch, 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_proxy special cases, the generic catch-all and the except handler in exception_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 _ProviderHTTPException Protocol so the helpers read status_code/message/response without Any, which keeps 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 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 raises TypeError) into a generic APIConnectionError. 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 == 422 elif branches, where the second can never run because the elif chain 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 to UnprocessableEntityError untouched; 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:

$ uv run basedpyright litellm/litellm_core_utils/exception_mapping_utils.py 2>&1 | grep "too complex"
  litellm/litellm_core_utils/exception_mapping_utils.py:247:5 - error: Code is too complex to analyze; reduce complexity by refactoring into subroutines or reducing conditional code paths (reportGeneralTypeIssues)

After this PR the bailout is gone and the per-rule budget gate passes on the unchanged budget:

$ uv run basedpyright litellm/litellm_core_utils/exception_mapping_utils.py 2>&1 | grep -c "too complex"
0

$ make lint-basedpyright
OK: every rule is within its basedpyright ceiling

$ git diff --stat basedpyright-code-budget.json
(no changes)

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.

  1. Start the proxy
python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver 2>&1 | tee litellm.log
  1. Billed success through the shared path (the module is on every request; this confirms the happy path is intact)
curl -s http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"model":"anthropic-haiku-4-5","messages":[{"role":"user","content":"Reply with exactly: exception refactor intact"}]}'
# -> "exception refactor intact"   (proxy-computed spend $0.00005)
  1. Real Anthropic context-window overflow, mapped by _map_anthropic_exception
# a >200k-token prompt to anthropic-haiku-4-5
# Anthropic returns 400 "prompt is too long: 207805 tokens > 200000 maximum"
HTTP status: 400
message: litellm.ContextWindowExceededError: litellm.BadRequestError: AnthropicError - prompt is too long: 207805 tokens > 200000 maximum
  1. Real OpenAI bad request, mapped by _map_openai_exception
curl -s http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.5","messages":[]}'
HTTP status: 400
type: invalid_request_error
message: litellm.BadRequestError: OpenAIException - Invalid 'messages': empty array. Expected an array with minimum length 1, but got an empty array instead.

The 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_type into 18 typed helper functions plus a _ProviderHTTPException Protocol 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_type so basedpyright can type-check it: the giant per-provider if/elif ladder moves into 18 _map_*_exception helpers, with a _ProviderHTTPException Protocol and a cast on the incoming provider error.

exception_type keeps 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 the exception_type argument so replicate ModelError and cohere CohereConnectionError branches still match on the SDK type name (previously a local shadowed the function name and could break silently). It also removes a duplicate unreachable replicate 422 branch 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.

…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

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors exception_type in exception_mapping_utils.py by extracting its ~18 per-provider exception-mapping branches into individual typed helpers (_map_openai_exception, _map_anthropic_exception, etc.) so basedpyright can analyze the function. It also threads the class-name local variable that was shadowing the function name into each helper and drops the dead duplicate replicate 422 branch.

  • Provider dispatch extracted into 18 typed helpers plus a _ProviderHTTPException Protocol; exception_type body now stays within basedpyright's complexity ceiling with no budget change.
  • Latent bug fixed: two branches (replicate \"ModelError\" and cohere \"CohereConnectionError\") keyed on type(original_exception).__name__, stored in a local that shadowed the function; the class-name string is now threaded into each helper as exception_type: str.
  • Dead branch removed: the unreachable second status_code == 422 branch in the replicate ladder is dropped; three regression tests cover the ModelError path, the CohereConnectionError path, and the surviving 422→UnprocessableEntityError path.

Confidence Score: 5/5

Safe 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.

Important Files Changed

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

Comment thread litellm/litellm_core_utils/exception_mapping_utils.py Outdated
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.
@mateo-berri

Copy link
Copy Markdown
Collaborator Author

@greptileai


Generated by Claude Code

@mateo-berri

Copy link
Copy Markdown
Collaborator Author

bugbot run


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@mateo-berri mateo-berri merged commit 1ea91bf into litellm_internal_staging Jun 22, 2026
124 checks passed
@mateo-berri mateo-berri deleted the litellm_refactor_exception_type_complexity branch June 22, 2026 06:47
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
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.

2 participants