fix(agent): downgrade xhigh→max on Anthropic pre-4.7 adaptive models
Regression from #11161 (Claude Opus 4.7 migration, commit 0517ac3e).
The Opus 4.7 migration changed `ADAPTIVE_EFFORT_MAP["xhigh"]` from "max"
(the pre-migration alias) to "xhigh" to preserve the new 4.7 effort level
as distinct from max. This is correct for 4.7, but Opus/Sonnet 4.6 only
expose 4 levels (low/medium/high/max) — sending "xhigh" there now 400s:
BadRequestError [HTTP 400]: This model does not support effort
level 'xhigh'. Supported levels: high, low, max, medium.
Users who set reasoning_effort=xhigh as their default (xhigh is the
recommended default for coding/agentic on 4.7 per the Anthropic migration
guide) now 400 every request the moment they switch back to a 4.6 model
via `/model` or config. Verified live against the Anthropic API on
`anthropic==0.94.0`.
Fix: make the mapping model-aware. Add `_supports_xhigh_effort()`
predicate (matches 4-7/4.7 substrings, mirroring the existing
`_supports_adaptive_thinking` / `_forbids_sampling_params` pattern).
On pre-4.7 adaptive models, downgrade xhigh→max (the strongest effort
those models accept, restoring pre-migration behavior). On 4.7+, keep
xhigh as a distinct level.
Per Anthropic's migration guide, xhigh is 4.7-only:
https://platform.claude.com/docs/en/about-claude/models/migration-guide
> Opus 4.7 effort levels: max, xhigh (new), high, medium, low.
> Opus 4.6 effort levels: max, high, medium, low.
SDK typing confirms: `anthropic.types.OutputConfigParam.effort: Literal[
"low", "medium", "high", "max"]` (v0.94.0 not yet updated for xhigh).
## Test plan
Verified live on macOS 15.5 / anthropic==0.94.0:
claude-opus-4-6 + effort=xhigh → output_config.effort=max → 200 OK
claude-opus-4-7 + effort=xhigh → output_config.effort=xhigh → 200 OK
claude-opus-4-6 + effort=max → output_config.effort=max → 200 OK
claude-opus-4-7 + effort=max → output_config.effort=max → 200 OK
`tests/agent/test_anthropic_adapter.py` — 120 pass (replaced 1 bugged
test that asserted the broken behavior, added 1 for 4.7 preservation).
Full adapter suite: 120 passed in 1.05s.
Broader suite (agent + run_agent + cli/gateway reasoning): 2140 passed
(2 pre-existing failures on clean upstream/main, unrelated).
## Platforms
Tested on macOS 15.5. No platform-specific code paths touched.
This commit is contained in:
@@ -30,9 +30,11 @@ logger = logging.getLogger(__name__)
|
|||||||
THINKING_BUDGET = {"xhigh": 32000, "high": 16000, "medium": 8000, "low": 4000}
|
THINKING_BUDGET = {"xhigh": 32000, "high": 16000, "medium": 8000, "low": 4000}
|
||||||
# Hermes effort → Anthropic adaptive-thinking effort (output_config.effort).
|
# Hermes effort → Anthropic adaptive-thinking effort (output_config.effort).
|
||||||
# Anthropic exposes 5 levels on 4.7+: low, medium, high, xhigh, max.
|
# Anthropic exposes 5 levels on 4.7+: low, medium, high, xhigh, max.
|
||||||
# We preserve xhigh as xhigh (the recommended default for coding/agentic on
|
# Opus/Sonnet 4.6 only expose 4 levels: low, medium, high, max — no xhigh.
|
||||||
# 4.7) and expose max as a distinct ceiling. "minimal" is a legacy alias that
|
# We preserve xhigh as xhigh on 4.7+ (the recommended default for coding/
|
||||||
# maps to low. See:
|
# agentic work) and downgrade it to max on pre-4.7 adaptive models (which
|
||||||
|
# is the strongest level they accept). "minimal" is a legacy alias that
|
||||||
|
# maps to low on every model. See:
|
||||||
# https://platform.claude.com/docs/en/about-claude/models/migration-guide
|
# https://platform.claude.com/docs/en/about-claude/models/migration-guide
|
||||||
ADAPTIVE_EFFORT_MAP = {
|
ADAPTIVE_EFFORT_MAP = {
|
||||||
"max": "max",
|
"max": "max",
|
||||||
@@ -43,6 +45,12 @@ ADAPTIVE_EFFORT_MAP = {
|
|||||||
"minimal": "low",
|
"minimal": "low",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Models that accept the "xhigh" output_config.effort level. Opus 4.7 added
|
||||||
|
# xhigh as a distinct level between high and max; older adaptive-thinking
|
||||||
|
# models (4.6) reject it with a 400. Keep this substring list in sync with
|
||||||
|
# the Anthropic migration guide as new model families ship.
|
||||||
|
_XHIGH_EFFORT_SUBSTRINGS = ("4-7", "4.7")
|
||||||
|
|
||||||
# Models where extended thinking is deprecated/removed (4.6+ behavior: adaptive
|
# Models where extended thinking is deprecated/removed (4.6+ behavior: adaptive
|
||||||
# is the only supported mode; 4.7 additionally forbids manual thinking entirely
|
# is the only supported mode; 4.7 additionally forbids manual thinking entirely
|
||||||
# and drops temperature/top_p/top_k).
|
# and drops temperature/top_p/top_k).
|
||||||
@@ -113,6 +121,17 @@ def _supports_adaptive_thinking(model: str) -> bool:
|
|||||||
return any(v in model for v in _ADAPTIVE_THINKING_SUBSTRINGS)
|
return any(v in model for v in _ADAPTIVE_THINKING_SUBSTRINGS)
|
||||||
|
|
||||||
|
|
||||||
|
def _supports_xhigh_effort(model: str) -> bool:
|
||||||
|
"""Return True for models that accept the 'xhigh' adaptive effort level.
|
||||||
|
|
||||||
|
Opus 4.7 introduced xhigh as a distinct level between high and max.
|
||||||
|
Pre-4.7 adaptive models (Opus/Sonnet 4.6) only accept low/medium/high/max
|
||||||
|
and reject xhigh with an HTTP 400. Callers should downgrade xhigh→max
|
||||||
|
when this returns False.
|
||||||
|
"""
|
||||||
|
return any(v in model for v in _XHIGH_EFFORT_SUBSTRINGS)
|
||||||
|
|
||||||
|
|
||||||
def _forbids_sampling_params(model: str) -> bool:
|
def _forbids_sampling_params(model: str) -> bool:
|
||||||
"""Return True for models that 400 on any non-default temperature/top_p/top_k.
|
"""Return True for models that 400 on any non-default temperature/top_p/top_k.
|
||||||
|
|
||||||
@@ -1392,8 +1411,13 @@ def build_anthropic_kwargs(
|
|||||||
"type": "adaptive",
|
"type": "adaptive",
|
||||||
"display": "summarized",
|
"display": "summarized",
|
||||||
}
|
}
|
||||||
|
adaptive_effort = ADAPTIVE_EFFORT_MAP.get(effort, "medium")
|
||||||
|
# Downgrade xhigh→max on models that don't list xhigh as a
|
||||||
|
# supported level (Opus/Sonnet 4.6). Opus 4.7+ keeps xhigh.
|
||||||
|
if adaptive_effort == "xhigh" and not _supports_xhigh_effort(model):
|
||||||
|
adaptive_effort = "max"
|
||||||
kwargs["output_config"] = {
|
kwargs["output_config"] = {
|
||||||
"effort": ADAPTIVE_EFFORT_MAP.get(effort, "medium"),
|
"effort": adaptive_effort,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget}
|
kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget}
|
||||||
|
|||||||
@@ -959,11 +959,13 @@ class TestBuildAnthropicKwargs:
|
|||||||
assert "temperature" not in kwargs
|
assert "temperature" not in kwargs
|
||||||
assert kwargs["max_tokens"] == 4096
|
assert kwargs["max_tokens"] == 4096
|
||||||
|
|
||||||
def test_reasoning_config_maps_xhigh_to_xhigh_effort_for_4_6_models(self):
|
def test_reasoning_config_downgrades_xhigh_to_max_for_4_6_models(self):
|
||||||
# Opus 4.7 added "xhigh" as a distinct effort level (the recommended
|
# Opus 4.7 added "xhigh" as a distinct effort level (low/medium/high/
|
||||||
# default for coding/agentic work). Earlier mapping aliased xhigh→max,
|
# xhigh/max). Opus 4.6 only supports low/medium/high/max — sending
|
||||||
# which silently over-efforted every request. 2026-04-16 migration
|
# "xhigh" there returns an API 400. Preserve the pre-migration
|
||||||
# guide: xhigh and max are distinct levels.
|
# behavior of aliasing xhigh→max on pre-4.7 adaptive models so users
|
||||||
|
# who prefer xhigh as their default don't 400 every request when
|
||||||
|
# switching back to 4.6.
|
||||||
kwargs = build_anthropic_kwargs(
|
kwargs = build_anthropic_kwargs(
|
||||||
model="claude-sonnet-4-6",
|
model="claude-sonnet-4-6",
|
||||||
messages=[{"role": "user", "content": "think harder"}],
|
messages=[{"role": "user", "content": "think harder"}],
|
||||||
@@ -972,6 +974,19 @@ class TestBuildAnthropicKwargs:
|
|||||||
reasoning_config={"enabled": True, "effort": "xhigh"},
|
reasoning_config={"enabled": True, "effort": "xhigh"},
|
||||||
)
|
)
|
||||||
assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
|
assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
|
||||||
|
assert kwargs["output_config"] == {"effort": "max"}
|
||||||
|
|
||||||
|
def test_reasoning_config_preserves_xhigh_for_4_7_models(self):
|
||||||
|
# On 4.7+ xhigh is a real level and the recommended default for
|
||||||
|
# coding/agentic work — keep it distinct from max.
|
||||||
|
kwargs = build_anthropic_kwargs(
|
||||||
|
model="claude-opus-4-7",
|
||||||
|
messages=[{"role": "user", "content": "think harder"}],
|
||||||
|
tools=None,
|
||||||
|
max_tokens=4096,
|
||||||
|
reasoning_config={"enabled": True, "effort": "xhigh"},
|
||||||
|
)
|
||||||
|
assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"}
|
||||||
assert kwargs["output_config"] == {"effort": "xhigh"}
|
assert kwargs["output_config"] == {"effort": "xhigh"}
|
||||||
|
|
||||||
def test_reasoning_config_maps_max_effort_for_4_7_models(self):
|
def test_reasoning_config_maps_max_effort_for_4_7_models(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user