From 74d8361a7e4521a8ce765e07ef1ed98b9aa8f423 Mon Sep 17 00:00:00 2001 From: George Weale Date: Tue, 28 Oct 2025 11:56:01 -0700 Subject: [PATCH] fix: Add a fallback user message to LiteLLM requests if the last user message is empty Related to #3255 Close #2560 PiperOrigin-RevId: 825143315 --- src/google/adk/models/lite_llm.py | 48 ++++++++++++++++++++++++++ tests/unittests/models/test_litellm.py | 35 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/google/adk/models/lite_llm.py b/src/google/adk/models/lite_llm.py index 94d9831c..58297036 100644 --- a/src/google/adk/models/lite_llm.py +++ b/src/google/adk/models/lite_llm.py @@ -155,6 +155,53 @@ def _safe_json_serialize(obj) -> str: return str(obj) +def _part_has_payload(part: types.Part) -> bool: + """Checks whether a Part contains usable payload for the model.""" + if part.text: + return True + if part.inline_data and part.inline_data.data: + return True + if part.file_data and (part.file_data.file_uri or part.file_data.data): + return True + return False + + +def _append_fallback_user_content_if_missing( + llm_request: LlmRequest, +) -> None: + """Ensures there is a user message with content for LiteLLM backends. + + Args: + llm_request: The request that may need a fallback user message. + """ + for content in reversed(llm_request.contents): + if content.role == "user": + parts = content.parts or [] + if any(_part_has_payload(part) for part in parts): + return + if not parts: + content.parts = [] + content.parts.append( + types.Part.from_text( + text="Handle the requests as specified in the System Instruction." + ) + ) + return + llm_request.contents.append( + types.Content( + role="user", + parts=[ + types.Part.from_text( + text=( + "Handle the requests as specified in the System" + " Instruction." + ) + ), + ], + ) + ) + + def _content_to_message_param( content: types.Content, ) -> Union[Message, list[Message]]: @@ -818,6 +865,7 @@ class LiteLlm(BaseLlm): """ self._maybe_append_user_content(llm_request) + _append_fallback_user_content_if_missing(llm_request) logger.debug(_build_request_log(llm_request)) messages, tools, response_format, generation_params = ( diff --git a/tests/unittests/models/test_litellm.py b/tests/unittests/models/test_litellm.py index 2fbacc0f..8138da74 100644 --- a/tests/unittests/models/test_litellm.py +++ b/tests/unittests/models/test_litellm.py @@ -548,6 +548,41 @@ async def test_generate_content_async(mock_acompletion, lite_llm_instance): ) +@pytest.mark.asyncio +async def test_generate_content_async_adds_fallback_user_message( + mock_acompletion, lite_llm_instance +): + llm_request = LlmRequest( + contents=[ + types.Content( + role="user", + parts=[], + ) + ] + ) + + async for _ in lite_llm_instance.generate_content_async(llm_request): + pass + + mock_acompletion.assert_called_once() + + _, kwargs = mock_acompletion.call_args + user_messages = [ + message for message in kwargs["messages"] if message["role"] == "user" + ] + assert any( + message.get("content") + == "Handle the requests as specified in the System Instruction." + for message in user_messages + ) + assert ( + sum(1 for content in llm_request.contents if content.role == "user") == 1 + ) + assert llm_request.contents[-1].parts[0].text == ( + "Handle the requests as specified in the System Instruction." + ) + + litellm_append_user_content_test_cases = [ pytest.param( LlmRequest(