feat(spend-logs): mark the auto-router's own classifier calls - #35300
Conversation
Greptile SummaryThe PR adds trusted classifier-call attribution to spend logs.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains, and the previously requested classifier-origin constant is now centralized with its type and consumed by the classifier paths.
|
| Filename | Overview |
|---|---|
| litellm/constants.py | Adds the centralized metadata-key constant used for classifier-call attribution. |
| litellm/types/utils.py | Centralizes the typed auto-router classifier-origin value, fully addressing the prior review thread. |
| litellm/proxy/_types.py | Adds the classifier-origin field to the spend-log metadata allowlist. |
| litellm/proxy/litellm_pre_call_utils.py | Reserves the origin field by stripping caller-supplied values from untrusted metadata. |
| litellm/proxy/spend_tracking/spend_tracking_utils.py | Includes a stable default for the new origin field in generated spend-log metadata. |
| litellm/router_strategy/complexity_router/complexity_router.py | Stamps classifier sub-calls with the centralized origin value and forwards parent session and trace identifiers. |
| tests/test_litellm/router_strategy/test_complexity_router.py | Covers centralized classifier metadata, bucket preservation, sanitization, and parent trace propagation. |
| tests/test_litellm/proxy/test_litellm_pre_call_utils.py | Verifies that callers cannot forge the reserved classifier-origin marker. |
| tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py | Verifies spend-log projection and the unmarked ordinary-traffic case. |
Reviews (3): Last reviewed commit: "feat(spend-logs): record when a spend lo..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
b61fe85 to
08722d3
Compare
|
@greptileai can you score this PR and explain why |
|
bugbot run |
…n classifier call The complexity router's classifier sub-call copies the parent request's metadata verbatim, so its spend log row carries the caller's key, team and user and is indistinguishable from traffic the caller actually sent. Nothing on the row says otherwise: call_type is "acompletion" either way, model_group is overwritten to the classifier's own model group so the row never looks auto-routed, and routing_decision is absent exactly as it is on an ordinary request. Record the fact the system already knows at call time. internal_call_origin is declared on SpendLogsMetadata, which is the allowlist _get_spend_logs_metadata projects onto, and stamped in _classifier_call_metadata; both classifier paths already route through that one function and it feeds the metadata and litellm_metadata buckets alike, so every request surface is covered at one site. The key is reserved rather than caller-supplied, so it joins routing_decision in the untrusted-metadata strip and a caller cannot label their own traffic as router overhead. The classifier call also inherited no session identity, so the router minted a fresh trace id and the row landed in a session of its own. Forwarding the parent's session puts it in the trace of the request that triggered it, which is where an operator looks for what the routing cost.
08722d3 to
f1481c3
Compare
|
@greptileai can you score this PR and explain why |
|
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 f1481c3. Configure here.
TLDR
Problem this solves:
How it solves it:
internal_call_originon the spend log metadata allowlistRelevant issues
Linear ticket
Resolves LIT-5014
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
Live proxy on a throwaway Postgres, one auto-router whose LLM classifier and tiers both point at a real Anthropic model. Upstream was the sandbox gateway (
anthropic/us.anthropic.claude-opus-4-8) rather than api.anthropic.com, because the Anthropic and OpenAI keys on this machine are both out of credit; the calls are real and cost real money, and a direct-to-provider re-run is owedSame three commands on both runs, so the diff is the only discriminator
Before, at
fb79a4ee3bRow 1 is the classifier call and row 3 is a request the user actually sent. They agree on every visible field: same
model_group, because the router overwrites it to the classifier's own model group, and samecall_type. Nothing distinguishes them. Row 1 also sits in06418c33-66f9-4689, a session of its own, rather than theSESSION-BASE-001its parent ran in, so it is absent from the caller's trace. It cost more than the completion it decidedAfter, at
f1481c3b46The classifier row is labelled and now shares
SESS-CHATwith the request that triggered it. The ordinary request onSESS-ORDINARYstays unlabelled, which is the negative class the whole treatment depends on; without it the marker would only look right.SESS-FORGEDis the same request withinternal_call_originsupplied by the caller, and it is unlabelled tooAll three LLM endpoints are covered:
acompletion,anthropic_messagesandaresponseseach show a labelled classifier row chained to the parent's session./v1/messageswas sent withlitellm_metadataseeded so thelitellm_metadatabucket arm is genuinely exercised rather than falling through tometadataThe strip is surgical rather than a blanket drop; the caller's own spend-log metadata on that same forged request survived:
Type
🆕 New Feature
Changes
internal_call_origindeclared onSpendLogsMetadata, the allowlist_get_spend_logs_metadataprojects onto_classifier_call_metadata, which both the LLM and embedding classifier paths already call, and which feeds themetadataandlitellm_metadatabuckets alikerouting_decision, so a caller-supplied value is dropped at ingress on every routelitellm_session_id/litellm_trace_id, so its row joins the parent's session instead of a fresh oneWhat does not change: nothing new is billed, no schema migration, no new endpoint or query parameter. The classifier rows already existed and were already billed; they were simply unlabelled and attributed to a session of their own
Things a reviewer will ask about
Why a declared field rather than the existing free-form
spend_logs_metadatabucket: that bucket is populated from the caller'sx-litellm-spend-logs-metadataheader, so a value there is forgeable and would collide with a caller's own key of the same name. A reserved key can be stripped at ingress; a caller-owned bucket cannotWhy not reuse
routing_decision: it records which model an auto-router picked for a request. A classifier row did not route anything, and it is absent on ordinary un-routed requests too, so its presence or absence cannot identify a classifier callFinal Attestation
Note
Low Risk
Changes are limited to metadata stamping, ingress stripping, and session forwarding for internal router sub-calls; billing behavior and schema migrations are unchanged.
Overview
Adds
internal_call_originto spend-log metadata so auto-router LLM and embedding classifier sub-calls are distinguishable from real user traffic in Spend Logs.The field is allowlisted on
SpendLogsMetadata, stamped in_classifier_call_metadatafor both classifier paths, and stripped at proxy ingress (alongsiderouting_decision) so callers cannot forgeautorouter_classifier. Classifieracompletion/ embedding calls also inherit the parent’slitellm_session_idandlitellm_trace_idso those rows appear in the same session/trace as the routed request.Reviewed by Cursor Bugbot for commit f1481c3. Bugbot is set up for automated code reviews on this repo. Configure here.