Skip to content

Commit 8eb1c07

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 68a7803 commit 8eb1c07

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
@@ -922,9 +922,15 @@ async def _content_to_message_param(
922922
"""
923923
_ensure_litellm_imported()
924924

925+
# A types.Content may have parts=None (e.g. types.Content(role="user")) or an
926+
# empty list (e.g. from _append_fallback_user_content_if_missing). Normalize to
927+
# an iterable so the loops below do not raise, matching the google_llm adapter
928+
# which skips contents without parts.
929+
parts = content.parts or []
930+
925931
tool_messages: list[Message] = []
926932
non_tool_parts: list[types.Part] = []
927-
for part in content.parts:
933+
for part in parts:
928934
if part.function_response:
929935
response = part.function_response.response
930936
response_content = (
@@ -965,7 +971,7 @@ async def _content_to_message_param(
965971
role = _to_litellm_role(content.role)
966972

967973
if role == "user":
968-
user_parts = [part for part in content.parts if not part.thought]
974+
user_parts = [part for part in parts if not part.thought]
969975
message_content = (
970976
await _get_content(user_parts, provider=provider, model=model) or None
971977
)
@@ -974,7 +980,7 @@ async def _content_to_message_param(
974980
tool_calls = []
975981
content_parts: list[types.Part] = []
976982
reasoning_parts: list[types.Part] = []
977-
for part in content.parts:
983+
for part in parts:
978984
if part.function_call:
979985
tool_call_id = part.function_call.id or ""
980986
tool_call_dict: ChatCompletionAssistantToolCall = {

tests/unittests/models/test_litellm.py

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

19301930

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

0 commit comments

Comments
 (0)