nanobot/tests/providers/test_anthropic_thinking.py
chengyongru 356eeeb48c 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.
2026-08-04 13:04:42 +08:00

210 lines
7.2 KiB
Python

"""Tests for Anthropic provider thinking / reasoning_effort modes."""
from __future__ import annotations
from unittest.mock import patch
import pytest
from nanobot.providers.anthropic_provider import AnthropicProvider
def _make_provider(model: str = "claude-sonnet-4-6") -> AnthropicProvider:
with patch("anthropic.AsyncAnthropic"):
return AnthropicProvider(api_key="sk-test", default_model=model)
def _build(provider: AnthropicProvider, reasoning_effort: str | None, **overrides):
defaults = dict(
messages=[{"role": "user", "content": "hello"}],
tools=None,
model=None,
max_tokens=4096,
temperature=0.7,
reasoning_effort=reasoning_effort,
tool_choice=None,
supports_caching=False,
)
defaults.update(overrides)
return provider._build_kwargs(**defaults)
def test_adaptive_sets_type_adaptive() -> None:
kw = _build(_make_provider(), "adaptive")
assert kw["thinking"] == {"type": "adaptive"}
def test_adaptive_forces_temperature_one() -> None:
kw = _build(_make_provider(), "adaptive")
assert kw["temperature"] == 1.0
def test_adaptive_does_not_inflate_max_tokens() -> None:
kw = _build(_make_provider(), "adaptive", max_tokens=2048)
assert kw["max_tokens"] == 2048
def test_adaptive_no_budget_tokens() -> None:
kw = _build(_make_provider(), "adaptive")
assert "budget_tokens" not in kw["thinking"]
def test_high_uses_enabled_with_budget() -> None:
kw = _build(_make_provider(), "high", max_tokens=4096)
assert kw["thinking"]["type"] == "enabled"
assert kw["thinking"]["budget_tokens"] == max(8192, 4096)
assert kw["max_tokens"] >= kw["thinking"]["budget_tokens"] + 4096
def test_low_uses_small_budget() -> None:
kw = _build(_make_provider(), "low")
assert kw["thinking"] == {"type": "enabled", "budget_tokens": 1024}
def test_none_does_not_enable_thinking() -> None:
kw = _build(_make_provider(), None)
assert "thinking" not in kw
assert kw["temperature"] == 0.7
def test_empty_effort_does_not_enable_thinking() -> None:
kw = _build(_make_provider(), "")
assert "thinking" not in kw
assert kw["temperature"] == 0.7
def test_opus_4_7_omits_temperature_adaptive() -> None:
kw = _build(_make_provider("claude-opus-4-7"), "adaptive")
assert "temperature" not in kw
assert kw["thinking"] == {"type": "adaptive"}
def test_opus_4_7_high_uses_adaptive_effort() -> None:
kw = _build(_make_provider("claude-opus-4-7"), "high", max_tokens=4096)
assert "temperature" not in kw
assert kw["extra_body"]["thinking"] == {"type": "adaptive"}
assert kw["extra_body"]["output_config"] == {"effort": "high"}
assert kw["max_tokens"] == 4096
def test_opus_4_7_omits_temperature_none() -> None:
"""Without thinking, opus-4-7 must still omit temperature (API rejects it)."""
kw = _build(_make_provider("claude-opus-4-7"), None)
assert "temperature" not in kw
assert "thinking" not in kw
def test_opus_4_8_omits_temperature_adaptive() -> None:
kw = _build(_make_provider("claude-opus-4-8"), "adaptive")
assert "temperature" not in kw
def test_opus_4_8_high_uses_adaptive_effort() -> None:
kw = _build(_make_provider("claude-opus-4-8"), "high", max_tokens=4096)
assert "temperature" not in kw
assert kw["extra_body"]["thinking"] == {"type": "adaptive"}
assert kw["extra_body"]["output_config"] == {"effort": "high"}
def test_opus_4_8_omits_temperature_none() -> None:
kw = _build(_make_provider("claude-opus-4-8"), None)
assert "temperature" not in kw
def test_fable_omits_temperature_adaptive() -> None:
kw = _build(_make_provider("claude-fable-5"), "adaptive")
assert "temperature" not in kw
def test_fable_high_uses_adaptive_effort() -> None:
kw = _build(_make_provider("claude-fable-5"), "high", max_tokens=4096)
assert "temperature" not in kw
assert kw["extra_body"]["thinking"] == {"type": "adaptive"}
assert kw["extra_body"]["output_config"] == {"effort": "high"}
def test_fable_omits_temperature_none() -> None:
kw = _build(_make_provider("claude-fable-5"), None)
assert "temperature" not in kw
def test_sonnet_5_omits_temperature_adaptive() -> None:
kw = _build(_make_provider("claude-sonnet-5"), "adaptive")
assert "temperature" not in kw
assert kw["thinking"] == {"type": "adaptive"}
def test_sonnet_5_high_uses_adaptive_effort() -> None:
kw = _build(_make_provider("claude-sonnet-5"), "high", max_tokens=4096)
assert "temperature" not in kw
assert kw["extra_body"]["thinking"] == {"type": "adaptive"}
assert kw["extra_body"]["output_config"] == {"effort": "high"}
def test_sonnet_5_omits_temperature_none() -> 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:
kw = _build(_make_provider("claude-mythos-preview"), "high", max_tokens=4096)
assert "temperature" not in kw
assert kw["thinking"] == {"type": "enabled", "budget_tokens": 8192}
assert "extra_body" not in kw
@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)
assert kw["extra_body"]["thinking"] == {"type": "adaptive"}
assert kw["extra_body"]["output_config"] == {"effort": reasoning_effort}
assert kw["max_tokens"] == 4096
def test_dated_opus_5_model_uses_family_capabilities() -> None:
kw = _build(_make_provider("claude-opus-5-20260724"), "medium")
assert "temperature" not in kw
assert kw["extra_body"]["thinking"] == {"type": "adaptive"}
assert kw["extra_body"]["output_config"] == {"effort": "medium"}
def test_dated_opus_4_model_does_not_treat_date_as_minor_version() -> None:
kw = _build(_make_provider("claude-opus-4-20250514"), "high")
assert kw["temperature"] == 1.0
assert kw["thinking"] == {"type": "enabled", "budget_tokens": 8192}
assert "extra_body" not in kw
def test_ordinary_model_sends_temperature() -> None:
kw = _build(_make_provider("claude-sonnet-4-6"), None)
assert kw["temperature"] == 0.7
def test_reasoning_effort_string_none_does_not_enable_thinking() -> None:
"""reasoning_effort='none' must not enable thinking — treated same as disabled."""
kw = _build(_make_provider(), "none")
assert "thinking" not in kw
assert kw["temperature"] == 0.7