-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
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
- Configure a Claude model with Extended Thinking enabled (e.g.,
claude-sonnet-4-5withthinking.budgetTokens > 0) - Start a conversation that triggers tool use
- Continue the conversation for multiple turns
- Eventually, the API returns the error about unexpected
tool_useat 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