From f91e698adbd00b88c3114a276b8a3d0095302ffc Mon Sep 17 00:00:00 2001 From: MUSE Date: Tue, 21 Jul 2026 11:50:53 +0900 Subject: [PATCH] fix(batch): avoid reading a nonexistent output artifact for completed 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 #33987 --- litellm/batches/batch_utils.py | 11 ++++++++++ .../test_litellm/batches/test_batch_utils.py | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/litellm/batches/batch_utils.py b/litellm/batches/batch_utils.py index e73b887ae0a..cfd37864eac 100644 --- a/litellm/batches/batch_utils.py +++ b/litellm/batches/batch_utils.py @@ -58,6 +58,17 @@ async def _handle_completed_batch( model_name: Optional model name litellm_params: Optional litellm parameters containing credentials (api_key, api_base, etc.) """ + # A completed batch whose request lines all failed has no output file - the + # results are written to a separate error_file_id and output_file_id is None. + # There is nothing to price or measure, so report an empty result set instead + # of calling _fetch_batch_output_file_content, which raises on a missing + # output file. Without this guard the logging worker crashes on every + # aretrieve_batch poll and the completed batch's zero-cost accounting is lost. + # The generic retrieval helper keeps raising for callers that explicitly ask + # for a missing output file. + if batch.output_file_id is None: + return 0.0, Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0), [] + file_content = await _fetch_batch_output_file_content(batch, custom_llm_provider, litellm_params=litellm_params) if ( diff --git a/tests/test_litellm/batches/test_batch_utils.py b/tests/test_litellm/batches/test_batch_utils.py index 523b512e4cf..dbf1102b31d 100644 --- a/tests/test_litellm/batches/test_batch_utils.py +++ b/tests/test_litellm/batches/test_batch_utils.py @@ -976,6 +976,27 @@ async def test_handle_completed_batch_orchestration(monkeypatch): assert models == ["gpt-4o"] +@pytest.mark.asyncio +async def test_handle_completed_batch_no_output_file_is_zero(monkeypatch): + """ + Regression: an all-error batch completes with output_file_id=None (results go + to a separate error_file_id). _handle_completed_batch must report an empty + result set - zero cost, zero usage, no models - instead of letting the file + fetch raise "Output file id is None" on every aretrieve_batch logging poll. + """ + # The output-file fetch must not even be attempted when there is no output file. + async def _must_not_fetch(*args, **kwargs): + pytest.fail("_fetch_batch_output_file_content should not be called") + + monkeypatch.setattr(bu, "_fetch_batch_output_file_content", _must_not_fetch) + + cost, usage, models = await bu._handle_completed_batch(_batch(None), custom_llm_provider="openai") + + assert cost == 0.0 + assert (usage.prompt_tokens, usage.completion_tokens, usage.total_tokens) == (0, 0, 0) + assert models == [] + + @pytest.mark.asyncio async def test_handle_completed_batch_vertex_disable_transform_path(monkeypatch): raw_rows = [{"response": {"usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 2}}}]