mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Merge dff4a31ffc into 30ff3723b2
This commit is contained in:
commit
4bfdb54d41
4 changed files with 90 additions and 1 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue