fix(anthropic): drop whitespace-only text blocks reaching the Messages API - #77509
Merged
kshitijk4poor merged 2 commits intoAug 3, 2026
Merged
Conversation
…s API
Root cause: two independent bugs in convert_messages_to_anthropic()
(agent/anthropic_adapter.py), the final conversion step before every
Anthropic messages.create() call, both producing HTTP 400 "text content
blocks must contain non-whitespace text":
1. _ensure_leading_user_turn() synthesized a filler user turn with
content [{"type": "text", "text": " "}] (a single space) whenever the
built payload didn't start with role=user (e.g. after context
compaction leaves a leading assistant summary). The space is itself
whitespace-only, so the guard traded a "leading assistant turn" 400
for the "text content blocks" 400 it now hits. Fixed to reuse the
existing non-blank _EMPTY_TEXT_PLACEHOLDER ("(empty)").
2. _convert_user_message() filtered blank text blocks from list-type
user content with an all-or-nothing check:
all(blank for b in blocks if b.type == "text"). This is vacuously
true when a message has zero text-type blocks (silently destroying
valid non-text blocks like images/documents it never inspected), and
false as soon as any single text block is non-blank — which let a
*sibling* blank text block sit untouched next to valid content and
reach Anthropic as-is. Replaced with per-block filtering (mirroring
the assistant-side logic already in _convert_assistant_message),
preserving all non-blank/non-text blocks and relocating any
cache_control marker carried by a dropped block.
Also added _scrub_blank_text_blocks(), a final defense-in-depth pass run
as the last step of convert_messages_to_anthropic() (after every other
transform, including nested tool_result content lists) so a blank text
block from any current or future producer never reaches the wire. It
logs only structural metadata (message index, role, content location,
block index/type) — never message text, tool arguments, tokens, or
credentials.
An earlier local patch to sanitize_api_messages() (agent_runtime_
helpers.py) attempted to fix this by rewriting blank assistant content
before the OpenAI->Anthropic conversion step, but the real leaks were
introduced downstream of that sanitizer, inside the Anthropic-specific
converter itself — the patch never touched the actual defect and has
been fully reverted (agent_runtime_helpers.py is back to its committed
state; verified via `git diff` showing no changes).
Verified against a real Telegram message end-to-end: the gateway no
longer produces the "text content blocks must contain non-whitespace
text" error on a fresh conversation turn.
Testing:
- 9 new end-to-end regression tests in test_anthropic_adapter.py
(TestFinalPayloadHasNoBlankTextBlocks) covering content="",
content=" ", content=[{"type":"text","text":""}], mixed blank+valid
text, blank text next to a valid tool block, an assistant tool-call
message with blank content, the leading-synthesized-user-turn case,
and a blank text block nested inside a tool_result's own content list.
- Fixed one pre-existing test that had asserted the broken " " filler
behavior as correct.
- Full tests/agent/ + tests/run_agent/ suite (4671 tests) run against
both the patched tree and a stashed pre-fix baseline: identical 148
pre-existing failures in both runs (unrelated subsystems — codex
app-server integration, credential-pool interrupt handling, OpenAI
client lifecycle), zero failures unique to either side.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…in_list _convert_user_message hand-inlined the same blank-text-filter + cache_control-relocation + placeholder-fallback logic that _fix_blank_text_blocks_in_list (added in the cherry-picked commit) implements as a reusable helper. Replace the inline copy with a call to the helper, eliminating ~35 lines of duplication. Follow-up fix on top of PR NousResearch#77134 by @pooyan6.
kshitijk4poor
enabled auto-merge (rebase)
August 3, 2026 08:44
This was referenced Aug 11, 2026
teknium1
pushed a commit
that referenced
this pull request
Aug 15, 2026
Residual from PR #70910 after #77509 landed the message-list scrub: a whitespace-only system content block carrying a cache_control marker still reached the wire and 400'd the whole request ("text content blocks must contain non-whitespace text"), wedging the session on every retry. The block cannot be dropped (it carries the cache breakpoint), so coerce its text to the shared non-whitespace placeholder when extracting the system param, copying the block so caller message dicts are never mutated. Adds SHL0MS's request-level regression suite from #70910; four of its five cases already pass on main via #77509 — the system-block case fails without this fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes two bugs in
convert_messages_to_anthropic()that each cause Anthropic HTTP 400 "text content blocks must contain non-whitespace text", permanently wedging sessions after context compaction or when tool results contain blank text blocks.Root cause
Two independent bugs in the final conversion step before every Anthropic
messages.create()call:_ensure_leading_user_turn()synthesized a filler user turn withtext: " "(whitespace-only) — the guard traded a "leading assistant turn" 400 for the "text content blocks" 400._convert_user_message()filtered blank text blocks with an all-or-nothingall()check — vacuouslyTruewhen there are zero text-type blocks (nuking valid images/documents), andFalseas soon as any single text block is non-blank (leaving sibling blank text blocks untouched on the wire).Changes
agent/anthropic_adapter.py:_ensure_leading_user_turn()uses_EMPTY_TEXT_PLACEHOLDER("(empty)") instead of" "._convert_user_message()does per-block filtering (dropping only blank text, preserving images/tool blocks). New_scrub_blank_text_blocks()final defense-in-depth pass runs last inconvert_messages_to_anthropic(), also scrubbing nestedtool_resultcontent.tests/agent/test_anthropic_adapter.py: 9 new end-to-end regression tests (TestFinalPayloadHasNoBlankTextBlocks) covering all blank-content shapes. One pre-existing test fixed to expect the non-blank placeholder.contributors/emails/pooyan6@gmail.com: AUTHOR_MAP entry for contributor @pooyan6.Follow-up fix (on top of contributor commit)
_convert_user_messagehand-inlined the same blank-text-filter + cache_control-relocation + placeholder-fallback logic that_fix_blank_text_blocks_in_list(added in the same PR) implements as a reusable helper. Replaced the inline copy (~35 lines) with a call to the helper, eliminating the duplication.Validation
Closes #77134
Salvage of @pooyan6's work — cherry-picked with authorship preserved.
Infographic