Skip to content

fix: strip empty text blocks from assistant content before Bedrock/Anthropic send - #57985

Open
strye wants to merge 1 commit into
NousResearch:mainfrom
strye:fix/bedrock-empty-text-block-crash
Open

fix: strip empty text blocks from assistant content before Bedrock/Anthropic send#57985
strye wants to merge 1 commit into
NousResearch:mainfrom
strye:fix/bedrock-empty-text-block-crash

Conversation

@strye

@strye strye commented Jul 4, 2026

Copy link
Copy Markdown

Summary

Fixes a crash where a turn dies with a non-retryable HTTP 400: messages: text content blocks must be non-empty error from Bedrock/Anthropic.

Root cause

_convert_assistant_message() in agent/anthropic_adapter.py guards against sending empty assistant content with:

effective = blocks or content
if not effective or effective == "":
    effective = [{"type": "text", "text": "(empty)"}]

This checks whether effective (a list) is truthy, not whether the text inside its blocks is non-empty. When the model returns assistant content shaped like [{"type": "text", "text": ""}] — e.g. right after a tool call errors out (observed after a failed clarify call with no question text) — that list is non-empty, so the guard passes it through untouched. Bedrock/Anthropic then reject the empty text block outright, and Hermes treats the resulting 400 as non-retryable, killing the turn.

Fix

Filter out text blocks whose text is empty/whitespace-only before the emptiness check, so a genuinely-empty result still falls through to the existing "(empty)" placeholder — otherwise the real content is preserved unchanged.

Repro

from agent.anthropic_adapter import _convert_assistant_message

_convert_assistant_message({"role": "assistant", "content": [{"type": "text", "text": ""}]})
# before: {"role": "assistant", "content": [{"type": "text", "text": ""}]}   -> 400 from API
# after:  {"role": "assistant", "content": [{"type": "text", "text": "(empty)"}]}

Test plan

  • Ran tests/agent/test_anthropic_adapter.py (173 tests) before and after the change: 170 passed / 3 failed both times (the 3 failures are pre-existing and unrelated — TestRunOauthSetupToken, a MagicMock/json.loads issue, confirmed present on unmodified main too).
  • Manually verified the repro above, plus sanity-checked normal text content and fully-empty content still behave as before.

…thropic send

_convert_assistant_message()'s empty-content guard checked list
truthiness (`blocks or content`), not whether the text inside those
blocks was actually non-empty. When the model returns assistant content
shaped like [{"type": "text", "text": ""}] (e.g. immediately after a
failed clarify tool call), that non-empty *list* containing an empty
*text block* passed the guard untouched and was sent straight to
Bedrock/Anthropic, which reject empty text blocks with HTTP 400
'text content blocks must be non-empty'. Hermes treats this 400 as a
non-retryable client error, killing the turn outright.

Fix: filter out text blocks with empty/whitespace-only text before the
emptiness check, so a genuinely-empty result still falls through to the
existing '(empty)' placeholder.

Repro:
  _convert_assistant_message({'role': 'assistant', 'content': [{'type': 'text', 'text': ''}]})
  # before: {'role': 'assistant', 'content': [{'type': 'text', 'text': ''}]}  -> 400 from API
  # after:  {'role': 'assistant', 'content': [{'type': 'text', 'text': '(empty)'}]}

Verified against tests/agent/test_anthropic_adapter.py (173 tests):
170 passed / 3 pre-existing unrelated failures, same before and after.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API provider/bedrock AWS Bedrock (boto3, IAM) P2 Medium — degraded but workaround exists labels Jul 4, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for isolating the truthiness failure in the normal assistant-content path; that behavior is still present on current main at agent/anthropic_adapter.py:1982-1986 and :2032-2035.

Problems

  • The patch only filters the normal tail. Ordered replay returns earlier at agent/anthropic_adapter.py:1938-1979, while _sanitize_replay_block() reconstructs text from b.get("text", "") at :1874-1883; empty replayed text blocks therefore remain sendable. The cross-referenced fix(anthropic): filter blank text blocks in both normal and replay paths #63228 identifies this same second path.
  • The new filter runs after _apply_assistant_cache_control_to_last_cacheable_block(). Since agent/prompt_caching.py:46-49 marks the final content part, dropping that blank part can discard a cache breakpoint instead of transferring it to the fallback block.

Suggested changes

  • Normalize blank text before cache-control placement in the normal path and reject it in _sanitize_replay_block() for ordered replay.
  • Add regression coverage for normal and replayed empty/whitespace-only blocks, plus a cached blank-only assistant turn.

Automated hermes-sweeper review.

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API provider/bedrock AWS Bedrock (boto3, IAM) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants