Track per-member total spend on team memberships - #26195
Conversation
Adds total_spend column to LiteLLM_TeamMembership that accumulates continuously and is not zeroed by the budget cycle reset job. This enables UI surfaces to distinguish current-cycle spend (the existing spend column, which resets) from lifetime spend per team member. Also exposes budget_reset_at on LiteLLM_BudgetTable so /team/info callers can see when a member's budget window next resets. The field was already stored in the DB but stripped by the response Pydantic model. Includes regression tests that: - Guard the reset job against ever writing total_spend: 0 - Verify the spend writer increments both spend and total_spend in one UPDATE statement.
Greptile SummaryThis PR adds a non-resetting Confidence Score: 5/5Safe to merge — migration is additive, security boundaries are correct, both new code paths are covered by targeted tests. All remaining findings are P2 or lower. The core logic (atomic double-increment, reset-job exclusion, Union discriminator, additive migration) is correct and well-tested. The No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm-proxy-extras/litellm_proxy_extras/migrations/20260421135425_add_team_membership_total_spend/migration.sql | Additive migration: adds total_spend DOUBLE PRECISION NOT NULL DEFAULT 0.0 to LiteLLM_TeamMembership — no backfill, no removals, safe to apply on existing data. |
| litellm/proxy/_types.py | Adds total_spend: Optional[float] = 0.0 to LiteLLM_TeamMembership; changes litellm_budget_table to Union[LiteLLM_BudgetTableFull, LiteLLM_BudgetTable] so Pydantic surfaces budget_reset_at on Prisma reads while keeping it out of user-settable paths; adds docstring clarifying allowlist semantics on LiteLLM_BudgetTable. |
| litellm/proxy/db/db_spend_update_writer.py | Increments total_spend alongside spend in the same atomic update_many batch call — the two counters stay in sync with no separate DB round-trip. |
| tests/test_litellm/proxy/common_utils/test_reset_budget_job.py | Adds test_reset_budget_for_team_members_preserves_total_spend — a targeted regression guard that asserts the reset job writes exactly {"spend": 0} and never touches total_spend. |
| tests/test_litellm/proxy/db/test_db_spend_update_writer.py | Adds test_commit_spend_updates_to_db_increments_team_member_spend_and_total_spend — verifies both spend and total_spend are incremented by the same response_cost in a single update_many call. |
Sequence Diagram
sequenceDiagram
participant R as Request
participant W as DBSpendUpdateWriter
participant DB as Prisma (LiteLLM_TeamMembership)
participant J as ResetBudgetJob
R->>W: response_cost accrued
W->>DB: update_many(spend += cost, total_spend += cost)
Note over DB: Both fields incremented atomically
J->>DB: update_many(spend = 0)
Note over DB: total_spend untouched by reset job
DB-->>W: /team/info read
Note over W: Pydantic Union picks LiteLLM_BudgetTableFull when created_at present
Reviews (7): Last reviewed commit: "fmt: apply black to _types.py" | Re-trigger Greptile
|
@greptileai can you review |
5a0226e to
e5f3e15
Compare
LiteLLM_BudgetTable is documented as "user-controllable params" and its model_fields.keys() is used as the allowlist for extracting budget fields from incoming API request bodies (management_helpers/utils.py:88, organization_endpoints.py:112/255/537/549, project_endpoints.py:197/245/632, customer_endpoints.py:598). Request models like NewOrganizationRequest inherit from LiteLLM_BudgetTable, so anything on the base class becomes user-settable — a caller could set budget_reset_at far in the future and evade budget cycling. Move budget_reset_at from the base class to LiteLLM_BudgetTableFull so it appears on API responses without becoming writable, and type LiteLLM_TeamMembership.litellm_budget_table as Union[Full, Base] so Pydantic picks Full when the data has server-managed fields (/team/info reads Prisma rows that include budget_reset_at and created_at) and Base when callers construct with only user-settable fields (existing auth tests and caches).
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
ed38c1c to
9a6ddef
Compare
c4c1861
into
litellm_internal_staging
…al_spend Track per-member total spend on team memberships
Summary
Adds
total_spendtoLiteLLM_TeamMembership— a non-resetting counter that parallels the existing cycle-scopedspend, incremented in the same atomicupdate_manycall and untouched by the budget reset job. Gives the Teams > Members UI a way to show lifetime spend per member instead of only current-cycle spend.Also exposes
budget_reset_atonLiteLLM_BudgetTableFull(not the base class) so/team/inforesponses can show when a member's budget next resets. Keeping it offLiteLLM_BudgetTablematters — that base class'smodel_fields.keys()is the user-input allowlist across 9 write paths, and request models likeNewOrganizationRequestinherit from it. Putting the field on the base would let a caller pin their own reset date and evade cycling.LiteLLM_TeamMembership.litellm_budget_tableis typed asUnion[Full, Base]so Pydantic picks Full on Prisma reads (surfacingbudget_reset_at) and Base on existing auth/test constructions (which only carry user-settable fields).Migration is additive with
DEFAULT 0.0and no backfill — existing members start at 0 and accrue forward.Test cases
test_reset_budget_for_team_members_preserves_total_spend— regression guard. Asserts the reset job writes exactly{"spend": 0}and specifically excludestotal_spend, so any future refactor that adds"total_spend": 0to the resetdatadict trips this test.test_commit_spend_updates_to_db_increments_team_member_spend_and_total_spend— verifies the writer increments bothspendandtotal_spendby the sameresponse_costin a singleupdate_manycall.test_team_member_budget,test_auth_hot_path_network_requests,test_handle_jwt,test_auth_checks,test_zero_cost_model_budget_bypass) pass unchanged — the Union type keeps fixtures working without forcing them to providecreated_at./team/inforesponse includesteam_memberships[*].total_spendandteam_memberships[*].litellm_budget_table.budget_reset_at.