From 323cabb98d7e1afa544671ff9bb806991c0a0408 Mon Sep 17 00:00:00 2001 From: Oscar Craviotto <7584213+ocraviotto@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:45:09 +0300 Subject: [PATCH] fix: ensure Gemini calls include a user message (fixes #9733) When a Gemini model supports system messages, messages with the system role get filtered out from the list of messages, so never appended to the content (and passed as system_instructions instead). This causes a 400 error, as both generateContent and streamGenerateContent require contents (the systemInstruction object is optional). This is a quick fix that does not check whether there was a system msg or if the model supports it. It simply makes sure that if the list of messages passed to _gemini_convert_messages_with_history() yields no contents, that there is at least a single (empty space) user message set to prevent the call from failing. The potential side effect of this is that we will not fail a call lacking any message at all, but if that needs to be checked (and AFAIU the OpenAI spec messages are required) it should be done elsewhere (e.g. validate_and_fix_openai_messages() @ litellm/utils.py). PS: Applied `make format` (`poetry run black [file changed]`) only to files changed by me. The `make lint` command identified a bunch of files requiring formatting that I am not modifying as I did not touch them. --- .../llms/vertex_ai/gemini/transformation.py | 41 +++++++++++++------ tests/llm_translation/test_prompt_factory.py | 27 +++++++++--- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index 8ab212e255..61c8830051 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -298,6 +298,19 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 ) if len(tool_call_responses) > 0: contents.append(ContentType(parts=tool_call_responses)) + + if len(contents) == 0: + verbose_logger.warning( + """ + No contents in messages. Contents are required. See + https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.publishers.models/generateContent#request-body. + If the original request did not comply to OpenAI API requirements it should have failed by now, + but LiteLLM does not check for missing messages. + Setting an empty content to prevent an 400 error. + Relevant Issue - https://github.com/BerriAI/litellm/issues/9733 + """ + ) + contents.append(ContentType(role="user", parts=[PartType(text=" ")])) return contents except Exception as e: raise e @@ -403,19 +416,21 @@ def sync_transform_request_body( context_caching_endpoints = ContextCachingEndpoints() if gemini_api_key is not None: - messages, optional_params, cached_content = ( - context_caching_endpoints.check_and_create_cache( - messages=messages, - optional_params=optional_params, - api_key=gemini_api_key, - api_base=api_base, - model=model, - client=client, - timeout=timeout, - extra_headers=extra_headers, - cached_content=optional_params.pop("cached_content", None), - logging_obj=logging_obj, - ) + ( + messages, + optional_params, + cached_content, + ) = context_caching_endpoints.check_and_create_cache( + messages=messages, + optional_params=optional_params, + api_key=gemini_api_key, + api_base=api_base, + model=model, + client=client, + timeout=timeout, + extra_headers=extra_headers, + cached_content=optional_params.pop("cached_content", None), + logging_obj=logging_obj, ) else: # [TODO] implement context caching for gemini as well cached_content = optional_params.pop("cached_content", None) diff --git a/tests/llm_translation/test_prompt_factory.py b/tests/llm_translation/test_prompt_factory.py index 5831d7a3ec..1ec447b592 100644 --- a/tests/llm_translation/test_prompt_factory.py +++ b/tests/llm_translation/test_prompt_factory.py @@ -7,7 +7,7 @@ import pytest sys.path.insert(0, os.path.abspath("../..")) -from typing import Union +from typing import Union, List # from litellm.litellm_core_utils.prompt_templates.factory import prompt_factory import litellm @@ -28,6 +28,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import ( from litellm.llms.vertex_ai.gemini.transformation import ( _gemini_convert_messages_with_history, ) +from litellm.types.llms.openai import AllMessageValues from unittest.mock import AsyncMock, MagicMock, patch @@ -472,6 +473,20 @@ def test_vertex_only_image_user_message(): ) +def test_no_messages_yields_user_text(): + """ + Test that contents are not empty and have text when called without messages + This is to support blha blah + """ + messages: List[AllMessageValues] = [] + + contents = _gemini_convert_messages_with_history(messages=messages) + + expected_output = [{"role": "user", "parts": [{"text": " "}]}] + + assert contents == expected_output + + def test_convert_url(): convert_url_to_base64("https://picsum.photos/id/237/200/300") @@ -630,7 +645,6 @@ def test_azure_tool_call_invoke_helper(): def test_ensure_alternating_roles( messages, expected_messages, user_continue_message, assistant_continue_message ): - messages = get_completion_messages( messages=messages, assistant_continue_message=assistant_continue_message, @@ -651,7 +665,7 @@ def test_alternating_roles_e2e(): http_handler = HTTPHandler() with patch.object(http_handler, "post", new=MagicMock()) as mock_post: - try: + try: response = litellm.completion( **{ "model": "databricks/databricks-meta-llama-3-1-70b-instruct", @@ -663,7 +677,10 @@ def test_alternating_roles_e2e(): }, {"role": "user", "content": "What is Databricks?"}, {"role": "user", "content": "What is Azure?"}, - {"role": "assistant", "content": "I don't know anyything, do you?"}, + { + "role": "assistant", + "content": "I don't know anyything, do you?", + }, {"role": "assistant", "content": "I can't repeat sentences."}, ], "user_continue_message": { @@ -712,7 +729,7 @@ def test_alternating_roles_e2e(): "role": "user", "content": "Ok", }, - ] + ], } )