#17324

fix(claude): content_filter stop_reason leaves a corrupted thinking block, causing a permanent 'cannot be modified' 400 retry loop (regression context: 3.8.52 / #15505 / #7899)

Openmrnicholasbcarter-codeopened 2d ago

OmniRoute Version

3.8.52 (also reproduced on a fresh reinstall; resolved by downgrading to 3.8.51)

Provider(s) Involved

claude (BYOK via local gateway)

Model(s) Involved

cc/claude-opus-5-5 and cc/claude-fable-5-1 (both affected). cx/gpt-6.1-sol, cx/gpt-6-luna, and other non-Anthropic models on the same install were unaffected throughout.

Client Tool

Prime Agent (prime-agent) 0.10.0, Rust build, extended thinking enabled, long multi-turn autonomous session (22k+ stored events), openai-completions API shape proxied to Anthropic.

Description

This looks like the combination of the fix in #15488 (merged as #15505) and the separate, known-distinct error class named in #7899.

#15505 made the empty-stream detector in shouldAbortEmptyClaudeStream() exempt end_turn/stop_sequence, but explicitly kept content_filter (and missing stop_reason) as a treated-as-failure case. In practice, on a long-running extended-thinking session, a turn occasionally ends with stop_reason: content_filter on completely ordinary engineering/security-review content (nothing resembling actual policy-violating material). That now surfaces to the client as an upstream failure with empty content:

"diagnostics":[{"type":"provider_stream_failure","error":{"message":"Provider finish_reason: content_filter"}}]

The client then retries by resending conversation history, including the thinking block from that content_filter-truncated turn. Since OmniRoute never delivered that turn's thinking block intact to begin with, Anthropic rejects the resend with the separate error class #7899 explicitly calls out as out of its own scope:

400 [400]: messages.N.content.M: `thinking` or `redacted_thinking` blocks in the latest assistant message
cannot be modified. These blocks must remain as they were in the original response.

From here the session is permanently wedged: every retry resends the same now-malformed history and gets the same 400, in a loop (observed recurring every ~40s across many hours). The only way out is forking the session to a point before the content_filter turn and continuing on a non-Anthropic model, or downgrading OmniRoute.

Steps to reproduce

  1. Run OmniRoute 3.8.52 with an Anthropic model (claude-opus-5-5 or claude-fable-5-1) and extended thinking enabled.
  2. Drive a long, multi-turn autonomous coding session (large accumulated tool-use history) until a turn's stop_reason comes back content_filter on ordinary content (we could not force this on demand; it surfaced naturally over ~9 days of a continual session, and also reproduced within minutes of a fresh fork of that session's history).
  3. Observe the Provider finish_reason: content_filter failure, then the subsequent thinking blocks ... cannot be modified 400 on the very next turn, which then repeats indefinitely.

Expected behavior

  • content_filter should not be treated identically to a dropped/empty stream when the upstream payload was otherwise seen (mirroring the end_turn/stop_sequence exemption from #15505) — or at minimum, when the client retries after a content_filter-truncated turn, OmniRoute should drop/sanitize that incomplete turn's thinking block from the replayed history rather than resending it as-is, the same way #7899 asks for completed-turn thinking removal on signature rejection.

Workaround

Downgrading the global install from omniroute@3.8.52 to omniroute@3.8.51 resolved it immediately for the same client/session/model combination. Non-Anthropic models (cx/gpt-6.1-sol, cx/gpt-6-luna) were never affected on either version, pointing at the Claude-specific empty/filtered-stream handling path specifically.

Related

  • #15488 / #15505 (content_filter explicitly left as a treated-as-failure case)
  • #7899 (names "the separate 'latest assistant message cannot be modified' error" as explicitly out of its own scope — this report is that error)

Activity

0

No activity yet.