From accca32abaf95f3c10f655c4cf3aad8055be2059 Mon Sep 17 00:00:00 2001 From: Bharadwaj Pendyala Date: Sat, 15 Aug 2026 09:09:04 -0500 Subject: [PATCH] fix(vertex_ai): send systemInstruction, not the snake_case alias, on generateContent Gemini and Vertex generateContent bodies used the protobuf field name system_instruction while every sibling key in the same body is camelCase. Strict Gemini-compatible endpoints reject the alias with a protobuf oneof error. litellm's own google_genai path already sends systemInstruction to the same two endpoints. Fixes #37028 --- .../llms/vertex_ai/gemini/transformation.py | 4 +-- litellm/types/llms/vertex_ai.py | 2 +- .../test_amazing_vertex_completion.py | 2 +- .../test_vertex_ai_gemini_transformation.py | 25 ++++++++++++++++--- .../llms/vertex_ai/test_vertex.py | 4 +-- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index 11c026010ee..6d289ad6eba 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -1215,12 +1215,12 @@ def _transform_request_body( generation_config["mediaResolution"] = media_resolution_value["level"] data: Final = RequestBody(contents=content) - # Vertex rejects system_instruction/tools/toolConfig alongside cachedContent. + # Vertex rejects systemInstruction/tools/toolConfig alongside cachedContent. # Treat dropping these fields as a request mutation guarded by modify_params. can_send_cache_incompatible_fields: Final = cached_content is None or litellm.modify_params is False if can_send_cache_incompatible_fields: if system_instructions is not None: - data["system_instruction"] = system_instructions + data["systemInstruction"] = system_instructions if tools is not None: data["tools"] = tools if tool_choice is not None: diff --git a/litellm/types/llms/vertex_ai.py b/litellm/types/llms/vertex_ai.py index 3b95b786631..64b4d4ce5c5 100644 --- a/litellm/types/llms/vertex_ai.py +++ b/litellm/types/llms/vertex_ai.py @@ -338,7 +338,7 @@ class CachedContent(TypedDict, total=False): class RequestBody(TypedDict, total=False): contents: Required[list[ContentType]] - system_instruction: SystemInstructions + systemInstruction: SystemInstructions tools: Tools toolConfig: ToolConfig safetySettings: list[SafetSettingsConfig] diff --git a/tests/local_testing/test_amazing_vertex_completion.py b/tests/local_testing/test_amazing_vertex_completion.py index 9bd64719102..c90d29d2b90 100644 --- a/tests/local_testing/test_amazing_vertex_completion.py +++ b/tests/local_testing/test_amazing_vertex_completion.py @@ -2912,7 +2912,7 @@ def test_gemini_function_call_parameter_in_messages(): ], }, ], - "system_instruction": { + "systemInstruction": { "parts": [{"text": "Use search for most queries."}] }, "tools": [ diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_ai_gemini_transformation.py b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_ai_gemini_transformation.py index 8c1de12e7d9..70b613ab380 100644 --- a/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_ai_gemini_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_vertex_ai_gemini_transformation.py @@ -123,7 +123,7 @@ def test_cached_content_respects_modify_params_for_cache_incompatible_fields(): cached_content=cache_name, ) assert result.get("cachedContent") == cache_name - assert "system_instruction" in result + assert "systemInstruction" in result assert "tools" in result assert "toolConfig" in result assert "contents" in result @@ -139,7 +139,7 @@ def test_cached_content_respects_modify_params_for_cache_incompatible_fields(): cached_content=cache_name, ) assert result_modify_true.get("cachedContent") == cache_name - assert "system_instruction" not in result_modify_true + assert "systemInstruction" not in result_modify_true assert "tools" not in result_modify_true assert "toolConfig" not in result_modify_true assert "contents" in result_modify_true @@ -153,13 +153,32 @@ def test_cached_content_respects_modify_params_for_cache_incompatible_fields(): litellm_params={}, cached_content=None, ) - assert "system_instruction" in result_no_cache + assert "systemInstruction" in result_no_cache assert "tools" in result_no_cache assert "toolConfig" in result_no_cache finally: litellm.modify_params = original_modify_params +@pytest.mark.parametrize("custom_llm_provider", ["gemini", "vertex_ai"]) +def test_system_instruction_uses_canonical_rest_key(custom_llm_provider): + """Regression for #37028: strict generateContent endpoints reject the snake_case alias.""" + result = _transform_request_body( + messages=[ + {"role": "system", "content": "You are a concise assistant."}, + {"role": "user", "content": "Reply with exactly: ok"}, + ], + model="gemini-2.5-pro", + optional_params={}, + custom_llm_provider=custom_llm_provider, + litellm_params={}, + cached_content=None, + ) + + assert "system_instruction" not in result + assert result["systemInstruction"]["parts"][0]["text"] == "You are a concise assistant." + + # Tests for issue #14556: Labels field provider-aware filtering def test_google_genai_excludes_labels(): """Test that Google GenAI/AI Studio endpoints exclude labels when custom_llm_provider='gemini'""" diff --git a/tests/test_litellm/llms/vertex_ai/test_vertex.py b/tests/test_litellm/llms/vertex_ai/test_vertex.py index ec73e5e42be..27cdc79971f 100644 --- a/tests/test_litellm/llms/vertex_ai/test_vertex.py +++ b/tests/test_litellm/llms/vertex_ai/test_vertex.py @@ -1590,5 +1590,5 @@ def test_system_prompt_only_adds_blank_user_message(): ######################################################### # system message was passed in ######################################################### - assert len(data["system_instruction"]) == 1 - assert data["system_instruction"]["parts"][0]["text"] == SYSTEM_INSTRUCTION + assert len(data["systemInstruction"]) == 1 + assert data["systemInstruction"]["parts"][0]["text"] == SYSTEM_INSTRUCTION