Skip to content

Compaction always fails on first attempt when session contains thinking blocks (Anthropic extended thinking) #31188

Closed as not planned

Description

@ulm0

Summary

Manual /compact always fails on first attempt with the following error when the session uses a model with extended thinking enabled:

undefined: The model returned the following errors:
messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest assistant
message cannot be modified. These blocks must remain as they were in the original response.

A second compaction on the now-empty session succeeds. The first one never does.

Environment

  • opencode version: 1.16.2
  • Provider: Amazon Bedrock (@ai-sdk/amazon-bedrock)
  • Model: claude-sonnet-4-6 — max variant (enables extended thinking / budget tokens)
  • Trigger: Manual /compact on any session where the last assistant turn produced thinking blocks

Root Cause

In packages/opencode/src/session/compaction.ts, processCompaction calls:

const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
  stripMedia: true,
  toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
})

Inside toModelMessagesEffect (message-v2.ts), each assistant message is checked against the compaction model:

const differentModel =
  `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}`

Because the session model is amazon-bedrock/claude-sonnet-4-6 (max) and the compaction agent typically resolves to a different model/provider, differentModel = true is almost always the case. This triggers the branch that strips providerMetadata from every part:

assistantMessage.parts.push({
  type: "text",
  text,
  ...(differentModel ? {} : { providerMetadata: part.metadata }),  // stripped when differentModel
})

For Anthropic models with extended thinking, providerMetadata on reasoning parts contains the cryptographic signature Anthropic requires to validate the block hasn't been tampered with. Stripping it causes the API to reject the message as "modified."

The error targets the latest assistant message in the array because that is the only position where Anthropic enforces signature validation.

Why the Second Compaction Succeeds

After the first failure, the new empty session has no prior assistant messages with thinking blocks — nothing to trigger the validation error.

Proposed Fix

Compaction models don't need internal reasoning content to summarize a conversation. The cleanest fix is a stripReasoning option in toModelMessagesEffect that skips reasoning parts entirely when building messages for compaction.

packages/opencode/src/session/message-v2.ts:

 options?: {
   stripMedia?: boolean
   toolOutputMaxChars?: number
+  stripReasoning?: boolean
 }

 // inside the assistant message parts loop:
+if (options?.stripReasoning && part.type === "reasoning") continue

packages/opencode/src/session/compaction.ts:

 const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, {
   stripMedia: true,
   toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS,
+  stripReasoning: true,
 })

Note: The same option should also be passed to the estimate() call inside select(), since that path also invokes toModelMessagesEffect.

Edge case: if an assistant message consists only of reasoning parts (no text, no tool calls), stripping them yields an empty parts array. The existing empty-message guard should handle this, but worth verifying.

Activity

  1. github-actions commented on Jun 7, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. tiffanychum commented on Jun 7, 2026

    @tiffanychum

    I'd like to work on this. Root cause: toModelMessagesEffect strips providerMetadata from reasoning parts when the compaction model differs from the session model, removing the Anthropic cryptographic signature. Fix: add a stripReasoning option that skips reasoning parts entirely during compaction. Changes in message-v2.ts and compaction.ts. PR coming shortly.

  3. Skeptomenos commented on Jun 23, 2026

    @Skeptomenos

    Same error on 1.17.4, but via the opposite branch — differentModel === false (same-model compaction)

    Confirming this still reproduces on 1.17.4 (Amazon Bedrock), but through a different path than the issue body describes. The current write-up only covers differentModel === true; the same-model case is also broken and I think it's worth documenting here.

    Repro:

    • Session model: amazon-bedrock/claude-opus-4-8, xhigh variant (extended thinking on).
    • No compaction agent configured, so the compaction model resolves to the triggering message's model:
      const model = agent.model
        ? getModel(agent.model.providerID, agent.model.modelID)
        : getModel(userMessage.model.providerID, userMessage.model.modelID) // ← same as the session model
    • /compact → 400 from Bedrock converse-stream:
      messages.35.content.1: `thinking` or `redacted_thinking` blocks in the latest assistant
      message cannot be modified. These blocks must remain as they were in the original response.
      
    • Retrying after switching the session model to claude-sonnet-4-6 → succeeds.

    Why it fails on the same-model path: because the compaction model == the model that produced the history, differentModel === false, so toModelMessagesEffect replays the stored reasoning parts as real reasoning/thinking blocks with their original bedrock.signature. The replayed selected.head ends on an assistant turn, so a thinking block lands in the "latest assistant message", and after the reorder/merge into Bedrock Converse format it's no longer the pristine first block (note the error points at content.1) → Anthropic rejects it.

    Why the model-switch retry succeeds (and a correction to the issue body): in 1.17.4 the differentModel === true branch no longer strips just the signature — it downgrades the whole reasoning part to plain text:

    if (part.type === "reasoning") {
      if (differentModel) {
        if (part.text.trim().length > 0) parts.push({ type: "text", text: part.text })
        continue                                   // ← thinking block dropped entirely
      }
      parts.push({ type: "reasoning", text: part.text, providerMetadata: part.metadata })
    }

    So switching models flips differentModel to true, strips the thinking blocks, and sidesteps the error. The v1.16.2 mechanism in the description (differentModel=true keeping an unsigned thinking block) appears fixed, but the differentModel === false / same-model case is still broken.

    This is more evidence that the proposed stripReasoning: true option for compaction is the right fix regardless of branch — reasoning content isn't needed to summarize the conversation, and stripping it unconditionally avoids both variants.

    (Confirmed by inspecting the shipped 1.17.4 build plus the stored session messages; the failing request had the thinking block at content[1] of the final assistant message, produced by the same Opus model used for compaction.)

  4. github-actions commented on Aug 23, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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