From 3496249d79f6b7362cd734629d8403d32a59ae73 Mon Sep 17 00:00:00 2001 From: unknown <> Date: Wed, 1 Jul 2026 10:14:43 +0000 Subject: [PATCH] fix: use ConfigRepository.get_param() in _add_router_settings_from_db_config Router settings stored via /config/update were double-serialized as JSON strings in the LiteLLM_Config.param_value Json column. On reload, _add_router_settings_from_db_config used raw table.find_first() which bypassed ConfigRepository.get_param()'s string-to-dict parsing, causing isinstance(param_value, dict) to fail and settings like model_group_alias to be silently dropped. Fixes #31836 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/proxy_server.py | 14 ++++----- .../proxy/proxy_server/test_proxy_config.py | 30 ++++++++++++++++++- tests/test_litellm/proxy/test_proxy_server.py | 18 +++++------ .../test_router_retry_policy_update.py | 3 ++ 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 6298a5a98b1..1246fb3b50e 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5282,9 +5282,8 @@ class ProxyConfig: 4. Update router settings """ if llm_router is not None and prisma_client is not None: - db_router_settings = await ConfigRepository(prisma_client).table.find_first( - where={"param_name": "router_settings"} - ) + db_router_param = await ConfigRepository(prisma_client).get_param("router_settings") + db_param_value = db_router_param.param_value if db_router_param is not None else None config_router_settings = config_data.get("router_settings", {}) @@ -5292,16 +5291,15 @@ class ProxyConfig: if ( config_router_settings is not None and isinstance(config_router_settings, dict) - and db_router_settings is not None - and isinstance(db_router_settings.param_value, dict) + and isinstance(db_param_value, dict) ): from litellm.utils import _update_dictionary - combined_router_settings = _update_dictionary(config_router_settings, db_router_settings.param_value) + combined_router_settings = _update_dictionary(config_router_settings, db_param_value) elif config_router_settings is not None and isinstance(config_router_settings, dict): combined_router_settings = config_router_settings - elif db_router_settings is not None and isinstance(db_router_settings.param_value, dict): - combined_router_settings = db_router_settings.param_value + elif isinstance(db_param_value, dict): + combined_router_settings = db_param_value if combined_router_settings: llm_router.update_settings(**combined_router_settings) diff --git a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py index 196ff045208..f7fce24c6b5 100644 --- a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py @@ -1368,7 +1368,7 @@ async def test_ProxyConfig__add_router_settings_from_db_config_updates_router(): fake_router = MagicMock() fake_router.update_settings = MagicMock() fake_prisma = MagicMock() - fake_prisma.db.litellm_config.find_first = AsyncMock( + fake_prisma.db.litellm_config.find_unique = AsyncMock( return_value=SimpleNamespace( param_value={"timeout": 30, "retries": 2, "fallbacks": []} ) @@ -1393,6 +1393,34 @@ async def test_ProxyConfig__add_router_settings_from_db_config_updates_router(): } +@pytest.mark.asyncio +async def test_ProxyConfig__add_router_settings_from_db_config_handles_json_string(): + """Regression for #31836: param_value stored as a JSON string must be + deserialized before being applied to the router.""" + import json + + pc = ProxyConfig() + fake_router = MagicMock() + fake_router.update_settings = MagicMock() + fake_prisma = MagicMock() + fake_prisma.db.litellm_config.find_unique = AsyncMock( + return_value=SimpleNamespace( + param_value=json.dumps( + {"model_group_alias": {"codex/gpt-5.5:auto": "codex/gpt-5.5"}} + ) + ) + ) + await pc._add_router_settings_from_db_config( + config_data={}, + llm_router=fake_router, + prisma_client=fake_prisma, + ) + assert fake_router.update_settings.called + assert fake_router.update_settings.call_args.kwargs == { + "model_group_alias": {"codex/gpt-5.5:auto": "codex/gpt-5.5"}, + } + + @pytest.mark.asyncio async def test_ProxyConfig__add_router_settings_from_db_config_none_router_noop(): pc = ProxyConfig() diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 019a7dc90d2..88e9a4ecdab 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -3776,7 +3776,7 @@ async def test_add_router_settings_from_db_config_merge_logic(): # Mock prisma client mock_prisma_client = MagicMock() - mock_prisma_client.db.litellm_config.find_first = AsyncMock( + mock_prisma_client.db.litellm_config.find_unique = AsyncMock( return_value=mock_db_config ) @@ -3787,8 +3787,8 @@ async def test_add_router_settings_from_db_config_merge_logic(): prisma_client=mock_prisma_client, ) - # Verify find_first was called with correct parameters - mock_prisma_client.db.litellm_config.find_first.assert_called_once_with( + # Verify find_unique was called with correct parameters + mock_prisma_client.db.litellm_config.find_unique.assert_called_once_with( where={"param_name": "router_settings"} ) @@ -3853,7 +3853,7 @@ async def test_add_router_settings_from_db_config_edge_cases(): # Test Case 3: DB returns None (no router_settings in DB) mock_prisma_client = MagicMock() - mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=None) + mock_prisma_client.db.litellm_config.find_unique = AsyncMock(return_value=None) config_data = {"router_settings": {"routing_strategy": "usage-based"}} @@ -3870,7 +3870,7 @@ async def test_add_router_settings_from_db_config_edge_cases(): # Test Case 4: Config has no router_settings mock_db_config = MagicMock() mock_db_config.param_value = {"db_setting": "db_value"} - mock_prisma_client.db.litellm_config.find_first = AsyncMock( + mock_prisma_client.db.litellm_config.find_unique = AsyncMock( return_value=mock_db_config ) @@ -3885,7 +3885,7 @@ async def test_add_router_settings_from_db_config_edge_cases(): mock_router.reset_mock() # Test Case 5: Both config and DB router_settings are None/empty - mock_prisma_client.db.litellm_config.find_first = AsyncMock(return_value=None) + mock_prisma_client.db.litellm_config.find_unique = AsyncMock(return_value=None) await proxy_config._add_router_settings_from_db_config( config_data={}, llm_router=mock_router, prisma_client=mock_prisma_client @@ -3896,8 +3896,8 @@ async def test_add_router_settings_from_db_config_edge_cases(): # Test Case 6: DB config exists but param_value is not a dict mock_db_config_invalid = MagicMock() - mock_db_config_invalid.param_value = "not_a_dict" - mock_prisma_client.db.litellm_config.find_first = AsyncMock( + mock_db_config_invalid.param_value = 42 + mock_prisma_client.db.litellm_config.find_unique = AsyncMock( return_value=mock_db_config_invalid ) @@ -3951,7 +3951,7 @@ async def test_add_router_settings_shallow_merge_behavior(): } mock_prisma_client = MagicMock() - mock_prisma_client.db.litellm_config.find_first = AsyncMock( + mock_prisma_client.db.litellm_config.find_unique = AsyncMock( return_value=mock_db_config ) diff --git a/tests/test_litellm/test_router_retry_policy_update.py b/tests/test_litellm/test_router_retry_policy_update.py index 450391fd503..baef2ec9bdf 100644 --- a/tests/test_litellm/test_router_retry_policy_update.py +++ b/tests/test_litellm/test_router_retry_policy_update.py @@ -220,6 +220,9 @@ class _FakeConfigTable: async def find_first(self, where): return self.rows.get(where["param_name"]) + async def find_unique(self, where): + return self.rows.get(where["param_name"]) + async def upsert(self, where, data): name = where["param_name"] raw = (data["update"] if name in self.rows else data["create"])["param_value"]