Skip to content

repairToolPairs() corrupts thinking blocks → Anthropic 400 on long conversations #261

Description

@ichasco-heytrade

Description

Summary

repairToolPairs() partially mutates assistant messages that contain thinking blocks, causing Anthropic to reject the request with a 400 error. This manifests on conversations long enough for OpenCode's compaction to run.

Context

Today (June 30, 2026), Anthropic launched Claude Sonnet 5 as the default model across all plans: https://www.anthropic.com/news/claude-sonnet-5

Sonnet 5 uses adaptive thinking always on (effort: high by default). Every assistant response includes thinking blocks. Combined with OpenCode's automatic compaction (which prunes old messages and creates orphaned tool_use blocks), repairToolPairs() corrupts the thinking-block contract.

Error

messages.1.content.11: `thinking` or `redacted_thinking` blocks in the latest
assistant message cannot be modified. These blocks must remain as they were
in the original response.

Root cause

transformBody() calls repairToolPairs() on every request. The function scans all messages, finds orphaned tool_use / tool_result blocks and removes them from content[]:

const filtered = message.content.filter((block) => {
    // orphaned tool_use  → false (REMOVED)
    // orphaned tool_result → false (REMOVED)
    return true; // thinking blocks pass through untouched
});
return { ...message, content: filtered }; // ← new content[] = partial mutation

When an assistant message was originally [thinking, text, tool_use_1, tool_use_2] and its tool_result blocks were dropped by compaction, repairToolPairs() prunes the tool_use blocks but keeps the thinking blocks. The resulting content[] is [thinking, text] — a partial modification.

Anthropic's contract allows two things:

  • returning thinking blocks completely unmodified ✅
  • omitting them entirely from earlier turns ✅
  • but NOT partially rewriting the content[] that holds them ❌

Affected versions

Reproduced on 2.1.4 and 2.0.1repairToolPairs() exists in both. Downgrading does not help.

Suggested fix

When an assistant message contains thinking blocks and has tool_use blocks that would be pruned, omit the entire message instead of partially rewriting its content[] (Anthropic explicitly allows this):

// In repairToolPairs(), before the content filter:
const hasThinking = message.role === "assistant" && message.content.some(
    b => b.type === "thinking" || b.type === "redacted_thinking"
);
const hasToolUseToRemove = message.content.some((block) =>
    block.type === "tool_use" && typeof block.id === "string" && orphanedUses.has(block.id)
);

if (hasThinking && hasToolUseToRemove) {
    return null; // omit entire turn — allowed by Anthropic
}

…then filter out the null entries. Omitting the full turn is explicitly permitted; mutating it is not.

Workaround (for users hitting this now)

Disable automatic compaction temporarily:

"compaction": { "auto": false, "prune": false }

This prevents orphan generation but comes at the cost of unbounded context growth. Not a solution, just a stopgap.

Related

Steps to reproduce

  1. Use claude-sonnet-5 (adaptive thinking always on)
  2. Run an agentic session with several tool calls until compaction triggers
  3. Send another message → 400 Bad Request

Operating system

Linux

Plugin version

2.1.4

OpenCode version

No response

Debug logs

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions