Skip to content

Commit 9b4dbc8

Browse files
committed
fix(litellm): guard against Content with no parts in _content_to_message_param
types.Content(role="user") defaults parts to None, and the fallback path can produce an empty user turn, so iterating content.parts in _content_to_message_param raised TypeError: 'NoneType' object is not iterable during LiteLLM request assembly, before any model call. Normalize parts to content.parts or [] once (matching the existing idiom in this module and the google_llm adapter, which skips contents without parts) and iterate the normalized list in all three branches. A content with no parts now yields a message with empty content instead of crashing. Add regression tests covering user and assistant roles for both parts=None and parts=[].
1 parent e5fdb51 commit 9b4dbc8

2 files changed

Lines changed: 29 additions & 3 deletions

File tree

src/google/adk/models/lite_llm.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -983,9 +983,15 @@ async def _content_to_message_param(
983983
"""
984984
_ensure_litellm_imported()
985985

986+
# A types.Content may have parts=None (e.g. types.Content(role="user")) or an
987+
# empty list (e.g. from _append_fallback_user_content_if_missing). Normalize to
988+
# an iterable so the loops below do not raise, matching the google_llm adapter
989+
# which skips contents without parts.
990+
parts = content.parts or []
991+
986992
tool_messages: list[Message] = []
987993
non_tool_parts: list[types.Part] = []
988-
for part in content.parts:
994+
for part in parts:
989995
if part.function_response:
990996
response = part.function_response.response
991997
response_content = (
@@ -1026,7 +1032,7 @@ async def _content_to_message_param(
10261032
role = _to_litellm_role(content.role)
10271033

10281034
if role == "user":
1029-
user_parts = [part for part in content.parts if not part.thought]
1035+
user_parts = [part for part in parts if not part.thought]
10301036
message_content = (
10311037
await _get_content(user_parts, provider=provider, model=model) or None
10321038
)
@@ -1035,7 +1041,7 @@ async def _content_to_message_param(
10351041
tool_calls = []
10361042
content_parts: list[types.Part] = []
10371043
reasoning_parts: list[types.Part] = []
1038-
for part in content.parts:
1044+
for part in parts:
10391045
if part.function_call:
10401046
tool_call_id = part.function_call.id or ""
10411047
tool_call_dict: ChatCompletionAssistantToolCall = {

tests/unittests/models/test_litellm.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,26 @@ async def test_content_to_message_param_user_message():
19301930
assert message["content"] == "Test prompt"
19311931

19321932

1933+
@pytest.mark.asyncio
1934+
@pytest.mark.parametrize("parts", [None, []])
1935+
async def test_content_to_message_param_user_message_without_parts(parts):
1936+
# types.Content(role="user") defaults parts to None, so a content with no
1937+
# parts must not raise (mirrors the google_llm adapter which skips it).
1938+
content = types.Content(role="user", parts=parts)
1939+
message = await _content_to_message_param(content)
1940+
assert message["role"] == "user"
1941+
assert message["content"] is None
1942+
1943+
1944+
@pytest.mark.asyncio
1945+
@pytest.mark.parametrize("parts", [None, []])
1946+
async def test_content_to_message_param_assistant_message_without_parts(parts):
1947+
content = types.Content(role="assistant", parts=parts)
1948+
message = await _content_to_message_param(content)
1949+
assert message["role"] == "assistant"
1950+
assert message["content"] is None
1951+
1952+
19331953
@pytest.mark.asyncio
19341954
@pytest.mark.parametrize("file_uri,mime_type", FILE_URI_TEST_CASES)
19351955
async def test_content_to_message_param_user_message_with_file_uri(

0 commit comments

Comments
 (0)