Skip to content

Claude Extended Thinking: assistant message content order causes API error #9364

@Soein

Description

@Soein

Bug Description

When using Claude models with Extended Thinking enabled through a proxy (e.g., via @ai-sdk/anthropic), multi-turn conversations with tool use fail with the following error:

messages.45.content.0.type: Expected 'thinking' or 'redacted_thinking', but found 'tool_use'

Root Cause

Claude Extended Thinking requires that assistant messages must start with a thinking or redacted_thinking block when thinking is enabled. However, OpenCode's message reconstruction does not guarantee this ordering.

Code Analysis

In packages/opencode/src/session/message-v2.ts, the toModelMessage function builds message parts in iteration order:

for (const part of msg.parts) {
  if (part.type === "text") /* add text */
  if (part.type === "tool") /* add tool */  
  if (part.type === "reasoning") /* add reasoning */
}

The parts are sorted by their generated ID:

result.sort((a, b) => (a.id > b.id ? 1 : -1))

In packages/opencode/src/provider/transform.ts, the normalizeMessages function for Anthropic only filters empty content but does not reorder parts to ensure reasoning blocks come first:

if (model.api.npm === "@ai-sdk/anthropic") {
  msgs = msgs
    .map((msg) => {
      // ... only filters empty text/reasoning parts
      const filtered = msg.content.filter((part) => {
        if (part.type === "text" || part.type === "reasoning") {
          return part.text !== ""
        }
        return true
      })
      // ...
    })
}

Reproduction Steps

  1. Configure a Claude model with Extended Thinking enabled (e.g., claude-sonnet-4-5 with thinking.budgetTokens > 0)
  2. Start a conversation that triggers tool use
  3. Continue the conversation for multiple turns
  4. Eventually, the API returns the error about unexpected tool_use at position 0

Proposed Fix

Add reordering logic in normalizeMessages for Anthropic models to ensure reasoning parts always come before other content types:

if (model.api.npm === "@ai-sdk/anthropic") {
  msgs = msgs.map((msg) => {
    if (msg.role === "assistant" && Array.isArray(msg.content)) {
      // Ensure reasoning blocks come first (Claude Extended Thinking requirement)
      const reasoningParts = msg.content.filter(p => p.type === "reasoning")
      const otherParts = msg.content.filter(p => p.type !== "reasoning")
      return { ...msg, content: [...reasoningParts, ...otherParts] }
    }
    return msg
  })
  // ... existing empty content filtering
}

Environment

  • OpenCode version: latest
  • Provider: Custom proxy using Anthropic-compatible API
  • Model: Claude models with Extended Thinking enabled

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions