feat: add Sonnet 5 to model picker (#1651)

* chore: update Sonnet examples to Sonnet 5

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* chore: add Sonnet 5 to model picker

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-30 11:40:54 -07:00 • committed by GitHub
parent e1d85526b6
commit 289f5e3abf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 68 additions and 9 deletions

View file

@ -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"})

View file

@ -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"]

View file

@ -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",
)