From c4905ac01e8ea5ab5ff01298b414bdcb32f02c61 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:51:41 -0400 Subject: [PATCH] fix(models): make Claude family fallback recognize Bedrock ids (#119) #1651 added the family-aware `provider_fallback_pair` via `_claude_family_of`, but the helper only matched `anthropic:claude-*` ids. This fork serves Claude through Bedrock (`bedrock_converse:us.anthropic.claude-*`), so the family logic was dead code: a dropped Bedrock Sonnet fell back to the Bedrock Opus that sits first in the list instead of staying in the Sonnet family. Teach `_claude_family_of` to parse `bedrock_converse` ids and add regression coverage for the Sonnet-stays-on-Sonnet case. --- agent/dashboard/options.py | 13 +++++++++++-- tests/test_model_fallback_resolution.py | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/agent/dashboard/options.py b/agent/dashboard/options.py index 48e7932a..7a150466 100644 --- a/agent/dashboard/options.py +++ b/agent/dashboard/options.py @@ -99,9 +99,18 @@ def _provider_of(model_id: str) -> str | None: def _claude_family_of(model_id: str) -> str | None: provider, _, name = model_id.partition(":") - if provider != "anthropic" or not name.startswith("claude-"): + if provider == "anthropic": + claude = name + elif provider == "bedrock_converse": + # Bedrock ids embed the model as a region-prefixed path, e.g. + # "us.anthropic.claude-sonnet-5" — take the trailing "claude-*" segment so + # our Bedrock Claude models get the same family-aware fallback. + claude = name.rpartition(".")[2] + else: return None - parts = name.split("-") + if not claude.startswith("claude-"): + return None + parts = claude.split("-") if len(parts) < 2: return None return "-".join(parts[:2]) diff --git a/tests/test_model_fallback_resolution.py b/tests/test_model_fallback_resolution.py index c4fee9f3..dc11f9df 100644 --- a/tests/test_model_fallback_resolution.py +++ b/tests/test_model_fallback_resolution.py @@ -12,12 +12,21 @@ from agent.dashboard.team_settings import get_team_default_model STALE_ANTHROPIC = "bedrock_converse:us.anthropic.claude-opus-4-7" SUPPORTED_ANTHROPIC = "bedrock_converse:us.anthropic.claude-opus-4-8" +STALE_SONNET = "bedrock_converse:us.anthropic.claude-sonnet-4-9" +SUPPORTED_SONNET = "bedrock_converse:us.anthropic.claude-sonnet-5" def test_provider_fallback_preserves_provider_and_effort() -> None: assert provider_fallback_pair(STALE_ANTHROPIC, "xhigh") == (SUPPORTED_ANTHROPIC, "xhigh") +def test_provider_fallback_keeps_bedrock_sonnet_in_family() -> None: + # A dropped Bedrock Sonnet must prefer the current Bedrock Sonnet, not cross to + # the Bedrock Opus that happens to sit first in the provider's list. Requires + # _claude_family_of to understand bedrock_converse ids, not just anthropic:. + assert provider_fallback_pair(STALE_SONNET, "high") == (SUPPORTED_SONNET, "high") + + def test_provider_fallback_uses_default_effort_when_unsupported() -> None: assert provider_fallback_pair(STALE_ANTHROPIC, "bogus") == (SUPPORTED_ANTHROPIC, "high") assert provider_fallback_pair(STALE_ANTHROPIC, None) == (SUPPORTED_ANTHROPIC, "high")