diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index 49dba37f2..98cd6e826 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -1783,20 +1783,6 @@ def discover_claude_models_unbucketed(workspace: str, token: str) -> tuple[list[ return [m for m in ids if "claude-" in m.lower()], None -def _prefer_opus_4_8(models: dict[str, str], all_ids: list[str]) -> None: - """Swap the opus slot to claude-opus-4-8 when it's available. - - Discovery picks the newest opus (opus-5) but smart routing's - CLAUDE_ROUTE_ARMS require claude-opus-4-8. Pin to 4-8 when both - exist so the routing availability check passes. - """ - opus = models.get("opus") - if opus and "claude-opus-5" in opus: - opus_48 = next((m for m in all_ids if "claude-opus-4-8" in m), None) - if opus_48: - models["opus"] = opus_48 - - def discover_model_services( workspace: str, token: str ) -> tuple[dict[str, str], list[str], list[str], list[str], str | None]: @@ -1827,12 +1813,6 @@ def discover_model_services( ) if candidates: claude_models[family] = candidates[0] - # Smart routing's CLAUDE_ROUTE_ARMS require claude-opus-4-8, but the - # newest-wins sort above picks opus-5 when both exist — making the - # routing availability check fail. Pin opus-4-8 when it's available so - # routing works with the current task_v2 router. Revert to - # newest-wins once the router accepts opus-5 (PR databricks-eng/universe#2365446). - _prefer_opus_4_8(claude_models, ids) codex_models = sorted([m for m in ids if "gpt-" in m], key=model_version_sort_key) gemini_models = sorted([m for m in ids if "gemini-" in m], key=model_version_sort_key) @@ -2940,8 +2920,6 @@ def discover_claude_models(workspace: str, token: str) -> tuple[dict[str, str], ) if candidates: result[family] = candidates[0] - # Same opus-4-8 pin as discover_model_services — see comment there. - _prefer_opus_4_8(result, raw_ids) if result: return result, None if not raw_ids: diff --git a/tests/test_databricks.py b/tests/test_databricks.py index 20301da3b..3c8942851 100644 --- a/tests/test_databricks.py +++ b/tests/test_databricks.py @@ -243,6 +243,22 @@ def test_buckets_system_ai_claude_models(self, monkeypatch): "sonnet": "system.ai.claude-sonnet-4-6", } + def test_opus_5_wins_over_opus_4_8(self, monkeypatch): + # The opus slot is the newest opus the workspace serves. ucode used to swap opus-5 out for + # opus-4-8 to satisfy a frozen smart-routing arm menu, which downgraded every user. + payload = { + "data": [ + {"id": "databricks-claude-opus-4-8"}, + {"id": "databricks-claude-opus-5"}, + ] + } + monkeypatch.setattr(db_mod, "_http_get_json", lambda url, token, **kwargs: (payload, None)) + + models, reason = db_mod.discover_claude_models(WS, "token") + + assert reason is None + assert models["opus"] == "databricks-claude-opus-5" + def test_buckets_fable_family(self, monkeypatch): payload = { "data": [ @@ -320,6 +336,24 @@ def test_buckets_families_by_name(self, monkeypatch): "system.ai.kimi-k2-7-code", ] + def test_opus_5_wins_over_opus_4_8(self, monkeypatch): + # Same regression as the AI Gateway path: the opus slot is the newest opus, not a pinned + # opus-4-8. + payload = { + "model_services": [ + _model_service("system.ai.claude-opus-4-8"), + _model_service("system.ai.claude-opus-5"), + ] + } + monkeypatch.setattr( + db_mod, "_http_get_json", lambda url, token, timeout=10: (payload, None) + ) + + claude, _codex, _gemini, _oss, reason = db_mod.discover_model_services(WS, "token") + + assert reason is None + assert claude == {"opus": "system.ai.claude-opus-5"} + def test_oss_allowlist_drops_unsupported_families(self, monkeypatch): # Only explicitly supported chat families are retained. payload = { @@ -2707,9 +2741,8 @@ def test_the_two_discovery_helpers_share_one_walk(self, monkeypatch): claude, _codex, _gemini, _oss, _reason = db_mod.discover_model_services(WS, "tok") unbucketed, _ = db_mod.discover_claude_models_unbucketed(WS, "tok") assert calls["n"] == 1 - # Both views still come back intact: newest-per-family (pinned to opus-4-8 - # for smart-routing compatibility by _prefer_opus_4_8), and the full list. - assert claude["opus"] == "system.ai.claude-opus-4-8" + # Both views still come back intact: newest-per-family, and the full list. + assert claude["opus"] == "system.ai.claude-opus-5" assert unbucketed == ["system.ai.claude-opus-4-8", "system.ai.claude-opus-5"] def test_use_cache_false_forces_a_fresh_walk(self, monkeypatch):