refactor(management): move the logs end-user filter onto /management/v1 - #34691
Merged
yuneng-berri merged 5 commits intoJul 27, 2026
Merged
Conversation
`/customer/aliases` shipped two days ago and has not been in a release, so its
wire contract is still free to change. This lands it on the control-plane
contract before that stops being true, since after a release the path, the param
names and the envelope would all need a permanent legacy adapter
The endpoint becomes `GET /management/v1/spend_logs/end_users`. It is a facet,
the distinct values one column takes over a filtered query on a resource, not an
entity collection; naming it after `customers` implied it listed the end-user
table when it actually reads spend logs, which is a different row set. Serving it
under the parent resource means its filters are the parent's filters, so the
dropdown offers exactly the values the logs table can show without two endpoints
having to keep agreeing on that
Contract changes: `size` becomes `page_size`, `search` becomes `q`, the window
moves from flat `start_date` / `end_date` to `filter[startTime][gte]` / `[lte]`,
and the body becomes `{data, meta, links}`. Unknown query params are now a 400
rather than being silently dropped, because an ignored filter over-returns data.
Errors are RFC 9457 problem documents on this prefix only; every other route
keeps the shape its callers already parse
`links` is what makes the rest deferrable. The dashboard hook follows the
server's `links.next` instead of computing `page + 1`, so moving this to cursor
pagination later changes the links and nothing the client does. That matters
because the inner scan is a sliding window, so offset paging can currently skip
or repeat an end user across pages; the fix is a follow-up, and the hypermedia
means it will not be a breaking one
Cursor mode, `sort`, `include`, ETag / `If-None-Match` and the generic `ListSpec`
framework are all deliberately out of scope here. They are additive or internal,
so none of them needs to beat the release
…oint-standards-b1cd57
Contributor
Greptile SummaryRenames the spend-log end-user facet and aligns it with the management API contract.
Confidence Score: 5/5The PR appears safe to merge, with the renamed endpoint, authorization scope, error contract, and dashboard integration remaining internally consistent. The new endpoint preserves the existing spend-log visibility rules, constructs valid parameterized queries across search and scope combinations, bounds database work, and keeps its generated client contract synchronized with the dashboard.
|
| Filename | Overview |
|---|---|
| litellm/proxy/management_endpoints/management_v1/spend_logs.py | Implements the bounded and authorization-scoped spend-log end-user facet with consistent SQL parameterization and response pagination. |
| litellm/proxy/management_endpoints/management_v1/common.py | Adds shared strict-query validation, problem responses, and page-link construction for management-v1 routes. |
| litellm/proxy/proxy_server.py | Registers the new router and scopes problem-detail handling to the management-v1 API surface. |
| litellm/proxy/_types.py | Moves the renamed endpoint into the same spend-log access tiers as the route it supports. |
| ui/litellm-dashboard/src/app/(dashboard)/hooks/spendLogs/useSpendLogEndUsers.ts | Updates the dashboard query contract and derives subsequent page parameters from server-provided links. |
| ui/litellm-dashboard/src/components/view_logs/RequestLogsFilters.tsx | Migrates the end-user filter to the new facet response without changing dropdown behavior. |
| tests/test_litellm/proxy/management_endpoints/management_v1/test_spend_logs.py | Adds focused coverage for validation, SQL bounds, scoping, pagination, search escaping, and role reachability. |
Reviews (1): Last reviewed commit: "Merge branch 'litellm_internal_staging' ..." | Re-trigger Greptile
The RFC 9457 `type` was `https://docs.litellm.ai/errors/<slug>`, copied from the standard's own error example. That path is a 404 and there is no docs section behind it, so every error body shipped a broken link RFC 9457 only requires `type` to identify the problem type; it encourages, but does not require, that dereferencing it yield documentation. An https URI makes a promise we are not keeping, so use `urn:litellm:error:<slug>` instead, which carries the same machine-readable identity with nothing to resolve. Switching to an https base later is a contract change for anyone matching on `type`, so that should wait for pages that actually exist A test pins the identifier against regressing to an https docs URL, since the existing assertion built the expected value from the same constant and would have stayed green whatever it held
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
ryan-crabbe-berri
approved these changes
Jul 26, 2026
ryan-crabbe-berri
approved these changes
Jul 27, 2026
Both failures are from this branch, not pre-existing The component allowlist test asserts the gateway and backend route sets union to the whole app, so any route on neither is a 404 on both pods. Allowlist the `/management/v1/` prefix on the backend, next to the other control plane entries, so every resource that moves under it later is covered without a per-resource edit The otel handler test builds its request as a SimpleNamespace carrying only `state`. The validation handler now reads `request.url.path` to decide whether the caller is on a surface with its own error contract, so the fake needs a url; a real Request always has one, which is why the handler does not guard for it The control plane branch returns early, and nothing covered that it still closes the dangling SERVER span first, so those requests would have leaked a span apiece. Added a case that pins it; removing the close call fails it
Same cause as the otel handler test: this file builds its request as a SimpleNamespace carrying only `state`, and the validation handler now reads `request.url.path` to pick an error contract, so the fake needs a url While here, cover what the two existing tests do not. They only exercise the proxy-wide 422, and the control plane's 400 problem document was reachable only through the route test, which registers its own copy of the handler in a local app rather than the real one. Two cases now pin the real handler directly: a `/management/v1` path returns problem+json with a `detail` string, and paths that merely resemble the prefix (`/management`, `/v1/management/foo`) keep the 422 shape their callers parse
yuneng-berri
merged commit Jul 27, 2026
2b7e01b
into
litellm_internal_staging
78 of 79 checks passed
Merged
5 tasks
stvnksslr
pushed a commit
to stvnksslr/litellm
that referenced
this pull request
Aug 3, 2026
…point-standards-b1cd57 refactor(management): move the logs end-user filter onto /management/v1 (cherry picked from commit 2b7e01b)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Problem this solves:
/customer/aliasesshipped 2 days ago, not yet in a releaseHow it solves it:
/management/v1/spend_logs/end_userspage_size,q,filter[startTime][gte],{data, meta, links}links.nextinstead of computingpage + 1Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Not yet captured against a live proxy; the steps below are what I want run before merge, at commit
b7a3516232Start a proxy on port 4000 and the dashboard dev server on port 3000, then:
data,metaandlinks:links.nextis followable as-is, and paging through it never repeats a page 1 entry:_in an end user id matches itself rather than acting as a wildcard (expectacme_devonly, notacme-corp):content-type: application/problem+json,q_typonamed indetail, and atypeofurn:litellm:error:unknown-query-parameterrather than an https link):{"error": ...}body here):useron each team, then confirm each key only sees its own team's end users:Type
🧹 Refactoring
Changes
/customer/aliaseshas not been in a release yet, so its wire contract is still free to change. This lands it on the control plane contract from the List Endpoints + Tables standard before that stops being true; once it ships, the path, the param names and the envelope would each need a permanent legacy adapter carryingDeprecationandSunsetheaders.The endpoint becomes
GET /management/v1/spend_logs/end_users. It is a facet, the distinct values one column takes over a filtered query on a resource, rather than an entity collection. Naming it aftercustomersimplied it listedLiteLLM_EndUserTablewhen it readsLiteLLM_SpendLogs, which is a different row set: a customer with a budget row and no traffic in the window appears in one and not the other. Serving it under the parent resource means its filters are the parent's filters, so the dropdown offers exactly the values the logs table can show without two endpoints having to keep agreeing on that.On the contract itself,
sizebecomespage_sizeandsearchbecomesq, the window moves off flatstart_date/end_dateontofilter[startTime][gte]and[lte], and the body becomes{data, meta, links}. Unknown query parameters are now a 400 rather than being dropped, since a silently ignored filter over-returns data. Errors are RFC 9457 problem documents, scoped by path prefix so every other route keeps the shape its callers already parse. Theirtypeis a URN rather than an https URL: RFC 9457 only requires that it identify the problem type, and an https URI promises documentation at that address, which we do not have.linksis what makes the rest of the standard deferrable. The dashboard hook follows the server'slinks.nextinstead of computingpage + 1, so moving this to cursor pagination later changes the links and nothing the client does. That matters because the inner scan is a sliding window: it takes the newestSPEND_LOGS_FACET_SCAN_CAProws bystartTime, and as new logs land between requests, offset paging can skip or repeat an end user across pages. That is a real bug and it is not fixed here, but the hypermedia means fixing it will not be a breaking change.Scoping is carried over unchanged and still reuses
_get_permitted_team_ids_for_spend_logs, so it cannot drift from how/spend/logs/uiscopes the table itself. Cursor mode,sort,include, ETag andIf-None-Match, and the genericListSpecframework are all deliberately out of scope; each is either additive or internal, so none of them needs to beat the release.New code sits under
litellm/proxy/management_endpoints/management_v1/, with the contract machinery (problem documents, unknown-param rejection, link building) incommon.pyso the next facet inherits it rather than reimplementing it.Two repo-wide guards needed updating for the new prefix rather than the new route.
backend/routes/allowlist.pygains/management/v1/, since the component allowlist test asserts the gateway and backend route sets union to the whole app and a route on neither 404s on both pods; allowlisting the prefix covers every resource that moves under it later. The otel exception-handler test builds its request as aSimpleNamespace, and the validation handler now readsrequest.url.path, so that fake needed a url.Final Attestation