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)#16750altendky wants to merge 5 commits intoanomalyco:devfrom
Conversation
…es (anomalyco#16748) Add failing test demonstrating that normalizeMessages() removes empty text parts between reasoning blocks in assistant messages, invalidating Anthropic thinking block signatures. The test constructs [reasoning(sig1), text(''), reasoning(sig2), text('...'), tool-call] and asserts all 5 parts are preserved. Currently fails with Expected length: 5, Received length: 4.
|
Thanks for your contribution! This PR doesn't have a linked issue. All PRs must reference an existing issue. Please:
See CONTRIBUTING.md for details. |
|
The following comment was made by an LLM, it may be inaccurate: Based on my search results, I found related PRs addressing thinking block and empty content filtering issues, but these appear to be related fixes rather than duplicates of the current test reproduction PR (#16750): Related PRs:
The current PR #16750 is a test reproduction case that demonstrates the issue described in #16748, whereas the related PRs are attempted fixes or related provider handling. No exact duplicate reproduction test was found. |
…rmalizeMessages Assistant messages must be replayed verbatim because Anthropic thinking block signatures encode positional context. Removing an empty text part between two reasoning blocks changes the block arrangement and invalidates the cryptographic signatures, causing the API to reject with 'thinking blocks cannot be modified'. The empty-text filter is still applied to user and tool messages where Anthropic rejects empty content. Update existing tests to reflect that assistant content is now preserved, and add tests for non-assistant filtering.
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
The previous fix preserved all assistant messages verbatim in normalizeMessages to protect thinking block signatures. This was too broad — assistant messages without reasoning blocks (e.g. compaction summaries) also had empty text parts preserved, causing Anthropic to reject with 'text content blocks must be non-empty'. Now only assistant messages with reasoning blocks are preserved verbatim. Assistant messages without reasoning have empty text blocks filtered normally. Also clean up empty text parts at the source: in the processor text-end handler, remove parts that end up empty (no text, no metadata) instead of persisting them. Parts with metadata (thinking signatures) are still preserved.
…elete race The text-start handler persists an empty part to the DB, then text-end would conditionally delete it. If the process crashes between the two, a dangling empty part remains. The transform.ts and message-v2.ts replay-time defenses already handle empty text parts correctly, making the processor-level cleanup redundant.
History of the Modified CodeTimeline
How the bug emergedThe empty-content filter from Jan 5 ( Once Opus 4.6 arrived, it began producing patterns like: The empty
PRs in flight
SummaryThe code being modified was never designed for the Opus 4.6 world. The empty-content filter ( |
Issue for this PR
Closes #16748
Related: #13286, #16246, #15074, #10970, #14716, #6176, #9364, #8010
Type of change
What does this PR do?
normalizeMessages()intransform.ts:62-66filters empty text parts from all message roles, including assistant. When an empty text part between two reasoning blocks with cryptographic signatures is removed, the positional context encoded in the signatures changes, and the Anthropic API rejects the replayed message with:Fix: Skip the empty-text/reasoning filter for assistant messages. Assistant messages must be replayed verbatim because Anthropic thinking block signatures are positionally sensitive. The filter is still applied to user and tool messages where Anthropic rejects empty content.
Changes:
transform.ts: Addif (msg.role === "assistant") return msgbefore the filter. Also simplified the remaining filter to only checktextparts (sincereasoningparts only appear in assistant messages, which are now skipped).transform.test.ts: Added a failing test reproducing the exact scenario (reasoning with signatures around an empty text part). Updated 5 existing tests whose expectations assumed filtering applied to assistant messages.How did you verify your code works?
[reasoning(sig1), text(""), reasoning(sig2), text("..."), tool-call], runs throughProviderTransform.message()with an Anthropic model, and asserts all 5 parts are preservedExpected length: 5, Received length: 4Checklist