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
65 changes: 46 additions & 19 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,26 @@ def _smart_routing_v2_flag(enabled: bool) -> Iterator[None]:
os.environ[smart_routing_v2.ENV_VAR] = previous


@contextmanager
def _disable_smart_routing_for_subcommand(tool: str, ctx: Any) -> Iterator[None]:
"""Keep native agent subcommands out of every smart-routing path.

The environment flag is also consulted during bootstrap/version checks,
before the final launch options are built. Native positional subcommands
must therefore suppress the flag for the whole ucode launch flow. An
explicit prompt after `--` remains eligible for routing.
"""
if _smart_routing_launch_shape(tool, ctx.args, _has_explicit_prompt(ctx)):
yield
return
previous = os.environ.pop(smart_routing_v2.ENV_VAR, None)
try:
yield
finally:
if previous is not None:
os.environ[smart_routing_v2.ENV_VAR] = previous


def _migrate_legacy_smart_routing(state: dict) -> dict:
"""Remove the former persisted opt-in and its permanent routing hooks."""
if smart_routing_v2.LEGACY_STATE_KEY not in state:
Expand Down Expand Up @@ -1990,6 +2010,11 @@ def _should_launch_smart_routing(
) -> bool:
if model is not None or has_explicit_model_arg(tool_args):
return False
return _smart_routing_launch_shape(tool, tool_args, explicit_prompt)


def _smart_routing_launch_shape(tool: str, tool_args: list[str], explicit_prompt: bool) -> bool:
"""Whether the forwarded arguments represent an interactive launch."""
if not tool_args or explicit_prompt:
return True
return tool == "claude" and tool_args[0].startswith("-")
Expand Down Expand Up @@ -2558,15 +2583,16 @@ def codex_cmd(
print_success("Codex smart routing disabled; ug routing hooks removed")
return
with _smart_routing_v2_flag(enable_smart_routing_flag):
_launch_tool(
"codex",
ctx,
provider=provider,
refresh=refresh,
skip_preflight=skip_preflight,
workspace_url=workspace,
custom_oauth=custom_oauth,
)
with _disable_smart_routing_for_subcommand("codex", ctx):
_launch_tool(
"codex",
ctx,
provider=provider,
refresh=refresh,
skip_preflight=skip_preflight,
workspace_url=workspace,
custom_oauth=custom_oauth,
)


@app.command(
Expand Down Expand Up @@ -2659,16 +2685,17 @@ def claude_cmd(
if enable_model_discovery:
os.environ[claude_agent.GATEWAY_MODEL_DISCOVERY_ENV_VAR] = "1"
with _smart_routing_v2_flag(enable_smart_routing_flag):
_launch_tool(
"claude",
ctx,
provider=provider,
model=model,
refresh=refresh,
skip_preflight=skip_preflight,
workspace_url=workspace,
custom_oauth=custom_oauth,
)
with _disable_smart_routing_for_subcommand("claude", ctx):
_launch_tool(
"claude",
ctx,
provider=provider,
model=model,
refresh=refresh,
skip_preflight=skip_preflight,
workspace_url=workspace,
custom_oauth=custom_oauth,
)


@app.command("gemini", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
Expand Down
19 changes: 19 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,25 @@ def test_codex_enable_smart_routing_is_consumed_by_ucode(self):
assert "ENABLE_SMART_ROUTING_V2" not in os.environ
assert mock_launch.call_args.args[1].args == []

@pytest.mark.parametrize("tool, subcommand", [("codex", "app"), ("claude", "update")])
def test_native_subcommand_suppresses_inherited_smart_routing(
self, monkeypatch, tool, subcommand
):
monkeypatch.setenv("ENABLE_SMART_ROUTING_V2", "1")
observed = []

with patch(
"ucode.cli._launch_tool",
side_effect=lambda *_args, **_kwargs: observed.append(
os.environ.get("ENABLE_SMART_ROUTING_V2")
),
):
result = runner.invoke(app, [tool, subcommand])

assert result.exit_code == 0, result.output
assert observed == [None]
assert os.environ["ENABLE_SMART_ROUTING_V2"] == "1"

def test_claude_enable_smart_routing_forwards_positional_prompt_to_v2(self):
captured = []

Expand Down
Loading