fix(proxy): fetch background responses through the router in CheckResponsesCost - #35137
Conversation
…ponsesCost Closes #35131
🤖 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:
|
|
|
Greptile SummaryThis follow-up completes the background Responses API polling fix.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| enterprise/litellm_enterprise/proxy/common_utils/check_responses_cost.py | Adds deployment-aware response retrieval with an SDK fallback and expands terminal-status handling. |
| tests/proxy_unit_tests/test_check_responses_cost.py | Adds focused regression tests covering router selection, encrypted IDs, SDK fallback, removed deployments, and incomplete responses. |
Reviews (3): Last reviewed commit: "merge litellm_internal_staging" | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
TLDR
Problem this solves:
/v1/responsesrows stayqueuedforeverincompleteresponses were also treated as non-terminalHow it solves it:
llm_routerso deployment credentials applyincompleteas terminal, likefailed/cancelledUser Flow
Before: the background response finishes upstream, but the gateway never records its cost
After: the same background response shows up as billed spend shortly after it finishes
Relevant issues
Fixes #35131
Linear ticket
Resolves LIT-2361
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 proxies against real OpenAI spend, one per build, each on its own fresh postgres database. Before runs at 729bec6 (litellm_internal_staging lineage, fix absent; the branch tip 972c0d0 is also pre-fix and identical in the touched region), after runs at this PR's head 55c392b. Config pins
nano-c08(openai/gpt-5.4-nano) withapi_key: os.environ/OPENAI_API_KEY_C08QAandproxy_batch_polling_interval: 30To reproduce the credential shape deterministically, both proxy environments export a poisoned
OPENAI_API_KEY=sk-invalid-c08-qa, so SDK-level env resolution always 401s and only the router deployment's config credential can authenticate. One background response per leg, then hands off: no HTTP call touches the response afterwards, since aGET /v1/responses/{id}runs the full logging path and would write its own billed row, masking the observation. Observation is psql and proxy logs onlyBefore, the poll job swallows the credential failure every cycle and the row never moves, eight-plus consecutive cycles at a fixed 38s interval:
After, the first poll cycle fetches through the router deployment (succeeding despite the poisoned env var, which only the deployment-credential path can do), marks the row terminal, and bills it:
The billed amount is exact for gpt-5.4-nano: 13 input tokens at 2e-07 plus 5 output tokens at 1.25e-06 is 8.85e-06 USD. A foreground request on the same config independently billed an identical 8.85e-06 row as a control
Type
🐛 Bug Fix
Changes
CheckResponsesCostpollsLiteLLM_ManagedObjectTableforqueued/in_progressbackground responses and calledlitellm.aget_responses(response_id=...)directly. That SDK entrypoint only resolves credentials from provider env vars, while every deployment-scoped credential (Azureapi_base/api_version, a config-only or secret-managerapi_key) lives on the router deployment. So the fetch raised, the exception branch logged atinfoandcontinued, and the row never leftqueued; no cost was ever attributed and the same rows were re-polled every cycle.GET /v1/responses/{id}works for the exact same response because the proxy sends it throughllm_router.aget_responses, which decodes themodel_idembedded in the response id and applies that deployment'slitellm_params.The row stores the encrypted response id, whose plaintext is the LiteLLM-encoded id carrying
model_id, so after the existing decrypt step the id is routable:incompleteis a terminal status in the Responses API but was missing from the terminal set, which is a second way a row could be polled forever, so the status check is now a singleTERMINAL_RESPONSE_STATUSESmembership test overcompleted,failed,cancelled,incomplete. The skip log moved frominfotowarning: a row that cannot be fetched is a real misconfiguration and was previously invisible on default log levels.Tests in
tests/proxy_unit_tests/test_check_responses_cost.pycover a deployment-scoped id (both the raw encoded form and the encrypted form actually stored in the table) going through the router withlitellm.aget_responsespatched to raise if it is reached, an id with nomodel_idstill falling back to the SDK, and anincompleteresponse transitioning the row out ofqueued. All three fail onlitellm_internal_staging.Final Attestation
Link to Devin session: https://app.devin.ai/sessions/c5c0ca2f9cae4d32a3a02a02fb1104ed