fix: Handle mixed tool and non-tool parts in LiteLLM content conversion

When converting a `types.Content` to LiteLLM messages, if the content contains both `function_response` parts and other types of parts (e.g., text, image), the function now generates a list of LiteLLM messages

Close #4091

Co-authored-by: George Weale <gweale@google.com>
PiperOrigin-RevId: 854240746
This commit is contained in:
George Weale
2026-01-09 10:14:43 -08:00
committed by Copybara-Service
parent 62fa4e513c
commit fdc286a23c
2 changed files with 56 additions and 2 deletions
+16 -2
View File
@@ -461,7 +461,8 @@ async def _content_to_message_param(
A litellm Message, a list of litellm Messages.
"""
tool_messages = []
tool_messages: list[Message] = []
non_tool_parts: list[types.Part] = []
for part in content.parts:
if part.function_response:
response = part.function_response.response
@@ -477,9 +478,22 @@ async def _content_to_message_param(
content=response_content,
)
)
if tool_messages:
else:
non_tool_parts.append(part)
if tool_messages and not non_tool_parts:
return tool_messages if len(tool_messages) > 1 else tool_messages[0]
if tool_messages and non_tool_parts:
follow_up = await _content_to_message_param(
types.Content(role=content.role, parts=non_tool_parts),
provider=provider,
)
follow_up_messages = (
follow_up if isinstance(follow_up, list) else [follow_up]
)
return tool_messages + follow_up_messages
# Handle user or assistant messages
role = _to_litellm_role(content.role)
+40
View File
@@ -1813,6 +1813,46 @@ async def test_content_to_message_param_multi_part_function_response():
assert messages[1]["content"] == '{"value": 123}'
@pytest.mark.asyncio
async def test_content_to_message_param_function_response_with_extra_parts():
tool_part = types.Part.from_function_response(
name="load_image",
response={"status": "success"},
)
tool_part.function_response.id = "tool_call_1"
text_part = types.Part.from_text(text="[Image: img_123.png]")
image_bytes = b"test_image_data"
image_part = types.Part.from_bytes(data=image_bytes, mime_type="image/png")
content = types.Content(
role="user",
parts=[tool_part, text_part, image_part],
)
messages = await _content_to_message_param(content)
assert isinstance(messages, list)
assert messages == [
{
"role": "tool",
"tool_call_id": "tool_call_1",
"content": '{"status": "success"}',
},
{
"role": "user",
"content": [
{"type": "text", "text": "[Image: img_123.png]"},
{
"type": "image_url",
"image_url": {
"url": "data:image/png;base64,dGVzdF9pbWFnZV9kYXRh"
},
},
],
},
]
@pytest.mark.asyncio
async def test_content_to_message_param_function_response_preserves_string():
"""Tests that string responses are used directly without double-serialization.