fix(anthropic): non-whitespace placeholder for synthetic leading user turn - #70159
Conversation
… turn _ensure_leading_user_turn prepends a synthetic user message when the first message is assistant (happens after a second context compaction on the auto path, where the summary is emitted as role=assistant). The placeholder text was a single space " ". Anthropic's own API tolerates whitespace-only text blocks, but stricter Anthropic-compatible endpoints (e.g. zenmux.ai) reject them with HTTP 400 invalid_params: messages: text content blocks must contain non-whitespace text which is non-retryable and bricks every subsequent turn in any conversation that has compacted twice. Use a minimal non-blank placeholder "." instead. Tests updated from freezing the exact placeholder byte to asserting the behavior contract (leading turn is role=user with non-whitespace text).
Duplicate of #70081: both patches replace the whitespace-only synthesized Anthropic leading user turn with a printable placeholder to prevent the same strict-endpoint HTTP 400. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for identifying the assembly-time gap. The premise is confirmed on current main: agent/anthropic_adapter.py:2612-2613 still synthesizes a leading user block containing a single space, after the per-message conversion and filtering chain (agent/anthropic_adapter.py:2672-2674).
Suggested changes
- During salvage, use the existing
_EMPTY_TEXT_PLACEHOLDERrather than a new"."literal. Current main defines that canonical non-whitespace value atagent/anthropic_adapter.py:1931and uses it in the existing whitespace-safety path atagent/anthropic_adapter.py:1950. - Update the current leading-assistant regression at
tests/agent/test_anthropic_adapter.py:871-884to assert non-whitespace behavior; it still freezes the single-space value.
This is an automated hermes-sweeper review.
| """ | ||
| if result and result[0].get("role") != "user": | ||
| result.insert(0, {"role": "user", "content": [{"type": "text", "text": " "}]}) | ||
| # ponytail: some Anthropic-compatible endpoints (e.g. zenmux) reject a |
There was a problem hiding this comment.
Current main already has _EMPTY_TEXT_PLACEHOLDER for this exact non-whitespace invariant. Please use that canonical constant here rather than introducing a second placeholder literal.
Summary
_ensure_leading_user_turn(inagent/anthropic_adapter.py) prepends a synthetic user message whenmessages[0]is notrole=user. This happens after a second context compaction on the auto path, where the summary is emitted asrole=assistantwith nothing in front of it (the system prompt lives outsidemessages[]), somessages[0]ends up assistant and the Messages API would 400.The synthetic placeholder used a single space
" "as its text.The bug
Anthropic's own API tolerates a whitespace-only text block, but stricter Anthropic-compatible endpoints reject it. Observed against
zenmux.ai(/api/anthropic):This error is non-retryable, so once a conversation has compacted twice, every subsequent turn fails permanently. In our case a Feishu DM session silently stopped replying after auto-compaction — each inbound message hit the 400 and never recovered.
Fix
Use a minimal non-blank placeholder
"."instead of" ". One-line change in the shared helper, so every caller (all provider paths that route throughconvert_messages_to_anthropic) is fixed at once.Tests
The two existing tests froze the exact placeholder byte (
== [{"type": "text", "text": " "}]) — change-detector shape. Updated them to assert the behavior contract instead: the prepended leading turn isrole=userwith non-whitespace text. This keeps them meaningful regardless of the exact placeholder character.(3 unrelated
TestRunOauthSetupTokenfailures are pre-existing onmain, verified by stashing this change.)