diff --git a/README.md b/README.md index 9ae0bb69..94fcacf8 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,11 @@ On first launch of a model-backed agent, `ug` prompts for a Databricks workspace, authenticates, and writes local agent config. Later launches reuse the saved workspace and credentials. +Without a managed workspace config, `ug claude` automatically discovers gateway +models for Claude Code's `/model` picker. Discovery defaults to `system.ai` when +no provider or model location is selected. Use `--provider` or `--model-location` +to select another model source; managed workspace configs control their own sources. + ## Configure ```bash diff --git a/src/ucode/cli.py b/src/ucode/cli.py index 40479075..593d8bfd 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -2600,7 +2600,10 @@ def _launch_tool( # rewriting it; the admin's location exists only for this launch. provider = None parent_schema = managed_parent_schema - if tool == "claude" and (managed_provider or managed_parent_schema): + # Unmanaged Claude launches discover gateway models automatically; with no + # provider or parent header the gateway defaults to system.ai. Managed + # configs opt into discovery by selecting an MPS or Unity Catalog location. + if tool == "claude" and (managed is None or managed_provider or managed_parent_schema): os.environ[claude_agent.GATEWAY_MODEL_DISCOVERY_ENV_VAR] = "1" # The environment switch remains a developer override; managed config is the workspace # policy equivalent and must take effect before launch options are computed. @@ -3151,14 +3154,6 @@ def claude_cmd( str | None, typer.Option("--scopes", hidden=True, help="Comma-separated custom OAuth scopes."), ] = None, - enable_model_discovery: Annotated[ - bool, - typer.Option( - "--enable-model-discovery", - hidden=True, - help="Enable AI Gateway models in Claude Code's model picker.", - ), - ] = False, enable_smart_routing_flag: Annotated[ bool, typer.Option( @@ -3188,8 +3183,6 @@ def claude_cmd( claude_agent.disable_smart_routing(load_state()) print_success("Claude Code smart routing disabled; ug routing hooks removed") return - if enable_model_discovery or model_location is not None or provider is not None: - os.environ[claude_agent.GATEWAY_MODEL_DISCOVERY_ENV_VAR] = "1" with _smart_routing_v2_flag(enable_smart_routing_flag): with _disable_smart_routing_for_subcommand("claude", ctx): _launch_tool( diff --git a/tests/README.md b/tests/README.md index c6be4ec3..f73dd721 100644 --- a/tests/README.md +++ b/tests/README.md @@ -33,7 +33,7 @@ All tests live directly in `integration/`; shared mechanics live in `utils/`. | Test | User action | Expected evidence | | --- | --- | --- | -| `test_ug_configure_claude_databricks` | Configure Databricks Hosted; execute the generated auth helper; open Claude TUI and read a file | Generated helper invokes `ug` with clean token stdout; assistant returns an unpredictable file value; normal exit; reopen with working keyboard input | +| `test_ug_configure_claude_databricks` | Configure Databricks Hosted; execute the generated auth helper; launch plain `ug claude`, read a file, and open `/model` | Generated helper invokes `ug` with clean token stdout; assistant returns an unpredictable file value; native discovery caches `system.ai` models and the picker shows a discovered model without an opt-in flag; normal exit; reopen with working keyboard input | | `test_ug_configure_claude_anthropic_mps` | Select Anthropic MPS in the real configure picker; launch Claude | Saved provider in status; completed TUI file task; normal exit | | `test_ug_configure_codex_databricks` | Configure Databricks Hosted; execute the generated auth helper; open Codex TUI and read a file | Generated helper invokes `ug` with clean token stdout; completed assistant answer contains the file value; normal exit and reopen | | `test_ug_configure_codex_openai_mps` | Select OpenAI MPS in the real configure picker; launch Codex | Saved provider in status; completed TUI file task; normal exit | diff --git a/tests/integration/README.md b/tests/integration/README.md index a1b13314..1923d49e 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -80,6 +80,11 @@ bearer override and drive their real local web-search MCP handshake/tool listing These assert protocol stdout without stripping ANSI escapes and make no workspace requests. The live Hosted configure journeys additionally execute the actual `ug` auth helper written into each agent's configuration before completing a real TUI task. +The Claude journey also opens `/model` after a plain `ug claude` launch, requires +its native gateway cache to contain `system.ai` models, and checks that a discovered +model appears in the picker. No managed config, provider, model location, discovery +flag, or inherited discovery environment variable enables this path. This runs with +the pinned Claude version (currently 2.1.268 in CI). ## Test layout and format diff --git a/tests/integration/test_ug_configure_claude.py b/tests/integration/test_ug_configure_claude.py index 8f3c29b4..9b2ebb88 100644 --- a/tests/integration/test_ug_configure_claude.py +++ b/tests/integration/test_ug_configure_claude.py @@ -13,10 +13,12 @@ @pytest.mark.smoke def test_ug_configure_claude_databricks(live_session, workspace): - """Scenario: configure Claude with Databricks Hosted and use its TUI. + """Scenario: configure Databricks Hosted, launch plain ug claude, and open /model. - Expected: configure succeeds, Claude returns a file value through the real - gateway, exits normally, and can reopen the configuration ug created. + Expected: without managed config, a provider, a model location, or a discovery + flag, Claude caches system.ai models from the gateway and shows a discovered + model in its real picker. It returns a file value through the real gateway, + exits normally, and can reopen the configuration ug created. The generated auth helper uses ug and prints only the supplied bearer. Optional AI Tools are disabled; the selected agent version is kept pinned. """ @@ -49,6 +51,16 @@ def test_ug_configure_claude_databricks(live_session, workspace): tui.boot() tui.submit(task.prompt) tui.wait_for_task(task) + screen = tui.open_model_picker() + cache = json.loads((session.home / ".claude/cache/gateway-models.json").read_text()) + assert cache["baseUrl"] == workspace.rstrip("/") + "/ai-gateway/anthropic", cache + models = cache["models"] + system_ai_models = [model for model in models if model["id"].startswith("system.ai.")] + assert system_ai_models, cache + assert any( + model["id"] in screen or (model.get("display_name") and model["display_name"] in screen) + for model in system_ai_models + ), screen tui.exit_normally() task.assert_completed(session, "claude") session.assert_not_routed() diff --git a/tests/test_cli.py b/tests/test_cli.py index 2a0824a7..5bb6be07 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -729,30 +729,30 @@ def test_codex_forwarded_model_is_not_printed_in_launch_summary( assert "Model: system.ai.gpt-5-6-luna" not in output assert mock_launch.call_args.args[2] == forwarded_args - def test_claude_enable_model_discovery_sets_ucode_env(self): - with patch("ucode.cli._launch_tool") as mock_launch: - result = runner.invoke(app, ["claude", "--enable-model-discovery"]) + def test_unmanaged_claude_launch_enables_model_discovery(self): + with _launch_policy_patches(None) as calls: + result = runner.invoke(app, ["claude"]) assert result.exit_code == 0, result.output assert os.environ["ENABLE_CLAUDE_CODE_GATEWAY_MODEL_DISCOVERY"] == "1" - assert mock_launch.call_args.args[1].args == [] + calls["launch"].assert_called_once() def test_claude_model_location_is_forwarded(self): - with patch("ucode.cli._launch_tool") as mock_launch: + with _launch_policy_patches(None) as calls: result = runner.invoke(app, ["claude", "--model-location", "main.default"]) assert result.exit_code == 0, result.output - assert mock_launch.call_args.kwargs["parent_schema"] == "main.default" - assert mock_launch.call_args.args[1].args == [] + assert calls["configure"].call_args.kwargs["parent_schema"] == "main.default" + assert calls["launch"].call_args.args[2] == [] assert os.environ["ENABLE_CLAUDE_CODE_GATEWAY_MODEL_DISCOVERY"] == "1" def test_claude_provider_enables_model_discovery(self): - with patch("ucode.cli._launch_tool") as mock_launch: + with _launch_policy_patches(None) as calls: result = runner.invoke(app, ["claude", "--provider", "main.default.anthropic"]) assert result.exit_code == 0, result.output - assert mock_launch.call_args.kwargs["provider"] == "main.default.anthropic" - assert mock_launch.call_args.args[1].args == [] + assert calls["configure"].call_args.kwargs["provider"] == "main.default.anthropic" + assert calls["launch"].call_args.args[2] == [] assert os.environ["ENABLE_CLAUDE_CODE_GATEWAY_MODEL_DISCOVERY"] == "1" def test_codex_model_location_is_forwarded(self): @@ -803,12 +803,6 @@ def test_invalid_model_location_is_rejected(self, tool): assert result.exit_code == 1 assert "--model-location must be `.`." in _strip_ansi(result.output) - def test_claude_enable_model_discovery_is_hidden_from_help(self): - result = runner.invoke(app, ["claude", "--help"]) - - assert result.exit_code == 0, result.output - assert "--enable-model-discovery" not in result.output - def test_codex_disable_removes_hooks_without_launching(self): with ( patch("ucode.cli.load_state", return_value=MINIMAL_STATE), @@ -1027,6 +1021,19 @@ def test_persisted_provider_is_not_mistaken_for_an_explicit_option(self): class TestManagedClaudeModelDiscovery: + def test_managed_static_models_do_not_enable_discovery(self): + managed = { + "enabled_agents": { + "claude": {"model_config": {"model_services": ["system.ai.claude-sonnet-5"]}} + } + } + with _launch_policy_patches(managed) as calls: + result = runner.invoke(app, ["claude"]) + + assert result.exit_code == 0, result.output + calls["launch"].assert_called_once() + assert "ENABLE_CLAUDE_CODE_GATEWAY_MODEL_DISCOVERY" not in os.environ + MPS_CONFIG = { "enabled_agents": { "claude": {