From 902916adf0084e529eb429760458a69015187264 Mon Sep 17 00:00:00 2001 From: Ishan Raj Singh Date: Wed, 22 Oct 2025 21:49:31 +0530 Subject: [PATCH 1/5] fix(LiteLlm): add fallback user message in generate_content_async if messages empty due to include_contents='none' --- src/google/adk/models/lite_llm.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 4c6be95d38..1ad005c60c 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -806,6 +806,15 @@ async def generate_content_async( _get_completion_inputs(llm_request) ) + if not messages: + messages = [ + ChatCompletionUserMessage( + role="user", + content="Handle the requests as specified in the System Instruction." + ) + ] + + if "functions" in self._additional_args: # LiteLLM does not support both tools and functions together. tools = None From 7df3acf16de174332205493e6ae03f1e09dd4590 Mon Sep 17 00:00:00 2001 From: Ishan Raj Singh Date: Wed, 22 Oct 2025 22:02:20 +0530 Subject: [PATCH 2/5] fix(LiteLlm): add fallback user message in generate_content_async if messages empty due to include_contents='none' --- src/google/adk/models/lite_llm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 73ea994899..288594bdaf 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -828,7 +828,11 @@ async def generate_content_async( messages = [ ChatCompletionUserMessage( role="user", - content="Handle the requests as specified in the System Instruction." + content=( + llm_request.config.system_instruction + if llm_request.config.system_instruction + else "Handle the requests as specified in the System Instruction." + ) ) ] From e659e3649c28fed35c9976fbdc0538ca8a7e63f1 Mon Sep 17 00:00:00 2001 From: ISHAN RAJ SINGH Date: Wed, 22 Oct 2025 22:08:00 +0530 Subject: [PATCH 3/5] Update src/google/adk/models/lite_llm.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/google/adk/models/lite_llm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 288594bdaf..23ffae08d2 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -831,7 +831,7 @@ async def generate_content_async( content=( llm_request.config.system_instruction if llm_request.config.system_instruction - else "Handle the requests as specified in the System Instruction." + else "Provide a helpful response." ) ) ] From 67537020d0df4e2f42a550dd771a184f7a9c0fc6 Mon Sep 17 00:00:00 2001 From: ISHAN RAJ SINGH Date: Wed, 22 Oct 2025 22:08:11 +0530 Subject: [PATCH 4/5] Update src/google/adk/models/lite_llm.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/google/adk/models/lite_llm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 23ffae08d2..6792a275b7 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -829,10 +829,9 @@ async def generate_content_async( ChatCompletionUserMessage( role="user", content=( - llm_request.config.system_instruction + llm_request.config.system_instruction[:200] # Truncate to 200 characters if llm_request.config.system_instruction else "Provide a helpful response." - ) ) ] From cc071619510ae80d071df72fdbc14c90557c5b8d Mon Sep 17 00:00:00 2001 From: Ishan Raj Singh Date: Sat, 25 Oct 2025 15:26:20 +0530 Subject: [PATCH 5/5] fix(LiteLlm): align fallback content with BaseLlm._maybe_append_user_content and add tests - Add fallback user message when messages list is empty due to include_contents='none' - Use same fallback text as BaseLlm._maybe_append_user_content for consistency - Add comprehensive tests covering empty contents scenarios with and without tools - Fixes #3242 --- src/google/adk/models/lite_llm.py | 6 +- tests/integration/models/test_lite_llm.py | 231 ++++++++++++++++++++++ 2 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 tests/integration/models/test_lite_llm.py diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 6792a275b7..0ddc728ded 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -824,14 +824,12 @@ async def generate_content_async( _get_completion_inputs(llm_request) ) + # Ensure messages list is not empty (aligns with _maybe_append_user_content fallback) if not messages: messages = [ ChatCompletionUserMessage( role="user", - content=( - llm_request.config.system_instruction[:200] # Truncate to 200 characters - if llm_request.config.system_instruction - else "Provide a helpful response." + content="Handle the requests as specified in the System Instruction." ) ] diff --git a/tests/integration/models/test_lite_llm.py b/tests/integration/models/test_lite_llm.py new file mode 100644 index 0000000000..81956dc52a --- /dev/null +++ b/tests/integration/models/test_lite_llm.py @@ -0,0 +1,231 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for LiteLlm model with include_contents='none'.""" + +import pytest +from unittest.mock import AsyncMock, MagicMock, patch + +from src.google.adk.models.lite_llm import LiteLlm +from src.google.adk.models.llm_request import LlmRequest +from google.genai import types + + +@pytest.mark.asyncio +async def test_include_contents_none_with_fallback(): + """Test that LiteLlm handles include_contents='none' without empty content error.""" + + # Create a minimal LlmRequest with no contents + config = types.GenerateContentConfig( + system_instruction="Continue the phrase of the last agent with a short sentence" + ) + + llm_request = LlmRequest( + contents=[], # Empty contents simulating include_contents='none' + config=config + ) + + # Mock the LiteLLM client to avoid actual API calls + mock_response = MagicMock() + mock_response.get.return_value = [{ + "message": { + "content": "This is a test response." + }, + "finish_reason": "stop" + }] + mock_response.__getitem__ = lambda self, key: { + "choices": [{ + "message": { + "content": "This is a test response." + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15 + } + }[key] + + # Initialize LiteLlm model + model = LiteLlm(model="gemini/gemini-2.0-flash") + + # Mock the acompletion method + with patch.object(model.llm_client, 'acompletion', new_callable=AsyncMock) as mock_acompletion: + mock_acompletion.return_value = mock_response + + # This should not raise an error about empty content + # Instead, it should add fallback content + response_generator = model.generate_content_async(llm_request, stream=False) + + # Verify we can get a response without error + response = None + async for resp in response_generator: + response = resp + break + + # Assert response is not None and has expected structure + assert response is not None + assert response.content is not None + assert response.content.role == "model" + + # Verify that acompletion was called with non-empty messages + call_args = mock_acompletion.call_args + messages = call_args.kwargs.get('messages', []) + assert len(messages) > 0, "Messages should not be empty" + + # Verify the fallback message is present + user_messages = [m for m in messages if m.get('role') == 'user'] + assert len(user_messages) > 0, "Should have at least one user message" + assert "Handle the requests as specified in the System Instruction" in str(user_messages[0].get('content', '')) + + +@pytest.mark.asyncio +async def test_include_contents_none_with_tools(): + """Test that LiteLlm handles include_contents='none' with tools.""" + + # Create a function declaration + function_decl = types.FunctionDeclaration( + name="get_weather", + description="Get weather for a city", + parameters=types.Schema( + type=types.Type.OBJECT, + properties={ + "city": types.Schema(type=types.Type.STRING, description="City name") + }, + required=["city"] + ) + ) + + config = types.GenerateContentConfig( + system_instruction="You are a helpful assistant", + tools=[types.Tool(function_declarations=[function_decl])] + ) + + llm_request = LlmRequest( + contents=[], # Empty contents + config=config + ) + + # Mock response with tool call - use proper ChatCompletionMessageToolCall objects + from litellm import ChatCompletionMessageToolCall, Function + + mock_tool_call = ChatCompletionMessageToolCall( + type="function", + id="call_123", + function=Function( + name="get_weather", + arguments='{"city": "New York"}' + ) + ) + + mock_response = MagicMock() + mock_response.__getitem__ = lambda self, key: { + "choices": [{ + "message": { + "content": None, + "tool_calls": [mock_tool_call] + }, + "finish_reason": "tool_calls" + }], + "usage": { + "prompt_tokens": 15, + "completion_tokens": 10, + "total_tokens": 25 + } + }[key] + + model = LiteLlm(model="gemini/gemini-2.0-flash") + + # Mock the acompletion method + with patch.object(model.llm_client, 'acompletion', new_callable=AsyncMock) as mock_acompletion: + mock_acompletion.return_value = mock_response + + # Should handle empty contents gracefully + response_generator = model.generate_content_async(llm_request, stream=False) + + response = None + async for resp in response_generator: + response = resp + break + + assert response is not None + assert response.content is not None + + # Verify that acompletion was called with non-empty messages + call_args = mock_acompletion.call_args + messages = call_args.kwargs.get('messages', []) + assert len(messages) > 0, "Messages should not be empty with tools" + + # Verify tools were passed + tools = call_args.kwargs.get('tools', None) + assert tools is not None, "Tools should be passed to acompletion" + assert len(tools) > 0, "Should have at least one tool" + + +@pytest.mark.asyncio +async def test_include_contents_with_existing_content(): + """Test that LiteLlm works normally when contents are provided.""" + + config = types.GenerateContentConfig( + system_instruction="You are a helpful assistant" + ) + + # Provide actual content + llm_request = LlmRequest( + contents=[ + types.Content( + role="user", + parts=[types.Part(text="What is the weather in Paris?")] + ) + ], + config=config + ) + + mock_response = MagicMock() + mock_response.__getitem__ = lambda self, key: { + "choices": [{ + "message": { + "content": "The weather in Paris is sunny." + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 20, + "completion_tokens": 8, + "total_tokens": 28 + } + }[key] + + model = LiteLlm(model="gemini/gemini-2.0-flash") + + with patch.object(model.llm_client, 'acompletion', new_callable=AsyncMock) as mock_acompletion: + mock_acompletion.return_value = mock_response + + response_generator = model.generate_content_async(llm_request, stream=False) + + response = None + async for resp in response_generator: + response = resp + break + + assert response is not None + assert response.content is not None + assert response.content.role == "model" + + # Verify that user's actual content was used + call_args = mock_acompletion.call_args + messages = call_args.kwargs.get('messages', []) + user_messages = [m for m in messages if m.get('role') == 'user'] + assert any("Paris" in str(m.get('content', '')) for m in user_messages)