mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(proxy): stop /config/update writing unsent pydantic defaults over stored config
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
9de3315dad
commit
73e197b616
2 changed files with 81 additions and 2 deletions
|
|
@ -14942,7 +14942,7 @@ async def update_config(
|
|||
if config_info.general_settings is not None:
|
||||
existing = await _read_section("general_settings")
|
||||
before_general_settings: Final = copy.deepcopy(existing)
|
||||
updates = config_info.general_settings.dict(exclude_none=True)
|
||||
updates = config_info.general_settings.model_dump(exclude_none=True, exclude_unset=True)
|
||||
for k, v in updates.items():
|
||||
if k == "alert_to_webhook_url":
|
||||
if "alerting" not in existing:
|
||||
|
|
@ -15017,7 +15017,7 @@ async def update_config(
|
|||
if config_info.router_settings is not None:
|
||||
existing = await _read_section("router_settings")
|
||||
before_router_settings: Final = copy.deepcopy(existing)
|
||||
updates = config_info.router_settings.dict(exclude_none=True)
|
||||
updates = config_info.router_settings.model_dump(exclude_none=True, exclude_unset=True)
|
||||
new_router_settings: Final = {**existing, **updates}
|
||||
await _upsert_section("router_settings", new_router_settings)
|
||||
asyncio.create_task(
|
||||
|
|
|
|||
|
|
@ -100,6 +100,85 @@ def test_config_update_no_db_error(client, auth_as, monkeypatch):
|
|||
)
|
||||
|
||||
|
||||
def _stateful_config_table(mock_prisma: MagicMock, rows: dict[str, dict]) -> MagicMock:
|
||||
"""A litellm_config table that reads back what /config/update wrote, so a
|
||||
partial save can be checked against the pre-existing row."""
|
||||
table = _install_litellm_config(mock_prisma)
|
||||
|
||||
async def find_first(where):
|
||||
name = where["param_name"]
|
||||
if name not in rows:
|
||||
return None
|
||||
row = MagicMock()
|
||||
row.param_value = rows[name]
|
||||
return row
|
||||
|
||||
async def upsert(where, data):
|
||||
name = where["param_name"]
|
||||
raw = data["update"]["param_value"] if name in rows else data["create"]["param_value"]
|
||||
rows[name] = json.loads(raw) if isinstance(raw, str) else raw
|
||||
|
||||
table.find_first = find_first
|
||||
table.upsert = upsert
|
||||
return table
|
||||
|
||||
|
||||
def test_config_update_partial_router_settings_keeps_model_group_alias(client, auth_as, mock_prisma, monkeypatch):
|
||||
"""A router-settings save that never mentions model_group_alias must not
|
||||
clear it. UpdateRouterConfig defaults the field to {}, so serializing
|
||||
unset fields wipes the stored alias map on every unrelated edit."""
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
rows = {
|
||||
"router_settings": {
|
||||
"model_group_alias": {"security": "luna"},
|
||||
"timeout": 6000,
|
||||
"routing_strategy": "simple-shuffle",
|
||||
}
|
||||
}
|
||||
_stateful_config_table(mock_prisma, rows)
|
||||
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
|
||||
fake_proxy_config = MagicMock()
|
||||
fake_proxy_config.add_deployment = AsyncMock()
|
||||
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
|
||||
|
||||
with auth_as(LitellmUserRoles.PROXY_ADMIN):
|
||||
response = client.post(
|
||||
"/config/update",
|
||||
json={"router_settings": {"routing_strategy": "latency-based-routing"}},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert rows["router_settings"] == {
|
||||
"model_group_alias": {"security": "luna"},
|
||||
"timeout": 6000,
|
||||
"routing_strategy": "latency-based-routing",
|
||||
}
|
||||
|
||||
|
||||
def test_config_update_partial_general_settings_does_not_write_defaults(client, auth_as, mock_prisma, monkeypatch):
|
||||
"""Same hazard on general_settings: fields the caller never sent (here
|
||||
ui_access_mode, which gates who can reach the dashboard) must not be
|
||||
written from their pydantic defaults on an unrelated save."""
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
rows = {"general_settings": {"ui_access_mode": "admin_only", "max_parallel_requests": 5}}
|
||||
_stateful_config_table(mock_prisma, rows)
|
||||
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
|
||||
fake_proxy_config = MagicMock()
|
||||
fake_proxy_config.add_deployment = AsyncMock()
|
||||
monkeypatch.setattr(ps, "proxy_config", fake_proxy_config)
|
||||
|
||||
with auth_as(LitellmUserRoles.PROXY_ADMIN):
|
||||
response = client.post(
|
||||
"/config/update",
|
||||
json={"general_settings": {"max_parallel_requests": 9}},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert rows["general_settings"] == {"ui_access_mode": "admin_only", "max_parallel_requests": 9}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /config/field/update
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue