diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d83e3500ebe..563ac7e41bf 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -12267,11 +12267,24 @@ async def update_config( # noqa: PLR0915 updated_settings = prisma_client.jsonify_object(updated_settings) for k, v in updated_settings.items(): if k == "router_settings": + # Merge with existing DB value instead of full replacement, + # so that keys not included in the update (e.g. fallbacks) + # are preserved. + existing_record = ( + await prisma_client.db.litellm_config.find_first( + where={"param_name": k} + ) + ) + merged_value = v + if existing_record is not None and isinstance( + existing_record.param_value, dict + ): + merged_value = {**existing_record.param_value, **v} await prisma_client.db.litellm_config.upsert( where={"param_name": k}, data={ - "create": {"param_name": k, "param_value": v}, - "update": {"param_value": v}, + "create": {"param_name": k, "param_value": merged_value}, + "update": {"param_value": merged_value}, }, ) diff --git a/tests/proxy_unit_tests/test_proxy_server.py b/tests/proxy_unit_tests/test_proxy_server.py index 047193055d8..95cc57b0a31 100644 --- a/tests/proxy_unit_tests/test_proxy_server.py +++ b/tests/proxy_unit_tests/test_proxy_server.py @@ -2803,6 +2803,74 @@ async def test_update_config_success_callback_normalization(): assert "langfuse" in callbacks +@pytest.mark.asyncio +async def test_update_config_merges_existing_router_settings(): + import litellm.proxy.proxy_server as proxy_server + from litellm.proxy._types import ConfigYAML, LitellmUserRoles, UserAPIKeyAuth + + mock_prisma_client = MagicMock() + mock_prisma_client.jsonify_object = lambda value: value + + existing_router_settings = { + "fallbacks": [{"gpt-4": ["gpt-4o-mini"]}], + "routing_strategy": "simple-shuffle", + } + mock_db_record = MagicMock() + mock_db_record.param_value = existing_router_settings + + mock_prisma_client.db.litellm_config.find_first = AsyncMock( + return_value=mock_db_record + ) + mock_prisma_client.db.litellm_config.upsert = AsyncMock() + + class MockProxyConfig: + def __init__(self): + self.saved_config = None + + async def get_config(self): + return {} + + async def save_config(self, new_config: dict): + self.saved_config = new_config + + async def add_deployment(self, prisma_client=None, proxy_logging_obj=None): + return None + + admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-test" + ) + config_update = ConfigYAML( + router_settings={"routing_strategy": "latency-based-routing"} + ) + + with patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client), patch( + "litellm.proxy.proxy_server.proxy_config", MockProxyConfig() + ), patch("litellm.proxy.proxy_server.store_model_in_db", True): + await proxy_server.update_config(config_update, user_api_key_dict=admin_user) + + mock_prisma_client.db.litellm_config.find_first.assert_awaited_once_with( + where={"param_name": "router_settings"} + ) + mock_prisma_client.db.litellm_config.upsert.assert_awaited_once_with( + where={"param_name": "router_settings"}, + data={ + "create": { + "param_name": "router_settings", + "param_value": { + "fallbacks": [{"gpt-4": ["gpt-4o-mini"]}], + "routing_strategy": "latency-based-routing", + }, + }, + "update": { + "param_value": { + "fallbacks": [{"gpt-4": ["gpt-4o-mini"]}], + "routing_strategy": "latency-based-routing", + } + }, + }, + ) + + @pytest.mark.parametrize( "data", [