Skip to content

feat(spend): raise /spend/logs/v2 page_size cap to 1000 - #33994

Merged
yuneng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_/ticket-implementation-review-409e1d
Jul 20, 2026
Merged

feat(spend): raise /spend/logs/v2 page_size cap to 1000#33994
yuneng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_/ticket-implementation-review-409e1d

Conversation

@yuneng-berri

Copy link
Copy Markdown
Collaborator

Relevant issues

Linear ticket

Resolves LIT-4558

Pre-Submission checklist

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

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Captured against a live proxy on localhost:4001 backed by a real Postgres with ~247k LiteLLM_SpendLogs rows

Before, at 067c9bbc96

$ curl -s -G "http://0.0.0.0:4001/spend/logs/v2" \
    --data-urlencode "start_date=2020-01-01 00:00:00" \
    --data-urlencode "end_date=2030-01-01 00:00:00" \
    --data-urlencode "page_size=1000" \
    -H "Authorization: Bearer $LITELLM_MASTER_KEY" -w '\nHTTP %{http_code}\n'

{"detail":[{"type":"less_than_equal","loc":["query","page_size"],"msg":"Input should be less than or equal to 100","input":"1000","ctx":{"le":100}}]}
HTTP 422

After, at b7e7fab741

A real chat completion through the proxy, then retrieval of that exact row inside a 1000-row page:

$ curl -s -X POST "http://0.0.0.0:4001/v1/chat/completions" \
    -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
    -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Reply with exactly: LIT-4558 pagination proof"}]}'

id: chatcmpl-E3lkR6p2lKxuu0tJDkx12OQQl6ltq
content: LIT-4558 pagination proof

$ curl -s -G "http://0.0.0.0:4001/spend/logs/v2" \
    --data-urlencode "start_date=2020-01-01 00:00:00" \
    --data-urlencode "end_date=2030-01-01 00:00:00" \
    --data-urlencode "page_size=1000" \
    --data-urlencode "sort_by=startTime" --data-urlencode "sort_order=desc" \
    -H "Authorization: Bearer $LITELLM_MASTER_KEY"

page_size echoed : 1000
rows returned    : 1000
request_id       : chatcmpl-E3lkR6p2lKxuu0tJDkx12OQQl6ltq
model            : gpt-4o-mini / openai
spend            : 6.9e-06
total_tokens     : 25
status           : success

Boundary sweep, at b7e7fab741

page_size=50    http=200  rows=50    0.35s   202 KB
page_size=100   http=200  rows=100   0.23s   376 KB
page_size=500   http=200  rows=500   0.34s   2.0 MB
page_size=1000  http=200  rows=1000  0.42s   4.3 MB
page_size=1001  http=422  rejected
page_size=2000  http=422  rejected

Pagination equivalence, at b7e7fab741

The strongest regression check: the same 2000 rows fetched as 2 pages of 1000 against 20 pages of 100

rows @1000x2      : 2000  unique: 2000
rows @100x20      : 2000  unique: 2000
ordered sequences identical: True
same set          : True
page1/page2 overlap @1000: 0

Filters still narrow correctly at the new ceiling (status_filter=success at page_size=1000 returned 1000 rows, all success), and /spend/logs/ui still enriches every row with session_total_count at its usual page size

Type

🆕 New Feature

Changes

/spend/logs/v2 and /spend/logs/ui capped page_size at 100, so a client exporting a large window had to issue a request per 100 rows. This raises the ceiling to 1000

The cap is the only thing that moves. Every request that was valid before stays valid and takes the same path, since widening a bound is strictly permissive

Larger pages should reduce load for a bulk export rather than add to it. Each request runs one bounded COUNT whose cost is fixed by SPEND_LOGS_PAGINATION_COUNT_CAP regardless of page size, so pulling 10k rows drops from 100 of those probes to 10, and the cumulative OFFSET walked across the export shrinks by the same factor. Per-row cost does not grow either; the paginated SELECT already excludes messages, response, and proxy_server_request, which are the columns that can reach hundreds of KB per row

1000 rather than 2000 because a 1000-row page serialises to roughly 4.3 MB, and that work is synchronous. 1000 captures nearly all of the round-trip reduction while leaving headroom under concurrent readers; /v1/user/list and the user-agent analytics endpoints already use the same 1000 ceiling

The 10k pagination count cap from #31825 is deliberately untouched. It bounds the count probe to a fixed number of rows so the query cost stays flat on very large tables, and it never limited how deep a client can page; a request at offset 39000 still returns a full page today

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Clients exporting large spend-log ranges were forced into 100-row pages,
which meant a bounded COUNT plus an increasingly deep OFFSET scan per
request. Larger pages reduce both the request count and the cumulative
OFFSET cost for the same result set.

The handler already excludes the heavy JSON columns (messages, response,
proxy_server_request) from the paginated SELECT and bounds the COUNT via
SPEND_LOGS_PAGINATION_COUNT_CAP, so per-row cost does not grow with page
size. 1000 matches the ceiling already used by the user and user-agent
analytics list endpoints.
@greptile-apps

greptile-apps Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Raises the page_size upper bound on /spend/logs/v2 (and the shared /spend/logs/ui handler) from 100 to 1000, reducing round-trips needed for bulk exports by up to 10×.

  • The change is a single le=100le=1000 in the FastAPI query parameter declaration; handler logic, SQL query shape, and the existing SPEND_LOGS_PAGINATION_COUNT_CAP are all untouched.
  • A new parametrized test validates that page_size=1000 returns exactly 1000 rows (200 OK) and page_size=1001 is rejected with 422; the mock intentionally creates 1200 rows so the slice is actually exercised, not just vacuously satisfied.

Confidence Score: 5/5

Safe to merge — the change is additive only, widening an existing bound without touching handler logic.

The diff is a single-line cap increase with no logic changes. Both routes share the same handler and the new limit is validated end-to-end by a well-designed test that uses a 1200-row mock so pagination is genuinely exercised.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/spend_tracking/spend_management_endpoints.py Single-line change: raises the page_size FastAPI query constraint from le=100 to le=1000 for the shared ui_view_spend_logs handler (covers both /spend/logs/v2 and /spend/logs/ui). No handler logic changes.
tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py Adds test_ui_view_spend_logs_page_size_upper_bound parametrized over page_size=1000 (expects 200 + 1000 rows) and page_size=1001 (expects 422). Mock generates 1200 rows so the pagination limit is genuinely exercised by the mock's slice logic.

Reviews (1): Last reviewed commit: "Merge remote-tracking branch 'origin/lit..." | Re-trigger Greptile

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yuneng-berri
yuneng-berri enabled auto-merge July 20, 2026 17:22
@yuneng-berri
yuneng-berri merged commit 24b55d2 into litellm_internal_staging Jul 20, 2026
80 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_/ticket-implementation-review-409e1d branch July 20, 2026 17:26
@codspeed-hq

codspeed-hq Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_/ticket-implementation-review-409e1d (b7e7fab) with litellm_internal_staging (3fcd19d)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (b7e7fab) during the generation of this report, so 3fcd19d was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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.

2 participants