Repository navigation
feat(shadow_eval): scope a job to model groups, ANDed with its key, team, and user targets - #39828
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR adds an optional model-group scope to forward shadow-evaluation jobs and composes it with existing key, team, and user targeting.
Confidence Score: 5/5The PR appears safe to merge; the previously reported defects are resolved and no new actionable issue remains. Alias canonicalization now occurs at request matching time, proxy-wide validation accepts backed team-public names, reverse jobs reject model scopes, and the dashboard prevents submission above the API’s 100-model limit. All previous threads were also manually resolved.
|
| Filename | Overview |
|---|---|
| litellm/integrations/shadow_eval_logger.py | Adds model-scope admission and resolves aliases on both the request and job scope at match time. |
| litellm/proxy/management_endpoints/auto_router_endpoints.py | Validates proxy-requestable model scopes and persists and returns them for every shadow-eval leg. |
| litellm/types/management_endpoints/auto_router_endpoints.py | Adds bounded, deduplicated model-scope request and response fields and disallows scopes on reverse jobs. |
| litellm-proxy-extras/litellm_proxy_extras/migrations/20260904000000_shadow_eval_model_scope/migration.sql | Adds a non-null model-scope array with an empty legacy-compatible default. |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalStartForm.tsx | Adds the forward-only model picker, enforces the API limit, and serializes the selected scope. |
| ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.tsx | Displays configured model scopes in shadow-evaluation job headlines. |
| tests/test_litellm/integrations/test_shadow_eval_logger.py | Covers scoped and unscoped admission, aliases in either spelling, and immediate alias repointing. |
| tests/test_litellm/proxy/management_endpoints/test_auto_router_endpoints.py | Covers persistence, response behavior, validation, deduplication, team-public names, and reverse-job rejection. |
Reviews (2): Last reviewed commit: "feat(shadow_eval): scope a job to model ..." | Re-trigger Greptile
91afbca to
5d6d7b1
Compare
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Confidence Score: 3/5 The core feature is well wired: the model scope is persisted on every leg, returned by the API, applied as an AND filter in the logger, alias matching is covered in both directions, unscoped jobs remain compatible, and the dashboard/tests cover the main user flow. The reported CI result also indicates the only failing check is pre-existing. I’m not raising this to 4/5 because the two P1 correctness issues remain:
There is also a smaller UX issue: the dashboard picker does not enforce the API’s 100-model limit, allowing a submission that the backend will reject. Fixing the two P1 issues would make this a 4/5; the remaining implementation and test coverage are otherwise strong. |
…eam, and user targets A shadow eval job could only be scoped by identity, so "this user's traffic on model X across every key they own" was not expressible and a models field on the start body was silently dropped. The job now carries a models list that every target is narrowed to, matched on the requested model group with model_group_alias resolved on both sides. An unresolvable name is a 400 at start. Empty means every model, which is what every existing row reads as. The dashboard start form gains an "Only on models" picker and the job headline shows the scope.
5d6d7b1 to
8000cfb
Compare
|
Codecov's final pull API now reports 100.0% patch coverage (27 hits, 0 misses) for 8000cfb. The prior codecov/patch failure was a stale status; the only remaining CI red is the already-documented inherited Python 3.10 proxy-infra test |
|
@greptileai can you reply here with a score and why that score? |
|
bugbot run |
|
Confidence Score: 4/5 The two correctness issues from the previous review are addressed:
The implementation also persists and returns I’m keeping this at 4/5 rather than 5/5 because one CI check remains red, although the provided evidence indicates that failure is inherited from the merge base rather than introduced here. From the feature-correctness perspective, I have no remaining blocking issue. |
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 8000cfb. Configure here.
|
@greptileai review |
TLDR
Problem this solves:
modelsfield in the start body was silently ignoredHow it solves it:
modelslist on the job, ANDed with every targetUser Flow
Before: an admin wants to shadow one user's
haiku-realtraffic, but the job samples everything the user sends{"user_ids": ["ms-user-1"], "models": ["haiku-real"], "router_name": "auto-router-cheap", ...}modelsfield; the filter was droppedhaiku-realandsonnet-realfrom two of their keysby_current_modellisting both modelsAfter: the same job samples only the user's
haiku-realrequests, across every key they own"models": ["haiku-real"]haiku-real,sonnet-real, and the aliasfast-haikufrom two keys, over/chat/completions,/v1/messagesand/v1/responseshaiku-realand alias requests judged;by_current_modellists one model and the funnel counts nothing for thesonnet-realrequestsmodelsfails at start with 400models not served by this proxy: 'haiku-reel'Relevant issues
modelsbehaves exactly as before, and every existing job row reads as unscopedLinear ticket
Resolves LIT-7013
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Rig: proxy from this worktree on
:4640, real Postgres, upstream models served through the sandbox gateway (real Anthropic calls, real spend).ms-user-1owns two keys,ms-key-aandms-key-b, both allowedhaiku-real,sonnet-realandauto-router-cheap.router_settings.model_group_aliasmapsfast-haikutohaiku-real. The judge isjudge-sonnet45(claude-sonnet-4-5), which serves no tier of the router under eval. Every traffic round waited out the ~10s jobs cache before sendingBefore (d23bec8)
A model filter on the start body is accepted and dropped
curl -X POST :4640/auto_router/shadow_eval/start -d '{"user_ids":["ms-user-1"],"router_name":"auto-router-cheap","shadow_percentage":100,"judge_model":"judge-sonnet45","max_budget":1.0,"models":["haiku-real"],"model_names":["haiku-real"],"model_group":"haiku-real"}'HTTP 201, response hastargets: [{"target_type":"user","target_id":"ms-user-1",...}],router_names: ["auto-router-cheap"]and nomodelskey anywhereThe job samples every model the user touches
/chat/completions:haiku-realandsonnet-realfromms-key-a, then the same pair fromms-key-bcurl :4640/auto_router/shadow_eval/1ad0464e-...after the pipeline drained:Model-only scope is not a job
curl -X POST :4640/auto_router/shadow_eval/start -d '{"models":["haiku-real"],"router_name":"auto-router-cheap",...}'HTTP 422at least one target is required: pass api_key_ids, team_ids, or user_idsAfter (8000cfb)
A model filter on the start body is stored and echoed
curl -X POST :4640/auto_router/shadow_eval/start -d '{"user_ids":["ms-user-1"],"models":["haiku-real"],"router_name":"auto-router-cheap","shadow_percentage":100,"judge_model":"judge-sonnet45","max_budget":1.0}'HTTP 201, response carries"models":["haiku-real"]beside"router_names":["auto-router-cheap"]The job samples only the user's requests on that model, alias included, across keys and surfaces
/chat/completionsonhaiku-realandsonnet-realfromms-key-a, the same pair fromms-key-b,/v1/messageson the aliasfast-haikufromms-key-a,/v1/responsesonsonnet-realfromms-key-bcurl :4640/auto_router/shadow_eval/c8c59a6d-...after the pipeline drained:haiku-realrequests (one per key) and the alias request. The threesonnet-realrequests were neither sampled nor counted as skipped, because they are outside the job's trafficA name the proxy does not serve fails at start
curl -X POST :4640/auto_router/shadow_eval/start -d '{"user_ids":["ms-user-1"],"models":["haiku-real","haiku-reel"],...}'HTTP 400{"detail":"models not served by this proxy: 'haiku-reel'"}Model-only scope is still not a job
curl -X POST :4640/auto_router/shadow_eval/start -d '{"models":["haiku-real"],"router_name":"auto-router-cheap",...}'HTTP 422at least one target is required: pass api_key_ids, team_ids, or user_idsA scope on a reverse job is rejected
curl -X POST :4640/auto_router/shadow_eval/start -d '{"user_ids":["ms-user-1"],"models":["haiku-real"],"router_name":"auto-router-cheap","direction":"reverse","baseline_model":"sonnet-real",...}'HTTP 422models is only meaningful for a forward job; a reverse job samples its own router's trafficDashboard
Type
🆕 New Feature
Caveats (if any)
Medium
model_group_aliasat match time on both sides; a per-key or per-team alias is not consultedLow
modelsis validated as "could any caller on this proxy ask for this name" (unscoped resolution or a team-public name), never per team; an auto-router name is allowed, since a forward job on router A scoped to router B's group is a real questionFinal Attestation
Note
Medium Risk
Adds a DB column and changes shadow-eval admission on the success-hook hot path; mis-scoping or alias behavior could skew samples or spend, though empty
modelspreserves legacy behavior.Overview
Shadow eval jobs can now be narrowed with an optional
modelslist that ANDs with existing key, team, and user targets. An empty list keeps today’s behavior (all model groups for those targets).Persistence and API:
LiteLLM_ShadowEvalJobgains amodelstext array (migration + Prisma). Start/list/detail bodies accept and returnmodels; values are deduped and must be non-empty names.modelson reverse jobs is rejected at validation time. At start,_validate_model_scopereturns 400 if a name is not something any caller on the proxy could request (including team-public and auto-router names where applicable).Sampling:
ShadowEvalLoggeronly admits traffic when the request’smodel_groupmatches the job scope afterresolve_model_group_aliason both sides at match time. Out-of-scope requests are ignored with no funnel “not_sampled” event, same as a direction mismatch.Dashboard: Forward jobs get an “Only on models” multi-select; job headlines show the scope when set. OpenAPI/
schema.d.tsupdated.Reviewed by Cursor Bugbot for commit 8000cfb. Bugbot is set up for automated code reviews on this repo. Configure here.