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>
This commit is contained in:
unknown 2026-07-01 10:14:43 +00:00
parent 70eb4e5d00
commit 3496249d79
4 changed files with 47 additions and 18 deletions

View file

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

View file

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

View file

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

View file

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