diff --git a/src/ucode/cli.py b/src/ucode/cli.py index 1e15e844..5fba6e69 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -2031,13 +2031,8 @@ def _launch_tool( # provider). Skip model resolution, which would otherwise fail when # the workspace has no matching Databricks models. resolved_model = None - if tool == "claude" and relayed: - if model: - print_warning( - "This is a subscription-relay Model Provider Service; the gateway selects " - "the model, so --model is ignored." - ) - elif tool == "claude" and (model or provider_models): + # Relayed services forward --model to Claude Code's own flag at launch (below), not env. + if tool == "claude" and not relayed and (model or provider_models): route_root_model = resolve_provider_launch_model(model, provider_models or {}) else: # A managed default_model is the model the admin wants sessions to start on, so it goes @@ -2115,6 +2110,10 @@ def _launch_tool( # per-family target pins. custom_model=model if (tool == "claude" and not provider) else None, ) + # Relayed = a Claude subscription: forward --model to Claude Code's own flag, like `-- --model X`. + if tool == "claude" and provider and relayed and model and not forwarded_model: + ctx.args = ["--model", model, *ctx.args] + forwarded_model = model print_section(_launch_title(tool)) if managed is not None: print_kv("Config", "workspace-managed") diff --git a/tests/test_cli.py b/tests/test_cli.py index 704bde48..c54480c0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -614,7 +614,7 @@ def test_v2_model_sets_transient_launch_override(self, monkeypatch): @staticmethod def _provider_launch(monkeypatch, argv, provider_models, relayed=False): """Invoke a provider launch with model discovery/config stubbed, returning the - configure_tool mock so tests can assert what was threaded to it.""" + configure_tool and launch_agent mocks so tests can assert what was threaded to each.""" import ucode.cli as cli_mod monkeypatch.setattr(cli_mod, "ensure_bootstrap_dependencies", lambda *a, **k: None) @@ -623,19 +623,20 @@ def _provider_launch(monkeypatch, argv, provider_models, relayed=False): monkeypatch.setattr(cli_mod, "configure_shared_state", lambda *a, **k: MINIMAL_STATE) monkeypatch.setattr(cli_mod, "_fetch_managed_config", lambda s: (None, False)) monkeypatch.setattr(cli_mod, "_fetch_budget_recommendation", lambda s, m: None) - monkeypatch.setattr(cli_mod, "launch_agent", lambda *a, **k: None) + mock_launch = MagicMock() + monkeypatch.setattr(cli_mod, "launch_agent", mock_launch) monkeypatch.setattr( cli_mod, "resolve_provider_models", lambda t, s, p: (provider_models, None, relayed) ) mock_configure = MagicMock(return_value=MINIMAL_STATE) monkeypatch.setattr(cli_mod, "configure_tool", mock_configure) result = runner.invoke(app, argv) - return result, mock_configure + return result, mock_configure, mock_launch def test_model_and_provider_now_pin_the_launch_tier(self, monkeypatch): # --model under a provider is no longer rejected: a family alias resolves to that tier's # declared target and is threaded as route_root_model (ANTHROPIC_MODEL), not custom_model. - result, mock_configure = self._provider_launch( + result, mock_configure, _ = self._provider_launch( monkeypatch, ["claude", "--model", "haiku", "--provider", "cat.schema.svc"], {"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): def test_provider_without_opus_auto_picks_best_servable_tier(self, monkeypatch): # No --model, and the service declares no opus target: launch on the most capable tier it # does offer (sonnet) instead of dead-ending on Claude Code's opus default. - result, mock_configure = self._provider_launch( + result, mock_configure, _ = self._provider_launch( monkeypatch, ["claude", "--provider", "cat.schema.svc"], {"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): def test_provider_with_opus_keeps_claude_default(self, monkeypatch): # Opus is offered, so Claude Code's own default already works — pin nothing (no ANTHROPIC_MODEL # and no duplicate /model picker row). - result, mock_configure = self._provider_launch( + result, mock_configure, _ = self._provider_launch( monkeypatch, ["claude", "--provider", "cat.schema.svc"], {"opus": "claude-opus-4-8", "sonnet": "claude-sonnet-5"}, @@ -667,7 +668,7 @@ def test_provider_with_opus_keeps_claude_default(self, monkeypatch): assert mock_configure.call_args.kwargs["route_root_model"] is None def test_model_family_not_offered_by_provider_errors(self, monkeypatch): - result, _ = self._provider_launch( + result, _, _ = self._provider_launch( monkeypatch, ["claude", "--model", "opus", "--provider", "cat.schema.svc"], {"sonnet": "claude-sonnet-5", "haiku": "claude-haiku-4-5"}, @@ -675,17 +676,30 @@ def test_model_family_not_offered_by_provider_errors(self, monkeypatch): assert result.exit_code == 1 assert "does not offer a 'opus' model" in result.output - def test_model_ignored_for_relayed_provider(self, monkeypatch): - # A relayed (subscription) service selects the model server-side; --model can't be honored. - result, mock_configure = self._provider_launch( + def test_model_forwarded_to_claude_for_relayed_provider(self, monkeypatch): + # Relayed = a subscription: --model rides Claude Code's own flag, not gateway env. + result, mock_configure, mock_launch = self._provider_launch( monkeypatch, - ["claude", "--model", "haiku", "--provider", "cat.schema.svc"], + ["claude", "--model", "opus", "--provider", "cat.schema.svc"], None, relayed=True, ) assert result.exit_code == 0, result.output + assert mock_launch.call_args.args[2] == ["--model", "opus"] assert mock_configure.call_args.kwargs["route_root_model"] is None - assert "--model is ignored" in _strip_ansi(result.output) + assert mock_configure.call_args.kwargs["custom_model"] is None + assert "ignored" not in _strip_ansi(result.output) + + def test_relayed_provider_without_model_forwards_nothing(self, monkeypatch): + # No --model on a relayed launch: nothing to forward. + result, _, mock_launch = self._provider_launch( + monkeypatch, + ["claude", "--provider", "cat.schema.svc"], + None, + relayed=True, + ) + assert result.exit_code == 0, result.output + assert mock_launch.call_args.args[2] == [] def test_provider_sets_transient_claude_launch_marker(self): state = dict(MINIMAL_STATE)