Skip to content
Merged
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 4 additions & 11 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
5 changes: 5 additions & 0 deletions tests/integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 15 additions & 3 deletions tests/integration/test_ug_configure_claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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()
Expand Down
39 changes: 23 additions & 16 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -803,12 +803,6 @@ def test_invalid_model_location_is_rejected(self, tool):
assert result.exit_code == 1
assert "--model-location must be `<catalog>.<schema>`." 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),
Expand Down Expand Up @@ -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": {
Expand Down
Loading