From 61522e103e61601c553b4544abcd53fa7ebf9f1d Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 17 May 2026 07:54:06 +0000 Subject: [PATCH] fix(llm): skip Anthropic effort kwarg on non-supporting models (#831) Haiku 4.5 rejects the effort parameter with 400. AnthropicClient.get_llm() now drops effort when the model isn't in the supported set (Opus 4.5+, Sonnet 4.5+, mythos-preview). Forward-compat regex catches future claude-{opus,sonnet}-X-Y releases automatically; Haiku and unknown models stay excluded conservatively. 14 tests cover Haiku exclusion, current Opus/Sonnet inclusion, future- version inheritance via pattern, mythos-preview, unknown-default exclusion, and other passthrough kwargs surviving the effort-skip path. --- tests/test_anthropic_effort.py | 84 +++++++++++++++++++ tradingagents/llm_clients/anthropic_client.py | 24 +++++- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 tests/test_anthropic_effort.py diff --git a/tests/test_anthropic_effort.py b/tests/test_anthropic_effort.py new file mode 100644 index 000000000..bdef7317f --- /dev/null +++ b/tests/test_anthropic_effort.py @@ -0,0 +1,84 @@ +"""Tests for Anthropic effort-parameter gating (#831). + +Haiku 4.5 (and current Haiku versions) reject the ``effort`` parameter +with a 400. Opus 4.5+ and Sonnet 4.5+ accept it. The gate uses a +forward-compat regex so future ``claude-{opus,sonnet}-X-Y`` releases +inherit support automatically. +""" + +import pytest + +from tradingagents.llm_clients import anthropic_client as mod + + +def _capture_kwargs(monkeypatch): + captured: dict = {} + monkeypatch.setattr( + mod, "NormalizedChatAnthropic", + lambda **kwargs: captured.setdefault("kwargs", kwargs), + ) + return captured + + +@pytest.mark.unit +class TestEffortGate: + @pytest.mark.parametrize( + "model", + ["claude-haiku-4-5", "claude-haiku-5-0", "claude-haiku-4-7-preview"], + ) + def test_haiku_does_not_receive_effort(self, monkeypatch, model): + captured = _capture_kwargs(monkeypatch) + mod.AnthropicClient(model=model, effort="medium", api_key="x").get_llm() + assert "effort" not in captured["kwargs"] + + @pytest.mark.parametrize( + "model", + [ + "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", + "claude-sonnet-4-5", "claude-sonnet-4-6", + ], + ) + def test_current_opus_and_sonnet_receive_effort(self, monkeypatch, model): + captured = _capture_kwargs(monkeypatch) + mod.AnthropicClient(model=model, effort="high", api_key="x").get_llm() + assert captured["kwargs"]["effort"] == "high" + + @pytest.mark.parametrize( + "model", + ["claude-opus-5-0", "claude-opus-4-8", "claude-sonnet-5-0"], + ) + def test_future_opus_sonnet_inherit_effort_via_pattern(self, monkeypatch, model): + """Forward-compat: new Opus/Sonnet versions don't need a code change.""" + captured = _capture_kwargs(monkeypatch) + mod.AnthropicClient(model=model, effort="low", api_key="x").get_llm() + assert captured["kwargs"]["effort"] == "low" + + def test_mythos_preview_receives_effort(self, monkeypatch): + captured = _capture_kwargs(monkeypatch) + mod.AnthropicClient( + model="claude-mythos-preview", effort="medium", api_key="x" + ).get_llm() + assert captured["kwargs"]["effort"] == "medium" + + def test_unknown_anthropic_model_does_not_receive_effort(self, monkeypatch): + """Default is conservative — unknown models don't get effort to avoid 400s.""" + captured = _capture_kwargs(monkeypatch) + mod.AnthropicClient( + model="claude-experimental-x", effort="medium", api_key="x" + ).get_llm() + assert "effort" not in captured["kwargs"] + + def test_other_kwargs_still_forwarded_when_effort_skipped(self, monkeypatch): + """Skipping effort must not break other passthrough kwargs.""" + captured = _capture_kwargs(monkeypatch) + mod.AnthropicClient( + model="claude-haiku-4-5", + effort="medium", + api_key="placeholder", + max_tokens=1024, + timeout=30, + ).get_llm() + assert captured["kwargs"]["api_key"] == "placeholder" + assert captured["kwargs"]["max_tokens"] == 1024 + assert captured["kwargs"]["timeout"] == 30 + assert "effort" not in captured["kwargs"] diff --git a/tradingagents/llm_clients/anthropic_client.py b/tradingagents/llm_clients/anthropic_client.py index ae2c367a9..e0054f703 100644 --- a/tradingagents/llm_clients/anthropic_client.py +++ b/tradingagents/llm_clients/anthropic_client.py @@ -1,3 +1,4 @@ +import re from typing import Any, Optional from langchain_anthropic import ChatAnthropic @@ -10,6 +11,22 @@ _PASSTHROUGH_KWARGS = ( "callbacks", "http_client", "http_async_client", "effort", ) +# Anthropic's extended-thinking ``effort`` parameter is accepted by Opus 4.5+ +# and Sonnet 4.5+ only. Haiku (any version shipped to date) 400s with +# ``"This model does not support the effort parameter"`` (#831). Future +# ``claude-{opus,sonnet}-X-Y`` releases inherit effort support via the +# forward-compat pattern below; future Haiku stays excluded by default. +_EFFORT_EXACT = { + "claude-mythos-preview", # non-standard preview name; effort-capable +} +_EFFORT_PATTERN = re.compile(r"^claude-(opus|sonnet)-\d+-\d+$") + + +def _supports_effort(model: str) -> bool: + """Whether Anthropic accepts the ``effort`` parameter for this model.""" + model_lc = model.lower() + return model_lc in _EFFORT_EXACT or bool(_EFFORT_PATTERN.match(model_lc)) + class NormalizedChatAnthropic(ChatAnthropic): """ChatAnthropic with normalized content output. @@ -38,8 +55,11 @@ class AnthropicClient(BaseLLMClient): llm_kwargs["base_url"] = self.base_url for key in _PASSTHROUGH_KWARGS: - if key in self.kwargs: - llm_kwargs[key] = self.kwargs[key] + if key not in self.kwargs: + continue + if key == "effort" and not _supports_effort(self.model): + continue + llm_kwargs[key] = self.kwargs[key] return NormalizedChatAnthropic(**llm_kwargs)