fix(spend): bound the logs-tab pagination count to stop full-window scans - #31825
Conversation
…cans The spend-logs UI list endpoint (/spend/logs/ui, /spend/logs/v2) computed an exact pagination total over the whole selected time window on every load. That was a standalone SELECT COUNT(*) FROM (SELECT request_id FROM LiteLLM_SpendLogs WHERE ...) which, on a multi-TB SpendLogs table, scans a huge number of rows and spikes Aurora ACU. The later LIT-4027 change folded it into COUNT(*) OVER (), but a window count still drains every matching row before the LIMIT applies, so the full-window scan remained. Compute the total with a bounded SELECT COUNT(*) FROM (SELECT 1 ... LIMIT $cap+1) that probes at most cap+1 rows, and drop the window count from the page query so the page query is a plain indexed ORDER BY ... LIMIT. When more than the cap match, report the cap and set total_is_capped so the UI renders "<cap>+". The bounded subquery terminates early rather than aggregating across all tablets, so it stays safe on sharded engines like YugabyteDB too. Resolves LIT-4119
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes a full-table-scan on
Confidence Score: 5/5Safe to merge — the bounded count subquery correctly decouples the two query_raw calls and parameter indices are maintained without collision. The parameter-index reuse between the count and page queries is correct, the capping logic is sound, and tests cover all relevant paths. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/spend_tracking/spend_management_endpoints.py | Replaces COUNT(*) OVER () window-function with a bounded SELECT COUNT(*) FROM (SELECT 1 ... LIMIT cap+1) subquery, decouples count from page query, and threads total_is_capped through to the response. Parameter indexing for count and page queries is correct. |
| tests/test_litellm/proxy/spend_tracking/test_spend_query_optimization.py | Tests rewritten to use side_effect for two-call query_raw dispatch (count then page), covering exact-total, capped-total, out-of-range-page, and empty-result paths. Old window-function assertions replaced with bounded-count assertions. |
| tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py | All inline mocks updated to dispatch on "COUNT(*)" in sql_query, return [{"total_count": min(total, cap_plus_one)}] for count calls, and strip the old total_count column from page rows. Coverage preserved for all sort/filter/RBAC cases. |
| ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx | Renders "N+" for totals and page counts when total_is_capped is true, adds a hover tooltip explaining the cap, and falls back gracefully when the field is absent (optional chaining throughout). |
| ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx | Adds total_is_capped?: boolean as an optional field on PaginatedResponse, keeping the interface backward-compatible with older API responses. |
Reviews (3): Last reviewed commit: "test(spend): make zero-total mock reflec..." | Re-trigger Greptile
Greptile SummaryThis PR replaces the
Confidence Score: 4/5Safe to merge; the two-query approach is correctly parameterized, backward-compatible, and well-covered by tests. The only open items are minor UX polish on the cap-boundary UI affordance and a slightly fragile dispatch heuristic in the test mock. The core optimization is sound: the bounded subquery count terminates early, the page query no longer drags a window aggregate, and the response contract is purely additive with total_is_capped. Parameter indexing was traced and is correct for both queries. The test mock dispatch works for current SQL shapes but could misbehave if a future filter condition embeds that literal string. The UI silently disables the Next button at the cap boundary without explaining why, which could confuse users on large deployments. The test mock in test_spend_management_endpoints.py (line 168) uses a string-contains check to distinguish the count query from the page query — worth reviewing if WHERE-clause filtering is ever extended with LIKE patterns containing COUNT(*)
|
| Filename | Overview |
|---|---|
| litellm/proxy/spend_tracking/spend_management_endpoints.py | Replaces the COUNT(*) OVER () window-function with a bounded two-query approach: a capped subquery count followed by a plain paginated SELECT. Adds SPEND_LOGS_PAGINATION_COUNT_CAP constant and total_is_capped response field. Logic and parameter indexing are correct. |
| tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py | Mock updated to dispatch on COUNT(*) in the SQL string and return bounded count vs. page rows accordingly. WHERE-clause regex updated to accept either ORDER BY or LIMIT as terminator. Changes accurately reflect the new two-query behavior. |
| tests/test_litellm/proxy/spend_tracking/test_spend_query_optimization.py | New tests added for bounded-count behavior and the capped total scenario; existing tests updated to remove window-count assertions. Coverage improved, no regressions masked. |
| ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx | Appends + to both the total-results count and the page-count display when total_is_capped is true. Next-button navigation is already bounded by total_pages, so the capped indicator is purely informational. |
| ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx | Adds optional total_is_capped boolean to PaginatedResponse interface. Additive, backward-compatible change. |
Comments Outside Diff (1)
-
ui/litellm-dashboard/src/components/view_logs/LogsTableToolbar.tsx, line 215-218 (link)Next-page navigation silently stops at the cap boundary
When
total_is_cappedis true the toolbar displays "Page X of 200+" but the Next button becomes disabled the momentcurrentPage === total_pages(200), giving no indication to the user that the cut-off is intentional rather than a UI glitch. Consider adding a tooltip or a small inline message (e.g. "Showing first 10,000 results — refine your filters to see more") when the cap is active so users understand why forward navigation is unavailable.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (2): Last reviewed commit: "fix(spend): bound the logs-tab paginatio..." | Re-trigger Greptile
| if "COUNT(*)" in sql_query: | ||
| cap_plus_one = params[-1] | ||
| return [{"total_count": min(total, cap_plus_one)}] |
There was a problem hiding this comment.
Mock count dispatch is fragile on
COUNT(*) substring match
The query-type branch relies on "COUNT(*)" in sql_query. Any future WHERE-clause value that happens to contain the literal string COUNT(*) (e.g. in a LIKE pattern) would cause the mock to handle a page query as a count query, returning [{"total_count": ...}] instead of a row list and silently breaking the tests. A more robust discriminator would be checking whether the outer SQL starts with SELECT COUNT(*) (case-insensitive) or checking for FROM ( in combination with COUNT(*), which is unique to the bounded-count subquery shape.
…otal tooltip Address Greptile review on #31825: - the empty-result test now returns [{"total_count": 0}] for the bounded count query (real COUNT(*) always returns one row) instead of [], so the zero-total path exercises the normal branch rather than the defensive guard - the logs toolbar shows a tooltip explaining the cap when total_is_capped is set, so a disabled Next button at the cap boundary reads as intentional
Devin e2e test — both checks passedRan a live proxy on
A broken/unfixed build would show
Also confirmed over HTTP: Tested by Devin — https://app.devin.ai/sessions/66130e7189474b6e9b7346f96199a2a4 |
…itellm_lit_4119_spendlogs_bounded_count # Conflicts: # tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py # tests/test_litellm/proxy/spend_tracking/test_spend_query_optimization.py
Relevant issues
Linear ticket
Resolves LIT-4119
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
A customer reported RDS Aurora ACU spiking after an upgrade, with Performance Insights flagging
SELECT COUNT(*) FROM (SELECT request_id FROM "LiteLLM_SpendLogs" WHERE ...)at ~472s average latency against a multi-TB SpendLogs table. That is the spend-logs UI list endpoint computing an exact pagination total over the whole selected time window on every loadRepro on a live proxy backed by Postgres seeded with 30,000 spend-log rows in a 24h window
EXPLAIN (ANALYZE, BUFFERS)on the count and page queries; the row-count actually scanned is the signal (execution time is small only because this is 30k local rows, not the customer's billions)Same behavior over HTTP through the proxy (
GET /spend/logs/v2, 30,000 matching rows, page_size 50)When fewer than the cap match, the total stays exact and
total_is_cappedisfalse, so normal deployments see no changeUI proof (Logs tab, e2e)
Live proxy on
localhost:4000backed by Postgres seeded with 30,000 spend-log rows in the last-24h window, UI built from this branch. With more than the cap matching, the Logs tab rendersShowing 1 - 50 of 10000+ resultsandPage 1 of 200+(capped total plus the+indicator). Narrowing the filter to a 1-hour sub-window (1,152 rows, below the cap) shows an exactof 1152 results/Page 1 of 24with no+, confirming the cap only engages above 10,000Type
🐛 Bug Fix
Changes
The spend-logs UI list endpoint (
/spend/logs/ui,/spend/logs/v2) built itstotal/total_pagesfrom an exact count of every row in the selected window. On a largeLiteLLM_SpendLogstable that scan is what drives the ACU spike. The earlier LIT-4027 change moved the count intoCOUNT(*) OVER (), but a window count still has to drain every matching row before theLIMITapplies, so the full-window scan stayedThis computes the total with a bounded
SELECT COUNT(*) FROM (SELECT 1 FROM "LiteLLM_SpendLogs" WHERE ... LIMIT $cap+1)that probes at mostcap+1rows, and drops the window count from the page query so the page query is a plain indexedORDER BY "startTime" ... LIMIT/OFFSET. When more than the cap match, the endpoint reports the cap and sets a newtotal_is_cappedflag so the dashboard renders<cap>+and stops paging past it. The bounded subquery terminates early instead of aggregating across every tablet, so it also stays safe on sharded engines like YugabyteDB (the case LIT-4027 was about)The cap is 10,000. Deployments whose filtered window has fewer rows than that keep an exact total and unchanged pagination
Link to Devin session: https://app.devin.ai/sessions/66130e7189474b6e9b7346f96199a2a4