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.
This commit is contained in:
Oscar Craviotto 2025-07-16 19:45:09 +03:00
parent 7f5c92ddeb
commit 323cabb98d
No known key found for this signature in database
GPG key ID: 10FB01E36B4900BE
2 changed files with 50 additions and 18 deletions

View file

@ -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)

View file

@ -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",
},
]
],
}
)