Skip to content

bedrock: parallel tool calls fail with "Expected toolResult blocks" on Anthropic models (Converse lowering never merges consecutive user messages) #41445

Closed

Description

@aagrawal207

Summary

On the v2 beta, any assistant turn containing two or more parallel tool calls permanently breaks the session for Anthropic Claude models on the amazon-bedrock provider (Converse API). The next model call fails with HTTP 400: Expected toolResult blocks at messages.N.content for the following Ids: ..., and every retry fails the same way because the broken turn is replayed from history. Root cause appears to be in the Bedrock Converse protocol lowering: each tool result is emitted as its own {role:"user"} message instead of being merged into a single user message after the toolUse turn.

Environment

  • opencode version: 0.0.0-next-17055 (also the latest @next at time of filing)
  • OS: Darwin 25.3.0 (macOS, arm64)
  • Terminal: ghostty (TERM=xterm-ghostty)
  • Shell: /bin/zsh
  • Install/channel: next (beta), npm i -g @opencode-ai/cli@next
  • Active plugins: local plugins under ~/.config/opencode/plugins/ (credential refresh, permission triage, websearch default). None transform message history; failure is reproducible in the core request path.

Reproduction

  1. Configure an Anthropic Claude model through the amazon-bedrock provider (Bedrock Converse API).
  2. Start a session and give the model a task that makes it emit two or more tool calls in a single assistant turn (parallel tool calls).
  3. opencode executes the tools, then sends the follow-up request replaying the history.

Variant: the same failure triggers when switching models mid-session. A session whose history contains a parallel-tool-call turn from a different provider (OpenAI-protocol, call_... IDs) fails on the first request after switching to an Anthropic model on amazon-bedrock.

Expected Behavior

The follow-up request succeeds: all toolResult blocks for a toolUse turn are delivered in the single user message immediately following the assistant message, as the Anthropic models on Bedrock require.

Actual Behavior

The request is rejected by the provider and the session cannot continue on that model:

Error: Provider request failed with HTTP 400: Expected toolResult blocks at messages.5.content for the following Ids: call_2554e12f5777510eaeed8165009c2058, call_95c99415a5cd5b40bcaf624c2670b15d
timestamp=2026-08-09T19:25:56.270Z level=ERROR message="Failed to drain Session"
cause="AI.Error: RequestExecutor.execute: Provider request failed with HTTP 400: Expected toolResult blocks at messages.5.content for the following Ids: ...
    at <anonymous> (../ai/src/route/executor.ts:244:25)
    at SessionRunner.callModel (../core/src/session/runner/llm.ts:191:32)
    ...

Observed in two independent sessions on the same day: one with native Anthropic tooluse_... IDs (parallel calls made by the Claude model itself, failed at messages.3), one with call_... IDs (history from an OpenAI-protocol model, failed at messages.5 right after switching models). In both cases the missing IDs are exactly the 2nd..Nth tool calls of one assistant turn; the 1st tool's result was accepted. All tool results exist intact in the session DB with state.status: "completed", so this is purely a request-lowering problem.

Additional Context

Traced through the source maps shipped with build 17055:

  1. core/src/session/runner/to-llm-message.ts — assistant() emits each completed tool result as its own tool-role message:
const results = message.content
  .filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.executed !== true)
  .map((item) => toolResult(item, ...))
  .filter((message) => message !== undefined)
  .map(Message.tool)   // <- one Message per tool result
  1. ai/src/protocols/bedrock-converse.ts — lowerMessages() turns each tool-role message into its own Bedrock user message, with no merging of consecutive user messages:
const content: BedrockUserBlock[] = []
for (const part of message.content) {
  ...
  content.push(yield* lowerToolResult(part))
  ...
}
messages.push({ role: "user", content })   // <- never merged with the previous user message

So an assistant turn with 3 parallel tool calls lowers to: assistant(toolUse x3), user(toolResult 1), user(toolResult 2), user(toolResult 3). Anthropic models on Bedrock require all toolResult blocks for a toolUse turn in the single immediately-following user message, hence the 400 listing every ID except the first.

For contrast, ai/src/protocols/anthropic-messages.ts handles this case: it explicitly merges adjacent user/tool messages when building request content (previous.role === "user" || previous.role === "tool" || endsInServerToolUse(previous)). The Bedrock Converse lowering is missing the equivalent merge. The same merge is likely needed for Bedrock's role-alternation requirement in general (e.g. synthetic/compaction user messages adjacent to real user messages).

Frequency: deterministic. Workaround: compact the session to rewrite history before continuing on the Bedrock Anthropic model, or avoid models that make parallel tool calls.

Activity

  1. opencode-agent commented on Aug 12, 2026

    @opencode-agent
    Contributor

    Addressed by #41780. Bedrock Converse lowering now merges adjacent canonical user/tool-result content into the single user message required after parallel tool use. The exact two-tool regression was reproduced as HTTP 400 before the fix and recorded as HTTP 200 with a successful continuation afterward. Closing as resolved on the v2 branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions