fix(proxy): require premium only when enabling premium metadata fields (#30285) (#30506)

Co-authored-by: Sameer Kankute <sameer@berri.ai>
This commit is contained in:
Nitish Agarwal 2026-06-17 17:15:41 +05:30 committed by GitHub
parent 6e9c0b0dd2
commit d4915766d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 79 additions and 9 deletions

View file

@ -397,7 +397,7 @@ def _set_object_metadata_field(
field_name: Name of the metadata field to set
value: Value to set for the field
"""
if field_name in LiteLLM_ManagementEndpoint_MetadataFields_Premium:
if field_name in LiteLLM_ManagementEndpoint_MetadataFields_Premium and value:
_premium_user_check(field_name)
object_data.metadata = object_data.metadata or {}
@ -563,13 +563,11 @@ def _update_metadata_field(updated_kv: dict, field_name: str) -> None:
field_name: Name of the metadata field being updated
"""
if field_name in LiteLLM_ManagementEndpoint_MetadataFields_Premium:
value = updated_kv.get(field_name)
# Skip the premium check for empty collections ([] or {}).
# The UI sends these as defaults even when the user hasn't configured
# any enterprise features (see issue #20304). However, we still
# proceed with the update so that users can intentionally clear a
# previously-set field by sending an empty list/dict.
if value is not None and value != [] and value != {}:
# The UI sends falsy defaults (False, [], {}) even when the user has not
# enabled any enterprise feature (see #20304, #30285); require a license
# only for a truthy value. The falsy value is still persisted below so a
# previously-set field can be cleared.
if updated_kv.get(field_name):
_premium_user_check()
if field_name in updated_kv and updated_kv[field_name] is not None:

View file

@ -1793,7 +1793,8 @@ def prepare_metadata_fields(
if k in LiteLLM_ManagementEndpoint_MetadataFields_Premium:
from litellm.proxy.utils import _premium_user_check
_premium_user_check(k)
if v:
_premium_user_check(k)
casted_metadata[k] = v
except Exception as e:

View file

@ -157,6 +157,32 @@ class TestUpdateMetadataFieldsEmptyCollections:
assert "guardrails" not in updated_kv
assert updated_kv["metadata"]["guardrails"] == ["my-guardrail"]
@patch("litellm.proxy.management_endpoints.common_utils._premium_user_check")
def test_false_boolean_does_not_trigger_premium_check(self, mock_premium_check):
"""
Regression #30285: /team/update sends disable_global_guardrails=False
(the UI's unchanged default). A falsy boolean must not trigger the
premium check, so non-premium users are not wrongly 403'd.
"""
updated_kv = {"team_id": "test-team", "disable_global_guardrails": False}
_update_metadata_fields(updated_kv=updated_kv)
mock_premium_check.assert_not_called()
@patch("litellm.proxy.management_endpoints.common_utils._premium_user_check")
def test_false_boolean_still_updates_metadata(self, mock_premium_check):
"""A falsy boolean must still be moved into metadata so it persists."""
updated_kv = {"team_id": "test-team", "disable_global_guardrails": False}
_update_metadata_fields(updated_kv=updated_kv)
assert "disable_global_guardrails" not in updated_kv
assert updated_kv["metadata"]["disable_global_guardrails"] is False
@patch("litellm.proxy.management_endpoints.common_utils._premium_user_check")
def test_true_boolean_triggers_premium_check(self, mock_premium_check):
"""Control: enabling the premium feature (True) still requires a license."""
updated_kv = {"team_id": "test-team", "disable_global_guardrails": True}
_update_metadata_fields(updated_kv=updated_kv)
mock_premium_check.assert_called()
@patch("litellm.proxy.management_endpoints.common_utils._premium_user_check")
def test_ui_typical_payload_does_not_trigger_premium_check(
self, mock_premium_check

View file

@ -1547,6 +1547,51 @@ async def test_prepare_key_update_data_budget_limits_serializes_windows():
assert windows[0]["reset_at"] is not None
@pytest.mark.asyncio
async def test_prepare_key_update_data_disable_global_guardrails_false_no_premium(
monkeypatch,
):
"""
Regression #30285: editing a key via the UI sends disable_global_guardrails=False
(unchanged default). A non-premium user must NOT get a 403, and False must persist.
"""
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", False)
data = UpdateKeyRequest(key="sk-1", disable_global_guardrails=False)
existing_key = LiteLLM_VerificationToken(token="hashed")
result = await prepare_key_update_data(data=data, existing_key_row=existing_key)
assert result["metadata"]["disable_global_guardrails"] is False
@pytest.mark.asyncio
async def test_prepare_key_update_data_disable_global_guardrails_true_requires_premium(
monkeypatch,
):
"""Control: enabling the premium feature (True) without a license still 403s."""
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", False)
data = UpdateKeyRequest(key="sk-1", disable_global_guardrails=True)
existing_key = LiteLLM_VerificationToken(token="hashed")
with pytest.raises(HTTPException) as exc_info:
await prepare_key_update_data(data=data, existing_key_row=existing_key)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_prepare_key_update_data_disable_global_guardrails_true_premium_persists(
monkeypatch,
):
"""A premium user enabling the feature (True) succeeds and the value persists."""
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
data = UpdateKeyRequest(key="sk-1", disable_global_guardrails=True)
existing_key = LiteLLM_VerificationToken(token="hashed")
result = await prepare_key_update_data(data=data, existing_key_row=existing_key)
assert result["metadata"]["disable_global_guardrails"] is True
@pytest.mark.asyncio
async def test_validate_team_id_used_in_service_account_request_requires_team_id():
"""