Repository navigation
fix(proxy): attribute provider and model_info on pre_call_hook rejections - #41077
Conversation
…quests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…rometheus deployment state Stamp model_info only into the logging metadata so the router's failure callbacks do not count a key-level 429 or guardrail 403 against the deployment, treat a resolved plus an unresolved deployment as ambiguous provider attribution, and stop the prometheus deployment counters and deployment_state from treating a proxy-side reject as a selected deployment Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ct_provider_attribution Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> # Conflicts: # tests/e2e/quota_management/spend_tracking/test_spend_tracking_e2e.py # tests/test_litellm/proxy/utils/proxy_logging/test_post_call_failure_hook.py
…l is not a string Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… raising in failure hook Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 similar comment
…roxy-side in failure attribution Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
…on names Post-call GuardrailRaisedException failures kept their deployment labels on main but lost them on this branch because every GuardrailRaisedException was treated as a pre-routing reject. The proxy failure path now flags litellm_params with proxy_rejected_before_routing only when it adds deployment attribution itself, and the Prometheus logger keys deployment selection off that flag Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
…ler metadata Caller-supplied metadata.model_info (kept for keys allowed to override pricing) no longer suppresses proxy_rejected_before_routing. The hook now checks the logging object's first_api_call_start_time, which only the provider handoff sets, so Prometheus never records a deployment failure for a request that was rejected before routing. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
bugbot run |
…ing attribution Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
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 d6bc323. Configure here.
* fix: repair seven regressions caught by CircleCI on main - vertex_ai: stop treating fine-tuned endpoint ids (numeric or vertex_ai/gemini/<id>) and gemma models as Gemini 3+, which injected temperature=1.0 and Gemini 3 thinking config into their requests (#42465) - cost: price Azure DALL-E 3 from its azure/<quality>/<size>/dall-e-3 rows; it only worked through the OpenAI rows that #42435 removed - bedrock: stream bedrock/invoke/moonshot through an OpenAI-shaped chunk decoder; the generic decoder dropped every chunk, which the supports_response_schema flag from #42338 un-skipped in CI - proxy: keep the public model_group on pre-routing rejections so the Usage page groups them under the model name, not the deployment (#41077) - cost map: mirror the base rows' capability flags onto Bedrock regional and cross-region copies (#42254 and later syncs) - whitelist the new regional Bedrock rows from #42543 and #42588 for the converse routing check, following the existing regional-row convention * fix(model-prices): mirror capability flags onto ap-southeast-3 bedrock rows * refactor(bedrock): tighten types on the moonshot stream decoder and its tests
TLDR
Problem this solves:
pre_call_hookland on the spend log with no providercustom_llm_providerandmodel_idare empty on 429 and 403 failure rowsHow it solves it:
custom_llm_providerwhen every deployment in the group shares one providermodel_infoanddeploymentinto logging metadata when the group has one deploymentUser Flow
Before: a team whose key is limited to 1 request per minute sees the rejected second request logged with no provider
"model": "gpt-5.5"and get a 200Rate limit exceeded for api_keycustom_llm_provider: "openai"and amodel_idcustom_llm_provider: ""andmodel_id: "", so their provider report cannot count itAfter: the rejected request carries the same provider and deployment id as the served one
"model": "gpt-5.5"and get a 200Rate limit exceeded for api_keycustom_llm_provider: "openai"and amodel_idcustom_llm_provider: "openai"and the samemodel_id, so the provider report counts itRelevant issues
Requested by a customer (Pylon #8563)
Affected release
Linear ticket
Resolves LIT-7701
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)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
Setup: two proxies on the same Postgres database, the merge base 25af172 on :20702 and the PR tip d6bc323 on :20701, both started with
--config config.yaml --num_workers 1 --use_prisma_db_pushwhereconfig.yamlsetsgeneral_settings.master_key: sk-1234,store_model_in_db: true,proxy_batch_write_at: 5. Real OpenAI and Anthropic keys, no mocks.<key>is the virtual key returned by/key/generateVideo of the Admin UI Logs page (
/ui/?page=logs, request drawer) for the same two runs, Before at 25af172 and After at d6bc323 (normal 429 and callermetadata.model_info429 legs): https://berriaillm.slack.com/archives/C0B302ZJU05/p1790101309143669?thread_ts=1790069838.026499&cid=C0B302ZJU05Before (25af172)
single deployment, rpm_limit=1 key, second call rejected in pre_call_hook
curl -X POST http://localhost:20702/model/new -H "Authorization: Bearer sk-1234" -d '{"model_name":"precall-before-1790065409","litellm_params":{"model":"openai/gpt-5.5","api_key":"os.environ/OPENAI_API_KEY"}}'returnsmodel_id=81ffcace-18f8-40a5-8563-a6598c428010curl -X POST http://localhost:20702/key/generate -H "Authorization: Bearer sk-1234" -d '{"models":["precall-before-1790065409"],"rpm_limit":1}'curl -X POST http://localhost:20702/chat/completions -H 'Authorization: Bearer <key>' -d '{"model":"precall-before-1790065409","messages":[{"role":"user","content":"say ok 1"}],"max_tokens":5}'returns 200 with"id":"chatcmpl-EQq4i8uXGyYye1uXcZkYdYeAbrKn3"say ok 2returns HTTP 429{"error":{"message":"Rate limit exceeded for api_key: b0f912f0... Limit type: requests. Current limit: 1, Remaining: 0 ...","type":"throttling_error","code":"429"}}curl "http://localhost:20702/spend/logs?api_key=<key>" -H "Authorization: Bearer sk-1234"after 12s returns{'status': 'success', 'model': 'openai/gpt-5.5', 'custom_llm_provider': 'openai', 'model_id': '81ffcace-18f8-40a5-8563-a6598c428010', 'spend': 0.0002}{'status': 'failure', 'model': 'precall-before-1790065409', 'custom_llm_provider': '', 'model_id': '', 'spend': 0.0}Provider: -andModel ID: -mixed provider group (openai + anthropic behind one name), rejected call must not claim a provider
POST /model/newcalls registermixed-before-1790065409asopenai/gpt-5.5andanthropic/claude-sonnet-4-5, then/key/generatewithrpm_limit: 1/chat/completionsreturns 200"id":"chatcmpl-654cc3ca-2b04-40ff-aaa8-6e28385c443a"(served by the Anthropic deployment), second returns HTTP 429Rate limit exceeded for api_key: 36c608b3...GET /spend/logs?api_key=<key>returns{'status': 'success', 'model': 'anthropic/claude-sonnet-4-5', 'custom_llm_provider': 'anthropic', 'model_id': '11103f23-8176-4f7b-8153-38cc5af6ff32', 'spend': 0.000108}{'status': 'failure', 'model': 'mixed-before-1790065409', 'custom_llm_provider': '', 'model_id': '', 'spend': 0.0}After (d6bc323)
single deployment, rpm_limit=1 key, second call rejected in pre_call_hook
curl -X POST http://localhost:20701/model/new -H "Authorization: Bearer sk-1234" -d '{"model_name":"db-w1-head-1790096777","litellm_params":{"model":"openai/gpt-5.5","api_key":"os.environ/OPENAI_API_KEY"}}'returnsmodel_id=464a21e8-3ca8-4c5e-b5c3-8d6c082808b7curl -X POST http://localhost:20701/key/generate -H "Authorization: Bearer sk-1234" -d '{"models":["db-w1-head-1790096777"],"rpm_limit":1}'curl -X POST http://localhost:20701/chat/completions -H 'Authorization: Bearer <key>' -d '{"model":"db-w1-head-1790096777","messages":[{"role":"user","content":"say ok 1"}],"max_tokens":5}'returns 200say ok 2returns HTTP 429Rate limit exceeded for api_key ... Limit type: requests. Current limit: 1, Remaining: 0curl "http://localhost:20701/spend/logs?api_key=<key>" -H "Authorization: Bearer sk-1234"after 12s returns{'status': 'success', 'model': 'openai/gpt-5.5', 'custom_llm_provider': 'openai', 'model_id': '464a21e8-3ca8-4c5e-b5c3-8d6c082808b7'}{'status': 'failure', 'model': 'openai/gpt-5.5', 'custom_llm_provider': 'openai', 'model_id': '464a21e8-3ca8-4c5e-b5c3-8d6c082808b7'}Provider: openaiandModel ID: 464a21e8-...caller-supplied
metadata.model_infoon a key withallow_client_pricing_override, second call rejected before routingcurl -X POST http://localhost:<port>/chat/completions -H 'Authorization: Bearer <key rpm_limit=1>' -d '{"model":"gpt-5.5","messages":[{"role":"user","content":"say ok"}],"max_tokens":5,"metadata":{"model_info":{"id":"spoofed-deployment-1790096752"}}}'twice returns 200 then 429 on both sides. At the merge base the 429 spend row is{'custom_llm_provider': '', 'model_id': 'spoofed-deployment-1790096752'}and/metricscountslitellm_deployment_failure_responses_total{api_provider="None",model_id="spoofed-deployment-1790096752"}, so the caller's id is charged to deployment health. At the tip the row is{'custom_llm_provider': 'openai', 'model_id': 'spoofed-deployment-1790096752'}whilelitellm_deployment_failure_responses_totalandlitellm_deployment_total_requests_totalfor that key haveapi_provider=""andmodel_id="": the reject is attributed in spend logs but never counted as a selected deploymentmixed provider group (openai + anthropic behind one name), rejected call must not claim a provider
POST /model/newcalls registermixed-after-1790068533asopenai/gpt-5.5andanthropic/claude-sonnet-4-5, then/key/generatewithrpm_limit: 1/chat/completionsreturns 200"id":"chatcmpl-EQqt92evh7pC74qoObg685p7SmDyW"(served by the OpenAI deployment), second returns HTTP 429Rate limit exceeded for api_key: f5c868e9...GET /spend/logs?api_key=<key>returns{'status': 'success', 'model': 'openai/gpt-5.5', 'custom_llm_provider': 'openai', 'model_id': 'c014289b-dee5-4680-82bd-d56795d0d043', 'spend': 0.0002}{'status': 'failure', 'model': 'mixed-after-1790068533', 'custom_llm_provider': '', 'model_id': '', 'spend': 0.0}guardrails on a single deployment
guarded(openai/gpt-5.5), Prometheus enabled on both sidesA
generic_guardrail_apiguardrail points at a local API that always answersBLOCKED.generic-blockruns pre-call,generic-block-postruns post-call after the real OpenAI responsecurl -X POST http://localhost:<port>/chat/completions -H 'Authorization: Bearer <key>' -d '{"model":"guarded","messages":[{"role":"user","content":"hello"}],"guardrails":["generic-block"]}'returns HTTP 400policy: blocked by generic guardrail apion both sides. Spend row at the merge base:{'status': 'failure', 'model': 'guarded', 'custom_llm_provider': '', 'model_id': ''}; at the tip:{'status': 'failure', 'model': 'openai/gpt-5.5', 'custom_llm_provider': 'openai', 'model_id': '767c6383...'}. On both sideslitellm_deployment_failure_responses_totalandlitellm_deployment_total_requests_totalfor that key haveapi_provider=""andmodel_id="", so the pre-call block still does not count against the deploymentThe same call with
"guardrails":["generic-block-post"]and"max_tokens":40returns HTTP 400 on both sides, and on both sides the spend row is{'status': 'failure', 'model': 'openai/gpt-5.5', 'custom_llm_provider': 'openai', 'model_id': '767c6383...'}andlitellm_deployment_failure_responses_totalkeepsapi_provider="openai",litellm_model_name="guarded",model_id="767c6383...", so a post-call guardrail failure stays attributed to the deployment that served itmalformed request model (Prometheus callback enabled on both sides)
curl -X POST http://localhost:<port>/chat/completions -H 'Authorization: Bearer <key>' -d '{"model":["gpt-5.5"],"messages":[{"role":"user","content":"hi"}]}'returns HTTP 400'model' must be a string.on both sides. At the merge base the proxy log also recordsTypeError: unhashable type: 'list'from the Prometheus failure hook (2 occurrences per run); at the tip the log has none and/metricscounts the request underrequested_model="other"Same scenarios were also run with
--num_workers 2and--num_workers 4on both sides (8 or more requests per scenario, 4 server processes confirmed per side at 4 workers) with identical row shapes, and under 40 concurrent rejected requests with hostile metadata bodies and a 20s Postgres outage, where both sides kept returning 429 and recoveredteam public model name that collides with a global group, Prometheus and
generic_apisink enabled on both sidesConfig group
shared-nameis oneanthropic/claude-sonnet-4-5deployment.POST /model/newadds a team-onlyopenai/gpt-5.5deployment withteam_public_model_name: shared-name, then/key/generatewith that team andrpm_limit: 1. Two calls to/chat/completionswith"model":"shared-name"return 200 then HTTP 429Rate limit exceeded for api_key: 4ac97abb...GET /spend/logs?api_key=<key>at the merge base:{'status': 'failure', 'model': 'shared-name', 'custom_llm_provider': '', 'model_id': ''}; at the tip:{'status': 'failure', 'model': 'openai/gpt-5.5', 'custom_llm_provider': 'openai', 'model_id': 'team-openai-dep-head-1790111360'}, the team deployment and not the global anthropic one./metricsat the merge base labels that 429litellm_proxy_failed_requests_metric_total{api_provider="anthropic",model_id="None",requested_model="shared-name"}, the global group leaking into a team key's reject; at the tip it is{api_provider="openai",model_id="team-openai-dep-head-1790111360",requested_model="shared-name"}Prometheus deployment state A/B on the single deployment group
solo(openai/gpt-5.5), key withrpm_limit: 1, 200 then 429On both sides
litellm_deployment_state{model_id="2415f9c0..."}stays0.0andlitellm_deployment_failure_responses_total/litellm_deployment_total_requests_totalfor the reject keepapi_provider="",model_id="". At the merge baselitellm_proxy_failed_requests_metric_total{api_provider="openai",model_id="None",requested_model="solo"}andlitellm_llm_api_failed_requests_metric_total{model="solo",model_id=""}; at the tip both carrymodel_id="2415f9c0..."non-Postgres sink:
generic_apicallback posting to a local HTTP receiverBatch payloads for the two 429 rows above at the merge base:
status=failure custom_llm_provider=None model_id=''; at the tip:status=failure custom_llm_provider=openai model_id=team-openai-dep-head-1790111360andmodel_id=2415f9c0.... Success rows identical on both sidesType
🐛 Bug Fix
Caveats (if any)
Medium
metadata.model_infoandmetadata.deploymentare stamped, never top-levellitellm_params.model_info, so router cooldown and RPM tracking ignore these rejectionslitellm_params.proxy_rejected_before_routing, which Prometheus uses to clear deployment labels and skip deployment state updates. The flag keys off the logging object'sfirst_api_call_start_time(set at provider handoff), not on metadata contents, so caller-suppliedmetadata.model_infocannot suppress it and post-call failures never get itLow
model_groupis blank on failure payloads sent to thegeneric_apisink on both the merge base and the tip, unrelated to this changecode-qualityCI job fails onlitellm/proxy/bug_report_config.py::_render_json, which landed on main in a9cea9d and fails identically on mainQA runbook
openai/gpt-5.5under a fresh name withapi_key: os.environ/OPENAI_API_KEY(needsSTORE_MODEL_IN_DB=Trueand a realOPENAI_API_KEY), note the returnedmodel_id{"models":["<that name>"],"rpm_limit":1}max_tokens: 8, expect 200status: failurerow appears (batch writes flush every few seconds)custom_llm_providerto equal the success row's (openai) and itsmodel_idto equal the id from step 1Final Attestation
Note
Medium Risk
Touches failure logging, spend attribution, and Prometheus deployment metrics on hot proxy paths; behavior is guarded by router resolution rules and explicit proxy-rejection detection.
Overview
Pre-call proxy rejections (rate limits, guardrails, auth-style HTTPException / ProxyException) now get provider and deployment attribution on failure spend logs when the requested model group unambiguously maps through the router, without treating those rejects as deployment failures.
Proxy logging resolves the model group (respecting team-scoped deployments) and stamps
custom_llm_providerwhen all deployments share one provider, plusmetadata.model_info/deploymentonly for single-deployment groups—deliberately not top-levelmodel_infoso router cooldown and per-deployment RPM counters are unchanged.Prometheus treats the same proxy-side exception types as “no deployment picked” for deployment health metrics even when logs carry a
model_id, and collapses non-stringmodelvalues on proxy request metrics to theotherlabel to avoid cardinality / hook crashes.E2E and unit tests cover attributed failure rows, mixed-provider omission, and deployment-state behavior.
Reviewed by Cursor Bugbot for commit fc4f4cd. Bugbot is set up for automated code reviews on this repo. Configure here.