Skip to content

Commit a2754bd

Browse files
masonc08Isaac
andcommitted
MPS: forward --model to Claude Code for a relayed provider
A relayed (Claude Max/Team/Enterprise subscription) Model Provider Service is a subscription relay: the AI Gateway passes the client's requested model through to Anthropic and, for Team/Enterprise, validates it against the service's allowlist (only personal Max skips that) -- it never selects a model server-side. Real Enterprise relays are allow_all_targets with no declared Claude targets, so there is nothing for ucode to resolve --model against. So for a relayed provider, forward --model to Claude Code's own --model flag -- exactly what 'ucode claude --provider <mps> -- --model X' already does, now automatic. Claude Code selects the model natively and the relay honors it. Non-relayed providers (API-key / Bedrock) keep the env-pinning path unchanged. Verified end-to-end against a relayed Enterprise MPS: '-- --model opus' launches on Opus via the subscription. Co-authored-by: Isaac <no-reply@databricks.com>
1 parent fcc9470 commit a2754bd

2 files changed

Lines changed: 32 additions & 19 deletions

File tree

src/ucode/cli.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,13 +2031,8 @@ def _launch_tool(
20312031
# provider). Skip model resolution, which would otherwise fail when
20322032
# the workspace has no matching Databricks models.
20332033
resolved_model = None
2034-
if tool == "claude" and relayed:
2035-
if model:
2036-
print_warning(
2037-
"This is a subscription-relay Model Provider Service; the gateway selects "
2038-
"the model, so --model is ignored."
2039-
)
2040-
elif tool == "claude" and (model or provider_models):
2034+
# Relayed services forward --model to Claude Code's own flag at launch (below), not env.
2035+
if tool == "claude" and not relayed and (model or provider_models):
20412036
route_root_model = resolve_provider_launch_model(model, provider_models or {})
20422037
else:
20432038
# A managed default_model is the model the admin wants sessions to start on, so it goes
@@ -2115,6 +2110,10 @@ def _launch_tool(
21152110
# per-family target pins.
21162111
custom_model=model if (tool == "claude" and not provider) else None,
21172112
)
2113+
# Relayed = a Claude subscription: forward --model to Claude Code's own flag, like `-- --model X`.
2114+
if tool == "claude" and provider and relayed and model and not forwarded_model:
2115+
ctx.args = ["--model", model, *ctx.args]
2116+
forwarded_model = model
21182117
print_section(_launch_title(tool))
21192118
if managed is not None:
21202119
print_kv("Config", "workspace-managed")

tests/test_cli.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ def test_v2_model_sets_transient_launch_override(self, monkeypatch):
614614
@staticmethod
615615
def _provider_launch(monkeypatch, argv, provider_models, relayed=False):
616616
"""Invoke a provider launch with model discovery/config stubbed, returning the
617-
configure_tool mock so tests can assert what was threaded to it."""
617+
configure_tool and launch_agent mocks so tests can assert what was threaded to each."""
618618
import ucode.cli as cli_mod
619619

620620
monkeypatch.setattr(cli_mod, "ensure_bootstrap_dependencies", lambda *a, **k: None)
@@ -623,19 +623,20 @@ def _provider_launch(monkeypatch, argv, provider_models, relayed=False):
623623
monkeypatch.setattr(cli_mod, "configure_shared_state", lambda *a, **k: MINIMAL_STATE)
624624
monkeypatch.setattr(cli_mod, "_fetch_managed_config", lambda s: (None, False))
625625
monkeypatch.setattr(cli_mod, "_fetch_budget_recommendation", lambda s, m: None)
626-
monkeypatch.setattr(cli_mod, "launch_agent", lambda *a, **k: None)
626+
mock_launch = MagicMock()
627+
monkeypatch.setattr(cli_mod, "launch_agent", mock_launch)
627628
monkeypatch.setattr(
628629
cli_mod, "resolve_provider_models", lambda t, s, p: (provider_models, None, relayed)
629630
)
630631
mock_configure = MagicMock(return_value=MINIMAL_STATE)
631632
monkeypatch.setattr(cli_mod, "configure_tool", mock_configure)
632633
result = runner.invoke(app, argv)
633-
return result, mock_configure
634+
return result, mock_configure, mock_launch
634635

635636
def test_model_and_provider_now_pin_the_launch_tier(self, monkeypatch):
636637
# --model under a provider is no longer rejected: a family alias resolves to that tier's
637638
# declared target and is threaded as route_root_model (ANTHROPIC_MODEL), not custom_model.
638-
result, mock_configure = self._provider_launch(
639+
result, mock_configure, _ = self._provider_launch(
639640
monkeypatch,
640641
["claude", "--model", "haiku", "--provider", "cat.schema.svc"],
641642
{"sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5"},
@@ -647,7 +648,7 @@ def test_model_and_provider_now_pin_the_launch_tier(self, monkeypatch):
647648
def test_provider_without_opus_auto_picks_best_servable_tier(self, monkeypatch):
648649
# No --model, and the service declares no opus target: launch on the most capable tier it
649650
# does offer (sonnet) instead of dead-ending on Claude Code's opus default.
650-
result, mock_configure = self._provider_launch(
651+
result, mock_configure, _ = self._provider_launch(
651652
monkeypatch,
652653
["claude", "--provider", "cat.schema.svc"],
653654
{"sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5"},
@@ -658,7 +659,7 @@ def test_provider_without_opus_auto_picks_best_servable_tier(self, monkeypatch):
658659
def test_provider_with_opus_keeps_claude_default(self, monkeypatch):
659660
# Opus is offered, so Claude Code's own default already works — pin nothing (no ANTHROPIC_MODEL
660661
# and no duplicate /model picker row).
661-
result, mock_configure = self._provider_launch(
662+
result, mock_configure, _ = self._provider_launch(
662663
monkeypatch,
663664
["claude", "--provider", "cat.schema.svc"],
664665
{"opus": "claude-opus-4-8", "sonnet": "claude-sonnet-5"},
@@ -667,25 +668,38 @@ def test_provider_with_opus_keeps_claude_default(self, monkeypatch):
667668
assert mock_configure.call_args.kwargs["route_root_model"] is None
668669

669670
def test_model_family_not_offered_by_provider_errors(self, monkeypatch):
670-
result, _ = self._provider_launch(
671+
result, _, _ = self._provider_launch(
671672
monkeypatch,
672673
["claude", "--model", "opus", "--provider", "cat.schema.svc"],
673674
{"sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5"},
674675
)
675676
assert result.exit_code == 1
676677
assert "does not offer a 'opus' model" in result.output
677678

678-
def test_model_ignored_for_relayed_provider(self, monkeypatch):
679-
# A relayed (subscription) service selects the model server-side; --model can't be honored.
680-
result, mock_configure = self._provider_launch(
679+
def test_model_forwarded_to_claude_for_relayed_provider(self, monkeypatch):
680+
# Relayed = a subscription: --model rides Claude Code's own flag, not gateway env.
681+
result, mock_configure, mock_launch = self._provider_launch(
681682
monkeypatch,
682-
["claude", "--model", "haiku", "--provider", "cat.schema.svc"],
683+
["claude", "--model", "opus", "--provider", "cat.schema.svc"],
683684
None,
684685
relayed=True,
685686
)
686687
assert result.exit_code == 0, result.output
688+
assert mock_launch.call_args.args[2] == ["--model", "opus"]
687689
assert mock_configure.call_args.kwargs["route_root_model"] is None
688-
assert "--model is ignored" in _strip_ansi(result.output)
690+
assert mock_configure.call_args.kwargs["custom_model"] is None
691+
assert "ignored" not in _strip_ansi(result.output)
692+
693+
def test_relayed_provider_without_model_forwards_nothing(self, monkeypatch):
694+
# No --model on a relayed launch: nothing to forward.
695+
result, _, mock_launch = self._provider_launch(
696+
monkeypatch,
697+
["claude", "--provider", "cat.schema.svc"],
698+
None,
699+
relayed=True,
700+
)
701+
assert result.exit_code == 0, result.output
702+
assert mock_launch.call_args.args[2] == []
689703

690704
def test_provider_sets_transient_claude_launch_marker(self):
691705
state = dict(MINIMAL_STATE)

0 commit comments

Comments
 (0)