Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions src/ucode/agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
19 changes: 14 additions & 5 deletions tests/test_agents_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]},
Expand All @@ -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
Expand Down
Loading