From 6f31f6c975da42e56ec48db5b8deb6a300256c99 Mon Sep 17 00:00:00 2001 From: Vineeth Sai Date: Tue, 18 Aug 2026 20:40:53 -0700 Subject: [PATCH] fix(user_update): stop premium metadata fields reaching the user table as columns _update_internal_user_params filtered the incoming payload against LiteLLM_ManagementEndpoint_MetadataFields but never against LiteLLM_ManagementEndpoint_MetadataFields_Premium, so guardrails, prompts and policies survived into non_default_values as top-level keys. Only `policies` is a column on LiteLLM_UserTable, so /user/update handed prisma keys the table does not have and the write failed instead of storing the value. prepare_metadata_fields already copies these into metadata, which is where the request path reads them from, so nothing is lost by dropping the top-level key. This mirrors prepare_key_update_data, which skips both lists. --- .../internal_user_endpoints.py | 1 + .../test_internal_user_endpoints.py | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 2b88658e1b4..0c82de285d1 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -1188,6 +1188,7 @@ def _update_internal_user_params(data_json: dict, data: UpdateUserRequest | Upda {}, ) and k not in LiteLLM_ManagementEndpoint_MetadataFields + and k not in LiteLLM_ManagementEndpoint_MetadataFields_Premium ): # models default to [], spend defaults to 0, we should not reset these values non_default_values[k] = v diff --git a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py index 06ae02c17bb..02e58711923 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py @@ -716,6 +716,61 @@ def test_update_internal_user_params_email(): assert "budget_duration" not in non_default_values # Should not add default values +def test_update_internal_user_params_keeps_premium_metadata_out_of_columns(monkeypatch): + """ + guardrails / prompts / policies are metadata fields, not columns on LiteLLM_UserTable. + + /key/update and /team/update both strip them; /user/update filtered against only + LiteLLM_ManagementEndpoint_MetadataFields, so the premium list survived into the + prisma payload as top-level keys and the write failed. + """ + import litellm.proxy.utils as proxy_utils + from litellm.proxy._types import ( + LiteLLM_ManagementEndpoint_MetadataFields, + LiteLLM_ManagementEndpoint_MetadataFields_Premium, + UpdateUserRequest, + ) + from litellm.proxy.management_endpoints.internal_user_endpoints import ( + _update_internal_user_params, + ) + from litellm.proxy.management_endpoints.key_management_endpoints import ( + prepare_metadata_fields, + ) + + monkeypatch.setattr(proxy_utils, "_premium_user_check", lambda field: None) + + data = UpdateUserRequest( + user_id="test_user_id", + guardrails=["my-guardrail"], + prompts=["my-prompt"], + policies=["my-policy"], + model_rpm_limit={"gpt-4": 5}, + ) + data_json = data.model_dump(exclude_unset=True) + + non_default_values = _update_internal_user_params(data_json=data_json, data=data) + + # No management-metadata field, premium or not, may reach the user table as a column. + for field in ( + LiteLLM_ManagementEndpoint_MetadataFields + LiteLLM_ManagementEndpoint_MetadataFields_Premium + ): + assert field not in non_default_values, f"{field} must not be sent as a user column" + assert non_default_values == {"user_id": "test_user_id"} + + # Nothing is lost: the values still land in metadata, which is where the request + # path reads guardrails / policies from. + non_default_values = prepare_metadata_fields( + data=data, + non_default_values=non_default_values, + existing_metadata={}, + ) + metadata = non_default_values["metadata"] + assert metadata["guardrails"] == ["my-guardrail"] + assert metadata["prompts"] == ["my-prompt"] + assert metadata["policies"] == ["my-policy"] + assert metadata["model_rpm_limit"] == {"gpt-4": 5} + + def test_update_internal_user_params_reset_spend_and_max_budget(): """ Relevant Issue: https://github.com/BerriAI/litellm/issues/10495