mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-25 05:54:53 +00:00
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."
This commit is contained in:
parent
7bd108139f
commit
edb8d8df36
2 changed files with 90 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue