Skip to content

fix(provider): Bedrock, Gemini, Vertex send a parallel tool call's results in one message - #368

Merged
yuanhao merged 1 commit into
mainfrom
fix/role-alternation
Oct 21, 2026
Merged

yuanhao merged 1 commit into
mainfrom
fix/role-alternation

Conversation

@yuanhao

@yuanhao yuanhao commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Closes #223.

Bug. The Bedrock, Gemini and Vertex request builders sent each Message::ToolResult as its own user-role message, and a steering message / stop marker / compaction summary after them as another. Converse requires alternating roles; Gemini requires a turn's function responses in one content, as many as its function calls (the well-known "number of function response parts" 400). So the request after any turn with ≥2 tool calls — the default parallel strategy — could be rejected.

Fix.

  • provider::merge_same_role(messages, key) merges consecutive same-role wire messages, appending their content (Bedrock) / parts (Gemini, Vertex) arrays in order: results in call order, then whatever followed. A message without the array is left as it is (get_mut, no null inserted).
  • Gemini/Vertex: google::function_responses_first puts each user content's functionResponse parts before its images and text, so a merged turn never interleaves a result's image between responses (images among responses are a reported cause of the same 400; Vertex matches responses by name and position).
  • Anthropic and OpenAI-compatible/Responses are unchanged: Anthropic merges consecutive turns itself, and the OpenAI formats carry one item per call.

Tests.

  • Per provider, from a shared parallel_tool_conversation() (request, 2 tool calls, 2 results, a steering message): roles user, assistant|model, user, results in call order, steering text last; on Gemini and Vertex also with an image result (responses first, then the image, then the text).
  • merge_same_role unit test: adjacency, order, assistant pairs, an empty message first/last in a run, a keyless message.
  • Bedrock end to end: a real agent run against wiremock streaming two tool calls; the second request's body has the two toolResults in one user message.

Not run against a live Bedrock or Gemini endpoint (no credentials), as the issue noted. Reviewed by code-reviewer (checked against Converse docs and Gemini's reported behaviour; no critical/important findings) and pr-test-analyzer; their suggestions are applied.

Checks: --all-features test / clippy / doc / fmt clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG

…sults in one message

Each tool result went out as its own user message (and a steering message,
stop marker or summary after them as another), while Converse requires
alternating roles and Gemini a turn's function responses in one content.
`provider::merge_same_role` merges consecutive same-role wire messages in
the three request builders, keeping every block in order; on Gemini and
Vertex `function_responses_first` puts a turn's function responses before
its images and text.

Tests: per-provider request bodies from a shared parallel-tool
conversation (images included on Gemini/Vertex), the helper's cases
(assistant pairs, empty and keyless messages), and a Bedrock run through
the real loop against wiremock. Not run against a live endpoint.

Closes #223.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7iq5hpndSiHQcnAsywKuG
@yuanhao
yuanhao merged commit 5a0c1e2 into main Oct 21, 2026
16 checks passed
@yuanhao
yuanhao deleted the fix/role-alternation branch October 21, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bedrock/Gemini: one user message per tool result breaks role alternation

1 participant