From c6f389d4bc4d2b91795003a3bd87ed1f1b854493 Mon Sep 17 00:00:00 2001 From: George Weale Date: Wed, 17 Dec 2025 11:25:12 -0800 Subject: [PATCH] fix: Refine Ollama content flattening and provider checks - Stripping whitespace from custom LLM provider and model names when checking for "ollama_chat". - Enhancing `_flatten_ollama_content` to correctly handle content that is None, a string, a dictionary, or an iterable (like a tuple) of content blocks, not just lists. This aligns with LiteLLM's `OpenAIMessageContent` type being an `Iterable`. Close #3928 Co-authored-by: George Weale PiperOrigin-RevId: 845848017 --- src/google/adk/models/lite_llm.py | 28 ++++++++++++++++++++------ tests/unittests/models/test_litellm.py | 11 ++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index aca230bc..57c7c930 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -628,9 +628,12 @@ def _is_ollama_chat_provider( model: Optional[str], custom_llm_provider: Optional[str] ) -> bool: """Returns True when requests should be normalized for ollama_chat.""" - if custom_llm_provider and custom_llm_provider.lower() == "ollama_chat": + if ( + custom_llm_provider + and custom_llm_provider.strip().lower() == "ollama_chat" + ): return True - if model and model.lower().startswith("ollama_chat"): + if model and model.strip().lower().startswith("ollama_chat"): return True return False @@ -644,11 +647,24 @@ def _flatten_ollama_content( join them with newlines, and fall back to a JSON string for non-text content. If both text and non-text parts are present, only the text parts are kept. """ - if not isinstance(content, list): + if content is None or isinstance(content, str): return content + # `OpenAIMessageContent` is typed as `Iterable[...]` in LiteLLM. Some + # providers or LiteLLM versions may hand back tuples or other iterables. + if isinstance(content, dict): + try: + return json.dumps(content) + except TypeError: + return str(content) + + try: + blocks = list(content) + except TypeError: + return str(content) + text_parts = [] - for block in content: + for block in blocks: if isinstance(block, dict) and block.get("type") == "text": text_value = block.get("text") if text_value: @@ -658,9 +674,9 @@ def _flatten_ollama_content( return _NEW_LINE.join(text_parts) try: - return json.dumps(content) + return json.dumps(blocks) except TypeError: - return str(content) + return str(blocks) def _normalize_ollama_chat_messages( diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index 54b0f176..a7fe91d0 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -1549,6 +1549,17 @@ async def test_generate_content_async_custom_provider_flattens_content( assert "Describe this image." in message_content +def test_flatten_ollama_content_accepts_tuple_blocks(): + from google.adk.models.lite_llm import _flatten_ollama_content + + content = ( + {"type": "text", "text": "first"}, + {"type": "text", "text": "second"}, + ) + flattened = _flatten_ollama_content(content) + assert flattened == "first\nsecond" + + @pytest.mark.asyncio async def test_content_to_message_param_user_message(): content = types.Content(