From 3dc148ab6f621bf8e0f689c3562d9db924b12767 Mon Sep 17 00:00:00 2001 From: Ahmad Ragab Date: Fri, 13 Mar 2026 03:21:13 +0100 Subject: [PATCH] fix: use adaptive thinking without budget_tokens for Claude 4.6 models For Claude 4.6 models (Opus and Sonnet), the Anthropic API rejects budget_tokens when thinking.type is 'adaptive'. This was causing a 400 error: 'thinking.adaptive.budget_tokens: Extra inputs are not permitted'. Changes: - Send thinking: {type: 'adaptive'} without budget_tokens for 4.6 - Move effort control to output_config: {effort: ...} per Anthropic docs - Map Hermes effort levels to Anthropic effort levels (xhigh->max, etc.) - Narrow adaptive detection to 4.6 models only (4.5 still uses manual) - Add tests for adaptive thinking on 4.6 and manual thinking on pre-4.6 Fixes #1126 --- agent/anthropic_adapter.py | 30 +++++++++++++++++++++++------- tests/test_anthropic_adapter.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index f00eb1c7..d604097d 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -25,6 +25,19 @@ except ImportError: logger = logging.getLogger(__name__) THINKING_BUDGET = {"xhigh": 32000, "high": 16000, "medium": 8000, "low": 4000} +ADAPTIVE_EFFORT_MAP = { + "xhigh": "max", + "high": "high", + "medium": "medium", + "low": "low", + "minimal": "low", +} + + +def _supports_adaptive_thinking(model: str) -> bool: + """Return True for Claude 4.6 models that support adaptive thinking.""" + return any(v in model for v in ("4-6", "4.6")) + # Beta headers for enhanced features (sent with ALL auth types) _COMMON_BETAS = [ @@ -398,20 +411,23 @@ def build_anthropic_kwargs( # Specific tool name kwargs["tool_choice"] = {"type": "tool", "name": tool_choice} - # Map reasoning_config to Anthropic's thinking parameter - # Newer models (4.6+) prefer "adaptive" thinking; older models use "enabled" + # Map reasoning_config to Anthropic's thinking parameter. + # Claude 4.6 models use adaptive thinking + output_config.effort. + # Older models use manual thinking with budget_tokens. if reasoning_config and isinstance(reasoning_config, dict): if reasoning_config.get("enabled") is not False: - effort = reasoning_config.get("effort", "medium") + effort = str(reasoning_config.get("effort", "medium")).lower() budget = THINKING_BUDGET.get(effort, 8000) - # Use adaptive thinking for 4.5+ models (they deprecate type=enabled) - if any(v in model for v in ("4-6", "4-5", "4.6", "4.5")): - kwargs["thinking"] = {"type": "adaptive", "budget_tokens": budget} + if _supports_adaptive_thinking(model): + kwargs["thinking"] = {"type": "adaptive"} + kwargs["output_config"] = { + "effort": ADAPTIVE_EFFORT_MAP.get(effort, "medium") + } else: kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget} # Anthropic requires temperature=1 when thinking is enabled on older models kwargs["temperature"] = 1 - kwargs["max_tokens"] = max(effective_max_tokens, budget + 4096) + kwargs["max_tokens"] = max(effective_max_tokens, budget + 4096) return kwargs diff --git a/tests/test_anthropic_adapter.py b/tests/test_anthropic_adapter.py index f2a48849..2d36cdd7 100644 --- a/tests/test_anthropic_adapter.py +++ b/tests/test_anthropic_adapter.py @@ -314,7 +314,7 @@ class TestBuildAnthropicKwargs: ) assert kwargs["model"] == "claude-sonnet-4-20250514" - def test_reasoning_config_maps_to_thinking(self): + def test_reasoning_config_maps_to_manual_thinking_for_pre_4_6_models(self): kwargs = build_anthropic_kwargs( model="claude-sonnet-4-20250514", messages=[{"role": "user", "content": "think hard"}], @@ -324,7 +324,34 @@ class TestBuildAnthropicKwargs: ) assert kwargs["thinking"]["type"] == "enabled" assert kwargs["thinking"]["budget_tokens"] == 16000 + assert kwargs["temperature"] == 1 assert kwargs["max_tokens"] >= 16000 + 4096 + assert "output_config" not in kwargs + + def test_reasoning_config_maps_to_adaptive_thinking_for_4_6_models(self): + kwargs = build_anthropic_kwargs( + model="claude-opus-4-6", + messages=[{"role": "user", "content": "think hard"}], + tools=None, + max_tokens=4096, + reasoning_config={"enabled": True, "effort": "high"}, + ) + assert kwargs["thinking"] == {"type": "adaptive"} + assert kwargs["output_config"] == {"effort": "high"} + assert "budget_tokens" not in kwargs["thinking"] + assert "temperature" not in kwargs + assert kwargs["max_tokens"] == 4096 + + def test_reasoning_config_maps_xhigh_to_max_effort_for_4_6_models(self): + kwargs = build_anthropic_kwargs( + model="claude-sonnet-4-6", + messages=[{"role": "user", "content": "think harder"}], + tools=None, + max_tokens=4096, + reasoning_config={"enabled": True, "effort": "xhigh"}, + ) + assert kwargs["thinking"] == {"type": "adaptive"} + assert kwargs["output_config"] == {"effort": "max"} def test_reasoning_disabled(self): kwargs = build_anthropic_kwargs(