fix(vertex): repair tool-call history for Gemini

This commit is contained in:
Meenal Gupta 2026-04-30 17:16:39 +05:30
parent 3e1479c052
commit d82aa1e494
2 changed files with 76 additions and 1 deletions

View file

@ -305,6 +305,16 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
user_message_types = {"user", "system"}
contents: List[ContentType] = []
# Pre-build tool_call_id → assistant message so we can recover
# last_message_with_tool_calls at the tool-response side when a provider
# (e.g. Codex CLI) sends assistant messages with an empty tool_calls list.
tool_call_map: dict = {}
for msg in messages:
if msg["role"] == "assistant":
for tc in msg.get("tool_calls") or []:
if "id" in tc:
tool_call_map[tc["id"]] = msg
last_message_with_tool_calls = None
msg_i = 0
@ -527,7 +537,10 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
## HANDLE ASSISTANT FUNCTION CALL
if (
assistant_msg.get("tool_calls", []) is not None
(
assistant_msg.get("tool_calls", []) is not None
and len(assistant_msg.get("tool_calls", [])) > 0
)
or assistant_msg.get("function_call") is not None
): # support assistant tool invoke conversion
gemini_tool_call_parts = convert_to_gemini_tool_call_invoke(
@ -589,6 +602,10 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
msg_i < len(messages)
and messages[msg_i]["role"] in tool_call_message_roles
):
_tid = messages[msg_i].get("tool_call_id")
if _tid and _tid in tool_call_map:
last_message_with_tool_calls = tool_call_map[_tid]
_part = convert_to_gemini_tool_call_result(
messages[msg_i], last_message_with_tool_calls # type: ignore
)

View file

@ -1913,3 +1913,61 @@ def test_multi_turn_function_calling_roles():
assert (
content["role"] == "user"
), f"Content block {i} with function_response has role='{content['role']}', expected 'user'"
def test_tool_call_history_repair_empty_tool_calls():
"""
Regression test for: Codex CLI sends assistant messages with empty tool_calls
list, causing 'Missing corresponding tool call for tool response message'.
Codex CLI sometimes emits:
{"role": "assistant", "content": "...", "tool_calls": []}
followed by a separate assistant message that has the actual tool_calls,
then a tool response referencing one of those calls.
Before fix: last_message_with_tool_calls pointed to the empty-tool_calls
message, name recovery failed, Vertex AI returned 400.
After fix: a pre-pass builds tool_call_id assistant message so the
correct originating message is always found.
"""
messages = [
{"role": "user", "content": "Run a shell command"},
{
"role": "assistant",
"content": "I'll run that for you.",
"tool_calls": [], # empty — Codex CLI quirk
},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "shell",
"arguments": '{"cmd": "echo hello"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "hello",
},
]
contents = _gemini_convert_messages_with_history(messages=messages)
# The function_response part must have the correct function name
found = False
for content in contents:
for part in content.get("parts", []):
if "function_response" in part:
assert part["function_response"]["name"] == "shell", (
f"Expected function name 'shell', got {part['function_response']['name']}"
)
found = True
assert found, "No function_response part found in converted history"