Prerequisites
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:
- Text content (from
content)
toolUse (from tool_calls)
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
- 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).
- 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.
- Receive the first response. It will contain a
tool_use (correct).
- 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.
- 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)
Prerequisites
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
toolUsecoming beforereasoningContent. This causes the request to fail with an HTTP 400 error from AWS Bedrock with the following content:More specifically
core/providers/bedrock/utils.go::convertMessageappends content blocks in this order:content)toolUse(fromtool_calls)reasoningContent(fromreasoning_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 atcore/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
transports/v1.5.3and on currentmain(b287d55).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.reasoning_detailsextension. Reproduced viapydantic-ai'sOpenAIChatModelagainst Bifrost's/v1/chat/completionswith a small_map_model_responseshim that attachesreasoning_detailscarryingThinkingPart.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
anthropic.claude-sonnet-4-6,anthropic.claude-sonnet-4-5-20250929-v1:0)./v1/chat/completionsrequest withreasoning_effort: "low"(or any non-noneeffort), one or more tools defined, and a user prompt that the model will answer by calling a tool.tool_use(correct).assistantturn included (containingcontent+tool_calls+reasoning_details[].signaturefrom step 3) followed by atoolmessage carrying the result.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[].signaturefrom 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::TestMultiTurnReasoningContentPassthroughcovers passthrough but doesn't assert ordering.Root cause
core/providers/bedrock/utils.go#L737-L778(onmain; 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, mirroringanthropic/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::TestMultiTurnReasoningContentPassthroughcurrently asserts only that areasoningContentblock 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 resultingBedrockMessage.Content[0].ReasoningContentis 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
Relevant logs/output
Regression?
Predates v1.4.23 and exists on main. It's likely always been there.
Severity
High (major functionality broken)