diff --git a/src/ucode/agents/__init__.py b/src/ucode/agents/__init__.py index 0785342f..cfbed398 100644 --- a/src/ucode/agents/__init__.py +++ b/src/ucode/agents/__init__.py @@ -18,7 +18,6 @@ from ucode.config_io import ToolSpec from ucode.databricks import ( - BEDROCK_PROVIDER_TYPES, get_databricks_token, install_ai_tools, install_databricks_cli, @@ -296,17 +295,18 @@ def resolve_provider_models( """Validate ``provider`` for ``tool`` and return the model ids to pin. Returns ``(provider_models, error, relayed)``. ``provider_models`` is a ``{family: model_id}`` - dict for a Bedrock-backed claude service (whose provider-side ids must be pinned explicitly), or - None for an Anthropic/canonical service or when ``provider`` is None. ``relayed`` is True for a - credential-less Anthropic subscription relay, which the launch path wires with the relayed - overlay + refresh proxy. A non-None ``error`` means the provider is invalid for the tool and the - caller should not launch. - - This is the *developer-configured* path (``ucode configure`` then ``ucode claude``) and its - behaviour is deliberately unchanged: only Bedrock pins, re-derived from the service's live - targets. The *managed* path pins from the admin's authored manifest slots instead — see - ``managed_resolve.managed_provider_family_models`` and its launch call site — so an admin's - chosen versions win rather than being re-derived here. + dict re-derived from the service's live targets for a non-relayed claude service — both Bedrock + (provider-side slugs) and API-key Anthropic (canonical ids) — so the client sends exactly the ids + the MPS allows rather than Claude Code's defaults, which may not match the declared targets. It is + None when ``provider`` is None, for a relayed subscription (see below), or for a non-Claude (e.g. + codex) service. ``relayed`` is True for a credential-less Anthropic subscription relay, which the + launch path wires with the relayed overlay + refresh proxy. A non-None ``error`` means the + provider is invalid for the tool and the caller should not launch. + + This is the *developer-configured* path (``ucode configure`` then ``ucode claude``). The *managed* + path pins from the admin's authored manifest slots instead — see + ``managed_resolve.managed_provider_family_models`` and its launch call site — so an admin's chosen + versions win rather than being re-derived here. """ if not provider: return None, None, False @@ -315,9 +315,17 @@ def resolve_provider_models( if error or service is None: return None, error, False relayed = bool(service.get("relayed")) - if service["provider_type"] in BEDROCK_PROVIDER_TYPES: - return map_claude_family_models(service.get("targets") or []), None, relayed - return None, None, relayed + # Relayed (Claude Max/Enterprise subscription) is exempt: the gateway disables + # model selection server-side for that tier, so there's nothing to reconcile. + if relayed: + return None, None, relayed + # Pin the declared targets by family so the client sends exactly the ids the MPS + # allows. Bedrock always needed this (its slugs aren't Claude Code's canonical + # names); an API-key Anthropic service needs it too, else the client sends + # canonical names that may miss the declared targets and the gateway 403s with + # "not in the allowed models list". map_claude_family_models maps both Bedrock + # slugs and canonical Anthropic ids, and yields nothing for a non-Claude service. + return map_claude_family_models(service.get("targets") or []) or None, None, relayed def configure_tool( diff --git a/tests/test_agents_init.py b/tests/test_agents_init.py index f346af90..69216fac 100644 --- a/tests/test_agents_init.py +++ b/tests/test_agents_init.py @@ -278,10 +278,10 @@ def test_none_provider_returns_none(self): models, error, relayed = agents_mod.resolve_provider_models("claude", self._STATE, None) assert (models, error, relayed) == (None, None, False) - def test_anthropic_returns_no_models(self, monkeypatch): - # The developer-configured path is deliberately unchanged: an Anthropic service pins nothing - # even with explicit targets — Claude Code's canonical names route fine. (The managed path - # pins from authored manifest slots instead; see managed_resolve.) + def test_anthropic_pins_family_targets(self, monkeypatch): + # An API-key Anthropic service pins its declared targets by family, so the client sends + # exactly the ids the MPS allows rather than Claude Code's canonical names (which may not + # match the declared targets → gateway 403 "not in the allowed models list"). self._patch( monkeypatch, {"provider_type": "anthropic", "targets": ["claude-sonnet-5", "claude-haiku-4-5"]}, @@ -291,9 +291,18 @@ def test_anthropic_returns_no_models(self, monkeypatch): "claude", self._STATE, "main.a.svc" ) assert error is None - assert models is None + assert models == {"sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5"} assert relayed is False + def test_anthropic_with_no_claude_targets_pins_nothing(self, monkeypatch): + # No Claude-family targets → no pins (the `or None` fallback), leaving Claude Code's + # defaults in place rather than an empty dict. + self._patch(monkeypatch, {"provider_type": "anthropic", "targets": []}, None) + models, error, relayed = agents_mod.resolve_provider_models( + "claude", self._STATE, "main.a.empty" + ) + assert (models, error, relayed) == (None, None, False) + def test_relayed_anthropic_flagged(self, monkeypatch): self._patch( monkeypatch, {"provider_type": "anthropic", "targets": [], "relayed": True}, None