Repository navigation
chore(proxy): tighten router-settings-override and mock-testing trust #26968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
yuneng-berri
merged 5 commits into
BerriAI:litellm_yj_may1_2
from
stuxf:chore/router-override-trust
May 2, 2026
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
a5b7eee
chore(proxy): close router-settings-override fallback smuggling path
stuxf cc9700f
Merge remote-tracking branch 'upstream/main' into fix/router-override…
stuxf e60a72e
fix(proxy): hardcode mock-testing strip list to avoid cyclic import
stuxf 60996eb
chore: retrigger PR checks
stuxf 8fc31a7
Merge branch 'litellm_yj_may1_2' into chore/router-override-trust
yuneng-berri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
tests/test_litellm/proxy/auth/test_router_override_fallback_auth.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| """ | ||
| VERIA-44: ``router_settings_override.fallbacks`` must be validated | ||
| against the API key's model allowlist at auth time. Without this, the | ||
| override is promoted to per-request kwargs after auth and lets a caller | ||
| execute requests against models their API key cannot call. | ||
| """ | ||
|
|
||
| from typing import List | ||
| from unittest.mock import AsyncMock, patch | ||
|
|
||
| import pytest | ||
|
|
||
| from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth | ||
| from litellm.proxy.auth.user_api_key_auth import ( | ||
| _enforce_key_and_fallback_model_access, | ||
| iter_router_fallback_model_names, | ||
| ) | ||
|
|
||
|
|
||
| def _key_with_models(models: List[str]) -> UserAPIKeyAuth: | ||
| return UserAPIKeyAuth( | ||
| api_key="hashed", | ||
| user_id="caller", | ||
| user_role=LitellmUserRoles.INTERNAL_USER, | ||
| models=models, | ||
| ) | ||
|
|
||
|
|
||
| # ── iter_router_fallback_model_names ───────────────────────────────────────── | ||
|
|
||
|
|
||
| def testiter_router_fallback_model_names_router_config_shape(): | ||
| """Router-config shape: ``[{primary: [fallback_list]}]``.""" | ||
| assert list( | ||
| iter_router_fallback_model_names( | ||
| [{"gpt-3.5-turbo": ["gpt-4", "claude-3"]}, {"gpt-4o": ["o1"]}] | ||
| ) | ||
| ) == ["gpt-4", "claude-3", "o1"] | ||
|
|
||
|
|
||
| def testiter_router_fallback_model_names_simple_string_shape(): | ||
| """Simple top-level shape: list of strings.""" | ||
| assert list(iter_router_fallback_model_names(["gpt-4", "claude-3"])) == [ | ||
| "gpt-4", | ||
| "claude-3", | ||
| ] | ||
|
|
||
|
|
||
| def testiter_router_fallback_model_names_client_side_shape(): | ||
| """ClientSideFallbackModel shape: ``[{"model": "..."}]``.""" | ||
| assert list( | ||
| iter_router_fallback_model_names([{"model": "gpt-4"}, {"model": "claude-3"}]) | ||
| ) == ["gpt-4", "claude-3"] | ||
|
|
||
|
|
||
| def testiter_router_fallback_model_names_empty_or_none(): | ||
| assert list(iter_router_fallback_model_names(None)) == [] | ||
| assert list(iter_router_fallback_model_names([])) == [] | ||
| assert list(iter_router_fallback_model_names("not a list")) == [] | ||
|
|
||
|
|
||
| # ── _enforce_key_and_fallback_model_access ──────────────────────────────────── | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_router_override_fallbacks_validated_against_key_allowlist(): | ||
| """A fallback nested inside ``router_settings_override`` is validated | ||
| against the API key's allowed models — not just the top-level | ||
| ``fallbacks`` field.""" | ||
| valid_token = _key_with_models(["gpt-3.5-turbo"]) | ||
| request_data = { | ||
| "model": "gpt-3.5-turbo", | ||
| "router_settings_override": { | ||
| "fallbacks": [{"gpt-3.5-turbo": ["unauthorized-model"]}], | ||
| }, | ||
| } | ||
|
|
||
| seen_models: List[str] = [] | ||
|
|
||
| async def fake_can_key_call_model(model, llm_model_list, valid_token, llm_router): | ||
| seen_models.append(model) | ||
|
|
||
| with ( | ||
| patch( | ||
| "litellm.proxy.auth.user_api_key_auth.can_key_call_model", | ||
| side_effect=fake_can_key_call_model, | ||
| ), | ||
| patch( | ||
| "litellm.proxy.auth.user_api_key_auth.is_valid_fallback_model", | ||
| new=AsyncMock(), | ||
| ), | ||
| ): | ||
| await _enforce_key_and_fallback_model_access( | ||
| valid_token=valid_token, | ||
| request_data=request_data, | ||
| route="/v1/chat/completions", | ||
| llm_model_list=None, | ||
| llm_router=None, | ||
| ) | ||
|
|
||
| # Both the primary model and the override-nested fallback must be | ||
| # checked against the API key's allowlist. | ||
| assert "gpt-3.5-turbo" in seen_models | ||
| assert "unauthorized-model" in seen_models | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| @pytest.mark.parametrize( | ||
| "fallback_field", | ||
| [ | ||
| "fallbacks", | ||
| "context_window_fallbacks", | ||
| "content_policy_fallbacks", | ||
| ], | ||
| ) | ||
| async def test_router_override_all_fallback_fields_validated(fallback_field): | ||
| """All three fallback fields the router accepts as per-request kwargs | ||
| are validated — context_window_fallbacks and content_policy_fallbacks | ||
| are promoted in route_llm_request.py too.""" | ||
| valid_token = _key_with_models(["gpt-3.5-turbo"]) | ||
| request_data = { | ||
| "model": "gpt-3.5-turbo", | ||
| "router_settings_override": { | ||
| fallback_field: [{"gpt-3.5-turbo": ["smuggled-model"]}], | ||
| }, | ||
| } | ||
|
|
||
| seen: List[str] = [] | ||
|
|
||
| async def fake_can_key_call_model(model, llm_model_list, valid_token, llm_router): | ||
| seen.append(model) | ||
|
|
||
| with ( | ||
| patch( | ||
| "litellm.proxy.auth.user_api_key_auth.can_key_call_model", | ||
| side_effect=fake_can_key_call_model, | ||
| ), | ||
| patch( | ||
| "litellm.proxy.auth.user_api_key_auth.is_valid_fallback_model", | ||
| new=AsyncMock(), | ||
| ), | ||
| ): | ||
| await _enforce_key_and_fallback_model_access( | ||
| valid_token=valid_token, | ||
| request_data=request_data, | ||
| route="/v1/chat/completions", | ||
| llm_model_list=None, | ||
| llm_router=None, | ||
| ) | ||
|
|
||
| assert "smuggled-model" in seen | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_router_override_without_fallbacks_does_not_break_auth(): | ||
| """``router_settings_override`` set without any fallback fields is a | ||
| no-op for the auth check — only the primary model is validated.""" | ||
| valid_token = _key_with_models(["gpt-3.5-turbo"]) | ||
| request_data = { | ||
| "model": "gpt-3.5-turbo", | ||
| "router_settings_override": {"num_retries": 3, "timeout": 30}, | ||
| } | ||
|
|
||
| seen: List[str] = [] | ||
|
|
||
| async def fake_can_key_call_model(model, llm_model_list, valid_token, llm_router): | ||
| seen.append(model) | ||
|
|
||
| with ( | ||
| patch( | ||
| "litellm.proxy.auth.user_api_key_auth.can_key_call_model", | ||
| side_effect=fake_can_key_call_model, | ||
| ), | ||
| patch( | ||
| "litellm.proxy.auth.user_api_key_auth.is_valid_fallback_model", | ||
| new=AsyncMock(), | ||
| ), | ||
| ): | ||
| await _enforce_key_and_fallback_model_access( | ||
| valid_token=valid_token, | ||
| request_data=request_data, | ||
| route="/v1/chat/completions", | ||
| llm_model_list=None, | ||
| llm_router=None, | ||
| ) | ||
|
|
||
| assert seen == ["gpt-3.5-turbo"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.