From edb8d8df36655fac842f081e88cdc6561076c237 Mon Sep 17 00:00:00 2001 From: "Luca (Sanity Horizon)" Date: Tue, 13 Oct 2026 22:35:37 +0000 Subject: [PATCH] fix(google_genai): map functionResponse parts in model turns to tool messages Google's Antigravity CLI sends tool results as role "model" turns containing functionResponse parts. The generateContent->completion adapter only mapped functionResponse inside user turns, so these were dropped, the request ended on an assistant turn, and Gemini rejected it with "Requests ending with a model turn are not supported." --- .../google_genai/adapters/transformation.py | 18 +++++ .../google_genai/test_google_genai_adapter.py | 72 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/litellm/google_genai/adapters/transformation.py b/litellm/google_genai/adapters/transformation.py index 10340eb7acf..98fde30b2c9 100644 --- a/litellm/google_genai/adapters/transformation.py +++ b/litellm/google_genai/adapters/transformation.py @@ -547,6 +547,9 @@ class GoogleGenAIAdapter: # Handle assistant messages with potential function calls combined_text = "" tool_calls: list[ChatCompletionAssistantToolCall] = [] + # Some clients (e.g. Google Antigravity CLI) send functionResponse + # parts in a "model" turn rather than a "user" turn. + model_tool_messages: list[ChatCompletionToolMessage] = [] for part in parts: if isinstance(part, dict): @@ -564,9 +567,23 @@ class GoogleGenAIAdapter: ), ) tool_calls.append(tool_call) + elif "functionResponse" in part: + func_response = part["functionResponse"] + model_tool_messages.append( + ChatCompletionToolMessage( + role="tool", + tool_call_id=f"call_{func_response.get('name', 'unknown')}", + content=json.dumps(func_response.get("response", {})), + ) + ) elif isinstance(part, str): combined_text += part + # A model turn holding only function responses becomes tool messages + if model_tool_messages and not combined_text and not tool_calls: + messages.extend(model_tool_messages) + continue + # Create assistant message if tool_calls: assistant_message = ChatCompletionAssistantMessage( @@ -581,6 +598,7 @@ class GoogleGenAIAdapter: ) messages.append(assistant_message) + messages.extend(model_tool_messages) return messages diff --git a/tests/unit/google_genai/test_google_genai_adapter.py b/tests/unit/google_genai/test_google_genai_adapter.py index 5e521506699..b5f4de1160e 100644 --- a/tests/unit/google_genai/test_google_genai_adapter.py +++ b/tests/unit/google_genai/test_google_genai_adapter.py @@ -1641,3 +1641,75 @@ async def test_generate_content_sends_response_schema_and_tool_parameters_to_the } assert response["candidates"][0]["content"]["parts"] == [{"text": '{"park_name": "EPCOT"}'}] assert "text" not in response + + +def test_function_response_in_model_turn_becomes_tool_message(): + """Function responses sent in a "model" turn must not be dropped. + + Google's Antigravity CLI sends tool results as + {"role": "model", "parts": [{"functionResponse": ...}]}. Dropping them left + the request ending on an assistant turn, which Gemini rejects with + "Requests ending with a model turn are not supported." + """ + from litellm.google_genai.adapters.transformation import GoogleGenAIAdapter + + adapter = GoogleGenAIAdapter() + contents = [ + {"role": "user", "parts": [{"text": "Run the tests"}]}, + { + "role": "model", + "parts": [ + { + "functionCall": { + "name": "run_command", + "args": {"command": "python3 test_calc.py"}, + } + } + ], + }, + { + "role": "model", + "parts": [ + { + "functionResponse": { + "name": "run_command", + "response": {"output": "AssertionError"}, + } + } + ], + }, + ] + + completion_request = adapter.translate_generate_content_to_completion( + model="gemini-pro", contents=contents + ) + messages = completion_request["messages"] + + assert [m["role"] for m in messages] == ["user", "assistant", "tool"] + assert messages[1]["tool_calls"][0]["id"] == "call_run_command" + assert messages[2]["tool_call_id"] == "call_run_command" + assert json.loads(messages[2]["content"]) == {"output": "AssertionError"} + + +def test_model_turn_with_call_and_response_keeps_order(): + """A model turn holding both a call and its response yields assistant then tool.""" + from litellm.google_genai.adapters.transformation import GoogleGenAIAdapter + + adapter = GoogleGenAIAdapter() + contents = [ + {"role": "user", "parts": [{"text": "Weather?"}]}, + { + "role": "model", + "parts": [ + {"functionCall": {"name": "get_weather", "args": {"city": "Oslo"}}}, + {"functionResponse": {"name": "get_weather", "response": {"temp": "4C"}}}, + ], + }, + ] + + messages = adapter.translate_generate_content_to_completion( + model="gemini-pro", contents=contents + )["messages"] + + assert [m["role"] for m in messages] == ["user", "assistant", "tool"] + assert messages[2]["tool_call_id"] == messages[1]["tool_calls"][0]["id"]