From 4cfb0f8d89093ca2ffd1c9c7184eaa4cba417ee8 Mon Sep 17 00:00:00 2001 From: htourinho-clgx Date: Tue, 21 Jul 2026 13:24:19 +0200 Subject: [PATCH 1/2] fix: handle explicit outputInfo: null in Vertex AI batch response Vertex AI can return HTTP 200 for a create_batch/get_batch call with an explicit "outputInfo": null body (the output directory is assigned asynchronously and may not be populated yet at response time). _get_output_file_id_from_vertex_ai_batch_response did: response.get("outputInfo", OutputInfo()).get("gcsOutputDirectory", "") dict.get(key, default) only substitutes default when the key is absent, not when it is present but explicitly None, so this crashed with: AttributeError: 'NoneType' object has no attribute 'get' surfaced to callers as an opaque openai.InternalServerError 500 from litellm.create_batch()/retrieve_batch() for any Vertex AI batch job, regardless of whether the job ultimately succeeds. Fixed by guarding with `response.get("outputInfo") or OutputInfo()`, matching the existing null-safe pattern already used by the sibling _get_input_file_id_from_vertex_ai_batch_response for inputConfig. The existing outputConfig fallback branch (a few lines below) already handles this case correctly once it's reachable - it just never was. Added 2 regression tests covering outputInfo: null with and without an outputConfig fallback available. --- .../llms/vertex_ai/batches/transformation.py | 3 ++- .../vertex_ai/batches/test_transformation.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/litellm/llms/vertex_ai/batches/transformation.py b/litellm/llms/vertex_ai/batches/transformation.py index 6bbe8f757012..df903ba7ef0c 100644 --- a/litellm/llms/vertex_ai/batches/transformation.py +++ b/litellm/llms/vertex_ai/batches/transformation.py @@ -123,7 +123,8 @@ def _get_output_file_id_from_vertex_ai_batch_response(cls, response: VertexBatch Gets the output file id from the Vertex AI Batch response """ - output_file_id: str = response.get("outputInfo", OutputInfo()).get("gcsOutputDirectory", "") + output_info = response.get("outputInfo") or OutputInfo() + output_file_id: str = output_info.get("gcsOutputDirectory", "") if output_file_id: output_file_id = output_file_id.rstrip("/") + "/predictions.jsonl" if output_file_id and output_file_id != "/predictions.jsonl": diff --git a/tests/test_litellm/llms/vertex_ai/batches/test_transformation.py b/tests/test_litellm/llms/vertex_ai/batches/test_transformation.py index 71da1d398767..29f23b7fe998 100644 --- a/tests/test_litellm/llms/vertex_ai/batches/test_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/batches/test_transformation.py @@ -226,6 +226,23 @@ def test_get_output_file_id_empty_output_info_falls_through_to_output_config(): assert T._get_output_file_id_from_vertex_ai_batch_response(resp) == "gs://b/cfg/predictions.jsonl" +def test_get_output_file_id_output_info_explicit_none_falls_through_to_output_config(): + # Vertex AI can return "outputInfo": null in a 200 response (e.g. before it has + # asynchronously assigned the output directory). dict.get(key, default) does NOT + # substitute default when the key is present but explicitly None, so this must be + # handled explicitly instead of crashing with AttributeError: 'NoneType' object + # has no attribute 'get'. + resp = { + "outputInfo": None, + "outputConfig": {"gcsDestination": {"outputUriPrefix": "gs://b/cfg"}}, + } + assert T._get_output_file_id_from_vertex_ai_batch_response(resp) == "gs://b/cfg/predictions.jsonl" + + +def test_get_output_file_id_output_info_explicit_none_and_no_output_config(): + assert T._get_output_file_id_from_vertex_ai_batch_response({"outputInfo": None}) == "" + + def test_get_output_file_id_no_output_info_and_no_output_config(): assert T._get_output_file_id_from_vertex_ai_batch_response({}) == "" From c5d9dfdeae39aed87093664402678dee547a553c Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Thu, 23 Jul 2026 23:51:54 -0700 Subject: [PATCH 2/2] test: drop explanatory comment from regression test --- .../llms/vertex_ai/batches/test_transformation.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_litellm/llms/vertex_ai/batches/test_transformation.py b/tests/test_litellm/llms/vertex_ai/batches/test_transformation.py index 29f23b7fe998..1b37ade6b303 100644 --- a/tests/test_litellm/llms/vertex_ai/batches/test_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/batches/test_transformation.py @@ -227,11 +227,6 @@ def test_get_output_file_id_empty_output_info_falls_through_to_output_config(): def test_get_output_file_id_output_info_explicit_none_falls_through_to_output_config(): - # Vertex AI can return "outputInfo": null in a 200 response (e.g. before it has - # asynchronously assigned the output directory). dict.get(key, default) does NOT - # substitute default when the key is present but explicitly None, so this must be - # handled explicitly instead of crashing with AttributeError: 'NoneType' object - # has no attribute 'get'. resp = { "outputInfo": None, "outputConfig": {"gcsDestination": {"outputUriPrefix": "gs://b/cfg"}},