fix(proxy): keep budget_id in typed customer responses

Address review feedback on the Customer Management response_model typing.
Greptile flagged that response_model=LiteLLM_EndUserTable on /customer/new
and /customer/update silently drops fields the raw Prisma model_dump()
echoed. Checking the schema, budget_id is the only such scalar column that
was missing from the Pydantic model (created_at/updated_at/tpm_limit do not
exist on litellm_endusertable), so add budget_id to LiteLLM_EndUserTable.

This restores budget_id on new/update and also fixes the pre-existing gap
where /customer/info and /customer/list (already typed) dropped it, which
the UI Customer type expects. A regression test pins budget_id surviving the
response_model filter on /customer/update.

Also document UnblockUsersResponse.blocked_users via a Field description: it
holds the users that remain blocked after the call. The key name predates
this PR and is kept to avoid a backwards-incompatible rename on a beta route.
This commit is contained in:
ryan-crabbe-berri 2026-06-23 10:44:39 -07:00
parent d857b1c2ea
commit f6eb18436e
3 changed files with 32 additions and 2 deletions

View file

@ -21,6 +21,7 @@ class LiteLLM_EndUserTable(LiteLLMPydanticObjectBase):
spend: float = 0.0
allowed_model_region: Optional[Literal["eu", "us"]] = None
default_model: Optional[str] = None
budget_id: Optional[str] = None
litellm_budget_table: Optional[LiteLLM_BudgetTable] = None
object_permission_id: Optional[str] = None
object_permission: Optional[LiteLLM_ObjectPermissionTable] = None

View file

@ -1,6 +1,6 @@
from typing import List
from pydantic import BaseModel
from pydantic import BaseModel, Field
from litellm.models.end_user import LiteLLM_EndUserTable
@ -10,7 +10,9 @@ class BlockUsersResponse(BaseModel):
class UnblockUsersResponse(BaseModel):
blocked_users: List[str]
blocked_users: List[str] = Field(
description="User IDs that remain blocked after this unblock call"
)
class DeleteCustomersResponse(BaseModel):

View file

@ -360,6 +360,33 @@ def test_customer_new_documented_in_openapi_schema():
assert json_schema["$ref"].endswith("/LiteLLM_EndUserTable")
def test_update_customer_response_preserves_budget_id(
mock_prisma_client, mock_user_api_key_auth
):
"""
Regression for the response_model field-stripping concern: budget_id is a real
column on the end-user table that /customer/update echoes. response_model=
LiteLLM_EndUserTable must NOT drop it, so budget_id stays in LiteLLM_EndUserTable.
"""
existing = LiteLLM_EndUserTable(user_id="cust-1", blocked=False)
updated = LiteLLM_EndUserTable(
user_id="cust-1", blocked=False, budget_id="budget-123"
)
mock_prisma_client.db.litellm_endusertable.find_first = AsyncMock(
return_value=existing
)
mock_prisma_client.db.litellm_endusertable.update = AsyncMock(return_value=updated)
response = client.post(
"/customer/update",
json={"user_id": "cust-1", "budget_id": "budget-123"},
headers={"Authorization": "Bearer test-key"},
)
assert response.status_code == 200
assert response.json()["budget_id"] == "budget-123"
def test_block_customer_success_serializes_through_response_model(
mock_prisma_client, mock_user_api_key_auth
):