Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion litellm/llms/vertex_ai/gemini/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,19 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
excluded_keys=["thoughtSignature"],
):
assistant_content.append(gemini_tool_call_part)
last_message_with_tool_calls = assistant_msg
# Only record this as the active tool-call message when it actually
# carries tool calls. The `if` guard above is also entered for a
# text-only assistant message (`assistant_msg.get("tool_calls", [])
# is not None` is True for an empty list), so without this check a
# later assistant message with no tool calls would clobber the
# reference. The following tool result would then be matched against
# an assistant message that has no tool_calls, raising "Missing
# corresponding tool call for tool response message".
if (
assistant_msg.get("tool_calls")
or assistant_msg.get("function_call") is not None
):
last_message_with_tool_calls = assistant_msg

## HANDLE SERVER-SIDE TOOL INVOCATIONS (context circulation)
_psf = assistant_msg.get("provider_specific_fields")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Regression test for tool-call / tool-result matching in the Gemini message converter.

When an assistant message that contains tool_calls is followed by a *second* assistant
message that has no tool_calls (e.g. the model emits a short narration turn after the
tool call but before the tool result), the converter used to overwrite its
`last_message_with_tool_calls` reference with the text-only assistant message. The
subsequent tool result could then no longer be matched to its tool call, and conversion
failed with:

Exception: Missing corresponding tool call for tool response message.

This happens for any OpenAI-style history with that shape, independent of provider/model.
"""

import pytest

from litellm.llms.vertex_ai.gemini.transformation import (
_gemini_convert_messages_with_history,
)


def _messages_with_text_assistant_between_tool_call_and_result():
return [
{"role": "user", "content": "list the files"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {"name": "shell", "arguments": '{"command": ["ls"]}'},
}
],
},
# text-only assistant message in between (no tool_calls)
{"role": "assistant", "content": "Running the command now."},
{"role": "tool", "tool_call_id": "call_abc123", "content": "math.py"},
]


def test_tool_result_matches_tool_call_with_text_assistant_in_between():
messages = _messages_with_text_assistant_between_tool_call_and_result()

# Should not raise "Missing corresponding tool call for tool response message".
contents = _gemini_convert_messages_with_history(messages=messages)

# The function response must be present and carry the correct tool name.
function_responses = [
part["function_response"]
for content in contents
for part in content["parts"]
if isinstance(part, dict) and part.get("function_response")
]
assert function_responses, f"expected a functionResponse part, got: {contents}"
assert function_responses[0]["name"] == "shell"
Loading