mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy): allow clearing empty-collection user fields via /user/update
_update_internal_user_params dumps the request with exclude_unset and then
discards any value equal to [] or {}, so an explicitly sent empty collection
never reached the database. Because exclude_unset already drops fields the
caller never sent, that filter only ever fired on values set on purpose,
leaving a user's old models, allowed_cache_controls and model_max_budget in
place after a clear attempt. Route those real user columns through the same
fields_set check max_budget already uses so an explicit empty value is written.
config, permissions and aliases are not user-table columns, so they keep going
through the empty-collection filter and never reach Prisma.
This commit is contained in:
parent
3fc73c1aff
commit
bbc62c34f6
2 changed files with 59 additions and 1 deletions
|
|
@ -1127,6 +1127,15 @@ def _process_keys_for_user_info(
|
|||
return returned_keys
|
||||
|
||||
|
||||
# Real LiteLLM_UserTable columns whose empty value ([] / {}) is a deliberate
|
||||
# "clear it". The caller dumps with exclude_unset, so an empty value here always
|
||||
# means the field was sent on purpose and must reach the DB instead of being
|
||||
# dropped by the generic empty-collection filter below.
|
||||
USER_FIELDS_CLEARABLE_WHEN_SET = frozenset(
|
||||
{"models", "allowed_cache_controls", "model_max_budget"}
|
||||
)
|
||||
|
||||
|
||||
def _update_internal_user_params(
|
||||
data_json: dict, data: Union[UpdateUserRequest, UpdateUserRequestNoUserIDorEmail]
|
||||
) -> dict:
|
||||
|
|
@ -1137,6 +1146,9 @@ def _update_internal_user_params(
|
|||
if k == "max_budget":
|
||||
if "max_budget" in fields_set:
|
||||
non_default_values[k] = v
|
||||
elif k in USER_FIELDS_CLEARABLE_WHEN_SET:
|
||||
if k in fields_set:
|
||||
non_default_values[k] = v
|
||||
elif (
|
||||
v is not None
|
||||
and v
|
||||
|
|
@ -1145,7 +1157,7 @@ def _update_internal_user_params(
|
|||
{},
|
||||
)
|
||||
and k not in LiteLLM_ManagementEndpoint_MetadataFields
|
||||
): # models default to [], spend defaults to 0, we should not reset these values
|
||||
): # drop [] / {} so list/dict fields left unset aren't reset to empty
|
||||
non_default_values[k] = v
|
||||
|
||||
is_internal_user = False
|
||||
|
|
|
|||
|
|
@ -1696,6 +1696,52 @@ def test_update_internal_user_params_keeps_original_max_budget_when_not_provided
|
|||
assert "user_alias" in non_default_values
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field, empty_value",
|
||||
[
|
||||
("models", []),
|
||||
("allowed_cache_controls", []),
|
||||
("model_max_budget", {}),
|
||||
],
|
||||
)
|
||||
def test_update_internal_user_params_clears_empty_collection_field(field, empty_value):
|
||||
"""
|
||||
Regression for LIT-3888: an explicitly provided empty list/dict for a real
|
||||
user column must reach the DB update so the field can be cleared via
|
||||
/user/update. Previously [] / {} were filtered out as "defaults" and the old
|
||||
values persisted.
|
||||
"""
|
||||
data = UpdateUserRequest(user_id="test_user", **{field: empty_value})
|
||||
data_json = data.model_dump(exclude_unset=True)
|
||||
|
||||
non_default_values = _update_internal_user_params(data_json=data_json, data=data)
|
||||
|
||||
assert field in non_default_values
|
||||
assert non_default_values[field] == empty_value
|
||||
|
||||
|
||||
def test_update_internal_user_params_sets_personal_models():
|
||||
"""A non-empty models list is still applied unchanged."""
|
||||
data = UpdateUserRequest(user_id="test_user", models=["gpt-4o"])
|
||||
data_json = data.model_dump(exclude_unset=True)
|
||||
|
||||
non_default_values = _update_internal_user_params(data_json=data_json, data=data)
|
||||
|
||||
assert non_default_values["models"] == ["gpt-4o"]
|
||||
|
||||
|
||||
def test_update_internal_user_params_keeps_clearable_fields_when_not_provided():
|
||||
"""When a clearable field is omitted, its column must be left untouched."""
|
||||
data = UpdateUserRequest(user_id="test_user", user_alias="test_alias")
|
||||
data_json = data.model_dump(exclude_unset=True)
|
||||
|
||||
non_default_values = _update_internal_user_params(data_json=data_json, data=data)
|
||||
|
||||
assert "models" not in non_default_values
|
||||
assert "allowed_cache_controls" not in non_default_values
|
||||
assert "model_max_budget" not in non_default_values
|
||||
|
||||
|
||||
def test_generate_request_base_validator():
|
||||
"""
|
||||
Test that GenerateRequestBase validator converts empty string to None for max_budget
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue