feat(auto-router)!: scope shadow eval jobs to multiple keys - #37251
Conversation
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4/5 Here's why: What's done well:
What holds it to 4 rather than 5:
None of these are blockers. The design is solid, the migration is safe, and the test suite covers the semantics that actually matter. |
Greptile SummaryThe PR extends auto-router shadow evaluations from one key to grouped multi-key jobs while preserving independent budgets and lifecycle state for each key
Confidence Score: 5/5The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking defects identified Grouped persistence, per-leg sampling state, pooled aggregation, API contracts, and dashboard consumers remain coordinated, and the sampler continues to operate correctly on each unique leg ID
|
| Filename | Overview |
|---|---|
| litellm/proxy/management_endpoints/auto_router_endpoints.py | Groups independently persisted key legs for creation, listing, detail aggregation, labeling, and stopping without an accepted correctness issue |
| litellm/types/management_endpoints/auto_router_endpoints.py | Replaces the single-key request and response contracts with nonempty key collections and per-key lifecycle data |
| litellm-proxy-extras/litellm_proxy_extras/migrations/20260817000000_shadow_eval_multi_key/migration.sql | Adds, backfills, constrains, and indexes the grouping column without destructive schema changes |
| schema.prisma | Adds the required grouping field and index, synchronized with both schema copies |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.tsx | Updates shadow-evaluation creation and detail rendering for multiple selected keys and per-key results |
| ui/litellm-dashboard/src/components/shared/PaginatedMultiSelect.tsx | Adds paginated multi-selection while retaining selected values across changing option pages |
| tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py | Expands endpoint coverage for atomic grouped creation, grouped reads, filtering, stopping, and aggregation |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.test.tsx | Updates dashboard tests to cover multi-key submission and grouped job presentation |
Reviews (1): Last reviewed commit: "feat(auto-router): scope shadow eval job..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
5b83c89 to
e92f912
Compare
|
api_key_ids now capped at 100, bounding every job read. The config invariant is _group_response's docstring; by_key grp is query-bounded to leg_ids |
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4/5 Here's the breakdown: What earns the 4:
What keeps it from a 5:
None of these are blocking. The core design is solid, the migration is safe, and the test coverage is thorough. |
|
All _group_response call sites filter by group_id; ShadowEvalSlice.group is the established shared shape; any head leg yields the same group_id |
e92f912 to
b12cb28
Compare
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4/5 Why: What's well done:
What holds it from a 5:
None of these are blocking — the core design is sound. |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit b12cb28. Configure here.
b12cb28 to
83c1533
Compare
|
by_group now builds via sorted plus groupby; ordering stays owned by the head query. The two-phase window is read-only; no release ships the old shape |
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4/5 What earns the high mark:
What keeps it from 5/5:
Overall this is a clean, well-reasoned feature with a design that holds up under concurrent starts, budget exhaustion, and backward compatibility. The four points above are minor enough that none of them are blockers. |
83c1533 to
6bdc02e
Compare
|
Split per review direction: this PR is backend plus a minimal dashboard adaptation; the multi-select picker and per key table moved to stacked #37389 |
… stops Two operators can both pass the derived-status guard in the race window. The stop UPDATE now claims only legs with stopped_by still null and the endpoint judges by its row count, so exactly one caller ever gets the 200 and the loser gets the same already-stopped 400 a late caller gets
|
Racing stops now resolve in the statement itself: it claims only stopped_by null legs and the row count picks the one winner; loser gets the late-caller 400 |
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 2128db6. Configure here.
|
Score: 4/5 This is genuinely well-engineered. Here's the breakdown: What earns the 4:
What keeps it from 5:
|
The status guard ran before the UPDATE, so a stop racing the last budgeted attempt still claimed the job and it read stopped forever instead of completed. The statement now claims the job only while a leg still samples inside the window with no stop recorded, and the endpoint reads once after writing: a racing operator, a same-instant budget spend, and a repeat stop all get the 400 naming the status the job actually holds. The pre-write guard and the hand-built response go away
|
The stop statement now claims only a still-sampling job, so a stop racing the last budgeted attempt reads already completed and records no operator; the pre-write guard is deleted |
|
On config duplication: true DB enforcement needs a btree_gist EXCLUDE or a trigger for a state no writer can produce; the immutable design plus the uniform-payload test stays the guard |
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit dad260f. Configure here.
|
Score: 4/5 Here's the reasoning: What's excellent:
What keeps it from 5/5:
|
|
Score: 4/5 This is a well-engineered PR. The legs/group_id model is the right shape for the problem, the migrations are safe and additive, and the test coverage is genuinely thorough (racing stops, budget exhaustion, timezone handling, backfilled legacy jobs). Here's the breakdown: What works well:
Issues keeping it from 5/5:
Summary: The design is sound, the migrations are safe, and the tests are among the most complete I've seen for a feature of this scope. The issues above are minor. This is ready for maintainer review. |
|
On the three 4/5 notes: keys[] never exposes per-key stopped_by, so nothing can diff it; the freeze SQL is live-proven in the body's straggler run; fake fidelity noted |
|
Context for the three 4/5 notes: keys[] never exposes per-key stopped_by, so no consumer can diff it against budget_spent; the attempt-freeze SQL is live-proven in the body's fabricated-straggler run against real Postgres; the fake's fidelity is the stated price of trustworthy tests. Types-sync regen pushed. @greptileai can you reply here with a score and why that score? |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 7a919c1. Configure here.
|
Based on your clarifications:
Given those three, the residual risks are:
Nothing substantive remains open from the 4/5 holds. Score: 5/5. The breaking API reshape ( |
…itellm_shadoweval_multikey2 # Conflicts: # litellm/proxy/management_endpoints/auto_router_endpoints.py
|
Merged current staging to clear the conflict; the typing sweep's Protocol accessors are adopted across the multi-key code with the job-table Protocol adapted to find_many and create_many. @greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Score: 4.5 / 5 What's strongMigration design is correct and zero-risk. Two additive migrations: Race safety is built into the write path, not bolted on. A single
UTC handling is thorough. The Test coverage is exceptional. ~30 unit tests including:
The fake Performance is bounded. The 100-key cap propagates to every downstream read. The Minor concerns (no blockers)
These are all minor and the code is clearly correct. The overall design — additive schema, atomic multi-leg write, single-statement stop with EXISTS-guarded predicate, |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 5494d22. Configure here.
TLDR
Problem this solves:
How it solves it:
User Flow
Before: an admin shadow testing an auto-router across two of their keys can only cover one key at a time, so no single result describes the router
"api_key_ids": ["<hash A>", "<hash B>"]and get back 422 sayingapi_key_idis requiredAfter: the same admin covers both keys with one job and reads one result
"api_key_ids": ["<hash A>", "<hash B>"]and get back 201 with a single job id listing both keys, each with its ownmax_turnsand display labelsRelevant issues
group_idcolumn instead of a child table, so the migration moves no data, the destructive column drops are gone, and the sampler hot path plus its whole test file have a zero line diffgroup_id = id, so their job ids keep resolving on every endpointGROUP BY group_id ORDER BY MAX(created_at)per the tag-management precedent, so it never leans on Prisma's in-memory distinctLinear 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)Screenshots / Proof of Fix
Rig: proxy from this branch on :4272, real Postgres, an OpenAI-compatible stub upstream serving the router tiers and the judge (every local provider credential is dead; the judged-verdict pipeline itself is unchanged by this PR and was live-proven against real providers when it merged). Two keys
mkey-alphaandmkey-beta;my-routeris a configured complexity router. The design choice a reviewer will care about: a job's legs sharegroup_idand identical config written by ONEcreate_many(a single INSERT), so a partial-unique-index loser rolls back the whole claim; race safety rides the existing(api_key_id, direction) WHERE stopped_at IS NULLindex unchangedBefore (68d4ba5)
one job per key only
POST /auto_router/shadow_eval/startwith"api_key_ids": [hashA, hashB]->422 {"detail":[{"type":"missing","loc":["body","api_key_id"],...}]}cmsy09htf0000...for alpha,cmsy09hu10001...for beta)mkey-alpha judged: 2 win_pct: 100.0,mkey-beta judged: 2 win_pct: 0.0; pooling is manualAfter (5494d22)
a pre-migration job id still resolves
group_idbackfilled= idGET /auto_router/shadow_eval/cmsy09htf0000mgneub781r3k-> 200,status: stopped,keys: [{mkey-alpha, max_turns 5}],judged_count: 2one start covers both keys, and the key set is bounded
POST /auto_router/shadow_eval/startwith"api_key_ids": [hashA, hashB],max_turns: 2-> 201, onejob_id,keyslisting both hashes with per keymax_turns,key_alias,key_name,status: running422 too_longonapi_key_ids; the 100-key cap bounds the claim write, every job read, and the 409 messageeach key spends its own budget; results pool and slice per key
GET /auto_router/shadow_eval/<job id>->judged: 4,overall_shadow_win_rate_pct: 50.0,by_key: [alpha turns 2, beta turns 2]; alpha judged exactly its budget while its third turn sampled nothing, and beta kept samplingclaim errors name the key, and exhausted slots free themselves
409 "Already in an active forward shadow eval job: 712a7342... (job 6b7da151-...). Stop it first.", nothing created400 "api_key_ids not on this proxy: nope-1, nope-2; ...", every unknown named at onceforward and reverse coexist; one stop ends every key
POST .../startwithdirection: reverse,baseline_model-> 201 over the same two keysPOST /auto_router/shadow_eval/<reverse job id>/stoponce ->status: stopped, both keys stamped; a second stop -> 400 already stoppedstopped, so stop answers 400 for it toothe list collapses legs into jobs
GET /auto_router/shadow_eval?limit=3-> 3 entries, each one JOB (a two-key job is one row with both aliases), newest first; with the newest job holding 2 legs,limit=2still returns 2 distinct jobsGET /auto_router/shadow_eval?api_key_id=<hash B>-> every job containing that key, sibling keys included, among them the pre-migration single-key jobaggregation cost at scale
EXPLAIN ANALYZEshows a Bitmap Index Scan on the existingjob_idindex, execution time 1.5 ms. Attempts stay budget-bounded per job (at most 2000 per key), never per-request unboundedUI (minimal adaptation)
api_key_ids: [key]; the multi-select picker and per key table are in the stacked UI PRType
🆕 New Feature
Caveats (if any)
api_key_id->api_key_idson start, top level key fields ->keys[]on responses. Internal admin endpoints, weeks old, no external consumers; the dashboard is adapted in this PR and upgraded in the stacked UI PRcreate_many, no update path exists, pinned by a uniform-payload testmax_turns, unchanged pre-existing semantics, out of scope hereFinal Attestation
Note
Cursor Bugbot is generating a summary for commit 5494d22. Configure here.