fix(team): surface budget_reset_at on /team/info and cloned member budgets

Two independent bugs both masked budget_reset_at from consumers that
needed it:

1. /team/info.team_member_budget_table was typed as LiteLLM_BudgetTable
   (the user-settable allowlist), which dropped server-managed fields.
   Switched to LiteLLM_BudgetTableFull so budget_reset_at and created_at
   are serialized.

2. _clone_team_default_budget_for_member copied the pool's numeric
   fields but never set budget_reset_at on the cloned row. With
   budget_duration present but no reset timestamp, the reset job never
   fires on the member's budget (its query is reset_at <= now, which
   never matches NULL). Now computes budget_reset_at from the cloned
   budget_duration via get_budget_reset_time so each member's cycle
   starts at clone time rather than inheriting the pool's stale reset.
This commit is contained in:
Ryan Crabbe 2026-04-22 18:27:36 -07:00
parent befddec87a
commit ac453c958e
No known key found for this signature in database
2 changed files with 9 additions and 1 deletions

View file

@ -3907,7 +3907,7 @@ class OrganizationMemberUpdateResponse(MemberUpdateResponse):
class TeamInfoResponseObjectTeamTable(LiteLLM_TeamTable):
team_member_budget_table: Optional[LiteLLM_BudgetTable] = None
team_member_budget_table: Optional[LiteLLM_BudgetTableFull] = None
# Resources inherited from access groups (separate from direct assignments)
access_group_models: Optional[List[str]] = None
access_group_mcp_server_ids: Optional[List[str]] = None

View file

@ -9,6 +9,7 @@ from fastapi import HTTPException, Request
import litellm
from litellm._logging import verbose_logger
from litellm._uuid import uuid
from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
from litellm.proxy._types import ( # key request types; user request types; team request types; customer request types
BudgetNewRequest,
DeleteCustomerRequest,
@ -192,6 +193,13 @@ async def _clone_team_default_budget_for_member(
continue
cloned_data[field] = value
# Start the member's budget window at clone time, not the pool's reset
# timestamp — otherwise a member joining mid-cycle inherits a stale reset.
if cloned_data.get("budget_duration"):
cloned_data["budget_reset_at"] = get_budget_reset_time(
cloned_data["budget_duration"]
)
new_budget = await prisma_client.db.litellm_budgettable.create(data=cloned_data)
return new_budget.budget_id