From 356eeeb48cdaf37e423c5134c598d9c4314c3729 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Tue, 4 Aug 2026 13:04:42 +0800 Subject: [PATCH] fix(anthropic): honor disabled thinking on Opus 5 Send an explicit disabled thinking mode for default-on Opus and Sonnet 5 models while preserving unset provider defaults and minimum-SDK compatibility. --- nanobot/providers/anthropic_provider.py | 13 ++++++++++++- tests/providers/test_anthropic_thinking.py | 19 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/nanobot/providers/anthropic_provider.py b/nanobot/providers/anthropic_provider.py index 082d1e408..8c43ce562 100644 --- a/nanobot/providers/anthropic_provider.py +++ b/nanobot/providers/anthropic_provider.py @@ -41,6 +41,10 @@ _ADAPTIVE_ONLY_MIN_VERSIONS = { "fable": (5, 0), "mythos": (5, 0), } +_THINKING_DISABLE_MIN_VERSIONS = { + "opus": (5, 0), + "sonnet": (5, 0), +} _SAMPLING_DEPRECATED_MODELS = {"claude-mythos-preview"} @@ -606,7 +610,14 @@ class AnthropicProvider(LLMProvider): if system: kwargs["system"] = system - if reasoning_effort_lower == "adaptive": + if reasoning_effort_lower == "none" and _model_version_at_least( + model_name, _THINKING_DISABLE_MIN_VERSIONS + ): + # These models think by default, so omission would not honor an + # explicit request to disable thinking. Use extra_body for SDKs + # that predate the typed thinking parameter. + kwargs["extra_body"] = {"thinking": {"type": "disabled"}} + elif reasoning_effort_lower == "adaptive": # Adaptive thinking: model decides when and how much to think # Also auto-enables interleaved thinking between tool calls. kwargs["thinking"] = {"type": "adaptive"} diff --git a/tests/providers/test_anthropic_thinking.py b/tests/providers/test_anthropic_thinking.py index cbc823930..2e962175b 100644 --- a/tests/providers/test_anthropic_thinking.py +++ b/tests/providers/test_anthropic_thinking.py @@ -142,9 +142,10 @@ def test_sonnet_5_high_uses_adaptive_effort() -> None: def test_sonnet_5_omits_temperature_none() -> None: - kw = _build(_make_provider("anthropic/claude-sonnet-5"), None) + kw = _build(_make_provider("anthropic/claude-sonnet-5"), "none") assert "temperature" not in kw assert "thinking" not in kw + assert kw["extra_body"] == {"thinking": {"type": "disabled"}} def test_mythos_preview_omits_temperature_but_keeps_manual_budget() -> None: @@ -154,12 +155,26 @@ def test_mythos_preview_omits_temperature_but_keeps_manual_budget() -> None: assert "extra_body" not in kw -@pytest.mark.parametrize("reasoning_effort", [None, "adaptive", "low", "medium", "high", "xhigh", "max"]) +@pytest.mark.parametrize( + "reasoning_effort", [None, "none", "adaptive", "low", "medium", "high", "xhigh", "max"] +) def test_opus_5_omits_temperature(reasoning_effort: str | None) -> None: kw = _build(_make_provider("claude-opus-5"), reasoning_effort) assert "temperature" not in kw +def test_opus_5_none_disables_default_thinking() -> None: + kw = _build(_make_provider("claude-opus-5"), "none") + assert kw["extra_body"] == {"thinking": {"type": "disabled"}} + assert "thinking" not in kw + + +def test_opus_5_unset_preserves_provider_default() -> None: + kw = _build(_make_provider("claude-opus-5"), None) + assert "thinking" not in kw + assert "extra_body" not in kw + + @pytest.mark.parametrize("reasoning_effort", ["low", "medium", "high", "xhigh", "max"]) def test_opus_5_uses_adaptive_thinking_with_effort(reasoning_effort: str) -> None: kw = _build(_make_provider("claude-opus-5"), reasoning_effort, max_tokens=4096)