Skip to content

fix: set budget_reset_at when creating customer with budget_duration - #22009

Merged
1 commit merged into
BerriAI:litellm_oss_staging_03_04_2026from
araman-godaddy:end_user_spend_bug_fix
Mar 4, 2026
Merged

fix: set budget_reset_at when creating customer with budget_duration#22009
1 commit merged into
BerriAI:litellm_oss_staging_03_04_2026from
araman-godaddy:end_user_spend_bug_fix

Conversation

@araman-godaddy

@araman-godaddy araman-godaddy commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes #22013

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • [ x ] I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • [ x ] My PR passes all unit tests on make test-unit
  • [ x ] My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

🐛 Bug Fix

Changes

Problem

When an end-user is created via /customer/new with budget_duration but without an explicit budget_reset_at, the budget row is created with budget_reset_at = NULL. The periodic ResetBudgetJob cron has a query branch that matches budgets where budget_reset_at IS NULL AND budget_duration IS NOT NULL, which picks up these freshly created budgets and unconditionally resets the linked end-user's spend to 0.0 via an upsert — wiping out legitimate spend within minutes of it being recorded.

Meanwhile, LiteLLM_DailyEndUserSpend (which the reset job doesn't touch) retains the correct values, creating a discrepancy between the two tables.

Steps to reproduce

  1. Create an end-user with budget_duration but no budget_reset_at:
curl -X POST http://localhost:4000/customer/new \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -d '{"user_id": "test-user", "max_budget": 10.0, "budget_duration": "30d"}'
  1. Run a chat completion against that user to accumulate spend.

  2. Wait for the ResetBudgetJob cron to fire (~10 minutes by default).

  3. Observe LiteLLM_EndUserTable.spend is reset to 0.0, even though the 30-day duration has not elapsed. LiteLLM_DailyEndUserSpend retains the correct value.

Workaround: Explicitly passing budget_reset_at in the request avoids the issue.

Root cause

The new_budget_request() helper in customer_endpoints.py constructs a BudgetNewRequest from the incoming NewCustomerRequest but never initializes budget_reset_at when budget_duration is provided. The /budget/new endpoint already handles this correctly — the same logic was simply missing from the /customer/new path.

Fix

Added the same budget_reset_at initialization that /budget/new uses to new_budget_request():

if budget_request.budget_reset_at is None and budget_request.budget_duration is not None:
    budget_request.budget_reset_at = datetime.utcnow() + timedelta(
        seconds=duration_in_seconds(duration=budget_request.budget_duration)
    )

This ensures the budget row is created with a proper future budget_reset_at, so the reset cron only picks it up after the duration has actually elapsed.

Test

  • test_new_budget_request_sets_budget_reset_at_when_duration_provided in tests/test_litellm/proxy/management_endpoints/test_customer_budget.py — verifies budget_reset_at is auto-populated ~30 days in the future when budget_duration="30d" is provided without an explicit budget_reset_at

Note

Re: Greptile's comment on the update path

The update path is not affected by this bug.

This bug only occurs during budget creation (not linking to an existing budget via budget_id). UpdateCustomerRequest (_types.py:L1439) does not accept budget_duration as a field — Pydantic silently drops it. So even when /customer/update creates a new budget (line 595), budget_duration will always be NULL, which means the reset cron's query (budget_duration IS NOT NULL) will never match it.

@vercel

vercel Bot commented Feb 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 24, 2026 2:19pm

Request Review

@CLAassistant

CLAassistant commented Feb 24, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where creating a customer via /customer/new with budget_duration but no explicit budget_reset_at resulted in a NULL budget_reset_at in the database. The ResetBudgetJob cron would then immediately pick up these budgets and reset the customer's spend to zero, regardless of whether the budget duration had elapsed.

  • Adds budget_reset_at initialization in new_budget_request() to match the existing pattern in /budget/new — when budget_duration is set but budget_reset_at is not, it computes the first reset time as now + duration
  • Adds a unit test that verifies budget_reset_at is correctly auto-populated ~30 days in the future for budget_duration="30d"
  • Note: the same bug appears to exist in the update_end_user code path (line 591-605 of customer_endpoints.py) where a new budget is created without initializing budget_reset_at — this should be addressed as a follow-up

Confidence Score: 4/5

  • This PR is safe to merge — it fixes a real data-corruption bug with a minimal, well-tested change that mirrors an existing pattern
  • The fix is minimal, correctly mirrors the existing pattern in /budget/new, and includes a focused unit test. Deducting one point because the same bug exists in the update_end_user path in the same file and is not addressed.
  • litellm/proxy/management_endpoints/customer_endpoints.py — the update_end_user function (line 591-605) has the same budget_reset_at initialization gap when creating a new budget during customer update

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/customer_endpoints.py Correctly adds budget_reset_at initialization to new_budget_request(), mirroring the existing pattern in /budget/new. However, the same bug also exists in the update_end_user path (line 591-605) where a new budget is created without setting budget_reset_at.
tests/test_litellm/proxy/management_endpoints/test_customer_budget.py Adds a clean unit test that verifies budget_reset_at is auto-populated when budget_duration is provided. No network calls, proper assertions with time-range bounds.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["POST /customer/new\nwith budget_duration"] --> B["new_budget_request()"]
    B --> C{"budget_duration set\nbudget_reset_at null?"}
    C -- "Yes (NEW FIX)" --> D["Set budget_reset_at =\nnow + duration_in_seconds()"]
    C -- "No" --> E["Keep existing budget_reset_at"]
    D --> F["Create budget row in DB\nwith budget_reset_at populated"]
    E --> F
    F --> G["Create end-user row\nlinked to budget"]
    G --> H["ResetBudgetJob cron"]
    H --> I{"budget_reset_at\nin the future?"}
    I -- "Yes" --> J["Skip — budget not yet due"]
    I -- "No / NULL (OLD BUG)" --> K["Reset spend to 0.0\nprematurely"]
Loading

Last reviewed commit: 26f29c5

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps

greptile-apps Bot commented Feb 24, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

litellm/proxy/management_endpoints/customer_endpoints.py
Same bug exists in update path

The update_end_user function has the same issue you fixed in new_budget_request(). When a customer is updated with budget_duration (and the customer doesn't already have a budget), a new budget row is created at line 595 via litellm_budgettable.create using the raw budget_table_data dict — which won't have budget_reset_at populated. This would also result in a NULL budget_reset_at and the same premature spend-reset behavior from ResetBudgetJob.

Consider applying the same budget_reset_at initialization here:

        if budget_table_data:
            # Set budget_reset_at if budget_duration is present but budget_reset_at is not
            if "budget_reset_at" not in budget_table_data and "budget_duration" in budget_table_data:
                budget_table_data["budget_reset_at"] = datetime.utcnow() + timedelta(
                    seconds=duration_in_seconds(duration=budget_table_data["budget_duration"])
                )
            if end_user_budget_table is None:

@ghost

ghost commented Feb 25, 2026

Copy link
Copy Markdown

Review

1. Does this PR fix the issue it describes?
Yes. Fixes #22013 — when creating a customer with budget_duration but no budget_reset_at, the reset cron would wipe spend immediately. The fix initializes budget_reset_at to now + duration, matching /budget/new behavior.

2. Has this issue already been solved elsewhere?
Partially — /budget/new had this logic, but /customer/new didn't. This aligns the two paths.

3. Are there other PRs addressing the same problem?
No duplicates found for #22013.

4. Are there other issues this potentially closes?
Any reports of spend being unexpectedly reset for new customers with budget durations.

✅ LGTM — clean, focused fix with test coverage. Well-documented root cause analysis.

@ghost
ghost changed the base branch from main to litellm_oss_staging_03_04_2026 March 4, 2026 04:28
@ghost
ghost merged commit 9bf49d8 into BerriAI:litellm_oss_staging_03_04_2026 Mar 4, 2026
29 of 30 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: ResetBudgetJob zeroes out end-user spend when /customer/new is called with budget_duration but no budget_reset_at

2 participants