Skip to content

Commit bc82495

Browse files
masonc08Isaac
andauthored
Pin declared Anthropic MPS targets by family, like Bedrock (#376)
`ucode claude --provider <mps>` sends whatever model string Claude Code's alias resolves to, and the gateway's direct-MPS path exact-matches that against the service's declared targets (then forwards it verbatim). For a Bedrock service ucode already pins ANTHROPIC_DEFAULT_*_MODEL to the service's target slugs, but for an API-key Anthropic service it pinned nothing and trusted Claude Code's canonical names to match — which they may not: a custom/dated target name, or an enterprise managed-settings pin like `system.ai.claude-haiku-4-5`, yields a 403 "not in the allowed models list". resolve_provider_models now derives the family->target map for a non-relayed Anthropic service too (map_claude_family_models already handles canonical ids), so the client sends exactly the ids the MPS declares. Bedrock is unchanged; relayed Claude Max/Enterprise stays exempt (model selection is off server-side). Co-authored-by: Isaac <no-reply@databricks.com>
1 parent 6ab063f commit bc82495

2 files changed

Lines changed: 31 additions & 20 deletions

File tree

src/ucode/agents/__init__.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
from ucode.config_io import ToolSpec
2020
from ucode.databricks import (
21-
BEDROCK_PROVIDER_TYPES,
2221
get_databricks_token,
2322
install_ai_tools,
2423
install_databricks_cli,
@@ -296,17 +295,18 @@ def resolve_provider_models(
296295
"""Validate ``provider`` for ``tool`` and return the model ids to pin.
297296
298297
Returns ``(provider_models, error, relayed)``. ``provider_models`` is a ``{family: model_id}``
299-
dict for a Bedrock-backed claude service (whose provider-side ids must be pinned explicitly), or
300-
None for an Anthropic/canonical service or when ``provider`` is None. ``relayed`` is True for a
301-
credential-less Anthropic subscription relay, which the launch path wires with the relayed
302-
overlay + refresh proxy. A non-None ``error`` means the provider is invalid for the tool and the
303-
caller should not launch.
304-
305-
This is the *developer-configured* path (``ucode configure`` then ``ucode claude``) and its
306-
behaviour is deliberately unchanged: only Bedrock pins, re-derived from the service's live
307-
targets. The *managed* path pins from the admin's authored manifest slots instead — see
308-
``managed_resolve.managed_provider_family_models`` and its launch call site — so an admin's
309-
chosen versions win rather than being re-derived here.
298+
dict re-derived from the service's live targets for a non-relayed claude service — both Bedrock
299+
(provider-side slugs) and API-key Anthropic (canonical ids) — so the client sends exactly the ids
300+
the MPS allows rather than Claude Code's defaults, which may not match the declared targets. It is
301+
None when ``provider`` is None, for a relayed subscription (see below), or for a non-Claude (e.g.
302+
codex) service. ``relayed`` is True for a credential-less Anthropic subscription relay, which the
303+
launch path wires with the relayed overlay + refresh proxy. A non-None ``error`` means the
304+
provider is invalid for the tool and the caller should not launch.
305+
306+
This is the *developer-configured* path (``ucode configure`` then ``ucode claude``). The *managed*
307+
path pins from the admin's authored manifest slots instead — see
308+
``managed_resolve.managed_provider_family_models`` and its launch call site — so an admin's chosen
309+
versions win rather than being re-derived here.
310310
"""
311311
if not provider:
312312
return None, None, False
@@ -315,9 +315,11 @@ def resolve_provider_models(
315315
if error or service is None:
316316
return None, error, False
317317
relayed = bool(service.get("relayed"))
318-
if service["provider_type"] in BEDROCK_PROVIDER_TYPES:
319-
return map_claude_family_models(service.get("targets") or []), None, relayed
320-
return None, None, relayed
318+
# Relayed (Claude Max/Enterprise subscription) is exempt: the gateway disables
319+
# model selection server-side for that tier, so there's nothing to reconcile.
320+
if relayed:
321+
return None, None, relayed
322+
return map_claude_family_models(service.get("targets") or []) or None, None, relayed
321323

322324

323325
def configure_tool(

tests/test_agents_init.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,10 @@ def test_none_provider_returns_none(self):
278278
models, error, relayed = agents_mod.resolve_provider_models("claude", self._STATE, None)
279279
assert (models, error, relayed) == (None, None, False)
280280

281-
def test_anthropic_returns_no_models(self, monkeypatch):
282-
# The developer-configured path is deliberately unchanged: an Anthropic service pins nothing
283-
# even with explicit targets — Claude Code's canonical names route fine. (The managed path
284-
# pins from authored manifest slots instead; see managed_resolve.)
281+
def test_anthropic_pins_family_targets(self, monkeypatch):
282+
# An API-key Anthropic service pins its declared targets by family, so the client sends
283+
# exactly the ids the MPS allows rather than Claude Code's canonical names (which may not
284+
# match the declared targets → gateway 403 "not in the allowed models list").
285285
self._patch(
286286
monkeypatch,
287287
{"provider_type": "anthropic", "targets": ["claude-sonnet-5", "claude-haiku-4-5"]},
@@ -291,9 +291,18 @@ def test_anthropic_returns_no_models(self, monkeypatch):
291291
"claude", self._STATE, "main.a.svc"
292292
)
293293
assert error is None
294-
assert models is None
294+
assert models == {"sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5"}
295295
assert relayed is False
296296

297+
def test_anthropic_with_no_claude_targets_pins_nothing(self, monkeypatch):
298+
# No Claude-family targets → no pins (the `or None` fallback), leaving Claude Code's
299+
# defaults in place rather than an empty dict.
300+
self._patch(monkeypatch, {"provider_type": "anthropic", "targets": []}, None)
301+
models, error, relayed = agents_mod.resolve_provider_models(
302+
"claude", self._STATE, "main.a.empty"
303+
)
304+
assert (models, error, relayed) == (None, None, False)
305+
297306
def test_relayed_anthropic_flagged(self, monkeypatch):
298307
self._patch(
299308
monkeypatch, {"provider_type": "anthropic", "targets": [], "relayed": True}, None

0 commit comments

Comments
 (0)