hermes - ✅(Solved) Fix anthropic_adapter: thinking-only guard silently wiped by merges 3x — add DO NOT REMOVE comment [4 pull requests, 2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#16823Fetched 2026-04-29 06:38:52
View on GitHub
Comments
2
Participants
3
Timeline
14
Reactions
0
Timeline (top)
cross-referenced ×4labeled ×4referenced ×3commented ×2

Root Cause

These are removed because they look like defensive boilerplate with no obvious reason. The fix: add a prominent comment block above each guard.

Fix Action

Fixed

PR fix notes

PR #16842: fix(anthropic): restore 3 thinking-only guards with DO NOT REMOVE comments

Description (problem / solution / changelog)

Problem

All three guards in convert_messages_to_anthropic() that prevent Anthropic HTTP 400 errors were silently removed by merges (3x in Apr 2026). Each removal caused fleet-wide 400s:

  • The final block in an assistant message cannot be thinking.
  • text content blocks must be non-empty
  • messages: final assistant content cannot end with trailing whitespace

Verification before this fix:

grep -c pattern agent/anthropic_adapter.py  # returns 0

Fix

Restore all 3 guards with prominent DO NOT REMOVE comment blocks referencing this issue.

  • Guard 1 (first-pass): inject companion . text when only thinking blocks present
  • Guard 2 (final-pass): second sweep after orphan-stripping passes that can strip Guard 1
  • Guard 3 (trailing whitespace): rstrip() every text block; replace empty with .

Verification after fix:

grep -c pattern agent/anthropic_adapter.py  # returns 3

Fixes #16823

Changed files

  • agent/anthropic_adapter.py (modified, +61/-0)
  • tools/delegate_tool.py (modified, +8/-0)

PR #16921: fix(anthropic): append stub text when final content block is thinking

Description (problem / solution / changelog)

Salvage of #13010 by @RichardHojunJang — cherry-picked onto current main with authorship preserved.

Summary

Anthropic rejects assistant messages whose final content block is thinking / redacted_thinking (HTTP 400: "The final block in an assistant message cannot be `thinking`"). This happens on thinking-prefill paths where the model produced reasoning but no visible text before a tool call. Append a (continued) stub text block in convert_messages_to_anthropic() so the turn ends on a valid block.

Changes

  • agent/anthropic_adapter.py: +13 LOC — stub-text append at first-pass assistant assembly
  • tests/agent/test_anthropic_adapter.py: +41 LOC — regression test
  • scripts/release.py: AUTHOR_MAP entry for @RichardHojunJang

Validation

BeforeAfter
tests/agent/test_anthropic_adapter.py -k thinking14 passed15 passed
New test test_trailing_thinking_block_gets_stub_text_appendedn/apass

Supersedes

  • #16842 (@ygd58): 3-guard defense-in-depth. Guard 1 crashes at runtime — _THINKING_TYPES is a local var defined further down the function. 10 thinking tests fail on that PR.
  • #11098 (@c0nSpIc0uS7uRk3r): 1 guard at a different (final-pass) site, no regression test.

#13010 is the cleanest of the three — single guard at the correct site, inline literal (no scoping bug), ships its own regression test.

Fixes #16823

Changed files

  • agent/anthropic_adapter.py (modified, +13/-0)
  • scripts/release.py (modified, +1/-0)
  • tests/agent/test_anthropic_adapter.py (modified, +41/-0)

PR #13010: fix(anthropic): append stub text when trailing content block is thinking

Description (problem / solution / changelog)

Problem

Anthropic's Messages API returns HTTP 400 with:

messages.N: The final block in an assistant message cannot be thinking``

This surfaces on thinking-prefill paths — when the model produces structured reasoning (reasoning_details) but no visible text content. In convert_messages_to_anthropic(), the assistant message ends up as:

[{type: "thinking", ...}]   # from reasoning_details
# content = '' → falsy → no text block appended
# no tool_calls → no tool_use block

The final block is thinking, which Anthropic rejects.

Fix

After assembling the effective content list in convert_messages_to_anthropic(), check if the last block has type thinking or redacted_thinking. If so, append a non-empty stub text block {"type": "text", "text": "(continued)"}.

  • Empty string is intentionally avoided — Anthropic also rejects empty text blocks (existing code uses "(empty)" placeholder for the same reason)
  • Only fires when the last block is a thinking type — [thinking, tool_use] is already valid and unaffected

Test

Added test_trailing_thinking_block_gets_stub_text_appended to TestThinkingBlockSignatureManagement — verifies the stub is appended and is non-empty.

Changed files

  • agent/anthropic_adapter.py (modified, +13/-0)
  • tests/agent/test_anthropic_adapter.py (modified, +41/-0)

PR #16959: fix(agent): drop thinking-only assistant turns before provider call

Description (problem / solution / changelog)

Summary

Adds a pre-call sanitizer that detects assistant turns containing only reasoning (no visible content, no tool_calls) and drops them from the wire copy. Adjacent user messages left behind are merged so role alternation stays intact.

Mirrors Claude Code's filterOrphanedThinkingOnlyMessages + mergeAdjacentUserMessages pattern (src/utils/messages.ts). Chosen after comparing against three contributor PRs (#11098, #13010, #16842) that tried to fix the same 400 class by fabricating stub text (. / (continued)) — which puts words in the model's mouth. Dropping the turn is honest; merging preserves the provider's invariant.

Changes

  • run_agent.py: +_is_thinking_only_assistant() detector, +_drop_thinking_only_and_merge_users() pass; wired in after _sanitize_api_messages in the main loop and in the iteration-limit-summary retry path
  • tests/run_agent/test_thinking_only_sanitizer.py: 25 unit tests

The stored conversation history (self.messages) is never mutated — only the per-call api_messages copy. Users still see the reasoning block in CLI/gateway transcripts; session persistence keeps the full trace.

Validation

Unit tests

tests/run_agent/test_thinking_only_sanitizer.py: 25 passed
tests/run_agent/ + tests/agent/test_anthropic_adapter.py: 1290 passed (2 pre-existing failures on main, unrelated)

Live E2E (poisoned history → clean response), 5 providers

Provider (via)Poisoned historyHappy path
OpenRouter → Anthropic claude-sonnet-4.6✓ 'Hello! Blue.'✓ '4'
OpenRouter → OpenAI gpt-5✓ 'Hi.\n\nBlue.'✓ '4'
OpenRouter → DeepSeek R1✓ 'Hello.\n\nBlue.'✓ '4'
OpenRouter → Qwen3-Max✓ 'Hello!\n\nBlue.'✓ '4'
Native Gemini 2.5-flash✓ 'Hello!\n\nBlue.'✓ '4'

Poisoned history = [user(A), assistant(empty + reasoning_content), user(B)]. Sanitizer drops the assistant turn and merges users → [user(A + B)]. Verified via trace: 4 input messages → 2 output messages, and the merged content contains both original user texts concatenated with \\n\\n. Happy path verifies it's a noop when no thinking-only turn exists.

Related

  • #16823 (wontfix) — the stub-text approach these PRs tried
  • Closed: #11098, #13010, #16842 — all three fabricated text; this PR implements the alternative

Changed files

  • run_agent.py (modified, +153/-0)
  • tests/run_agent/test_thinking_only_sanitizer.py (added, +249/-0)

Code Example

# ── DO NOT REMOVEAnthropic API hard requirement ───────────────────────────
# Anthropic rejects assistant messages where the final block is a thinking
# block (HTTP 400: "The final block in an assistant message cannot be
# `thinking`."). This guard has been silently wiped by merges 3x (Apr 2026)
# and causes fleet-wide 400s every time. The companion "." text block is the
# minimal safe value — empty string and whitespace-only are also rejected.
# See skill: hermes-anthropic-thinking-block-400
# ─────────────────────────────────────────────────────────────────────────────

---

grep -c '_has_thinking and not _has_non_thinking\|_has_thinking_final and not _has_non_thinking_final\|b\["text"\].rstrip()' \
  agent/anthropic_adapter.py
# Must print 3. If < 3, guards are missing.

---

python -m pytest tests/agent/test_anthropic_adapter.py -k 'thinking' -x -q
# All thinking tests must pass.
RAW_BUFFERClick to expand / collapse

Problem

The three guards in agent/anthropic_adapter.py that prevent Anthropic HTTP 400 errors have been silently removed by merges three times (April 2026). Each time they're dropped, every Anthropic agent in the fleet starts 400-ing mid-conversation with one of:

  • The final block in an assistant message cannot be thinking.
  • text content blocks must be non-empty
  • messages: final assistant content cannot end with trailing whitespace

The Three Guards

All three live near the end of convert_messages_to_anthropic():

Guard 1 — First-pass thinking-only (~line 1283): After assembling effective blocks, if the list contains thinking blocks but no text/tool_use block, inject a companion {"type": "text", "text": "."}. Covers the case where the model emitted only reasoning with no visible output.

Guard 2 — Final-pass thinking-only (~line 1521): After orphan-stripping and signature-management passes have run, a second sweep for the same condition. Those passes can strip the text blocks that Guard 1 added, leaving a thinking-only message again.

Guard 3 — Trailing whitespace (~line 1537): Anthropic also rejects text blocks whose content ends with whitespace. rstrip() every text block in every assistant message; replace with "." if stripping yields empty.

Recurrence Pattern

These are removed because they look like defensive boilerplate with no obvious reason. The fix: add a prominent comment block above each guard.

Proposed Comment Block

# ── DO NOT REMOVE — Anthropic API hard requirement ───────────────────────────
# Anthropic rejects assistant messages where the final block is a thinking
# block (HTTP 400: "The final block in an assistant message cannot be
# `thinking`."). This guard has been silently wiped by merges 3x (Apr 2026)
# and causes fleet-wide 400s every time. The companion "." text block is the
# minimal safe value — empty string and whitespace-only are also rejected.
# See skill: hermes-anthropic-thinking-block-400
# ─────────────────────────────────────────────────────────────────────────────

Apply to all three guard sites.

Verification

grep -c '_has_thinking and not _has_non_thinking\|_has_thinking_final and not _has_non_thinking_final\|b\["text"\].rstrip()' \
  agent/anthropic_adapter.py
# Must print 3. If < 3, guards are missing.
python -m pytest tests/agent/test_anthropic_adapter.py -k 'thinking' -x -q
# All thinking tests must pass.

extent analysis

TL;DR

Add prominent comment blocks above each of the three guards in agent/anthropic_adapter.py to prevent their removal and ensure Anthropic API compatibility.

Guidance

  • Apply the proposed comment block to all three guard sites in agent/anthropic_adapter.py to clearly document their purpose and importance.
  • Verify the presence of the guards using the provided grep command, which should print 3 if all guards are present.
  • Run the pytest command to ensure all thinking tests pass, confirming the guards are functioning correctly.
  • Review the code history to understand why these guards were removed in the past and consider adding additional safeguards to prevent similar issues in the future.

Example

The proposed comment block can be applied to each guard site, such as:

# ── DO NOT REMOVE — Anthropic API hard requirement ───────────────────────────
# Anthropic rejects assistant messages where the final block is a thinking
# block (HTTP 400: "The final block in an assistant message cannot be
# `thinking`."). This guard has been silently wiped by merges 3x (Apr 2026)
# and causes fleet-wide 400s every time. The companion "." text block is the
# minimal safe value — empty string and whitespace-only are also rejected.
# See skill: hermes-anthropic-thinking-block-400
# ─────────────────────────────────────────────────────────────────────────────
if _has_thinking and not _has_non_thinking:
    # ...

Notes

The provided solution focuses on preventing the removal of the guards rather than addressing the underlying issue that led to their removal. It is essential to review the code history and development process to ensure that similar issues do not occur in the future.

Recommendation

Apply the workaround by adding the proposed comment blocks to the three guard sites, as this will provide a clear indication of their importance and prevent their removal.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix anthropic_adapter: thinking-only guard silently wiped by merges 3x — add DO NOT REMOVE comment [4 pull requests, 2 comments, 3 participants]