mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-30 08:22:54 +00:00
fix(gemini): add missing role="user" to function response content blocks
Gemini API only accepts "user" and "model" roles. Function responses were being sent without a role field, causing 400 errors on multi-turn tool calling conversations. Fixes #22003 Fixes #20690
This commit is contained in:
@@ -500,7 +500,7 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
|
||||
messages[msg_i]["role"] not in tool_call_message_roles
|
||||
):
|
||||
if len(tool_call_responses) > 0:
|
||||
contents.append(ContentType(parts=tool_call_responses))
|
||||
contents.append(ContentType(role="user", parts=tool_call_responses))
|
||||
tool_call_responses = []
|
||||
|
||||
if msg_i == init_msg_i: # prevent infinite loops
|
||||
@@ -510,7 +510,7 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
|
||||
)
|
||||
)
|
||||
if len(tool_call_responses) > 0:
|
||||
contents.append(ContentType(parts=tool_call_responses))
|
||||
contents.append(ContentType(role="user", parts=tool_call_responses))
|
||||
|
||||
if len(contents) == 0:
|
||||
verbose_logger.warning(
|
||||
|
||||
@@ -1323,4 +1323,127 @@ def test_assistant_message_with_images_in_conversation_history():
|
||||
# Verify assistant message has image in history
|
||||
inline_data_parts = [part for part in contents[1]["parts"] if "inline_data" in part]
|
||||
assert len(inline_data_parts) == 1
|
||||
assert inline_data_parts[0]["inline_data"]["mime_type"] == "image/png"
|
||||
assert inline_data_parts[0]["inline_data"]["mime_type"] == "image/png"
|
||||
|
||||
|
||||
def test_function_response_has_user_role():
|
||||
"""
|
||||
Test that function response ContentType blocks include role="user".
|
||||
|
||||
Gemini API only accepts two roles: "user" and "model". Function responses
|
||||
must be sent with role="user". Previously, LiteLLM omitted the role field
|
||||
entirely, causing 400 errors from the Gemini API.
|
||||
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/22003
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/20690
|
||||
"""
|
||||
messages = [
|
||||
{"role": "user", "content": "What is the weather in Berlin?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"city": "Berlin"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"content": '{"temperature": "15°C", "condition": "Cloudy"}',
|
||||
},
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages=messages)
|
||||
|
||||
# Expect: user -> model (functionCall) -> user (functionResponse)
|
||||
assert len(contents) == 3
|
||||
|
||||
assert contents[0]["role"] == "user"
|
||||
assert contents[1]["role"] == "model"
|
||||
assert "function_call" in contents[1]["parts"][0]
|
||||
|
||||
# The critical assertion: function response must have role="user"
|
||||
assert contents[2]["role"] == "user"
|
||||
assert "function_response" in contents[2]["parts"][0]
|
||||
|
||||
|
||||
def test_multi_turn_function_calling_roles():
|
||||
"""
|
||||
Test a full multi-turn function calling conversation produces correct roles.
|
||||
|
||||
Simulates: user asks → model calls tool → tool responds → model answers → user asks again.
|
||||
Every content block must have an explicit role of "user" or "model".
|
||||
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/22003
|
||||
"""
|
||||
messages = [
|
||||
{"role": "user", "content": "What is the weather in Berlin?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_001",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"city": "Berlin"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_001",
|
||||
"content": '{"temperature": "15°C"}',
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The weather in Berlin is 15°C.",
|
||||
},
|
||||
{"role": "user", "content": "And in Paris?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_002",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"city": "Paris"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_002",
|
||||
"content": '{"temperature": "18°C"}',
|
||||
},
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages=messages)
|
||||
|
||||
# Every content block must have a valid role
|
||||
for i, content in enumerate(contents):
|
||||
assert "role" in content, f"Content block {i} missing 'role' field"
|
||||
assert content["role"] in (
|
||||
"user",
|
||||
"model",
|
||||
), f"Content block {i} has invalid role: {content.get('role')}"
|
||||
|
||||
# Verify the function response blocks specifically have role="user"
|
||||
for i, content in enumerate(contents):
|
||||
for part in content["parts"]:
|
||||
if "function_response" in part:
|
||||
assert (
|
||||
content["role"] == "user"
|
||||
), f"Content block {i} with function_response has role='{content['role']}', expected 'user'"
|
||||
Reference in New Issue
Block a user