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}}}]