Merge consecutive same-role contents for native Gemini
_build_gemini_contents emitted one contents entry per source message and
never merged adjacent same-role entries. Gemini's generateContent requires
strict user/model alternation and rejects consecutive same-role turns with
HTTP 400 ("Please ensure that multiturn requests alternate between user and
model"). A parallel tool call turns into two tool results in a row, which
become two consecutive user functionResponse contents, so every multi-tool
turn produced an unsendable history.
Fold adjacent same-role contents into one by concatenating their parts after
the per-message loop, matching the Anthropic and Bedrock converters. For a
parallel call this yields the grouped multi-functionResponse user turn Gemini
expects.
This commit is contained in:
parent
885e80df74
commit
936af2f4f5
2 changed files with 69 additions and 0 deletions
|
|
@ -337,6 +337,22 @@ def _build_gemini_contents(messages: List[Dict[str, Any]]) -> tuple[List[Dict[st
|
|||
if parts:
|
||||
contents.append({"role": gemini_role, "parts": parts})
|
||||
|
||||
# Gemini's generateContent requires strict user/model alternation;
|
||||
# consecutive same-role contents are rejected with HTTP 400 "Please ensure
|
||||
# that multiturn requests alternate between user and model". The loop above
|
||||
# emits one content per source message, so parallel tool calls (N tool
|
||||
# results become N user functionResponse contents), back-to-back user turns,
|
||||
# or merged assistant turns would each violate that. Merge adjacent
|
||||
# same-role contents by concatenating their parts. For parallel calls this
|
||||
# also produces the grouped multi-functionResponse turn Gemini expects.
|
||||
merged_contents: List[Dict[str, Any]] = []
|
||||
for content in contents:
|
||||
if merged_contents and merged_contents[-1]["role"] == content["role"]:
|
||||
merged_contents[-1]["parts"].extend(content["parts"])
|
||||
else:
|
||||
merged_contents.append(content)
|
||||
contents = merged_contents
|
||||
|
||||
system_instruction = None
|
||||
joined_system = "\n".join(part for part in system_text_parts if part).strip()
|
||||
if joined_system:
|
||||
|
|
|
|||
|
|
@ -85,6 +85,59 @@ def test_build_native_request_uses_original_function_name_for_tool_result():
|
|||
assert tool_response["name"] == "get_weather"
|
||||
|
||||
|
||||
def test_parallel_tool_results_merge_into_one_user_content():
|
||||
"""Gemini requires strict user/model alternation; two consecutive `user`
|
||||
contents are rejected with HTTP 400. Parallel tool calls produce two tool
|
||||
results in a row, so their functionResponses must be grouped into a single
|
||||
user content instead of two consecutive ones."""
|
||||
from agent.gemini_native_adapter import _build_gemini_contents
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Read a.txt and b.txt"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{"id": "call_1", "type": "function",
|
||||
"function": {"name": "read_file", "arguments": '{"path": "a.txt"}'}},
|
||||
{"id": "call_2", "type": "function",
|
||||
"function": {"name": "read_file", "arguments": '{"path": "b.txt"}'}},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": "AAA"},
|
||||
{"role": "tool", "tool_call_id": "call_2", "content": "BBB"},
|
||||
]
|
||||
|
||||
contents, _ = _build_gemini_contents(messages)
|
||||
roles = [c["role"] for c in contents]
|
||||
|
||||
# No two adjacent contents may share a role.
|
||||
assert all(roles[i] != roles[i - 1] for i in range(1, len(roles))), roles
|
||||
assert roles == ["user", "model", "user"]
|
||||
|
||||
# Both parallel functionResponses land in the single trailing user content.
|
||||
response_parts = [
|
||||
p for p in contents[2]["parts"] if "functionResponse" in p
|
||||
]
|
||||
outputs = [p["functionResponse"]["response"]["output"] for p in response_parts]
|
||||
assert outputs == ["AAA", "BBB"]
|
||||
|
||||
|
||||
def test_consecutive_user_messages_merge_for_gemini_alternation():
|
||||
"""Back-to-back user messages must also be merged, not sent as two
|
||||
consecutive user contents."""
|
||||
from agent.gemini_native_adapter import _build_gemini_contents
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "first"},
|
||||
{"role": "user", "content": "second"},
|
||||
{"role": "assistant", "content": "ok"},
|
||||
]
|
||||
contents, _ = _build_gemini_contents(messages)
|
||||
roles = [c["role"] for c in contents]
|
||||
assert roles == ["user", "model"], roles
|
||||
|
||||
|
||||
def test_build_native_request_strips_json_schema_only_fields_from_tool_parameters():
|
||||
from agent.gemini_native_adapter import build_gemini_request
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue