Skip to content

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

Open
anxkhn wants to merge 2 commits 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 2 commits into
google:mainfrom
anxkhn:fix/litellm-content-none-parts

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

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=[].
@rohityan rohityan self-assigned this Jul 6, 2026
@rohityan rohityan added the models [Component] Issues related to model support label Jul 6, 2026
@GWeale GWeale self-assigned this Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

models [Component] Issues related to model support

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants