fix(budgets): report budget spend from LiteLLM - #207
Conversation
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Renumber the personal organization budget cleanup migration to follow the current revision 147 head and update its regression-test import. Co-authored-by: openhands <openhands@all-hands.dev>
Persist each governed member's cumulative LiteLLM spend at cycle start and derive their absolute cap from that stable baseline instead of current spend on every sync. Co-authored-by: openhands <openhands@all-hands.dev>
Use LiteLLM cumulative team and membership spend minus stored cycle baselines for budget UI and alert calculations. Keep per-member baselines for all LiteLLM members so direct SDK/API usage is visible in reporting and threshold checks.\n\nCo-authored-by: openhands <openhands@all-hands.dev>
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Resolve billing behavior in favor of the latest main hotfix while preserving LiteLLM budget reporting and synchronization verification. Co-authored-by: openhands <openhands@all-hands.dev>
|
Ok, so looking through at the code some more, I think with some iterations we can thread the needle here with the idea LiteLLM as the source of truth for spend metering and enforcement. (1) I agree the LiteLLM response contract is a risk, but I think we can account for it. One approach is to put a strict adapter at the boundary: validate the fields and types we actually depend on, isolate that contract in one place, and treat missing or malformed data as unavailable rather than silently as $0 (which is what can happen now). (2) Catching network errors at the boundary is reasonable, but I think the current fallback behavior in #207 could cause issues because it allows a failed or empty fetch to appear as $0 spend. During a reset, that can persist a zero baseline, and the next sync can set an absolute LiteLLM cap below existing cumulative spend. I think reads should use a persisted last-known-good snapshot and explicitly report live/stale/unavailable, while rollover or other baseline-changing writes should require a fresh validated snapshot and abort if one isn't available. (3) I think the TOCTOU gap is inherent if LiteLLM remains the enforcement point. The dashboard should be treated as an eventually consistent projection, not a promise that the next request will be admitted. We can make that honest by displaying the snapshot timestamp/freshness and explaining that enforcement happens in LiteLLM. Pub/sub would reduce lag, but it wouldn't make a UI read atomic with a later inference request, so I don't think we need it here even in an ideal solution. Overall, my core concern with #207 as written isn't that LiteLLM is the spend source of truth, it's that unavailable data is currently indistinguishable from real zero spend, and the same unvalidated data can influence durable cycle baselines and absolute caps. There are also a few implicit refetches that could make the app both slow and semantically unsafe during an outage. I drafted a PR on top of the really solid parts of #207 to demonstrate some additional safeguards: #242. Feel free to check it out and let's continue discussing so we can get to a better version of budgets. |
Summary
Existing override reconciliation
Periodic maintenance already loads every stored override for each enabled non-personal organization and rewrites every LiteLLM team membership. Once this stack is deployed, an existing override whose individual LiteLLM cap is absent or stale is repaired automatically on the next maintenance cycle. No one-time database migration or manual re-save is required.
The main limitation is a stored override whose user has no LiteLLM team membership: synchronization cannot create or verify a member it cannot see. Separate membership repair would be needed if the member was absent before synchronization began.
Stack
Validation
PYTHONPATH=. poetry run pytest tests/unit/test_org_budget_service.py -qfromenterprise/— 18 passedpoetry run ruff check enterprise/server/services/org_budget_service.py enterprise/tests/unit/test_org_budget_service.pyThis pull request was updated by an AI agent (OpenHands) on behalf of the user.
Enterprise server image for this PR: