mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(rate-limiting): stop a mid-chain fallback rejection from bypassing an apply_to_models cap (ported from #38347)
_pre_call_with_fallbacks only checked the original exception for cross_model_scope before starting the fallback loop; a later fallback attempt's own cross_model_scope rejection was swallowed by the loop's plain except-continue and the next, unlisted fallback model silently served the request instead (baefd27677).
This commit is contained in:
parent
0070385683
commit
01b75cdfd9
2 changed files with 77 additions and 1 deletions
|
|
@ -2045,7 +2045,19 @@ class ProxyBaseLLMRequestProcessing:
|
|||
route_type=route_type,
|
||||
llm_router=llm_router,
|
||||
)
|
||||
except ProxyRateLimitError:
|
||||
except ProxyRateLimitError as fallback_exc:
|
||||
# A fallback attempt's own rejection can carry the
|
||||
# identical cross_model_scope marker (this fallback
|
||||
# model is itself covered by the same apply_to_models
|
||||
# chain-wide cap) -- continuing to the next fallback
|
||||
# would silently serve the request through a model
|
||||
# outside that cap, defeating it just as much as not
|
||||
# checking the original exception would.
|
||||
if (
|
||||
isinstance(fallback_exc.detail, Mapping)
|
||||
and fallback_exc.detail.get("cross_model_scope") is True
|
||||
):
|
||||
raise
|
||||
continue
|
||||
except BaseException:
|
||||
self.data["model"] = original_model
|
||||
|
|
|
|||
|
|
@ -5535,6 +5535,70 @@ class TestPreCallWithFallbacksOnLocalRateLimit:
|
|||
assert call_count == 1
|
||||
assert processor.data["model"] == primary_model
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cross_model_scoped_rejection_mid_chain_stops_further_fallback_attempts(self):
|
||||
"""
|
||||
The check above only covers the ORIGINAL exception, before the
|
||||
fallback loop starts. A later fallback attempt's own rejection was
|
||||
never checked the same way -- the loop's ``except
|
||||
ProxyRateLimitError: continue`` swallowed it and moved on to the next
|
||||
fallback model. If a chain-wide ``apply_to_models`` cap covers both
|
||||
the primary model and the first fallback, and a second fallback model
|
||||
isn't covered, this let the second fallback silently serve the
|
||||
request the cap was meant to block. The original (non-scoped)
|
||||
rejection enters the loop normally; the FIRST fallback's own
|
||||
rejection carries ``cross_model_scope=True`` and must stop the loop
|
||||
immediately, never reaching the second fallback.
|
||||
"""
|
||||
from litellm.proxy.common_utils.proxy_rate_limit_error import ProxyRateLimitError
|
||||
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
|
||||
|
||||
primary_model = "opus-chain"
|
||||
|
||||
processor = ProxyBaseLLMRequestProcessing(data={"model": primary_model})
|
||||
|
||||
attempted_models: list[str] = []
|
||||
|
||||
async def mock_pre_call_logic(**kwargs):
|
||||
attempted_models.append(processor.data["model"])
|
||||
if processor.data["model"] == primary_model:
|
||||
# Original attempt: a plain, non-scoped rejection (e.g. a
|
||||
# per-deployment limit), not the chain-wide cap itself.
|
||||
raise ProxyRateLimitError(detail={"error": "tag_rate_limit_exceeded"}, headers={"retry-after": "30"})
|
||||
if processor.data["model"] == "sonnet-chain":
|
||||
# First fallback: rejected by the SAME chain-wide cap.
|
||||
raise ProxyRateLimitError(
|
||||
detail={"error": "tag_rate_limit_exceeded", "cross_model_scope": True},
|
||||
headers={"retry-after": "30"},
|
||||
)
|
||||
raise AssertionError(f"must not attempt a second fallback model: {processor.data['model']}")
|
||||
|
||||
mock_router = MagicMock()
|
||||
mock_router.fallbacks = [{"opus-chain": ["sonnet-chain", "haiku-chain"]}]
|
||||
|
||||
with patch.object(processor, "common_processing_pre_call_logic", side_effect=mock_pre_call_logic):
|
||||
with pytest.raises(ProxyRateLimitError) as exc_info:
|
||||
await processor._pre_call_with_fallbacks(
|
||||
request=MagicMock(),
|
||||
general_settings={},
|
||||
proxy_logging_obj=MagicMock(),
|
||||
user_api_key_dict=MagicMock(router_settings=None),
|
||||
version=None,
|
||||
proxy_config=MagicMock(),
|
||||
user_model=None,
|
||||
user_temperature=None,
|
||||
user_request_timeout=None,
|
||||
user_max_tokens=None,
|
||||
user_api_base=None,
|
||||
model=primary_model,
|
||||
route_type="acompletion",
|
||||
llm_router=mock_router,
|
||||
)
|
||||
|
||||
assert attempted_models == [primary_model, "sonnet-chain"]
|
||||
assert exc_info.value.detail.get("cross_model_scope") is True
|
||||
assert processor.data["model"] == primary_model
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_parallel_request_limiter_model_tpm_limit_triggers_fallback(self):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue