fix(batches): stop uncostable batches from starving the cost poll page - #36714
Conversation
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:
|
Greptile SummaryThe PR prevents uncostable managed batches from permanently occupying the bounded cost-poll page.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py | Adds bounded cleanup and targeted retirement paths for managed batch rows that otherwise block newer cost-poll work. |
| tests/proxy_unit_tests/test_check_batch_cost.py | Adds focused tests for stale cleanup, unroutable IDs, guarded provider 404s, transient retries, legacy schemas, and page advancement. |
Reviews (4): Last reviewed commit: "refactor(batches): decode unified ids th..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
| if self._is_batch_gone_at_provider(e): | ||
| await self._retire_job(job, f"batch {batch_id} no longer exists at the provider") |
There was a problem hiding this comment.
🟡 A batch can be permanently abandoned for cost tracking when a temporary or misconfigured lookup returns "not found"
Any "not found" response from the provider is treated as permanent and the batch is dropped from future cost polling (_is_batch_gone_at_provider at enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py:749-750), so a batch whose lookup only fails because of a fixable configuration mistake never gets its spend recorded, even after the mistake is corrected.
Impact: Spend for real, completed batches can silently disappear from usage reporting with no way to recover it other than manual database edits.
How a fixable misconfiguration produces a permanent 404 retirement
_is_batch_gone_at_provider (enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py:237-245) matches any NotFoundError/openai.NotFoundError, not specifically "this batch id does not exist". Two concrete non-permanent sources of a 404:
- Azure returns HTTP 404
DeploymentNotFoundwhen the deployment name in config is wrong or was renamed; litellm maps that toNotFoundError. - If the deployment id encoded in the unified batch id is no longer in the router and default fallbacks are configured,
Router._common_checks_available_deployment(litellm/router.py:10812-10826) silently substitutes the fallback model, and the retrieve then hits a provider that has never heard of this batch id, returning 404.
In both cases _retire_job sets batch_processed = True (enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py:206-215), which permanently excludes the row from the poll query at enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py:695-710, so restoring the deployment/config later does not bring the batch back for costing. Narrowing the check (e.g. requiring the provider error body to reference the batch id, or requiring the resolved deployment id to actually still exist in the router before treating the 404 as permanent) would avoid retiring recoverable rows.
Prompt for agents
In enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py, check_batch_cost now permanently retires a managed-object row whenever llm_router.aretrieve_batch raises any NotFoundError (see _is_batch_gone_at_provider). A 404 is not always evidence that the provider dropped the batch: Azure returns 404 DeploymentNotFound for a wrong/renamed deployment, and if the deployment id embedded in the unified batch id is missing from the router while default fallbacks are configured, Router._common_checks_available_deployment substitutes a fallback deployment whose provider has never seen this batch id, also yielding a 404. In both cases the row is marked batch_processed=True and can never be costed again, even after the operator fixes the config. Consider tightening the permanence test, for example by requiring that the resolved model_id still exists in the router (llm_router.has_model_id) before treating a 404 as permanent, and/or by only retiring after the 404 repeats across several poll cycles.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch, tightened in 8947008: retirement now also requires the 404 to name the batch id, so Azure's DeploymentNotFound keeps retrying alongside your deployment check.
|
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 c11ebbe. Configure here.
|
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 da84142. Configure here.
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 8947008. Configure here.
…itellm_check_batch_cost_poll_starvation
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 3b09484. Configure here.
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a customer submitting daily batches sees new batches stop showing any spend, with no error anywhere
input_file_idand get back202with a scrambled batch id and"status": "validating"After: the dead batches drop out on their own and the fresh batch gets costed like it always did
202with"status": "validating"Relevant issues
Fixes #36640
Linear ticket
Resolves LIT-5462
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
Full before and after pair at this PR's final head
3b09484344, before at the exact merge base5f2986a1f3(port 30397), after at the head (port 47613). Each leg ran in its own fresh worktree with its own venv and a fresh database, a live proxy against the real OpenAI batch API, andPROXY_BATCH_POLLING_INTERVAL=60withMAX_OBJECTS_PER_POLL_CYCLE=2so two dead rows are enough to fill the poll page. The model id is pinned so the seeded 404 row resolves to a live deploymentBoth legs ran the identical script. Seed two dead rows 10 and 9 days old at terminal status
completewithbatch_processed=false: one whose unified id decodes tolitellm_proxy;llm_batch_id:poison-no-model(no model id), one encodingmodel_id:qa-fixed-gpt4omini;llm_batch_id:batch_deadbeef0000000000000000000000(404 at OpenAI). Submit a real batch, fire and forget. Once OpenAI reports it complete, seed two more identical dead rows minutes old, too young for the 7 day sweep, and submit a second real batch. The batches are never retrieved through the proxy, because retrieval bills inline on both versions and would mask the background job; completion is confirmed against the provider directlyBefore, merge base
5f2986a1f3Both creates returned
202with a unified id and"status": "validating", and OpenAI finished each batch within about a minute of submissionThe final /spend/logs read at 01:37:36Z, about 20 poll cycles after batch 1 completed and 19 after batch 2, still holds nothing but the four $0 create rows. Neither batch is ever costed, the starvation in one screen
After, head
3b09484344Same creates, same fire and forget
The /spend/logs read at 01:23:19Z shows both batches costed by the background job with zero client retrievals at any point. Batch 1's spend row lands 29 seconds after provider completion, within one 60s cycle. Batch 2's lands 97 seconds after: the first post-completion cycle's two slots went to the fresh poison rows, retired on sight, and the very next cycle costed the batch
Kept from the earlier live run at
c11ebbed27: screenshots and a screen recording of the Admin UI logs page showing the costed batch under theLiteLLM Proxy/CheckBatchCostuser agent are in this Slack thread and in a comment below. That run also probed the other direction: a row pointed at an unreachable deployment kept retrying across 9 cycles and a raw non-base64 unified id was never retired, so only provably dead rows are dropped. A 404 alone is still not trusted, since retirement needs the error to name the batch id and the batch's own deployment to still be in the router, so a renamed Azure deployment or a fallback that never saw the batch keeps retryingObservations from this run:
Type
🐛 Bug Fix
Caveats (if any)
batch_processedretire by status insteadLink to Devin session: https://app.devin.ai/sessions/d5c8c2d2e44f4033a4500e9cf415b306
Requested by: @mateo-berri
Final Attestation
Note
Medium Risk
Changes batch spend attribution and when rows stop being polled; incorrect retirement could drop legitimate spend, though 404 handling is narrowed and heavily tested.
Overview
Fixes LIT-5462: uncostable managed batch rows could fill every slot in the paginated
CheckBatchCostpoll (MAX_OBJECTS_PER_POLL_CYCLE), so newer batches never got costed or showed spend.During each poll, rows that can never succeed are retired via
_retire_job(setsbatch_processed=True, orstale_expiredon schemas without that column): unified IDs that decode but have nomodel_id, and provider 404s that explicitly name the batch when the batch’s deployment is still on the router. Misrouted 404s (missing deployment, wrong Azure deployment name, transient errors) are not retired so spend can still be recovered after config fixes.Stale cleanup adds a second sweep for old
complete/completedrows still atbatch_processed=False, since the existing non-terminal staleness pass never touched them. Terminal status lists are centralized inTERMINAL_MANAGED_OBJECT_STATUSES.Regression coverage lives in
TestPollPageStarvationin the proxy unit tests.Reviewed by Cursor Bugbot for commit 3b09484. Bugbot is set up for automated code reviews on this repo. Configure here.