Skip to content

fix(batches): don't crash logging when a completed batch has no output file - #34067

Merged
mateo-berri merged 1 commit into
BerriAI:litellm_internal_stagingfrom
MUSE-CODE-SPACE:fix/batch-logging-null-output-file
Aug 17, 2026
Merged

fix(batches): don't crash logging when a completed batch has no output file#34067
mateo-berri merged 1 commit into
BerriAI:litellm_internal_stagingfrom
MUSE-CODE-SPACE:fix/batch-logging-null-output-file

Conversation

@MUSE-CODE-SPACE

@MUSE-CODE-SPACE MUSE-CODE-SPACE commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • All-error batches finish with an error file only, no output file
  • Polling such a batch crashed the gateway's success logging
  • None of those polls ever reached the spend logs

How it solves it:

  • Treat a batch with no output file as an empty result
  • Record zero cost and usage instead of raising

User Flow

Before: every status poll of the all-error batch vanishes from the gateway's spend records, and the gateway console prints a file-retrieval error each time

  1. They upload their batch input with POST https://litellm-domain/v1/files (purpose=batch, target_model_names=gpt-5.5), every JSONL line asking for an out-of-range temperature, and get back a long gateway file id
  2. They create the batch with POST https://litellm-domain/v1/batches using that input_file_id and get back a batch id with "status": "validating"
  3. They poll GET https://litellm-domain/v1/batches/{batch_id} with their virtual key until the response shows "status": "completed" with "output_file_id": null, a real "error_file_id", and request_counts 0 completed / 3 failed
  4. They open GET https://litellm-domain/spend/logs?api_key= and find rows for the upload and the batch creation but none for any status poll, while the console printed "ValueError: Output file id is None cannot retrieve file content" on every poll

After: every status poll is recorded at zero spend and the console stays clean

  1. They upload their batch input with POST https://litellm-domain/v1/files (purpose=batch, target_model_names=gpt-5.5), every JSONL line asking for an out-of-range temperature, and get back a long gateway file id
  2. They create the batch with POST https://litellm-domain/v1/batches using that input_file_id and get back a batch id with "status": "validating"
  3. They poll GET https://litellm-domain/v1/batches/{batch_id} with their virtual key until the response shows "status": "completed" with "output_file_id": null, a real "error_file_id", and request_counts 0 completed / 3 failed
  4. GET https://litellm-domain/spend/logs?api_key= now lists a $0.00 batch-retrieve row for each distinct batch state they polled, including the final completed one, and no error appears in the console

Relevant issues

Fixes #33987

Linear ticket

Resolves LIT-4852

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

Live e2e against real OpenAI batches, all request lines carrying an out-of-range "temperature": 42 so every line fails provider-side. Both legs ran the identical setup and differ only in the commit the proxy was booted from: fresh Postgres DB, config with gpt-5.5 (openai/gpt-5.5) plus a files_settings entry for the plain-provider flow, PROXY_BATCH_POLLING_ENABLED=false (the OSS shape from #33987, no enterprise cost poller), and a user-bound virtual key

Before, merge base add095b494cc75440543a240618a88174ea4a445 (proxy port 25200): polls never reach the spend logs

Managed flow (upload with target_model_names=gpt-5.5, then create and poll):

$ curl -s http://localhost:25200/v1/files -H "Authorization: Bearer $KEY_A" -F purpose=batch -F target_model_names=gpt-5.5 -F file=@all_error.jsonl
$ curl -s http://localhost:25200/v1/batches -H "Authorization: Bearer $KEY_A" -d '{"input_file_id":"<unified id>","endpoint":"/v1/chat/completions","completion_window":"24h"}'
$ curl -s http://localhost:25200/v1/batches/<unified batch id> -H "Authorization: Bearer $KEY_A"   # repeated until terminal
{"id": "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpkMGI3...", "status": "completed", "output_file_id": null,
 "error_file_id": "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29u...", "request_counts": {"completed": 0, "failed": 3, "total": 3}}

$ curl -s "http://localhost:25200/spend/logs?api_key=$KEY_A" -H "Authorization: Bearer sk-qa-master-before"
[{"call_type": "acreate_batch", "spend": 0.0, "startTime": "2026-08-16T20:47:04.432000Z"},
 {"call_type": "acreate_file",  "spend": 0.0, "startTime": "2026-08-16T20:46:55.295000Z"}]

3 polls made, zero aretrieve_batch rows. Plain-provider flow (POST /v1/files?provider=openai, raw ids) behaves the same:

$ curl -s "http://localhost:25200/v1/batches/batch_6a82210c6cb88190804d7ce1fcf18390?provider=openai" -H "Authorization: Bearer $KEY_B"
{"id": "batch_6a82210c6cb88190804d7ce1fcf18390", "status": "completed", "output_file_id": null,
 "error_file_id": "file-GvMrtsJxCK3qgtrSgtGJB9", "request_counts": {"completed": 0, "failed": 3, "total": 3}}

$ curl -s "http://localhost:25200/spend/logs?api_key=$KEY_B" -H "Authorization: Bearer sk-qa-master-before"
[{"call_type": "acreate_batch", "spend": 0.0, "startTime": "2026-08-16T20:43:55.945000Z"},
 {"call_type": "acreate_file",  "spend": 0.0, "startTime": "2026-08-16T20:43:47.250000Z"},
 {"call_type": "",              "spend": 0.0, "startTime": "2026-08-16T00:34:28.796000Z"}]

(the empty-call_type row is from an earlier rejected 403 poll, hours before this run; see observations below)

Every one of the 5 polls across both flows produced a LoggingWorker error: ... ValueError: Output file id is None cannot retrieve file content traceback in the proxy console, 1:1 with the polls, at any batch status, not just completed

After, PR head f91e698adbd00b88c3114a276b8a3d0095302ffc (proxy port 52847): every poll is accounted at $0.00

Same commands, same flows. Managed flow, 6 polls to the same terminal shape:

$ curl -s http://localhost:52847/v1/batches/<unified batch id> -H "Authorization: Bearer $KEY_A"   # final poll
{"id": "bGl0ZWxsbV9wcm94eTttb2RlbF9pZDpkMGI3...", "status": "completed", "output_file_id": null,
 "error_file_id": "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29u...", "request_counts": {"completed": 0, "failed": 3, "total": 3}}

$ curl -s "http://localhost:52847/spend/logs?api_key=$KEY_A" -H "Authorization: Bearer sk-qa-master-after"
[{"call_type": "aretrieve_batch", "spend": 0.0, "startTime": "2026-08-16T20:49:25.338000Z"},
 {"call_type": "aretrieve_batch", "spend": 0.0, "startTime": "2026-08-16T20:48:24.724000Z"},
 {"call_type": "aretrieve_batch", "spend": 0.0, "startTime": "2026-08-16T20:47:54.473000Z"},
 {"call_type": "aretrieve_batch", "spend": 0.0, "startTime": "2026-08-16T20:46:53.997000Z"},
 {"call_type": "acreate_batch",   "spend": 0.0, "startTime": "2026-08-16T20:46:43.203000Z"},
 {"call_type": "acreate_file",    "spend": 0.0, "startTime": "2026-08-16T20:46:36.830000Z"}]

Plain-provider flow, 5 polls:

$ curl -s "http://localhost:52847/v1/batches/batch_6a8221ca2c588190b4375348ceb759dd?provider=openai" -H "Authorization: Bearer $KEY_B"
{"id": "batch_6a8221ca2c588190b4375348ceb759dd", "status": "completed", "output_file_id": null,
 "error_file_id": "file-3rGHqXkyX5g5CQnmfN7sas", "request_counts": {"completed": 0, "failed": 3, "total": 3}}

$ curl -s "http://localhost:52847/spend/logs?api_key=$KEY_B" -H "Authorization: Bearer sk-qa-master-after"
[{"call_type": "aretrieve_batch", "spend": 0.0, "startTime": "2026-08-16T20:49:14.579000Z"},
 {"call_type": "aretrieve_batch", "spend": 0.0, "startTime": "2026-08-16T20:48:14.053000Z"},
 {"call_type": "aretrieve_batch", "spend": 0.0, "startTime": "2026-08-16T20:47:13.494000Z"},
 {"call_type": "acreate_batch",   "spend": 0.0, "startTime": "2026-08-16T20:47:05.839000Z"},
 {"call_type": "acreate_file",    "spend": 0.0, "startTime": "2026-08-16T20:46:58.840000Z"}]

Zero "Output file id is None" occurrences and zero tracebacks in the proxy console across all 11 after-leg polls. Row counts read 4 of 6 and 3 of 5 because the spend log keys retrieve rows on a hash of the response, so consecutive polls observing an identical batch state dedup to one row; that behavior predates this PR, and the row for the completed-observing poll, the one the bug erased, is present in both flows

Observed during QA, all pre-existing and untouched by this PR unless noted:

  • Crash fired on every poll status, this PR fixes all (broader than issue narrative)
  • Virtual keys without an attached user 403 on managed batches
  • Spend rows dedup identical consecutive poll states by response hash
  • All-failed OpenAI batches finish "completed", never "failed"
  • Plain-provider file upload requires a files_settings config block
  • Rejected 403 polls still write empty-call_type spend rows

Type

🐛 Bug Fix

@CLAassistant

CLAassistant commented Jul 21, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents all-error completed batches from crashing spend logging.

  • Returns zero cost, zero token usage, and no models when a completed batch has no output file.
  • Adds a regression test ensuring the output-file fetch is skipped for that state.

Confidence Score: 4/5

The PR does not yet appear safe to merge because completed Bedrock batches with missing output metadata can be logged as having zero billable usage.

The current guard equates every absent output file with an all-error batch, while the Bedrock normalization path can leave output_file_id unset when output-location metadata is unavailable; successful work on that path is therefore silently recorded as zero.

Files Needing Attention: litellm/batches/batch_utils.py

Important Files Changed

Filename Overview
litellm/batches/batch_utils.py Adds an early zero-accounting return for missing output files, but the condition also covers supported provider states that do not establish an all-error batch.
tests/test_litellm/batches/test_batch_utils.py Adds focused regression coverage for the intended all-error case, including verification that no output fetch occurs.

Reviews (2): Last reviewed commit: "fix(batch): avoid reading a nonexistent ..." | Re-trigger Greptile

Comment thread litellm/batches/batch_utils.py
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor

Per repo convention PRs should target litellm_internal_staging rather than litellm_oss_daily_2026_07_20. Please retarget the base branch so this lands on the default staging line.

@MUSE-CODE-SPACE
MUSE-CODE-SPACE changed the base branch from litellm_oss_daily_2026_07_20 to litellm_internal_staging August 13, 2026 18:25
@MUSE-CODE-SPACE
MUSE-CODE-SPACE force-pushed the fix/batch-logging-null-output-file branch from 0c1e22e to 492730f Compare August 13, 2026 18:31
@MUSE-CODE-SPACE

MUSE-CODE-SPACE commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

I rebased this PR onto litellm_internal_staging and updated the target branch accordingly. Since _handle_completed_batch now reads completed batch data through _fetch_batch_output_file_content, the nil-check has been moved to the call site before that helper is invoked, and the regression test has been updated to stub the helper instead of the previous implementation.

The functional change is otherwise identical to the original proposal: 2 modified files with a net addition of 32 lines. I also reran the entire tests/test_litellm/batches/test_batch_utils.py suite, and all 85 tests complete successfully on my machine.

@codspeed-hq

codspeed-hq Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing MUSE-CODE-SPACE:fix/batch-logging-null-output-file (f91e698) with litellm_internal_staging (add095b)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (c9b543d) during the generation of this report, so add095b was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

… batches

Completed batches that contain only failed requests do not generate an
output file, leaving output_file_id unset while the failures are recorded
through error_file_id instead.

The completion handler attempted to read the output payload regardless of
whether an output file actually existed. During retrieve polling this caused
the logging pipeline to fail with "Output file id is None cannot retrieve
file content", preventing normal completion bookkeeping from running.

Skip output retrieval when no output file is available and return an empty
batch summary (zero usage, zero cost, no model entries). The lower-level
file retrieval helper still reports an error if it is called directly with
an invalid or missing file identifier.

Closes BerriAI#33987
@MUSE-CODE-SPACE
MUSE-CODE-SPACE force-pushed the fix/batch-logging-null-output-file branch from 492730f to f91e698 Compare August 13, 2026 20:20
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor

cursor Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

PR Summary

Cursor Bugbot is generating a summary for commit f91e698. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 f91e698. Configure here.

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for the contribution!

@mateo-berri
mateo-berri enabled auto-merge August 16, 2026 21:00
@mateo-berri
mateo-berri merged commit 2bc87ec into BerriAI:litellm_internal_staging Aug 17, 2026
81 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Completed all-error batch crashes logging when output_file_id is null

3 participants