From 946dfb63c7896c317b99cfc4760877460f7d2cff Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 2 May 2026 08:15:42 +0000 Subject: [PATCH 1/2] fix(vertex-ai): set response=null on batch error entries per OpenAI spec The Vertex batch output transformer was emitting both a populated 'response' and 'error' for failed batch entries. The OpenAI Batch output spec defines them as mutually exclusive: on error 'response' MUST be null. This broke any consumer using 'result["response"] is None' to detect failures. --- .../llms/vertex_ai/files/transformation.py | 32 +++---------------- .../test_vertex_ai_files_transformation.py | 6 ++-- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/litellm/llms/vertex_ai/files/transformation.py b/litellm/llms/vertex_ai/files/transformation.py index 7df16723870..01b2125d030 100644 --- a/litellm/llms/vertex_ai/files/transformation.py +++ b/litellm/llms/vertex_ai/files/transformation.py @@ -731,25 +731,13 @@ class VertexAIFilesConfig(VertexBase, BaseFilesConfig): has_error = bool(status) if has_error: - # Return error response in OpenAI format return { "id": f"batch_req_{uuid.uuid4()}", "custom_id": custom_id, - "response": { - "status_code": 400, - "request_id": "", - "body": { - "error": { - "message": status, - "type": "vertex_ai_error", - "code": "vertex_ai_error", - } - }, - }, + "response": None, "error": { - "message": status, - "type": "vertex_ai_error", "code": "vertex_ai_error", + "message": status, }, } @@ -789,25 +777,13 @@ class VertexAIFilesConfig(VertexBase, BaseFilesConfig): } except Exception as e: - # If transformation fails, return error return { "id": f"batch_req_{uuid.uuid4()}", "custom_id": custom_id, - "response": { - "status_code": 500, - "request_id": "", - "body": { - "error": { - "message": f"Failed to transform response: {str(e)}", - "type": "transformation_error", - "code": "transformation_error", - } - }, - }, + "response": None, "error": { - "message": f"Failed to transform response: {str(e)}", - "type": "transformation_error", "code": "transformation_error", + "message": f"Failed to transform response: {str(e)}", }, } diff --git a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py index 4adad53a98f..58d6d2902db 100644 --- a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py @@ -431,10 +431,12 @@ class TestVertexBatchOutputTransformation: ) result = json.loads(transformed_content.decode("utf-8")) - # Verify error format - assert result["response"]["status_code"] == 400 + # Per OpenAI Batch output spec, error entries set response to null + # and populate the top-level error object. + assert result["response"] is None assert result["error"] is not None assert "Invalid request" in result["error"]["message"] + assert result["error"]["code"] == "vertex_ai_error" assert result["custom_id"] == "request-error" def test_transform_vertex_batch_output_legacy_labels_only_sanitized(self, config): From cf9c2f0200ea9b1c76e5a11e31cb298031976697 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 2 May 2026 08:33:05 +0000 Subject: [PATCH 2/2] test(vertex-ai): cover transformation_error path emits response=null --- .../test_vertex_ai_files_transformation.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py index 58d6d2902db..b8482ce6aae 100644 --- a/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/files/test_vertex_ai_files_transformation.py @@ -439,6 +439,53 @@ class TestVertexBatchOutputTransformation: assert result["error"]["code"] == "vertex_ai_error" assert result["custom_id"] == "request-error" + def test_transform_exception_path_sets_response_null(self, config): + """ + The except-Exception branch in _transform_single_vertex_batch_output_to_openai + must also emit response=null per the OpenAI Batch output spec. The outer + _try_transform path swallows exceptions and falls back to original content, + so this test invokes the single-line transformer directly with a vertex_gemini_config + stub that raises during transformation. + """ + from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import ( + VertexGeminiConfig, + ) + + vertex_output = { + "status": "", + "processed_time": "2024-11-01T18:13:16.826+00:00", + "request": { + "contents": [{"role": "user", "parts": [{"text": "Hello world!"}]}], + "labels": {"litellm_custom_id": "request-boom"}, + }, + "response": {"modelVersion": "gemini-2.0-flash-001@default"}, + } + + class _RaisingGeminiConfig(VertexGeminiConfig): + def _transform_google_generate_content_to_openai_model_response( + self, *args, **kwargs + ): + raise ValueError("simulated transform failure") + + mock_response = httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + request=httpx.Request(method="POST", url="https://example.com"), + ) + + result = config._transform_single_vertex_batch_output_to_openai( + vertex_output=vertex_output, + vertex_gemini_config=_RaisingGeminiConfig(), + logging_obj=MagicMock(), + mock_httpx_response=mock_response, + ) + + assert result["response"] is None + assert result["error"] is not None + assert result["error"]["code"] == "transformation_error" + assert "simulated transform failure" in result["error"]["message"] + assert result["custom_id"] == "request-boom" + def test_transform_vertex_batch_output_legacy_labels_only_sanitized(self, config): """Older LiteLLM batches only stored litellm_custom_id (sanitized); read path still works.""" vertex_output = {