From 61ca8af41e547d6e16b65b5e527d2f31100747f9 Mon Sep 17 00:00:00 2001 From: Lucifus613 <263831199+Lucifus613@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:20:28 +0800 Subject: [PATCH 1/2] fix(anthropic): stop sending temperature in non-thinking requests Anthropic Python SDK v1 removed temperature/top_p/top_k from messages.create (passing one raises TypeError), and Claude Opus 4.7+ rejects non-default sampling values with HTTP 400. The constructor, chat and stream keep accepting temperature for compatibility but no longer send it. Co-Authored-By: Claude Opus 5.5 (1M context) --- agent_core/providers/anthropic.py | 17 +++--- changes/a4-anthropic-omit-sampling.fix.md | 1 + tests/test_provider_native_clients.py | 71 +++++++++++++++++++++++ tests/test_provider_protocol_reasoning.py | 4 +- 4 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 changes/a4-anthropic-omit-sampling.fix.md diff --git a/agent_core/providers/anthropic.py b/agent_core/providers/anthropic.py index 2b8ac6e..afd6a12 100644 --- a/agent_core/providers/anthropic.py +++ b/agent_core/providers/anthropic.py @@ -35,7 +35,11 @@ class AnthropicClient(LLMClient): - """Non-streaming-first Anthropic adapter.""" + """Non-streaming-first Anthropic adapter. + + Constructor, chat and stream accept but omit ``temperature`` for compatibility: + SDK v1 removed sampling parameters and newer models reject non-default values with 400. + """ def __init__( self, @@ -58,8 +62,7 @@ def __init__( # Extended thinking: when set (e.g. ``{"type": "adaptive", "display": # "summarized"}``) the request carries ``thinking=`` so responses return # thinking + signature blocks; the response parser keeps them verbatim - # (content_block) for faithful multi-turn replay. ``temperature`` is - # dropped when thinking is on (Anthropic 400s on the combo). ``effort`` + # (content_block) for faithful multi-turn replay. ``effort`` # (low|medium|high|xhigh|max) → ``output_config.effort`` via extra_body. self._thinking = thinking or None self._effort = (effort or "").strip() @@ -116,17 +119,11 @@ def _build_kwargs( if system: kwargs["system"] = system if self._thinking: - # Anthropic rejects ``temperature`` together with thinking, so it is - # OMITTED here regardless of the configured default. ``effort`` rides - # on ``extra_body.output_config`` so any value (incl. ``xhigh``) + # ``effort`` rides on ``extra_body.output_config`` so any value (incl. ``xhigh``) # reaches ``messages.create`` without the SDK's stricter validation. kwargs["thinking"] = self._thinking if self._effort: kwargs["extra_body"] = {"output_config": {"effort": self._effort}} - else: - eff_temp = temperature if temperature is not None else self.default_temperature - if eff_temp is not None: - kwargs["temperature"] = eff_temp if tools: kwargs["tools"] = [_to_anthropic_tool(t) for t in tools] if extra_headers: diff --git a/changes/a4-anthropic-omit-sampling.fix.md b/changes/a4-anthropic-omit-sampling.fix.md new file mode 100644 index 0000000..670fdda --- /dev/null +++ b/changes/a4-anthropic-omit-sampling.fix.md @@ -0,0 +1 @@ +Anthropic clients omit sampling parameters from requests in both thinking and non-thinking modes, avoiding `TypeError` with Anthropic Python SDK v1 and HTTP 400 responses from newer models. Constructor, `chat`, and `stream` temperature arguments remain accepted for compatibility but are no longer sent; thinking and effort settings are unchanged. diff --git a/tests/test_provider_native_clients.py b/tests/test_provider_native_clients.py index 6d1726d..2b003f2 100644 --- a/tests/test_provider_native_clients.py +++ b/tests/test_provider_native_clients.py @@ -9,6 +9,8 @@ from __future__ import annotations import asyncio +import inspect +import json from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock @@ -669,6 +671,75 @@ def test_openai_client_satisfies_protocol(): # ── Anthropic conversions (pure, no SDK call) ─────────────────────────────── +@pytest.mark.parametrize("temperature", [0.2, None]) +def test_anthropic_build_kwargs_omits_sampling_parameters(temperature): + c = ac.AnthropicClient("claude-x", api_key="x", temperature=0.0) + kwargs = c._build_kwargs( + [user_msg("hi")], tools=None, temperature=temperature, + max_tokens=None, extra_headers=None, timeout=None, + ) + sampling = {"temperature", "top_p", "top_k"} + assert sampling.isdisjoint(kwargs) + assert sampling.isdisjoint(kwargs.get("extra_body", {})) + + +@pytest.mark.parametrize("thinking", [None, {"type": "adaptive"}]) +def test_anthropic_build_kwargs_matches_installed_sdk_signature(thinking): + from anthropic.resources.messages import AsyncMessages + + c = ac.AnthropicClient( + "claude-x", api_key="x", temperature=0.0, thinking=thinking, effort="high", + ) + kwargs = c._build_kwargs( + [system_msg("s"), user_msg("hi")], + tools=[{"type": "function", "function": { + "name": "search", "parameters": {"type": "object"}, + }}], + temperature=0.2, max_tokens=256, + extra_headers={"x-test": "test"}, timeout=30, + ) + parameters = inspect.signature(AsyncMessages.create).parameters + assert set(kwargs) <= set(parameters) + assert kwargs["tools"][0]["name"] == "search" + assert kwargs["extra_headers"] == {"x-test": "test"} + assert kwargs["timeout"] == 30 + assert {"temperature", "top_p", "top_k"}.isdisjoint(kwargs.get("extra_body", {})) + if thinking: + assert kwargs["thinking"] == thinking + assert kwargs["extra_body"] == {"output_config": {"effort": "high"}} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("temperature", [0.2, None]) +async def test_anthropic_chat_real_sdk_omits_sampling_parameters(temperature): + import httpx2 + from anthropic import AsyncAnthropic + + requests = [] + + def respond(request): + requests.append(json.loads(request.content)) + return httpx2.Response(200, json={ + "id": "msg_1", "type": "message", "role": "assistant", "model": "claude-x", + "content": [{"type": "text", "text": "hello"}], + "stop_reason": "end_turn", "stop_sequence": None, + "usage": {"input_tokens": 1, "output_tokens": 1}, + }) + + c = ac.AnthropicClient("claude-x", api_key="test", temperature=0.0) + await c._client.close() + async with AsyncAnthropic( + api_key="test", base_url="https://anthropic.invalid", max_retries=0, + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(respond)), + ) as sdk: + c._client = sdk + response = await c.chat([user_msg("hi")], temperature=temperature) + + assert response.content == "hello" + assert len(requests) == 1 + assert {"temperature", "top_p", "top_k"}.isdisjoint(requests[0]) + + def test_anthropic_split_system(): sys, rest = ac._split_system([system_msg("you are x"), user_msg("hi")]) assert sys == "you are x" diff --git a/tests/test_provider_protocol_reasoning.py b/tests/test_provider_protocol_reasoning.py index c4e745d..62a6110 100644 --- a/tests/test_provider_protocol_reasoning.py +++ b/tests/test_provider_protocol_reasoning.py @@ -55,13 +55,13 @@ def test_anthropic_build_kwargs_sends_thinking_and_drops_temperature(): assert kw["extra_body"] == {"output_config": {"effort": "high"}} -def test_anthropic_build_kwargs_keeps_temperature_when_thinking_off(): +def test_anthropic_build_kwargs_drops_temperature_when_thinking_off(): c = AnthropicClient("claude-x", api_key="x", temperature=0.3) kw = c._build_kwargs( [user_msg("hi")], tools=None, temperature=None, max_tokens=None, extra_headers=None, timeout=None, ) - assert kw["temperature"] == 0.3 + assert "temperature" not in kw assert "thinking" not in kw and "extra_body" not in kw From 98c8bf4c6f2bc7c0bcf0c1caeb9b0eb188fd109d Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Thu, 1 Oct 2026 20:46:39 +0800 Subject: [PATCH 2/2] test(anthropic): support both SDK HTTP transports --- tests/test_provider_native_clients.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test_provider_native_clients.py b/tests/test_provider_native_clients.py index 2b003f2..7109efe 100644 --- a/tests/test_provider_native_clients.py +++ b/tests/test_provider_native_clients.py @@ -712,14 +712,20 @@ def test_anthropic_build_kwargs_matches_installed_sdk_signature(thinking): @pytest.mark.asyncio @pytest.mark.parametrize("temperature", [0.2, None]) async def test_anthropic_chat_real_sdk_omits_sampling_parameters(temperature): - import httpx2 - from anthropic import AsyncAnthropic + from anthropic import AsyncAnthropic, DefaultAsyncHttpxClient + + # Anthropic 0.x uses httpx; 1.x uses httpx2. Match the installed SDK's + # public default client without requiring httpx2 in supported 0.x installs. + if issubclass(DefaultAsyncHttpxClient, httpx.AsyncClient): + sdk_httpx = httpx + else: + import httpx2 as sdk_httpx requests = [] def respond(request): requests.append(json.loads(request.content)) - return httpx2.Response(200, json={ + return sdk_httpx.Response(200, json={ "id": "msg_1", "type": "message", "role": "assistant", "model": "claude-x", "content": [{"type": "text", "text": "hello"}], "stop_reason": "end_turn", "stop_sequence": None, @@ -730,7 +736,7 @@ def respond(request): await c._client.close() async with AsyncAnthropic( api_key="test", base_url="https://anthropic.invalid", max_retries=0, - http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(respond)), + http_client=DefaultAsyncHttpxClient(transport=sdk_httpx.MockTransport(respond)), ) as sdk: c._client = sdk response = await c.chat([user_msg("hi")], temperature=temperature)