Skip to content

Commit 2b64715

Browse files
GWealecopybara-github
authored andcommitted
fix: Handle string function responses in LiteLLM conversion
When converting `types.Content` with a `function_response` to LiteLLM's `ChatCompletionToolMessage`, if the response is already a string, use it directly. Otherwise, serialize the response to JSON. This prevents double-serialization of string payloads Close #3676 Co-authored-by: George Weale <[email protected]> PiperOrigin-RevId: 840013822
1 parent 0a07a66 commit 2b64715

File tree

2 files changed

+44
-1
lines changed

2 files changed

+44
-1
lines changed

src/google/adk/models/lite_llm.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,11 +410,17 @@ async def _content_to_message_param(
410410
tool_messages = []
411411
for part in content.parts:
412412
if part.function_response:
413+
response = part.function_response.response
414+
response_content = (
415+
response
416+
if isinstance(response, str)
417+
else _safe_json_serialize(response)
418+
)
413419
tool_messages.append(
414420
ChatCompletionToolMessage(
415421
role="tool",
416422
tool_call_id=part.function_response.id,
417-
content=_safe_json_serialize(part.function_response.response),
423+
content=response_content,
418424
)
419425
)
420426
if tool_messages:

tests/unittests/models/test_litellm.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,43 @@ async def test_content_to_message_param_multi_part_function_response():
13821382
assert messages[1]["content"] == '{"value": 123}'
13831383

13841384

1385+
@pytest.mark.asyncio
1386+
async def test_content_to_message_param_function_response_preserves_string():
1387+
"""Tests that string responses are used directly without double-serialization.
1388+
1389+
The google.genai FunctionResponse.response field is typed as dict, but
1390+
_content_to_message_param defensively handles string responses to avoid
1391+
double-serialization. This test verifies that behavior by mocking a
1392+
function_response with a string response attribute.
1393+
"""
1394+
response_payload = '{"type": "files", "count": 2}'
1395+
1396+
# Create a Part with a dict response, then mock the response to be a string
1397+
# to simulate edge cases where response might be set directly as a string
1398+
part = types.Part.from_function_response(
1399+
name="list_files",
1400+
response={"placeholder": "will be mocked"},
1401+
)
1402+
1403+
# Mock the response attribute to return a string
1404+
# Using Mock without spec_set to allow setting response to a string,
1405+
# which simulates the edge case we're testing
1406+
mock_function_response = Mock(spec=types.FunctionResponse)
1407+
mock_function_response.response = response_payload
1408+
mock_function_response.id = "tool_call_1"
1409+
part.function_response = mock_function_response
1410+
1411+
content = types.Content(
1412+
role="tool",
1413+
parts=[part],
1414+
)
1415+
message = await _content_to_message_param(content)
1416+
1417+
assert message["role"] == "tool"
1418+
assert message["tool_call_id"] == "tool_call_1"
1419+
assert message["content"] == response_payload
1420+
1421+
13851422
@pytest.mark.asyncio
13861423
async def test_content_to_message_param_assistant_message():
13871424
content = types.Content(

0 commit comments

Comments
 (0)