Skip to content

[Bug]: Bedrock provider emits reasoning after tool use in assistant content, breaking Anthropic multi-turn tool-use with extended thinking #3688

Description

@hoodunit

Prerequisites

  • I have searched existing issues and discussions to avoid duplicates
  • I am using the latest version (or have tested against main/nightly)

Description

Anthropic Bedrock models cannot be reliably used in multi-turn use with both extended thinking and tool use.

Claude models will sometimes return responses with both tool calls and extended thinking. When Bifrost translates an OpenAI Chat Completions request with both into the Bedrock Converse format, the assistant's content blocks are emitted in the wrong order, with toolUse coming before reasoningContent. This causes the request to fail with an HTTP 400 error from AWS Bedrock with the following content:

The model returned the following errors: messages.1: `tool_use` ids were found without `tool_result` blocks immediately after: tooluse_nnn. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'

More specifically core/providers/bedrock/utils.go::convertMessage appends content blocks in this order:

  1. Text content (from content)
  2. toolUse (from tool_calls)
  3. reasoningContent (from reasoning_details)

But Anthropic via Bedrock requires the reasoning/thinking block to be first in the assistant message's content array on any multi-turn request that includes a prior tool-use turn.

Bifrost's Anthropic-direct provider already follows this rule — see core/providers/anthropic/chat.go#L674-L683 ("First add reasoning details") and the explicit comment at core/providers/anthropic/responses.go#L3043 ("When thinking blocks exist, they MUST come first before tool_use blocks"). The Bedrock provider just doesn't.

Steps to reproduce

Dependencies

  • Bifrost: reproduced on transports/v1.5.3 and on current main (b287d55).
  • Provider: AWS Bedrock, any Claude model that supports extended thinking with tool use. Reproduced reliably with Claude Sonnet 4.6 (anthropic.claude-sonnet-4-6). Also expected to affect Sonnet 4 / 4.5, Haiku 4.5, Opus 4 / 4.5 / 4.6 / 4.7 — any model on the Extended thinking models list when invoked through the bedrock chat-completions translation.
  • Client: any OpenAI Chat Completions client that round-trips reasoning via Bifrost's reasoning_details extension. Reproduced via pydantic-ai's OpenAIChatModel against Bifrost's /v1/chat/completions with a small _map_model_response shim that attaches reasoning_details carrying ThinkingPart.signature (Bifrost's documented round-trip path). The same defect is reachable from any caller that sends {"role":"assistant","content":"...", "tool_calls":[...], "reasoning_details":[...]} to Bifrost's chat-completions endpoint with a Bedrock Claude target.

Steps

  1. Configure Bifrost with a Bedrock provider and any Claude model that supports extended thinking with tool use (e.g. anthropic.claude-sonnet-4-6, anthropic.claude-sonnet-4-5-20250929-v1:0).
  2. Issue a /v1/chat/completions request with reasoning_effort: "low" (or any non-none effort), one or more tools defined, and a user prompt that the model will answer by calling a tool.
  3. Receive the first response. It will contain a tool_use (correct).
  4. Execute the tool and continue the conversation: send the same request back with the prior assistant turn included (containing content + tool_calls + reasoning_details[].signature from step 3) followed by a tool message carrying the result.
  5. Observe Bedrock's 400 surfaced through Bifrost.

Minimal repro payload (second-turn request body to Bifrost /v1/chat/completions)

{
  "model": "bedrock/anthropic.claude-sonnet-4-6",
  "reasoning_effort": "low",
  "tools": [{
    "type": "function",
    "function": {
      "name": "current_date_and_time",
      "description": "Get current date and time in Helsinki.",
      "parameters": {"type": "object", "properties": {}}
    }
  }],
  "messages": [
    { "role": "user", "content": "What's the time in Helsinki?" },
    {
      "role": "assistant",
      "content": "Let me check the time for you!",
      "tool_calls": [{
        "id": "tooluse_oY81xax8aoUnSFH0vCth76",
        "type": "function",
        "function": { "name": "current_date_and_time", "arguments": "{}" }
      }],
      "reasoning_details": [{
        "index": 0,
        "type": "reasoning.text",
        "text": "The user is asking for the current time...",
        "signature": "<a valid signature returned by Bedrock on the previous turn>"
      }]
    },
    {
      "role": "tool",
      "tool_call_id": "tooluse_oY81xax8aoUnSFH0vCth76",
      "content": "\"2026-05-21T12:25:48+03:00 (Thursday)\""
    }
  ]
}

To run end-to-end you need a real signature from Bedrock — the easiest way is to make a real first-turn request to Bifrost, capture reasoning_details[].signature from the streamed response, then replay it in turn 2 as above. The existing test infrastructure could probably be extended to cover this case: core/providers/bedrock/bedrock_test.go::TestMultiTurnReasoningContentPassthrough covers passthrough but doesn't assert ordering.

Root cause

core/providers/bedrock/utils.go#L737-L778 (on main; identical structure in v1.4.23–v1.5.3)

The companion Anthropic-direct path already has the right ordering (core/providers/anthropic/chat.go#L674-L683)

Proposed fix

This has not been tested, but moving the reasoning-details block to the top of convertMessage, mirroring anthropic/chat.go, should fix it. Diff:

 func convertMessage(ctx context.Context, msg schemas.ChatMessage) (BedrockMessage, error) {
     bedrockMsg := BedrockMessage{
         Role: BedrockMessageRole(msg.Role),
     }

     // Convert content
     var contentBlocks []BedrockContentBlock
     if msg.Content != nil {
         var err error
         contentBlocks, err = convertContent(ctx, *msg.Content)
         if err != nil {
             return BedrockMessage{}, fmt.Errorf("failed to convert content: %w", err)
         }
     }
+
+    // Add reasoning content FIRST. Anthropic via Bedrock requires thinking
+    // blocks to precede text and tool_use blocks in an assistant message
+    // (see anthropic/chat.go and anthropic/responses.go:3043). Out-of-order
+    // blocks cause Bedrock to reject the next turn with HTTP 400.
+    if msg.ChatAssistantMessage != nil && len(msg.ChatAssistantMessage.ReasoningDetails) > 0 {
+        for _, detail := range msg.ChatAssistantMessage.ReasoningDetails {
+            if detail.Type == schemas.BifrostReasoningDetailsTypeText {
+                contentBlocks = append(contentBlocks, BedrockContentBlock{
+                    ReasoningContent: &BedrockReasoningContent{
+                        ReasoningText: &BedrockReasoningContentText{
+                            Text:      detail.Text,
+                            Signature: detail.Signature,
+                        },
+                    },
+                })
+            }
+        }
+    }

     // Add tool calls if present (for assistant messages)
     if msg.ChatAssistantMessage != nil && msg.ChatAssistantMessage.ToolCalls != nil {
         for _, toolCall := range msg.ChatAssistantMessage.ToolCalls {
             toolUseBlock := convertToolCallToContentBlock(toolCall)
             contentBlocks = append(contentBlocks, toolUseBlock)
         }
     }

     bedrockMsg.Content = contentBlocks
     return bedrockMsg, nil
 }

Test coverage

core/providers/bedrock/bedrock_test.go::TestMultiTurnReasoningContentPassthrough currently asserts only that a reasoningContent block is present but does not check ordering. It could be extended or a new test added to assert that when an assistant message contains text + tool_use + reasoning_details, the resulting BedrockMessage.Content[0].ReasoningContent is non-nil — i.e. reasoning is the first block.

Expected behavior

Model returns appropriate 200 response.

Actual behavior

Model returns HTTP 400 with a validation error

Affected area(s)

Core (Go)

Version

Recent main (b287d55), v1.4.23 - v1.5.3.

Environment

- Tested Docker image: `maximhq/bifrost:v1.4.23`

Relevant logs/output

Bedrock HTTP 400 error with Claude Sonnet 4.6:


The model returned the following errors: messages.1: `tool_use` ids were found without `tool_result` blocks immediately after: tooluse_oY81xax8aoUnSFH0vCth76. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'

Regression?

Predates v1.4.23 and exists on main. It's likely always been there.

Severity

High (major functionality broken)

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions