From c01b8383f3b98d82e56d304f48883de2207c7f1c Mon Sep 17 00:00:00 2001 From: Chandini Vasana Date: Fri, 2 Oct 2026 21:28:26 +0530 Subject: [PATCH] fix(proxy): prevent router_settings_override leak when request carries api_key --- litellm/proxy/route_llm_request.py | 52 ++++++++++--------- .../proxy/test_route_llm_request.py | 42 ++++++++++++++- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 536c58df65a..3301c345749 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -485,6 +485,33 @@ async def _route_request_single_attempt( # noqa: ANN202 # returns unawaited pr # Map generationConfig to config parameter for Google GenAI compatibility if "generationConfig" in data and "config" not in data: data["config"] = data.pop("generationConfig") + + has_router_settings_override: Final = "router_settings_override" in data + if has_router_settings_override: + # 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", + ) + + if isinstance(override_settings, dict): + # 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] + if "api_key" in data or "api_base" in data: if llm_router is not None: return getattr(llm_router, f"{route_type}")(**data) @@ -508,30 +535,7 @@ 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] - + elif has_router_settings_override: # Use main router with overridden kwargs if llm_router is not None: return getattr(llm_router, f"{route_type}")(**data) diff --git a/tests/test_litellm/proxy/test_route_llm_request.py b/tests/test_litellm/proxy/test_route_llm_request.py index 0b51062dd66..a37ec3858d7 100644 --- a/tests/test_litellm/proxy/test_route_llm_request.py +++ b/tests/test_litellm/proxy/test_route_llm_request.py @@ -1,8 +1,6 @@ - import pytest - from typing import Final from unittest.mock import MagicMock @@ -474,6 +472,45 @@ async def test_route_request_with_router_settings_override_preserves_existing(): assert call_kwargs["timeout"] == 30 +@pytest.mark.asyncio +async def test_route_request_with_router_settings_override_and_api_key(): + """ + Test that router_settings_override is merged and stripped from data + even when the request carries api_key or api_base. + """ + data = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "Hello"}], + "api_key": "sk-user-test-key", + "api_base": "https://api.example.com/v1", + "router_settings_override": { + "fallbacks": [{"gpt-3.5-turbo": ["gpt-4"]}], + "num_retries": 5, + "timeout": 30, + "model_group_retry_policy": {"gpt-3.5-turbo": {"RateLimitErrorRetries": 3}}, + "routing_strategy": "least-busy", + "model_group_alias": {"alias": "real_model"}, + }, + } + + 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 call_kwargs["api_key"] == "sk-user-test-key" + assert call_kwargs["api_base"] == "https://api.example.com/v1" + assert "router_settings_override" not in call_kwargs + assert call_kwargs["fallbacks"] == [{"gpt-3.5-turbo": ["gpt-4"]}] + assert call_kwargs["num_retries"] == 5 + assert call_kwargs["timeout"] == 30 + assert call_kwargs["model_group_retry_policy"] == {"gpt-3.5-turbo": {"RateLimitErrorRetries": 3}} + assert call_kwargs["routing_strategy"] == "least-busy" + assert "model_group_alias" not in call_kwargs + + def test_gated_mock_params_cover_mock_router_testing_params(): """``GATED_MOCK_PARAM_NAMES`` is hardcoded to avoid a cyclic import against ``litellm.types.router``. This test guards against drift — if a @@ -1257,6 +1294,7 @@ async def test_route_request_read_through_disabled_without_store_model_in_db(mon assert table.find_many_wheres == [] + @pytest.mark.asyncio async def test_route_request_routing_group_name_passes_model_gate(): from unittest.mock import AsyncMock, patch