Skip to content

fix: preserve thinking block signatures and fix compaction headroom asymmetry#14393

Open
gnadaban wants to merge 1 commit intoanomalyco:devfrom
gnadaban:bugfix/fix-thinking-block-compaction
Open

fix: preserve thinking block signatures and fix compaction headroom asymmetry#14393
gnadaban wants to merge 1 commit intoanomalyco:devfrom
gnadaban:bugfix/fix-thinking-block-compaction

Conversation

@gnadaban
Copy link

@gnadaban gnadaban commented Feb 20, 2026

Issue for this PR

Closes #13286
Related: #10634, #8089, #12621, #8185

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Two compounding bugs cause sessions to crash with 'thinking' or 'redacted_thinking' blocks in the latest assistant message cannot be modified when compaction fires for models with extended thinking enabled (e.g. Claude Opus on Bedrock).

Bug 1 — Thinking block signature stripping (message-v2.ts)

toModelMessages() compares the current model against each historical message's original model via a differentModel guard. When they differ — which always happens during compaction since it uses its own model — all providerMetadata is stripped from message parts. This removes the cryptographic signature that Anthropic requires on thinking blocks, causing the API to reject the messages.

The fix removes the differentModel guard entirely and always passes providerMetadata / callProviderMetadata through. This is safe because:

  • Anthropic's docs explicitly recommend always passing thinking blocks back — the API filters them server-side
  • Provider metadata is namespaced (e.g. { anthropic: { signature: "..." } }), so other providers ignore unknown keys
  • For non-Anthropic providers, part.metadata is undefined, and the Vercel AI SDK's convertToModelMessages already null-checks before setting providerOptions (except reasoning parts, where it's a no-op)

Bug 2 — Compaction headroom asymmetry (compaction.ts)

isOverflow() had an asymmetric buffer calculation:

  • With limit.input: reserved = Math.min(20_000, maxOutputTokens) → only 20K headroom
  • Without limit.input: reserved = maxOutputTokens → full 32K headroom

This meant models with limit.input could grow ~12K tokens larger before compaction triggered, making context overflow more likely.

The fix removes the 20K cap and uses maxOutputTokens() (itself capped at 32K via OUTPUT_TOKEN_MAX) for both paths. Also fixed the non-input path to respect config.compaction.reserved — previously it hardcoded maxOutputTokens(), ignoring the user's configured override from #12924.

How did you verify your code works?

  • 49 tests pass across 3 test files (message-v2, compaction, revert-compact), 0 failures
  • 5 new tests for metadata preservation (same-model, different-model, text/reasoning/tool parts, undefined metadata)
  • 3 formerly-failing tests (marked BUG: in the test file) now pass with correct expectations
  • LSP diagnostics clean on all changed files
  • Typecheck passes across all 12 packages
  • Manually verified with lower token limits that Opus 4.6 and Sonnet 4.6 on AWS Bedrock compacts successfully

Screenshots / recordings

N/A — not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions
Copy link
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Related PRs Found:

  1. PR fix(provider): preserve redacted_thinking blocks and fix signature validation #12131 - "fix(provider): preserve redacted_thinking blocks and fix signature validation"

    • Related to thinking block preservation and signature handling, which is a core part of the current PR
  2. PR fix: compaction bugs #13946 and #13980 #14245 - "fix: compaction bugs Bug: opencode run exits after compaction when compaction model's token usage exceeds overflow threshold #13946 and Bug: compaction.reserved config is ignored for models without explicit 'input' limit #13980"

    • Addresses compaction-related issues, overlapping with the compaction headroom asymmetry fix in the current PR
  3. PR fix(opencode): prevent context overflow during compaction #11453 - "fix(opencode): prevent context overflow during compaction"

    • Related to compaction overflow prevention, potentially overlapping with the headroom fix

These PRs address similar domains (thinking block signatures and compaction behavior), though they may be addressing different specific bugs or prior versions of the same issues.

@gnadaban
Copy link
Author

Re: related PRs flagged by bot

Checked all three:

…symmetry

Two compounding bugs caused sessions to crash with 'thinking blocks cannot
be modified' when compaction fired for models with extended thinking:

1. toModelMessages() stripped providerMetadata (including cryptographic
   signatures) from message parts when the current model differed from the
   original. Anthropic's API requires signatures to be byte-identical.
   Fix: always pass providerMetadata through — the API handles filtering.

2. isOverflow() used an asymmetric buffer when limit.input was set
   (capped at 20K via COMPACTION_BUFFER) vs the full maxOutputTokens on
   the non-input path. This caused compaction to trigger too late.
   Fix: use maxOutputTokens (capped at 32K) for both paths. Also fixed
   the non-input path to respect config.compaction.reserved.
@gnadaban gnadaban force-pushed the bugfix/fix-thinking-block-compaction branch from fd795c0 to c045a8f Compare February 24, 2026 12:17
PrakharMNNIT added a commit to PrakharMNNIT/opencode that referenced this pull request Feb 25, 2026
…rategy UI

Root cause fix (from PR anomalyco#14393):
- Always pass providerMetadata for reasoning parts (removed differentModel guard)
- Always pass callProviderMetadata for tool parts
- Fix asymmetric compaction buffer (use maxOutputTokens consistently)

Configurable thinking strategy (none/strip/compact):
- Settings > General: Thinking Strategy dropdown
- Context tab: Always-visible strategy selector
- Error card: Retry buttons for thinking block errors
- Processor: Auto-compact on thinking error with compact strategy

Default 'none' preserves original behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude Opus 4.5 (latest) eventually fails: thinking block cannot be modified

1 participant