Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions litellm/llms/vertex_ai/gemini/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
Loading