This commit is contained in:
devin-ai-integration[bot] 2026-08-27 17:43:13 -05:00 committed by GitHub
commit 4bfdb54d41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 90 additions and 1 deletions

View file

@ -10,7 +10,7 @@ import json
from datetime import datetime
from typing import Final, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from litellm.models.object_permission import LiteLLM_ObjectPermissionTable
from litellm.types.llms.base import LiteLLMPydanticObjectBase
@ -81,6 +81,13 @@ class TeamBase(LiteLLMPydanticObjectBase):
access_group_ids: list[str] | None = None
default_team_member_models: list[str] | None = None
@field_validator("budget_duration", mode="before")
@classmethod
def check_budget_duration(cls, v):
if isinstance(v, str) and v.strip() == "":
return None
return v
class LiteLLM_TeamTable(TeamBase):
team_id: str

View file

@ -1106,6 +1106,12 @@ from litellm.types.object_permission import ( # noqa: E402
)
def normalize_empty_budget_duration(value: Optional[str]) -> Optional[str]:
if isinstance(value, str) and value.strip() == "":
return None
return value
class GenerateRequestBase(LiteLLMPydanticObjectBase):
"""
Overlapping schema between key and user generate/update requests
@ -1151,6 +1157,11 @@ class GenerateRequestBase(LiteLLMPydanticObjectBase):
return None
return v
@field_validator("budget_duration", mode="before")
@classmethod
def check_budget_duration(cls, v):
return normalize_empty_budget_duration(v)
class AllowedVectorStoreIndexItem(LiteLLMPydanticObjectBase):
index_name: str
@ -1787,6 +1798,11 @@ class BudgetNewRequest(LiteLLMPydanticObjectBase):
description="Datetime when the budget is reset",
)
@field_validator("budget_duration", mode="before")
@classmethod
def check_budget_duration(cls, v):
return normalize_empty_budget_duration(v)
class BudgetRequest(LiteLLMPydanticObjectBase):
budgets: list[str]
@ -1980,6 +1996,11 @@ class PatchTeamRequest(UpdateTeamRequest):
team_id: str | None = None
@field_validator("budget_duration", mode="before")
@classmethod
def check_budget_duration(cls, v):
return normalize_empty_budget_duration(v)
class ResetTeamBudgetRequest(LiteLLMPydanticObjectBase):
"""

View file

@ -2079,6 +2079,41 @@ def test_generate_request_base_validator():
assert req.max_budget is None
def test_generate_request_base_normalizes_empty_budget_duration():
"""
Regression test for https://github.com/BerriAI/litellm/issues/32474
Clearing budget_duration from the UI sends an empty string; downstream duration
parsing rejects "" with "Invalid duration format". The request model must normalize
empty/whitespace-only values to None so resetting a budget to unlimited succeeds.
"""
from litellm.proxy._types import GenerateRequestBase
assert GenerateRequestBase(budget_duration="").budget_duration is None
assert GenerateRequestBase(budget_duration=" ").budget_duration is None
assert GenerateRequestBase(budget_duration=None).budget_duration is None
assert GenerateRequestBase(budget_duration="30d").budget_duration == "30d"
def test_update_internal_user_params_reset_budget_duration_to_unlimited():
"""
Regression test for https://github.com/BerriAI/litellm/issues/32474
Sending an empty budget_duration alongside an empty max_budget (what the UI sends
when resetting a user's budget to unlimited) must not raise "Invalid duration format"
and must not fabricate a budget_reset_at.
"""
data = UpdateUserRequest(user_id="test_user", max_budget="", budget_duration="")
assert data.max_budget is None
assert data.budget_duration is None
non_default_values = _update_internal_user_params(data_json=data.model_dump(exclude_unset=True), data=data)
assert non_default_values["max_budget"] is None
assert non_default_values.get("budget_duration") is None
assert "budget_reset_at" not in non_default_values
@pytest.mark.asyncio
async def test_get_user_daily_activity_non_admin_cannot_view_other_users(monkeypatch):
"""

View file

@ -102,6 +102,32 @@ def test_user_api_key_auth_hashes_authorization_header_form_of_key():
assert not from_header.api_key.lower().startswith("bearer")
@pytest.mark.parametrize(
"model_name, extra_kwargs",
[
("GenerateKeyRequest", {}),
("NewUserRequest", {}),
("UpdateUserRequest", {"user_id": "u1"}),
("NewCustomerRequest", {"user_id": "c1"}),
("NewTeamRequest", {}),
("UpdateTeamRequest", {"team_id": "t1"}),
],
)
def test_request_models_normalize_empty_budget_duration(model_name, extra_kwargs):
"""Regression for https://github.com/BerriAI/litellm/issues/32474: clearing a budget
from the UI sends budget_duration="", which downstream duration parsing rejects with
"Invalid duration format". Request models must coerce empty/whitespace values to None
while leaving real durations untouched."""
import litellm.proxy._types as proxy_types
model = getattr(proxy_types, model_name)
assert model(budget_duration="", **extra_kwargs).budget_duration is None
assert model(budget_duration=" ", **extra_kwargs).budget_duration is None
assert model(budget_duration=None, **extra_kwargs).budget_duration is None
assert model(budget_duration="30d", **extra_kwargs).budget_duration == "30d"
def test_proxy_exception_str_returns_message():
"""ProxyException must stringify to its message: OTEL's
``span.record_exception`` and ``str(exc)``-based logging read the string