Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions agent_core/providers/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions changes/a4-anthropic-omit-sampling.fix.md
Original file line number Diff line number Diff line change
@@ -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.
77 changes: 77 additions & 0 deletions tests/test_provider_native_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from __future__ import annotations

import asyncio
import inspect
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock

Expand Down Expand Up @@ -669,6 +671,81 @@ 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):
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 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,
"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=DefaultAsyncHttpxClient(transport=sdk_httpx.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"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_provider_protocol_reasoning.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading