Skip to content

fix: replace empty text in reasoning messages to preserve thinking block positions#21860

Open
chan1103 wants to merge 1 commit intoanomalyco:devfrom
chan1103:fix/preserve-thinking-signatures-v2
Open

fix: replace empty text in reasoning messages to preserve thinking block positions#21860
chan1103 wants to merge 1 commit intoanomalyco:devfrom
chan1103:fix/preserve-thinking-signatures-v2

Conversation

@chan1103
Copy link
Copy Markdown

@chan1103 chan1103 commented Apr 10, 2026

Issue for this PR

Closes #13286
Related: #18078, #16748, #21370, #16750, #18254

Type of change

  • Bug fix

What does this PR do?

Extended thinking sessions fail with "thinking blocks cannot be modified" when normalizeMessages removes empty text parts between reasoning blocks — shifting positions and breaking signatures.

While tracing that, I ran into two more constraints that altendky also documented in #16750:

  • there's another empty-text filter later in the AI SDK request path
  • Anthropic rejects text: "" in assistant history outright

I confirmed the API behavior with direct calls:

API test Result
text: "" in assistant history 400 — "text content blocks must be non-empty"
text: " " Accepted
text: "..." Accepted

So preserving the empty string wasn't enough even in the cases where I stopped normalizeMessages from removing it. What ended up working consistently was replacing those empty interstitial text parts with "...": it keeps the array shape the same, it survives the downstream filter, and the API accepts it. Unsigned empty text still gets removed like before.

I also checked real session data from a broken run and found three truly empty interstitial text parts inside a single assistant message, so this wasn't just a synthetic repro.

Relation to other work

I compared notes with #21370 and #16750 while working through this since they're in the same part of the pipeline. The extra piece I ran into here was the combination of downstream empty-text filtering plus Anthropic's non-empty text requirement, so this PR handles that path too.

How did you verify your code works?

Tests in transform.test.ts, each fails against unpatched code and passes with the fix:

  • empty text is replaced with a placeholder in signed reasoning messages
  • empty text is still removed in non-reasoning messages
  • trailing reasoning blocks still get text appended
  • unsigned empty parts are still removed

Full suite locally: 1937 pass, 0 fail.

Checklist

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

@github-actions github-actions bot added the needs:compliance This means the issue will auto-close after 2 hours. label Apr 10, 2026
@github-actions
Copy link
Copy Markdown
Contributor

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

Based on my search, I found several related PRs addressing similar issues. Here are the most relevant ones:

Directly Related (mentioned in the PR description):

  1. PR fix: preserve thinking block signatures and fix compaction headroom asymmetry #14393 - fix: preserve thinking block signatures and fix compaction headroom asymmetry #14393

    • "fix: preserve thinking block signatures and fix compaction headroom asymmetry"
    • Addresses cause 1 by removing the differentModel guard entirely. The current PR takes a narrower approach (reasoning only) and also covers causes 2 and 3.
  2. PR fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748) #16750 - fix(provider): skip empty-text filtering for assistant messages in normalizeMessages (#16748) #16750

Related to thinking block and reasoning content filtering:

  1. PR fix(opencode): filter empty text content blocks for all providers #17742 - fix(opencode): filter empty text content blocks for all providers #17742

    • "fix(opencode): filter empty text content blocks for all providers"
    • Addresses similar empty content filtering issues
  2. PR fix(provider): drop empty content messages after interleaved reasoning filter #17712 - fix(provider): drop empty content messages after interleaved reasoning filter #17712

    • "fix(provider): drop empty content messages after interleaved reasoning filter"
    • Related to empty reasoning parts handling
  3. PR fix(provider): preserve assistant message content when reasoning blocks present #21370 - fix(provider): preserve assistant message content when reasoning blocks present #21370

    • "fix(provider): preserve assistant message content when reasoning blocks present"
    • Directly relevant to preserving reasoning block integrity
  4. PR fix: strip reasoning parts when switching to non-interleaved models #11572 - fix: strip reasoning parts when switching to non-interleaved models #11572

    • "fix: strip reasoning parts when switching to non-interleaved models"
    • Related to model switching and reasoning content

The PR's description already acknowledges the relationship with #14393 and #12131, indicating these are complementary approaches to the same underlying issue.

@github-actions github-actions bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Apr 10, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@chan1103
Copy link
Copy Markdown
Author

CI note

The e2e (windows) failure is unrelated to this change — it's a runner infrastructure issue (ECONNRESET / timeouts). The same failure appears on recently merged PRs (#21827, #21803, #21796) as well.

All code-related checks pass:

  • ✅ unit (linux)
  • ✅ unit (windows)
  • ✅ e2e (linux)
  • ✅ typecheck
  • ✅ nix-eval

Happy to rebase or address any feedback if needed!

@chan1103 chan1103 force-pushed the fix/preserve-thinking-signatures-v2 branch from ebcaa01 to 6652498 Compare April 14, 2026 04:12
@chan1103
Copy link
Copy Markdown
Author

Rebased on latest dev and updated the PR:

  • Removed the trimEnd() fix — already landed in tweak: rm processor .trim calls #21958
  • Replaced the old normalizeMessages patch (which just preserved signed empty reasoning) with a more robust approach: empty text in signed reasoning messages is now replaced with "..." instead of removed. This keeps the array shape intact through the AI SDK's downstream filter and the API's non-empty text requirement.
  • Two commits, each reviewable independently.

@chan1103 chan1103 force-pushed the fix/preserve-thinking-signatures-v2 branch 2 times, most recently from f33041d to bcbf808 Compare April 14, 2026 04:34
@chan1103 chan1103 changed the title fix: preserve thinking block signatures across three independent corruption paths fix: preserve thinking block signatures across model switches and empty text filtering Apr 14, 2026
@rekram1-node
Copy link
Copy Markdown
Collaborator

  1. Model routing drops reasoning metadata (message-v2.ts)

The first one shows up when the current model doesn't match the model stored on the message, which can happen pretty easily with plugin-based routing. In that case, toModelMessages drops providerMetadata for every part, including reasoning parts. For Anthropic, that's where the thinking signature lives, so once that metadata is gone the next request is already broken.

The fix here is just to keep metadata on reasoning parts even when the model changes. Text and tool parts still keep the existing behavior. I was comfortable narrowing it that way because the metadata is already provider-scoped (anthropic: { signature }).

No you can't do this because a lot of models use anthropic messages api, for example minimax, if I started a session w/ minimax and then used anthropic models it would error, if this is due to "plugin based routing" I think ur plugin is bugged or we arent providing correct hooks for u

@chan1103
Copy link
Copy Markdown
Author

You're right — I went back and checked my sessions and differentModel was never actually true (same model throughout). This was a code-reading finding I didn't validate against real data. Dropping that commit and keeping just the empty text replacement fix.

…ock positions

normalizeMessages removes empty text parts, which shifts thinking block
positions and invalidates signatures. Simple preservation does not work
because the AI SDK has a second filter and the API rejects empty text.

In assistant messages with signed reasoning, replace empty text with a
placeholder instead of removing it. This preserves array positions through
all filtering layers.
@chan1103 chan1103 force-pushed the fix/preserve-thinking-signatures-v2 branch from bcbf808 to 3d85c39 Compare April 16, 2026 05:24
@chan1103 chan1103 changed the title fix: preserve thinking block signatures across model switches and empty text filtering fix: replace empty text in reasoning messages to preserve thinking block positions Apr 16, 2026
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

2 participants