litellm/tests/unit/proxy/hooks
Kerry Lu 172e16acde
feat(proxy): router_settings.fallback_on_saturation walks key and team fallbacks before a model saturates, replacing team model_preferences (#51439)
* feat(proxy): fallback_on_saturation walks key and team fallbacks before a model saturates, replacing team_metadata.model_preferences

Team router_settings.fallbacks now fire on proxy-side rate limits: the pre-call
fallback walk reads router settings Key > Team > Global instead of the key's
column alone, and skips a fallback the caller may not call.

A key or team with router_settings.fallback_on_saturation: true walks that same
fallback list as soon as the requested model reaches saturation_threshold in
dynamic_rate_limiter_v3, and goes back to the requested model when no fallback
admits the request. Responses carry x-litellm-fallback-reason:
rate_limit:model_saturation_fallback.

Removes the team_metadata.model_preferences steering and the
x-litellm-model-steered-from header from #51350, which did the same job through
a second knob.

* fix(proxy): strict bool for fallback_on_saturation, drop a stale import and settle the pre-call tests

fallback_on_saturation only turns on for a real boolean true, so a string like "yes" in router_settings no longer enables it. The saturation unit test reads saturation through the limiter's cache, so it asserts the threshold was reached instead of an exact count. The response polling test now hands the pre-call path an awaitable router_settings lookup, as the pre-call fallback walk reads Key > Team settings before the first call.

* fix(proxy): pre-call fallbacks pass the budget and access-group checks a direct request does

A fallback the pre-call walk picks now has to clear every per-model budget layer and the budgets of the access
groups that grant it, and its spend is charged to those groups instead of the requested model's. At saturation a
fallback that fails for any reason counts as a miss and the request goes back to the requested model. The client
body can no longer opt itself into the saturation walk through router_settings_override, only the saved key or team
router_settings can. The integration test for a forbidden fallback waits for the preferred model's own 429 so it
cannot pass before saturation.

* refactor(proxy): one helper decides which refused fallbacks end the pre-call walk

_pre_call_with_fallbacks had grown past the strict C901 ceiling, so the two except clauses fold into one and _fallback_refusal_ends_the_walk carries the rule: at saturation every refusal is a miss, otherwise only a non-rate-limit failure or a hard per-model 429 reaches the caller.

* fix(proxy): keep the caller's request object through the pre-call fallback walk

Copying the request to drop a client fallback_on_saturation opt-in handed the hooks a different dict than the one the caller holds, so header metadata written into one never reached the other (test_add_litellm_metadata_from_request_headers). drop_client_saturation_opt_in now edits the request in place.

* fix(proxy): pre-call fallbacks keep the pricing and budget-reservation rules a direct request passes

A fallback is skipped when unpriced models are blocked and it has no pricing, when a zero-cost request would land on a priced model, and when the budget auth reserved for the requested model does not fit it (higher per-token prices, or reserved against an access group that does not grant it).

* refactor(proxy): read fallback prices through the router registry entry point

router_surface_check only lets the router surface shrink, so the per-token price lookup goes through registry().group() instead of the deprecated get_model_group_info.

* fix(proxy): a fallback's group budget check skips the groups the request already holds

The auth-time reservation sits in the same counter the plain budget read uses, so a fallback in a group the request already reserved against was refused by its own hold once the group was within one estimate of max_budget. Groups without a hold keep the plain read.

* test(dynamic_rate_limiter_v3): the throttle metric test from main uses fallback_on_saturation instead of the removed steering

A saturation handoff carries no rate_limited_by reason, so it is never counted as a throttle.

* fix(proxy): the saturation handoff only fires under the fallback walk

_pre_call_with_fallbacks arms the request before the hooks run, so routes that call the hooks directly (batches, fine-tuning, polling, images) keep admitting or refusing an opted-in key on a saturated model as before instead of surfacing the handoff 429.

* style(proxy): keep the cast-ok reason within the line length

* fix(proxy): never invent a metadata bucket when arming the saturation fallback walk, take main's limiter metric tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): arm the saturation fallback walk in the request context, refuse fallbacks for agent-owned keys

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): keep the saturation walk state out of the body, check the end user's allowlist on fallbacks, release a refused fallback's rate limit hold

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): only load the end user for fallback checks when a database is connected

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): grant pre-call fallbacks on the model an alias serves and release the rate limited model's hold before the walk

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): check a pre-call fallback's per-model budgets on its listed name and its served group, as auth does

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): assert the listed fallback name's budget blocks an aliased fallback, not the order of budget reads

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): release the dynamic limiter's reservation when the fallback walk leaves a model, and replay an exhausted walk with the requested model

The dynamic rate limiter admits through its own inner v3 limiter, so the walk's hold release never reached its team-share
reservation. It now exposes async_post_call_failure_hook, which refunds that reservation when the proxy refuses a request
after admission, and _release_pre_call_hold calls it next to the registered limiter.

An exhausted saturation walk replayed with the already aliased model name, so chained model_alias_map entries moved the
request on to a further alias. It now replays with the model argument the first attempt saw.

The hold-release test drives the real v3 limiter with one slot per key instead of recording hook calls.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): type the dynamic limiter's failure hook request dict for the type-discipline gate

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-13 05:57:18 +00:00
..
litellm_skills test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00
__init__.py ci: move tests/proxy_unit_tests to tests/unit/proxy and run the proxy-db shards from litellm-tests (#42903) 2026-09-24 22:59:11 +00:00
test_async_post_call_streaming_iterator_hook.py fix(guardrails): run the end-of-stream post_call scan when the client disconnects mid-stream (#43839) 2026-10-05 01:44:43 -07:00
test_autorouter_baseline_cache.py feat(messages): serve anthropic messages on rust only (#51119) 2026-11-12 00:07:20 +00:00
test_banned_keyword_list.py chore: move workspace packages under litellm (#51000) 2026-11-12 04:19:57 +00:00
test_batch_enqueued_tokens.py test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00
test_batch_file_validation.py refactor(proxy): expose public names for private proxy helpers (#45170) 2026-10-07 18:59:27 -07:00
test_batch_rate_limiter.py feat(redis): serve the proxy's common Redis call with Rust stores (#50170) 2026-11-06 06:01:11 +00:00
test_dynamic_rate_limiter.py refactor(rust_bridge): route every Rust-to-Python call through host modules (#50822) 2026-11-11 01:57:54 +00:00
test_dynamic_rate_limiter_v3.py feat(proxy): router_settings.fallback_on_saturation walks key and team fallbacks before a model saturates, replacing team model_preferences (#51439) 2026-11-13 05:57:18 +00:00
test_image_generation_guardrails.py test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00
test_key_management_event_hooks.py refactor(typing): clear about 1,100 basedpyright Any errors across 303 files (#49986) 2026-11-06 13:31:15 -08:00
test_max_budget_per_session_limiter.py fix(redis): share one native circuit breaker between RedisCache and the Rust stores (#50473) 2026-11-07 17:50:06 +09:00
test_max_iterations_limiter.py refactor(typing): clear about 1,100 basedpyright Any errors across 303 files (#49986) 2026-11-06 13:31:15 -08:00
test_message_thread_ownership.py feat(anthropic): forward message threads on native /v1/messages (#50976) 2026-11-10 18:33:54 -08:00
test_model_budget_shared_windows.py feat(proxy)!: model family and access group entries in model_max_budget (#47722) 2026-10-28 15:40:57 -07:00
test_model_max_budget_limiter.py feat: add OpenRouter-style -latest model aliases for first-party providers (#50315) 2026-11-06 18:22:38 +08:00
test_parallel_request_limiter.py fix(proxy): count native Responses WebSocket turn tokens toward key and tag TPM limits (#49257) 2026-10-31 02:53:27 -07:00
test_parallel_request_limiter_v3.py feat(proxy): enforce customer org, team and member budgets and limits on the request path (#51489) 2026-11-12 20:43:44 -08:00
test_post_call_failure_hook_integration.py test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00
test_post_call_response_headers_hook.py test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00
test_post_call_streaming_hook_integration.py test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00
test_post_call_success_hook_integration.py test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00
test_priority_class_share.py feat(dynamic_rate_limiter_v3): priority_class_share_splits_equally_among_active_teams splits a priority class share among the teams active on a model (#51366) 2026-11-12 06:38:07 -08:00
test_prompt_cache_observer.py fix(proxy): keep a retried /v1/messages stream's spend log by logging the superseded attempt as a failure (#45136) 2026-10-16 21:19:58 +00:00
test_prompt_injection_detection.py refactor(rust_bridge): route every Rust-to-Python call through host modules (#50822) 2026-11-11 01:57:54 +00:00
test_proxy_hooks_init.py chore: move workspace packages under litellm (#51000) 2026-11-12 04:19:57 +00:00
test_proxy_rate_limit_provider_field.py refactor(router): move router callers onto the entry points and invert the proxy imports (#48847) 2026-10-29 17:26:33 +00:00
test_proxy_track_cost_callback.py test(proxy): await background spend counters before asserting (#51249) 2026-11-11 16:26:58 -08:00
test_rate_limiter_toctou.py feat(redis): serve the proxy's common Redis call with Rust stores (#50170) 2026-11-06 06:01:11 +00:00
test_search_free_tier.py fix(proxy): scope the search free-tier counter to the request's provider (#47646) 2026-10-20 22:01:53 +00:00
test_send_invite_email.py test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00
test_sensitive_data_routing.py fix(redis): share one native circuit breaker between RedisCache and the Rust stores (#50473) 2026-11-07 17:50:06 +09:00
test_tpm_concurrent.py feat(redis): serve the proxy's common Redis call with Rust stores (#50170) 2026-11-06 06:01:11 +00:00
test_unit_test_max_model_budget_limiter.py fix(router): inspect image content without translating routed requests (#49371) 2026-10-31 10:44:24 -07:00
test_user_management_event_hooks.py test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy (#43998) 2026-10-01 10:11:45 -07:00