fix(openrouter): never send reasoning field for adaptive Anthropic models (#43012)

The previous fix (#42991) only omitted reasoning when it was being disabled.
But reasoning-mandatory Anthropic models (Claude 4.6+, fable) 400 with
thinking.type.disabled on EVERY tool-continuation turn even when reasoning is
enabled: chat_completions never replays signed thinking blocks, so the prior
assistant tool_call has no thinking, and OpenRouter resolves "reasoning
requested but history has none" by emitting thinking.type.disabled — which
these models reject. Result: first turn works, every turn after the first tool
call dies (HTTP 400, non-retryable).

OpenRouter ignores reasoning.effort for adaptive Anthropic models anyway (the
model self-decides), so the reasoning field is pointless for them on every turn
and harmful on tool-replay turns. Omit it entirely → adaptive default.

- openrouter profile: drop the reasoning field for reasoning-mandatory Anthropic
  models regardless of enabled/disabled; legacy Anthropic + non-Anthropic models
  unchanged.
- tests: assert omission across enabled/disabled/effort variants; parity tests
  switched to a non-Anthropic reasoning model (deepseek) since Anthropic 4.6+ no
  longer carries a reasoning field.

Verified live end-to-end: a tool-replay turn on anthropic/claude-fable-5 with
reasoning enabled now builds extra_body=None and returns HTTP 200 (was 400).
This commit is contained in:
Siddharth Balyan 2026-06-10 00:18:23 +05:30 committed by GitHub
parent ba44de06da
commit 46fedef07f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 59 additions and 27 deletions

View file

@ -117,19 +117,25 @@ class OpenRouterProfile(ProviderProfile):
"""
extra_body: dict[str, Any] = {}
if supports_reasoning:
if reasoning_config is not None:
cfg = dict(reasoning_config)
# Reasoning-mandatory Anthropic models (Claude 4.6+ / fable /
# future named models) have no "off" switch. Forwarding
# ``{enabled: false}`` makes OpenRouter emit Anthropic's manual
# ``thinking: {type: "disabled"}``, which those models reject
# with a non-retryable HTTP 400. Omit reasoning entirely so the
# model falls back to its default (adaptive) thinking instead.
disabling = cfg.get("enabled") is False or cfg.get("effort") == "none"
if disabling and _anthropic_reasoning_is_mandatory(model):
pass # leave reasoning unset → adaptive default
else:
extra_body["reasoning"] = cfg
# Reasoning-mandatory Anthropic models (Claude 4.6+ / fable /
# future named models) use *adaptive* thinking: the model decides
# how much to think, and OpenRouter ignores ``reasoning.effort`` for
# them entirely. Sending any ``reasoning`` field is therefore both
# pointless and actively harmful:
# - ``{enabled: false}`` → OpenRouter emits Anthropic's manual
# ``thinking: {type: "disabled"}``, which these models 400 on.
# - any enabled form, on a tool-continuation turn whose prior
# assistant tool_call carries no thinking block (chat_completions
# never replays signed thinking blocks), ALSO makes OpenRouter
# emit ``thinking: {type: "disabled"}`` → the same 400 on every
# turn after the first tool call.
# The only reliable behavior is to omit ``reasoning`` and let the
# model default to adaptive. See hermes-agent#42991 (disable case)
# and the tool-replay follow-up.
if _anthropic_reasoning_is_mandatory(model):
pass # omit reasoning entirely → adaptive default
elif reasoning_config is not None:
extra_body["reasoning"] = dict(reasoning_config)
else:
extra_body["reasoning"] = {"enabled": True, "effort": "medium"}