From 1e904d1c8391ff16c957c1c7553f28e0a7626fb3 Mon Sep 17 00:00:00 2001 From: Tom Ron Date: Tue, 29 Sep 2026 11:36:50 +0300 Subject: [PATCH 1/3] fix(proxy): strip router_settings_override on the api_key/api_base routing branch route_request only removed router_settings_override in a later elif, so requests carrying api_key/api_base sent it to the provider (Anthropic 400) and dropped the key/team router settings. Pop it up front and merge its settings for every branch. Fixes #43582 Co-Authored-By: Claude Sonnet 5.5 --- litellm/proxy/route_llm_request.py | 56 ++++++++++--------- .../proxy/test_route_llm_request.py | 25 +++++++++ 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 536c58df65a..58995ee3a8f 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -423,6 +423,29 @@ RouteType = Literal[ ] +# Settings that the Router accepts as per-request kwargs. These override the +# global router settings for this specific request. +_PER_REQUEST_ROUTER_SETTINGS: Final = [ + "fallbacks", + "context_window_fallbacks", + "content_policy_fallbacks", + "num_retries", + "timeout", + "model_group_retry_policy", + "routing_strategy", + "enable_tag_filtering", +] + + +def _apply_router_settings_override(data: dict, override_settings: Any) -> None: + """Merge key/team router settings into ``data`` (request values win).""" + if not isinstance(override_settings, dict): + return + for key in _PER_REQUEST_ROUTER_SETTINGS: + if key in override_settings and key not in data: + data[key] = override_settings[key] + + async def route_request( data: dict, llm_router: LitellmRouter | None, @@ -476,6 +499,12 @@ async def _route_request_single_attempt( # noqa: ANN202 # returns unawaited pr data.pop("enable_tag_filtering", None) + # Always remove ``router_settings_override`` from the body so it can't leak + # to the provider, and apply its settings on every routing branch. + has_router_settings_override: Final = "router_settings_override" in data + if has_router_settings_override: + _apply_router_settings_override(data, data.pop("router_settings_override")) + team_id: Final = get_team_id_from_data(data) router_model_names: Final = llm_router.model_names if llm_router is not None else [] is_proxy_admin_without_team: Final = team_id is None and _is_proxy_admin_request(data) @@ -508,31 +537,8 @@ async def _route_request_single_attempt( # noqa: ANN202 # returns unawaited pr elif "user_config" in data: return _route_user_config_request(data, route_type) - elif "router_settings_override" in data: - # Apply per-request router settings overrides from key/team config - # Instead of creating a new Router (expensive), merge settings into kwargs - # The Router already supports per-request overrides for these settings - override_settings: Final = data.pop("router_settings_override") - - # Settings that the Router accepts as per-request kwargs - # These override the global router settings for this specific request - per_request_settings: Final = [ - "fallbacks", - "context_window_fallbacks", - "content_policy_fallbacks", - "num_retries", - "timeout", - "model_group_retry_policy", - "routing_strategy", - "enable_tag_filtering", - ] - - # Merge override settings into data (only if not already set in request) - for key in per_request_settings: - if key in override_settings and key not in data: - data[key] = override_settings[key] - - # Use main router with overridden kwargs + elif has_router_settings_override: + # Settings were already merged into ``data`` above; use the main router if llm_router is not None: return getattr(llm_router, f"{route_type}")(**data) else: diff --git a/tests/test_litellm/proxy/test_route_llm_request.py b/tests/test_litellm/proxy/test_route_llm_request.py index 0b51062dd66..246dfe56825 100644 --- a/tests/test_litellm/proxy/test_route_llm_request.py +++ b/tests/test_litellm/proxy/test_route_llm_request.py @@ -1325,3 +1325,28 @@ def test_proxy_model_not_found_error_keeps_the_raw_model_only_in_the_client_resp assert raw_model in error.detail["error"] assert raw_model not in error.spend_log_error_message assert error.spend_log_error_message.startswith("/chat/completions: Invalid model name passed in") + + +@pytest.mark.asyncio +@pytest.mark.parametrize("extra", [{"api_key": "sk-test"}, {"api_base": "http://localhost:1234"}]) +async def test_route_request_router_settings_override_stripped_with_api_key_or_base(extra): + """ + router_settings_override must not leak to the provider when the request + carries api_key/api_base, and its settings must still reach the Router. + """ + data = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}], + "router_settings_override": {"num_retries": 2}, + **extra, + } + + llm_router = MagicMock() + llm_router.acompletion.return_value = "success" + + response = await route_request(data, llm_router, None, "acompletion") + + assert response == "success" + call_kwargs = llm_router.acompletion.call_args[1] + assert "router_settings_override" not in call_kwargs + assert call_kwargs["num_retries"] == 2 From 0715efc444708a990751b1990a4ed5703ae28f33 Mon Sep 17 00:00:00 2001 From: Tom Ron Date: Tue, 29 Sep 2026 11:48:31 +0300 Subject: [PATCH 2/3] fix(lint): avoid Any in _apply_router_settings_override Co-Authored-By: Claude Sonnet 5.5 --- litellm/proxy/route_llm_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 58995ee3a8f..2c5fd7ec0f2 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -437,7 +437,7 @@ _PER_REQUEST_ROUTER_SETTINGS: Final = [ ] -def _apply_router_settings_override(data: dict, override_settings: Any) -> None: +def _apply_router_settings_override(data: dict, override_settings: object) -> None: """Merge key/team router settings into ``data`` (request values win).""" if not isinstance(override_settings, dict): return From 01f2e49096b1570efeb200aed8e20d4d72580caa Mon Sep 17 00:00:00 2001 From: Tom Ron Date: Tue, 29 Sep 2026 12:25:11 +0300 Subject: [PATCH 3/3] test(proxy): cover non-dict router_settings_override Co-Authored-By: Claude Sonnet 5.5 --- .../proxy/test_route_llm_request.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_litellm/proxy/test_route_llm_request.py b/tests/test_litellm/proxy/test_route_llm_request.py index 246dfe56825..6b68a4f2c1d 100644 --- a/tests/test_litellm/proxy/test_route_llm_request.py +++ b/tests/test_litellm/proxy/test_route_llm_request.py @@ -1350,3 +1350,24 @@ async def test_route_request_router_settings_override_stripped_with_api_key_or_b call_kwargs = llm_router.acompletion.call_args[1] assert "router_settings_override" not in call_kwargs assert call_kwargs["num_retries"] == 2 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("bad_override", [None, "not-a-dict", ["num_retries"]]) +async def test_route_request_router_settings_override_non_dict_is_stripped_and_ignored(bad_override): + """A malformed override is dropped from the body and applies no settings.""" + data = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}], + "router_settings_override": bad_override, + } + + llm_router = MagicMock() + llm_router.acompletion.return_value = "success" + + response = await route_request(data, llm_router, None, "acompletion") + + assert response == "success" + call_kwargs = llm_router.acompletion.call_args[1] + assert "router_settings_override" not in call_kwargs + assert "num_retries" not in call_kwargs