Skip to content
Open
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
2 changes: 1 addition & 1 deletion frontier_agent/infra/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class FrontierAgentConfig(BaseSettings):
openai_model: str = "gpt-4o"
openai_base_url: str = "https://api.openai.com/v1"
anthropic_api_key: str = ""
anthropic_model: str = "claude-sonnet-4-20250514"
anthropic_model: str = "claude-sonnet-5-5"
anthropic_base_url: str = ""
anthropic_thinking: bool = False
anthropic_thinking_type: str = "adaptive"
Expand Down
46 changes: 35 additions & 11 deletions frontier_agent/infra/llm_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import asyncio
import logging
import random
import re
import time
from collections.abc import AsyncIterator
from typing import Any, TypeGuard
Expand Down Expand Up @@ -365,7 +366,26 @@ async def stream(
raise last_error from fb_err


def _anthropic_thinking_kwarg(config: Any, max_tokens: int) -> dict[str, Any] | None:
# Claude 5 models and Opus 4.7+ reject non-default sampling parameters (temperature) with a 400.
# Not anchored, so OpenRouter (``anthropic/claude-sonnet-5.5``) and Bedrock
# (``global.anthropic.claude-sonnet-5-5``) ids match too.
_ANTHROPIC_NO_SAMPLING_MODEL = re.compile(
r"claude-(?:(?:opus|sonnet|haiku|fable|mythos)-5|opus-4[-.][78])"
)


def _anthropic_temperature(
model: str, thinking: dict[str, Any] | None, temperature: float = 0.3,
) -> float | None:
"""Sampling temperature for the Anthropic client, ``None`` to omit it."""
if thinking or _ANTHROPIC_NO_SAMPLING_MODEL.search(model):
return None
return temperature


def _anthropic_thinking_kwarg(
config: Any, max_tokens: int, model: str,
) -> dict[str, Any] | None:
"""Anthropic extended-thinking request kwarg, or ``None`` when off.

``ANTHROPIC_THINKING`` on → the shape selected by ``anthropic_thinking_type``
Expand All @@ -382,7 +402,9 @@ def _anthropic_thinking_kwarg(config: Any, max_tokens: int) -> dict[str, Any] |
if not getattr(config, "anthropic_thinking", False):
return None
ttype = str(getattr(config, "anthropic_thinking_type", "adaptive") or "adaptive").strip().lower()
if ttype == "enabled":
# Claude 5 and Opus 4.7+ require adaptive thinking; migrate legacy budgets
# using the resolved model so role and fallback overrides stay compatible.
if ttype == "enabled" and not _ANTHROPIC_NO_SAMPLING_MODEL.search(model):
budget = int(getattr(config, "anthropic_thinking_budget", 8192) or 8192)
budget = max(1024, min(budget, int(max_tokens) - 1))
return {"type": "enabled", "budget_tokens": budget}
Expand Down Expand Up @@ -432,13 +454,14 @@ def _create_single_provider(config: FrontierAgentConfig, model_override: str | N
from frontier_agent.infra.anthropic_client import AnthropicClient
# Opt-in extended thinking (ANTHROPIC_THINKING): standard enabled+budget
# thinking + drop temperature (Anthropic 400s on the combo). Flag off →
# prior kwargs preserved exactly (temperature=0.3, no thinking).
thinking = _anthropic_thinking_kwarg(config, max_tokens)
# prior kwargs preserved exactly (temperature=0.3 except on Claude 5 models, no thinking).
model = model_override or config.anthropic_model
thinking = _anthropic_thinking_kwarg(config, max_tokens, model)
return AnthropicClient(
model=model_override or config.anthropic_model,
model=model,
api_key=config.anthropic_api_key,
base_url=config.anthropic_base_url or None,
temperature=None if thinking else 0.3,
temperature=_anthropic_temperature(model, thinking),
max_tokens=max_tokens,
thinking=thinking,
effort=_anthropic_effort_kwarg(config, thinking),
Expand Down Expand Up @@ -619,16 +642,17 @@ def _create_overridden_provider(
# Opt-in extended thinking (see sibling branch above): standard
# enabled+budget thinking + drop temperature. Flag off → prior kwargs
# preserved exactly.
thinking = _anthropic_thinking_kwarg(config, max_tokens)
anthropic_model = model if (provider and model) else _resolve_model_name(
config, resolved_provider, model,
)
thinking = _anthropic_thinking_kwarg(config, max_tokens, anthropic_model)
return AnthropicClient(
model=model if (provider and model) else _resolve_model_name(
config, resolved_provider, model,
),
model=anthropic_model,
api_key=api_key if api_key is not None else config.anthropic_api_key,
# Honour a per-entry base_url (fallback-chain / override) — fall back
# to the configured default only when the caller didn't pass one.
base_url=(base_url if base_url is not None else config.anthropic_base_url) or None,
temperature=None if thinking else temperature,
temperature=_anthropic_temperature(anthropic_model, thinking, temperature),
max_tokens=max_tokens,
thinking=thinking,
effort=_anthropic_effort_kwarg(config, thinking),
Expand Down
84 changes: 84 additions & 0 deletions tests/test_anthropic_default_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import pytest

from frontier_agent.infra.config import FrontierAgentConfig
from frontier_agent.infra.llm_adapter import _anthropic_temperature

# claude-sonnet-4-20250514 was retired by Anthropic on 2026-06-15.
RETIRED_MODEL = "claude-sonnet-4-20250514"


def test_default_anthropic_model_is_not_retired():
assert FrontierAgentConfig.model_fields["anthropic_model"].default != RETIRED_MODEL


def test_default_anthropic_model_gets_no_temperature():
default = FrontierAgentConfig.model_fields["anthropic_model"].default

assert _anthropic_temperature(default, None) is None


def test_older_models_keep_the_temperature():
assert _anthropic_temperature("claude-sonnet-4-6", None) == 0.3


def test_thinking_drops_the_temperature():
assert _anthropic_temperature("claude-sonnet-4-6", {"type": "adaptive"}) is None


def test_prefixed_claude_5_ids_get_no_temperature():
assert _anthropic_temperature("anthropic/claude-sonnet-5.5", None) is None
assert _anthropic_temperature("global.anthropic.claude-sonnet-5-5", None) is None


def test_opus_4_7_and_4_8_get_no_temperature():
assert _anthropic_temperature("claude-opus-4-7", None) is None
assert _anthropic_temperature("claude-opus-4-8", None) is None
assert _anthropic_temperature("claude-opus-4-6", None) == 0.3


def test_role_temperature_is_kept_for_older_models():
assert _anthropic_temperature("claude-sonnet-4-6", None, 0.7) == 0.7
assert _anthropic_temperature("claude-sonnet-5-5", None, 0.7) is None


def test_overridden_provider_omits_temperature_for_claude_5(monkeypatch):
import frontier_agent.infra.anthropic_client as anthropic_client
from frontier_agent.infra.llm_adapter import _create_overridden_provider

captured = {}

class FakeClient:
def __init__(self, **kwargs):
captured.update(kwargs)

monkeypatch.setattr(anthropic_client, "AnthropicClient", FakeClient)
config = FrontierAgentConfig(llm_provider="anthropic", anthropic_api_key="test-key")

_create_overridden_provider(config, temperature=0.7)
assert captured["model"] == config.anthropic_model
assert captured["temperature"] is None

_create_overridden_provider(config, "claude-sonnet-4-6", temperature=0.7)
assert captured["temperature"] == 0.7


@pytest.mark.parametrize("factory_name", ["_create_single_provider", "_create_overridden_provider"])
@pytest.mark.parametrize("model", [None, "claude-sonnet-4-20250514", "claude-sonnet-5-5"])
def test_manual_thinking_migration_on_wire(factory_name, model):
from frontier_agent.infra import llm_adapter

config = FrontierAgentConfig(
_env_file=None, llm_provider="anthropic", anthropic_api_key="test-key",
anthropic_model="claude-sonnet-5-5", anthropic_thinking=True,
anthropic_thinking_type="enabled", anthropic_thinking_budget=2048,
)
client = getattr(llm_adapter, factory_name)(config, model)
payload = client._build_kwargs(
[{"role": "user", "content": "hello"}], tools=None, temperature=None,
max_tokens=None, extra_headers=None, timeout=None,
)
if model == "claude-sonnet-4-20250514":
assert payload["thinking"] == {"type": "enabled", "budget_tokens": 2048}
else:
assert payload["thinking"] == {"type": "adaptive", "display": "summarized"}
assert payload["model"] == (model or config.anthropic_model)
Loading