diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index dd38ff162b0..7404d635bdd 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -1523,7 +1523,7 @@ def prepare_metadata_fields( existing_value = existing_metadata.get(reserved_field) if existing_value is None: continue - if ( + if casted_metadata is None or ( reserved_field in casted_metadata and casted_metadata[reserved_field] != existing_value ): diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index cdf453fe3ee..82a8dc1108f 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -1300,6 +1300,25 @@ async def test_update_rejects_explicit_null_service_account_id(): assert exc_info.value.status_code == 400 +@pytest.mark.asyncio +async def test_update_rejects_whole_metadata_null_on_service_account_key(): + """ + `metadata: null` on a key with a reserved field would have dereferenced None + inside the reserved-field loop and returned a 500. Must surface as a 400 + since the effect would be to clear an immutable field. + """ + data = UpdateKeyRequest(key="sk-1", metadata=None, team_id="IJ") + existing_key = LiteLLM_VerificationToken( + token="hashed", + team_id="IJ", + metadata={"service_account_id": "sa-123"}, + ) + + 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 == 400 + + @pytest.mark.asyncio async def test_update_without_metadata_still_preserves_existing(): """Omitting metadata entirely must not drop existing metadata fields."""