feat(proxy): add expires filter to GET /key/list - #32953
Conversation
Add an opt-in expires query param to GET /key/list so callers can fetch only expired or only active keys without paginating every page and filtering client-side. 'expired' matches keys whose expires is in the past (NULL expires excluded); 'active' matches keys that never expire or expire in the future. Omitting the param preserves existing behavior for every caller. An unrecognized value returns HTTP 400 rather than silently returning all keys. The filter is pushed to the database via the existing Prisma where builder so callers avoid pulling the full key table into application memory. Resolves LIT-3387
Greptile SummaryThis PR adds an opt-in
Confidence Score: 5/5Safe to merge — the change is purely additive, the default behavior is unchanged, and invalid inputs are rejected with a 400. The filter is pushed to the database via the existing Prisma where-clause builder with no new query shape. The VALID_EXPIRES_FILTER_VALUES frozenset guards both the endpoint and the helper. NULL semantics for never-expiring keys are correct and directly tested. The list_keys except handler converts HTTPException to ProxyException (ProxyException.init calls str(code)), so the test assertion code == '400' is valid. No pre-existing callers are affected. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/management_endpoints/key_management_endpoints.py | Adds expires query param to list_keys with validation, a new _build_expires_where_clause helper, and plumbs expires_filter through _build_key_filter_conditions and _list_key_helper. Logic is correct: invalid values raise HTTP 400 (converted to ProxyException by the existing except handler), NULL semantics for never-expiring keys are handled properly, and the double-guard in _build_key_filter_conditions provides defense-in-depth. |
| tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py | Adds 9 new tests covering the clause builder shape, where-builder integration, UTC boundary semantics, 400 on invalid values, and correct forwarding of the filter (including None for existing callers). All tests use mocks/patches — no real network calls, consistent with the rule for this test directory. |
| ui/litellm-dashboard/src/lib/http/schema.d.ts | Adds `expires?: string |
Reviews (2): Last reviewed commit: "refactor(proxy): declare VALID_EXPIRES_F..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
@greptileai moved |
|
Both points check out against the diff. The implementation looks solid — the filter is pushed into |
|
@greptileai re review |
cb24864
into
litellm_internal_staging
Relevant issues
Linear ticket
Resolves LIT-3387
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Captured against a local proxy on
localhost:4000backed by a real Postgres, at commit 08c5180. The flow seeds three keys (one already expired, one active with a future expiry, one that never expires), then lists with each filter value.Type
🆕 New Feature
Changes
Adds an opt-in
expiresquery parameter toGET /key/list.expires=expiredreturns only keys whoseexpiresis in the past (keys with a NULLexpiresnever expire, so they are excluded);expires=activereturns keys that either never expire or expire in the future. Omittingexpirespreserves the current behavior for every existing caller, and an unrecognized value returns HTTP 400 instead of silently falling back to returning all keys.Before this, fetching expired keys for an internal cleanup job meant paginating through every page of
/key/listand filtering in application code; on a deployment with thousands of keys that is dozens of round-trips pulling the entire key table into memory. The filter is pushed to the database through the existing Prismawherebuilder (_build_key_filter_conditions), the same path already used forproject_id,access_group_id, andagent_id, so no new query shape is introduced.Note on indexing: the only index touching
expirestoday is the composite@@index([budget_reset_at, expires]), which a filter onexpiresalone cannot use efficiently. The main win here is avoiding the full-table fetch into application memory. A dedicated single-column index onexpireswould make the filtered query itself index-friendly, but that is a schema migration with its own write-amplification tradeoff, so I left it out of this PR; happy to file a follow-up if we want itTests live in
tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.pyand cover the pure clause builder (exactlt/gteshape and NULL semantics for both values), the where-builder integration (filter applied for valid values, omitted forNoneor an unrecognized value, boundary computed at call time as tz-aware UTC), and the endpoint (400 on a typo, verbatim forwarding to the helper forexpired/active/None, and the no-expirescall still forwardingNoneso existing callers are unaffected). I ran a local mutation pass over the new logic (flippinglttogte, dropping the NULL exclusion, weakening the validation and the value guard); each mutation was caught by a failing test