fix(proxy): prevent router_settings_override leak when request carries api_key

This commit is contained in:
Chandini Vasana 2026-10-02 21:28:26 +05:30
parent 6f5ad78a1f
commit c01b8383f3
2 changed files with 68 additions and 26 deletions

View file

@ -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)

View file

@ -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