From 05d2ccdf566ca0b9daf18198cd609060added7fd Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 5 Mar 2026 15:49:40 -0800 Subject: [PATCH] [Fix] PATCH /update/ui_settings now merges with existing record instead of overwriting Previously, model_dump(exclude_none=True) included all bool fields (since False != None), causing a partial PATCH to overwrite every other setting to its default. Fix uses exclude_unset=True and reads the existing DB record before merging, giving proper PATCH semantics. This was a pre-existing bug but is fixed here since we're touching this code. Co-Authored-By: Claude Opus 4.6 --- .../proxy_setting_endpoints.py | 17 +++++++++++++++-- .../test_proxy_setting_endpoints.py | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 7ef6d6b67f..2f7f81a703 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -1061,13 +1061,26 @@ async def update_ui_settings( }, ) - settings_dict = settings.model_dump(exclude_none=True) + # Only include fields the caller actually sent (not Pydantic defaults). + settings_dict = settings.model_dump(exclude_unset=True) # Enforce allowlist and drop anything unexpected - ui_settings = { + incoming = { k: v for k, v in settings_dict.items() if k in ALLOWED_UI_SETTINGS_FIELDS } + # Merge with existing persisted settings so a partial PATCH doesn't + # overwrite fields the caller didn't send. + existing: dict = {} + db_existing = await prisma_client.db.litellm_uisettings.find_unique( + where={"id": "ui_settings"} + ) + if db_existing and db_existing.ui_settings: + raw = db_existing.ui_settings + existing = json.loads(raw) if isinstance(raw, str) else dict(raw) + + ui_settings = {**existing, **incoming} + await prisma_client.db.litellm_uisettings.upsert( where={"id": "ui_settings"}, data={ diff --git a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py index 31baab0092..f955a6134b 100644 --- a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py +++ b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py @@ -867,6 +867,7 @@ class TestProxySettingEndpoints: monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True) mock_prisma = MagicMock() mock_prisma.db.litellm_uisettings.upsert = AsyncMock() + mock_prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=None) monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma) payload = {"disable_model_add_for_internal_users": True} @@ -908,6 +909,7 @@ class TestProxySettingEndpoints: monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True) mock_prisma = MagicMock() mock_prisma.db.litellm_uisettings.upsert = AsyncMock() + mock_prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=None) monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma) payload = {