From a7cad9cb25997e5fdeb97587e53203f08f2d2ff2 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Thu, 29 Oct 2026 08:35:15 -0400 Subject: [PATCH 1/3] fix(llm): replace the retired claude-sonnet-4-20250514 default With LLM_PROVIDER=anthropic and no ANTHROPIC_MODEL, FrontierAgent requested claude-sonnet-4-20250514, which Anthropic retired on 2026-06-15, so every call failed. Default to claude-sonnet-5-5. Claude 5 models reject non-default sampling parameters, so omit the temperature for them (it is still sent as 0.3 for older models when thinking is off). Co-Authored-By: Claude Sonnet 5.5 --- frontier_agent/infra/config.py | 2 +- frontier_agent/infra/llm_adapter.py | 19 ++++++++++++++++--- tests/test_anthropic_default_model.py | 23 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 tests/test_anthropic_default_model.py diff --git a/frontier_agent/infra/config.py b/frontier_agent/infra/config.py index 4c94249b..7c6b1d2f 100644 --- a/frontier_agent/infra/config.py +++ b/frontier_agent/infra/config.py @@ -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" diff --git a/frontier_agent/infra/llm_adapter.py b/frontier_agent/infra/llm_adapter.py index 70e7ca50..12d81891 100644 --- a/frontier_agent/infra/llm_adapter.py +++ b/frontier_agent/infra/llm_adapter.py @@ -5,6 +5,7 @@ import asyncio import logging import random +import re import time from collections.abc import AsyncIterator from typing import Any, TypeGuard @@ -365,6 +366,17 @@ async def stream( raise last_error from fb_err +# Claude 5 models reject non-default sampling parameters (temperature) with a 400. +_ANTHROPIC_NO_SAMPLING_MODEL = re.compile(r"^claude-(opus|sonnet|haiku|fable|mythos)-5") + + +def _anthropic_temperature(model: str, thinking: dict[str, Any] | None) -> float | None: + """Sampling temperature for the Anthropic client, ``None`` to omit it.""" + if thinking or _ANTHROPIC_NO_SAMPLING_MODEL.match(model): + return None + return 0.3 + + def _anthropic_thinking_kwarg(config: Any, max_tokens: int) -> dict[str, Any] | None: """Anthropic extended-thinking request kwarg, or ``None`` when off. @@ -432,13 +444,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). + # prior kwargs preserved exactly (temperature=0.3 except on Claude 5 models, no thinking). thinking = _anthropic_thinking_kwarg(config, max_tokens) + model = model_override or config.anthropic_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), diff --git a/tests/test_anthropic_default_model.py b/tests/test_anthropic_default_model.py new file mode 100644 index 00000000..7e74d931 --- /dev/null +++ b/tests/test_anthropic_default_model.py @@ -0,0 +1,23 @@ +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 From da4f1e83927261364f549815370b4d63339dc359 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Fri, 30 Oct 2026 11:12:35 -0400 Subject: [PATCH 2/3] fix(llm): honour Claude 5 / Opus 4.7+ sampling limits on the override path Co-Authored-By: Claude Sonnet 5.5 --- frontier_agent/infra/llm_adapter.py | 25 +++++++++++------- tests/test_anthropic_default_model.py | 37 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/frontier_agent/infra/llm_adapter.py b/frontier_agent/infra/llm_adapter.py index 12d81891..94489944 100644 --- a/frontier_agent/infra/llm_adapter.py +++ b/frontier_agent/infra/llm_adapter.py @@ -366,15 +366,21 @@ async def stream( raise last_error from fb_err -# Claude 5 models reject non-default sampling parameters (temperature) with a 400. -_ANTHROPIC_NO_SAMPLING_MODEL = re.compile(r"^claude-(opus|sonnet|haiku|fable|mythos)-5") +# 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) -> float | None: +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.match(model): + if thinking or _ANTHROPIC_NO_SAMPLING_MODEL.search(model): return None - return 0.3 + return temperature def _anthropic_thinking_kwarg(config: Any, max_tokens: int) -> dict[str, Any] | None: @@ -633,15 +639,16 @@ def _create_overridden_provider( # 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, + ) 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), diff --git a/tests/test_anthropic_default_model.py b/tests/test_anthropic_default_model.py index 7e74d931..cbb00017 100644 --- a/tests/test_anthropic_default_model.py +++ b/tests/test_anthropic_default_model.py @@ -21,3 +21,40 @@ def test_older_models_keep_the_temperature(): 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 From d4d117a4429f677992b8758b4213ad490a0b0c57 Mon Sep 17 00:00:00 2001 From: zhanghanduo Date: Mon, 2 Nov 2026 10:52:38 +0800 Subject: [PATCH 3/3] fix(llm): migrate legacy thinking budgets for current Claude models --- frontier_agent/infra/llm_adapter.py | 12 ++++++++---- tests/test_anthropic_default_model.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/frontier_agent/infra/llm_adapter.py b/frontier_agent/infra/llm_adapter.py index 94489944..87cdf719 100644 --- a/frontier_agent/infra/llm_adapter.py +++ b/frontier_agent/infra/llm_adapter.py @@ -383,7 +383,9 @@ def _anthropic_temperature( return temperature -def _anthropic_thinking_kwarg(config: Any, max_tokens: int) -> dict[str, Any] | None: +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`` @@ -400,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} @@ -451,8 +455,8 @@ def _create_single_provider(config: FrontierAgentConfig, model_override: str | N # 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 except on Claude 5 models, no thinking). - thinking = _anthropic_thinking_kwarg(config, max_tokens) model = model_override or config.anthropic_model + thinking = _anthropic_thinking_kwarg(config, max_tokens, model) return AnthropicClient( model=model, api_key=config.anthropic_api_key, @@ -638,10 +642,10 @@ 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=anthropic_model, api_key=api_key if api_key is not None else config.anthropic_api_key, diff --git a/tests/test_anthropic_default_model.py b/tests/test_anthropic_default_model.py index cbb00017..0736476d 100644 --- a/tests/test_anthropic_default_model.py +++ b/tests/test_anthropic_default_model.py @@ -1,3 +1,5 @@ +import pytest + from frontier_agent.infra.config import FrontierAgentConfig from frontier_agent.infra.llm_adapter import _anthropic_temperature @@ -58,3 +60,25 @@ def __init__(self, **kwargs): _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)