diff --git a/app/main.py b/app/main.py index 45b95bc..3bb4c45 100644 --- a/app/main.py +++ b/app/main.py @@ -584,32 +584,28 @@ def add_credits(email: str, delta: float, reason: str, reference: str | None = N def compute_max_budget(allowance: float, credit_balance: float, team_spend: float | None) -> dict: """Compute the effective LiteLLM max_budget from the two buckets. - Model: the member has a monthly ALLOWANCE (resets via LiteLLM's - budget_duration) plus a NON-EXPIRING credit balance. Spend draws the - allowance first; once spend exceeds the allowance within a cycle, credits - cover the overflow. + Model: the member has a monthly ALLOWANCE (renews on each membership + payment) plus a credit balance that adds spendable headroom ON TOP of the + allowance. The total spendable cap is simply `allowance + credit_balance` + — credits never shrink the allowance; they extend it. - `team_spend` is the member's spend so far this cycle (from LiteLLM). - Credits already consumed this cycle are excluded from the available pool - so the pushed max_budget reflects exactly what's left: - - max_budget = allowance + credits_remaining + `team_spend` is the member's spend so far this cycle (from LiteLLM). It is + used only to split the spend between allowance vs credits for display, NOT + to reduce the cap. Returns {"max_budget", "allowance_used", "credits_used"} for dashboards. """ + max_budget = allowance + credit_balance if team_spend is None: - # Unknown spend: be permissive (full allowance + full credits) so we - # never wrongly block a member over a metrics gap. return { - "max_budget": allowance + credit_balance, + "max_budget": max_budget, "allowance_used": 0.0, "credits_used": 0.0, } allowance_used = min(max(team_spend, 0.0), allowance) credits_used = max(0.0, team_spend - allowance) - credits_remaining = max(0.0, credit_balance - credits_used) return { - "max_budget": allowance + credits_remaining, + "max_budget": max_budget, "allowance_used": allowance_used, "credits_used": credits_used, }