Skip to content

fix(litellm): guard against Content with no parts in _content_to_message_param#6312

Open
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/litellm-content-none-parts
Open

fix(litellm): guard against Content with no parts in _content_to_message_param#6312
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/litellm-content-none-parts

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 6, 2026

Copy link
Copy Markdown

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • Closes: N/A
  • Related: N/A

2. Or, if no issue exists, describe the change:

Problem:

LiteLlm._content_to_message_param iterates content.parts directly in three
places. A types.Content can legitimately have parts=None, so those iterations raise
TypeError: 'NoneType' object is not iterable during LiteLLM request assembly,
before any model call.

types.Content(role="user") defaults parts to None, and an empty user turn can
reach the adapter through the request contents. Minimal reproduction:

import asyncio
from google.genai import types
from google.adk.models.lite_llm import _content_to_message_param

# parts defaults to None
content = types.Content(role="user")
asyncio.run(_content_to_message_param(content))
# TypeError: 'NoneType' object is not iterable

The native Gemini adapter already tolerates this case: google_llm.py skips a
content with no parts (if not content.parts: continue). The LiteLLM adapter does
not, so the two paths behave inconsistently for the same input.

Solution:

Normalize parts once at the top of _content_to_message_param
(parts = content.parts or []) and iterate the normalized list in all three
branches (the function-response iteration, the user-branch comprehension, and the
assistant-branch iteration). This mirrors the content.parts or [] idiom already used
in this same module and keeps the LiteLLM adapter consistent with the Gemini
adapter. A content with no parts now yields a message with empty content instead of
crashing.

The change is limited to the normalization; no other behavior is altered. Normal
text content, mixed tool + text content, and multipart function responses are
unaffected.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added parametrized regression tests in
tests/unittests/models/test_litellm.py covering both parts=None and
parts=[] for the user and assistant roles, asserting the role is preserved and
content is None (no exception raised):

  • test_content_to_message_param_user_message_without_parts
  • test_content_to_message_param_assistant_message_without_parts

These fail on main at the first for part in content.parts iteration with the
reported TypeError and pass with this change.

pytest summary for the affected module:

$ pytest tests/unittests/models/test_litellm.py -q
321 passed, 1 skipped

Broader model suite (no regressions):

$ pytest tests/unittests/models/ -q
817 passed, 1 skipped

Manual End-to-End (E2E) Tests:

Direct reproduction via the adapter entry point (see the snippet under
"Problem"): before the change, _content_to_message_param(types.Content(role="user"))
raises TypeError: 'NoneType' object is not iterable; after the change it returns
{'role': 'user', 'content': None}. The same holds for role="assistant" and for
parts=[].

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules. (N/A)

Additional context

The guard makes the LiteLLM adapter consistent with the Gemini adapter
(google_llm.py), which already tolerates a content without parts.


The diff (for reference; already committed on the branch)

src/google/adk/models/lite_llm.py (+9/-3), tests/unittests/models/test_litellm.py (+20). Net +29/-3.

  • Add parts = content.parts or [] right after _ensure_litellm_imported().
  • Replace the three for part in content.parts / [... for part in content.parts ...]
    sites (function-response iteration, user comprehension, assistant iteration) with the
    normalized parts.
  • Two parametrized tests (user + assistant, each None and []).

…age_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=[].
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant