fix: don't retire a completed batch from cost recovery while output_file_id is lagging - #37715
Conversation
…ile_id is still lagging
Greptile SummaryThe PR completes the batch cost-recovery fix by retaining completed batches while their output file ID is lagging and retiring them only when output exists or no requests succeeded.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported premature-retirement path is guarded in both relevant production paths, and the helper parameters are now typed.
|
| Filename | Overview |
|---|---|
| enterprise/litellm_enterprise/proxy/common_utils/check_batch_cost.py | The terminal poller path now preserves completed jobs whose output file may still arrive, resolving the previously reported premature-retirement path. |
| litellm/proxy/openai_files_endpoints/common_utils.py | The typed helper centralizes the safe-retirement invariant and guards database updates from prematurely setting batch_processed. |
| tests/proxy_unit_tests/test_check_batch_cost.py | Poller tests distinguish zero-success terminal batches from lagging-output and unknown-count batches. |
| tests/test_litellm/proxy/openai_files_endpoint/test_files_common_utils.py | Helper-level tests cover all four retirement outcomes using typed fixture parameters. |
Reviews (2): Last reviewed commit: "fix(check_batch_cost): leave a lagging-o..." | Re-trigger Greptile
…ng existing _completed_batch
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
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 af18f77. Configure here.
mateo-berri
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the contribution!
0fca861
into
BerriAI:litellm_internal_staging
TLDR
Problem this solves:
How it solves it:
User Flow
Before: a developer who checks their batch right as it finishes sees it complete and downloads results, but its spend never appears
input_file_idand get back"status": "validating"with a batch id"status": "in_progress""status": "completed"with"output_file_id": nullwhilerequest_countsshows completed linesoutput_file_idpopulated, and GET https://litellm-domain/v1/files/{output_file_id}/content downloads the results fineAfter: the same unlucky poll no longer costs the spend record, which appears once the output file id arrives
input_file_idand get back"status": "validating"with a batch id"status": "in_progress""status": "completed"with"output_file_id": nullwhilerequest_countsshows completed linesoutput_file_idpopulated, and GET https://litellm-domain/v1/files/{output_file_id}/content downloads the results fineRelevant issues
Fixes #37713
Linear ticket
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
QA'd live at the PR tip
af18f77db64ff47fd33f1c863d5bd19a1102e2d7(after) against the merge base490c9f9f3f(before). Each leg ran 2 proxy instances with--num_workers 2(4 worker processes) sharing one Postgres, creating real OpenAI batches ongpt-5.4-minithat cost real money, with the proxy'sapi_basepointed at a local passthrough shim tohttps://api.openai.comthat reproduces the reported provider lag deterministically: while a flag file exists,GET /v1/batches/{id}responses whose real upstream status iscompletedgetoutput_file_idnulled, and everything else passes through untouched. Both cases confirmed the upstream batch was genuinely complete with a real output file id by callingapi.openai.comdirectly outside the shimCase A, the user-retrieve door
Both legs: upload a 2-line JSONL on instance A (
POST /v1/fileswithpurpose=batchandtarget_model_names), create the batch on instance B (POST /v1/batches), then poll alternating across both instances with the lag window on and background polling disabled until the unlucky poll lands (before leg shown, port 41283; the after leg observed the identical shape on its own ports){"status": "completed", "output_file_id": null, "request_counts": {"completed": 2, "failed": 0, "total": 2}, "model": "gpt-5.4-mini-2026-03-17"}Spend at that point on both instances of both legs:
Then the lag flag is removed and both proxies restart with
PROXY_BATCH_POLLING_ENABLED=trueBefore (merge base 490c9f9): the spend never appears
Still
[]on both instances 15 and 27 minutes later, and the batch GET keeps servingoutput_file_id: nullforever{"status": "completed", "output_file_id": null, "request_counts": {"completed": 2, "failed": 0, "total": 2}}After (tip af18f77): exactly one spend row appears on both instances within 2 minutes
[{"request_id": "..._batch_cost", "spend": 2.625e-05, "model": "gpt-5.4-mini", "total_tokens": 30, "prompt_tokens": 22, "completion_tokens": 8, "call_type": "aretrieve_batch", "request_tags": ["User-Agent: LiteLLM Proxy", "User-Agent: LiteLLM Proxy/CheckBatchCost"]}]The batch GET on the same instance now also shows a populated
output_file_id, and the second instance (51237) returns the same single row: 1 row total across 2 instances x 2 workers, so no double billingCase B, the background poller door
Both legs: proxies stay up with polling enabled (45s interval), lag flag back on, a fresh batch uploaded on B and created on A. The unlucky sighting reproduces (
"status": "completed", "output_file_id": null,request_counts.completed: 2), the flag is held about 2 minutes past the first sighting so the poller runs multiple cycles against the lagging batch, then the flag is removedBefore (merge base): the poller wrote the batch off during the window, so removing the lag never brings the spend back
Still
[]on both instances 16 minutes after the flag came off, withoutput_file_idstill null on both. A control batch created on the same before rig with the lag flag off billed normally about 70 seconds after create (spend: 3.075e-05, taggedUser-Agent: LiteLLM Proxy/CheckBatchCost), so the poller was alive at the merge base and the loss is attributable to the lag window aloneAfter (tip): exactly one spend row within two poll cycles of the lag clearing
[{"request_id": "..._batch_cost", "spend": 3e-05, "model": "gpt-5.4-mini", "total_tokens": 30, "prompt_tokens": 20, "completion_tokens": 10, "call_type": "aretrieve_batch", "request_tags": ["User-Agent: LiteLLM Proxy", "User-Agent: LiteLLM Proxy/CheckBatchCost"]}]Same single row on the second instance, and a stability pass one poll cycle later still shows exactly 1 row per batch per instance
Observations from the runs a reviewer cannot see in the diff, all pre-existing behavior this PR leaves untouched, except the last, which this PR adds:
Type
🐛 Bug Fix
Caveats (if any)
Final Attestation
The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
af18f77 passes /live-pr-risk
Note
Medium Risk
Touches batch spend accounting (
batch_processedretirement). The change is conservative (retry instead of drop spend) but incorrect gating could delay or skip billing.Overview
Stops permanently retiring a completed batch from spend tracking when the provider reports
status=completedbutoutput_file_idis still missing.Adds
_completed_batch_safe_to_retire: retire only if the output file has arrived, orrequest_counts.completed == 0(nothing to bill). Unknown counts stay eligible for the next poll.The same gate is applied on the retrieve-path DB update (
batch_processed=True) and onCheckBatchCost’s unbilled-terminal branch, so a lagging output id no longer drops the spend record forever. Regression tests cover both paths.Reviewed by Cursor Bugbot for commit af18f77. Bugbot is set up for automated code reviews on this repo. Configure here.