fix(proxy): trigger gateway fallbacks on local rate limit errors - #31788
Conversation
When pre-call hooks (parallel_request_limiter, dynamic_rate_limiter_v3) reject a request with ProxyRateLimitError, the router's fallback logic was never reached because the exception was raised before route_request was called. Add _pre_call_with_fallbacks that catches ProxyRateLimitError, resolves configured fallbacks (key-level router_settings -> router-level), and retries with each fallback model in order. If all fallbacks are also rate-limited, the original error is re-raised.
|
|
Greptile SummaryThis PR fixes a gap where
Confidence Score: 5/5Safe to merge — the change is additive, backward-compatible, and well-tested. The new No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/common_request_processing.py | Adds _pre_call_with_fallbacks and _resolve_fallback_models; replaces direct common_processing_pre_call_logic call with the new wrapper at the single call-site in common_proxy_request_processing. Control flow, exception handling (BaseException guard for model-state restore), and backward-compatibility (no-op when no router or no fallbacks) all look correct. |
| tests/test_litellm/proxy/test_common_request_processing.py | Adds six tests covering the new fallback path. The integration test (test_real_parallel_request_limiter_model_tpm_limit_triggers_fallback) uses real limiter code with an in-memory DualCache (no network calls), and its hardcoded cache-key format matches the actual parallel_request_limiter key format ({api_key}::{model}::{precise_minute}::request_count). |
Reviews (6): Last reviewed commit: "test(proxy): cover per-key per-model TPM..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes a gap where
Confidence Score: 3/5The new fallback wrapper has two correctness gaps that affect production behaviour: model state is left dirty on certain exception paths, and the primary model's logging object is silently dropped rather than closed with a failure callback. The core fallback loop mutates self.data["model"] before calling common_processing_pre_call_logic for each fallback, but only restores it after the loop exhausts all fallbacks via ProxyRateLimitError. Any other exception from a fallback escapes with the model name stuck at the last-tried fallback. Separately, every call to common_processing_pre_call_logic creates a fresh LiteLLMLoggingObj; when the primary model's attempt is caught and swallowed, its logging object receives no callback, so rate-limit events on the primary are invisible to every configured handler. litellm/proxy/common_request_processing.py — specifically the fallback loop in _pre_call_with_fallbacks and the exception handling around the primary model's logging object lifecycle.
|
| Filename | Overview |
|---|---|
| litellm/proxy/common_request_processing.py | Adds _pre_call_with_fallbacks and _resolve_fallback_models to catch ProxyRateLimitError before routing and retry with configured fallback models; two bugs: (1) self.data["model"] is not restored when a fallback raises a non-rate-limit exception, and (2) the primary model's LiteLLMLoggingObj is abandoned without firing any failure callback. |
| tests/test_litellm/proxy/test_common_request_processing.py | Adds 5 focused unit tests for the new fallback logic; all tests use mocks and make no real network calls; missing a test case for the non-ProxyRateLimitError exception path in the fallback loop (the unguarded state mutation bug). |
Reviews (2): Last reviewed commit: "fix(proxy): trigger gateway fallbacks on..." | Re-trigger Greptile
…ack loop Addresses Greptile review feedback: wrap the fallback loop in try/except BaseException to always restore self.data['model'] to the original value when a non-ProxyRateLimitError exception escapes a fallback attempt. Add regression test for this edge case
|
@greptileai review |
…lback Drive the real parallel_request_limiter through _pre_call_with_fallbacks for the LIT-3890 customer scenario: a key-level model_tpm_limit raises ProxyRateLimitError from the pre-call hook and the configured gateway fallback serves the request instead of returning a 429. Unlike the existing tests, this exercises the actual limiter rather than a hand-built error. Also switch the new _pre_call_with_fallbacks return annotation to builtin tuple to stay within the ruff UP006 strict-rule budget.
f8606b8
into
litellm_internal_staging
Relevant issues
Fixes #8822
Linear ticket
Resolves LIT-3890
Pre-Submission checklist
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Config used (
test_fallback_config.yaml):A custom callback (
test_rate_limit_hook.py) that raisesProxyRateLimitErrorforanthropic-haiku-4-5on every call, simulating whatparallel_request_limiterdoes whenmodel_tpm_limitis exceeded:Started the proxy:
Test 1: Rate-limited model falls back successfully
{ "id": "chatcmpl-e989d131-abcb-4fb0-9501-9c62da9bc34b", "model": "anthropic-haiku-4-5", "choices": [ { "finish_reason": "stop", "index": 0, "message": { "content": "Hello to you.", "role": "assistant" } } ], "usage": { "completion_tokens": 7, "prompt_tokens": 15, "total_tokens": 22 } }Proxy log confirms the fallback:
Test 2: Non-rate-limited model works directly (no fallback)
{ "id": "chatcmpl-db87e126-7cab-421e-ba29-67c089669c8d", "model": "anthropic-sonnet-4-5", "choices": [ { "finish_reason": "stop", "index": 0, "message": { "content": "Hi there!", "role": "assistant" } } ] }Test 3:
disable_fallbacks=truereturns 429 (no fallback){ "error": { "message": "Simulated local rate limit: model=anthropic-haiku-4-5 exceeded TPM limit (call #2)", "type": "throttling_error", "code": "429" } }Type
Bug Fix
Changes
ProxyRateLimitErrorraised by pre-call hooks (parallel_request_limiter,dynamic_rate_limiter_v3, etc.) happens inproxy_logging_obj.pre_call_hook()which runs beforeroute_request(). Since the router'sasync_function_with_fallbacks()never sees the error, configured fallback models are never triedThis PR adds
_pre_call_with_fallbacks()inCommonRequestProcessingthat wrapscommon_processing_pre_call_logic(). When aProxyRateLimitErroris caught, it resolves fallback models from key-levelrouter_settingsor router-level fallbacks (using the sameget_fallback_model_group()the router uses), then retries pre-call logic with each fallback. If all fallbacks also fail, the original error is re-raisedThe
disable_fallbacksrequest flag is respected; when set, the original error propagates immediatelyFive regression tests cover: fallback triggered on rate limit, no fallbacks configured, all fallbacks also rate-limited, key-level router_settings precedence, and the
disable_fallbacksflag