diff --git a/CUSTOMIZATION.md b/CUSTOMIZATION.md index 6739a3b8..33037489 100644 --- a/CUSTOMIZATION.md +++ b/CUSTOMIZATION.md @@ -140,7 +140,7 @@ The model is configured in the `get_agent()` function in `agent/server.py`. By d ```bash # Set the model via environment variable (uses provider:model format) -LLM_MODEL_ID="anthropic:claude-sonnet-4-6" +LLM_MODEL_ID="anthropic:claude-sonnet-5" ``` If `LLM_MODEL_ID` is not set, the default model (`openai:gpt-5.5`) is used. @@ -153,7 +153,7 @@ Use the `provider:model` format: ```python # Anthropic -model=make_model("anthropic:claude-sonnet-4-6", temperature=0, max_tokens=16_000) +model=make_model("anthropic:claude-sonnet-5", temperature=0, max_tokens=16_000) # OpenAI (uses Responses API by default) model=make_model("openai:gpt-5.5", max_tokens=128_000, reasoning={"effort": "medium"}) @@ -167,7 +167,7 @@ The `make_model()` helper in `agent/utils/model.py` wraps `langchain.chat_models ```python from langchain_anthropic import ChatAnthropic -model = ChatAnthropic(model_name="claude-sonnet-4-6", temperature=0, max_tokens=16_000) +model = ChatAnthropic(model_name="claude-sonnet-5", temperature=0, max_tokens=16_000) return create_deep_agent( model=model, @@ -185,7 +185,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: if source == "slack": # Faster model for Slack Q&A - model = make_model("anthropic:claude-sonnet-4-6", temperature=0, max_tokens=16_000) + model = make_model("anthropic:claude-sonnet-5", temperature=0, max_tokens=16_000) else: # Full model for code changes from Linear model = make_model("openai:gpt-5.5", max_tokens=128_000, reasoning={"effort": "medium"}) diff --git a/agent/dashboard/options.py b/agent/dashboard/options.py index ff7cb3e8..cc3c0048 100644 --- a/agent/dashboard/options.py +++ b/agent/dashboard/options.py @@ -21,6 +21,13 @@ SUPPORTED_MODELS: list[ModelOption] = [ "default_effort": "high", "supports_images": True, }, + { + "id": "anthropic:claude-sonnet-5", + "label": "Sonnet 5", + "efforts": ["low", "medium", "high", "xhigh", "max"], + "default_effort": "high", + "supports_images": True, + }, { "id": "openai:gpt-5.5", "label": "GPT-5.5", @@ -83,6 +90,16 @@ def _provider_of(model_id: str) -> str | None: return provider if rest else None +def _claude_family_of(model_id: str) -> str | None: + provider, _, name = model_id.partition(":") + if provider != "anthropic" or not name.startswith("claude-"): + return None + parts = name.split("-") + if len(parts) < 2: + return None + return "-".join(parts[:2]) + + def _fallback_effort_for(model: ModelOption, effort: object) -> str | None: if not isinstance(effort, str): return None @@ -98,19 +115,25 @@ def _fallback_effort_for(model: ModelOption, effort: object) -> str | None: def provider_fallback_pair(model_id: object, effort: object = None) -> tuple[str, str] | None: - """Newest supported ``(model_id, effort)`` for the same provider as ``model_id``. + """Newest supported ``(model_id, effort)`` for the same provider/family. Keeps a stored selection on its original provider when its exact id has - dropped out of the supported set (e.g. an Opus minor-version bump), instead - of falling through to the cross-provider global default. Preserves ``effort`` - when the fallback model supports it, otherwise uses that model's default - effort. Returns ``None`` when no supported model shares the provider. + dropped out of the supported set (e.g. an Opus minor-version bump), preferring + the same Claude family when available instead of falling through to the + cross-provider global default. Preserves ``effort`` when the fallback model + supports it, otherwise uses that model's default effort. Returns ``None`` when + no supported model shares the provider. """ if not isinstance(model_id, str): return None provider = _provider_of(model_id) if provider is None: return None + family = _claude_family_of(model_id) + if family is not None: + for m in SUPPORTED_MODELS: + if _provider_of(m["id"]) == provider and _claude_family_of(m["id"]) == family: + return m["id"], _fallback_effort_for(m, effort) or m["default_effort"] for m in SUPPORTED_MODELS: if _provider_of(m["id"]) == provider: return m["id"], _fallback_effort_for(m, effort) or m["default_effort"] diff --git a/tests/test_anthropic_model.py b/tests/test_anthropic_model.py new file mode 100644 index 00000000..5257bdb3 --- /dev/null +++ b/tests/test_anthropic_model.py @@ -0,0 +1,36 @@ +import pytest + +from agent.dashboard.options import SUPPORTED_MODELS, provider_fallback_pair +from agent.utils.model import provider_model_kwargs + +SONNET_5_ID = "anthropic:claude-sonnet-5" + + +def test_sonnet_5_is_supported_with_documented_efforts() -> None: + sonnet = next(m for m in SUPPORTED_MODELS if m["id"] == SONNET_5_ID) + assert sonnet["label"] == "Sonnet 5" + assert sonnet["efforts"] == ["low", "medium", "high", "xhigh", "max"] + assert sonnet["default_effort"] == "high" + assert sonnet["supports_images"] is True + + +@pytest.mark.parametrize("effort", ["low", "medium", "high", "xhigh", "max"]) +def test_sonnet_5_efforts_map_to_anthropic_kwargs(effort: str) -> None: + kwargs = provider_model_kwargs(SONNET_5_ID, effort, max_tokens=16_000) + assert kwargs["max_tokens"] == 16_000 + assert kwargs["effort"] == effort + assert kwargs["thinking"] == {"type": "adaptive", "display": "summarized"} + + +def test_sonnet_46_fallback_uses_sonnet_5() -> None: + assert provider_fallback_pair("anthropic:claude-sonnet-4-6", "xhigh") == ( + SONNET_5_ID, + "xhigh", + ) + + +def test_opus_fallback_stays_on_opus_family() -> None: + assert provider_fallback_pair("anthropic:claude-opus-4-7", "xhigh") == ( + "anthropic:claude-opus-4-8", + "xhigh", + )