fix(batches): an unreadable error file no longer drops the output line events

Costing counts an error file it could not fetch as no failed lines; line logging
refetched it and the raise aborted the whole fan-out, so the successful lines were
never emitted. The error file now reads as empty with a warning.
This commit is contained in:
yucheng-berri 2026-10-08 16:22:04 -07:00
parent 317764c3a1
commit 85fc4498cc
2 changed files with 46 additions and 1 deletions

View file

@ -419,6 +419,22 @@ async def _fetch_managed_file_or_empty(
)
async def _fetch_error_file_or_empty(
batch: LiteLLMBatch,
custom_llm_provider: _BatchLineProvider,
fetch_params: dict[str, object] | None, # mutable-ok: batch_utils file fetch takes the shared litellm_params dict
) -> bytes:
try:
return await _fetch_managed_file_or_empty(batch.error_file_id, custom_llm_provider, fetch_params)
except Exception as e: # noqa: BLE001 # costing counts an unreadable error file as no failed lines, so the output lines still go out
verbose_logger.warning(
"batch line-item callbacks could not fetch the error file, emitting the output lines only. batch_id=%s error=%s",
batch.id,
e,
)
return b""
async def log_batch_line_items(
batch: LiteLLMBatch,
custom_llm_provider: str,
@ -488,7 +504,7 @@ async def log_batch_line_items(
error_content: Final = (
result_files.error
if result_files is not None and result_files.error is not None
else await _fetch_managed_file_or_empty(batch.error_file_id, line_provider, fetch_params)
else await _fetch_error_file_or_empty(batch, line_provider, fetch_params)
)
for content in (output_content, error_content):
for entry in _output_entries(content):

View file

@ -1241,6 +1241,35 @@ async def test_supplied_result_files_skip_the_output_and_error_fetch(recorder):
assert file_mock.await_args.kwargs["file_id"] == "input-file-1"
def _error_file_unreadable(file_id: str, **_kwargs):
if file_id == "error-file-1":
raise ValueError("error file gone")
return SimpleNamespace(content=_FILE_BYTES[file_id])
@pytest.mark.asyncio
async def test_an_unreadable_error_file_still_emits_the_output_lines(recorder):
file_mock: Final = AsyncMock(side_effect=_error_file_unreadable)
with patch(
"litellm.files.main.afile_content", file_mock
): # test-quality-ok: afile_content is the provider boundary; no injection seam for managed file fetch
emitted: Final = await log_batch_line_items(
batch=_batch(),
custom_llm_provider="openai",
parent=_parent_logging(),
model_name="gpt-4o",
litellm_params=None,
model_info=None,
result_files=BatchResultFiles(output=OUTPUT_JSONL, error=None),
claim_cache=DualCache(),
)
assert emitted == 1
assert len(recorder.success_events) == 1
assert len(recorder.failure_events) == 0
assert sorted(call.kwargs["file_id"] for call in file_mock.await_args_list) == ["error-file-1", "input-file-1"]
@pytest.mark.asyncio
async def test_empty_result_files_fall_back_to_fetching_everything(recorder):
file_mock: Final = AsyncMock(side_effect=_file_content)