Skip to content
Closed
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
9 changes: 8 additions & 1 deletion docs/claude-mod.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ can list what the module reads and writes, and `plugin validate` rejects a varia
position: "$.env.get takes a literal name as its first argument". Keep those names inline.

The mod merges the launch recipe into the `Databricks-Smart-Router-Recipe` header
via `ANTHROPIC_CUSTOM_HEADERS` and intercepts the existing skill's `/smart-router on|off` command locally, without inference or command
via `ANTHROPIC_CUSTOM_HEADERS` and intercepts the existing skill's `/smart-router on|off|status` command locally, without inference or command
registration. Off sets every routing flag to `0` and the recipe to `DISABLED`; on
restores the original flags and recipe, including originally absent variables.
Unrelated headers are preserved, and `CLAUDE_CODE_EXTRA_BODY` is left untouched.
Duplicate recipe headers are replaced case-insensitively. Recipe line breaks are rejected.
A failed write attempts to restore prior values and reports failure; the native environment API does not provide an atomic transaction.

`/smart-router status` reads the effective routing flags through UG's read-only
`claude --smart-routing-status` command, including the latest session overrides.
The `CommandOutput` render hook displays a green ON or gray OFF badge using native
`Box` and `Text` elements. Its text remains available in headless output, and
errors retain Claude's normal rendering. Status does not change routing or orchestration.
Codex and Claude sessions without mods use the skill's corresponding CLI command.

If a caller supplies `smart_router_recipe_name` in the request body, gateways with
body-recipe parsing enabled give it precedence over the header; toggles preserve that override.

Expand Down
13 changes: 9 additions & 4 deletions skills/smart-router/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
name: smart-router
description: Enable or disable Unity Gateway subagent model routing for the current smart-routed Claude or Codex session, along with Smart Router Orchestrator when opted in.
allowed-tools: Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli claude --enable-smart-routing), Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli claude --disable-smart-routing), Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli codex --enable-smart-routing), Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli codex --disable-smart-routing)
description: Show status, enable, or disable Unity Gateway subagent model routing for the current smart-routed Claude or Codex session, along with Smart Router Orchestrator when opted in.
argument-hint: "on|off|status"
allowed-tools: Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli claude --enable-smart-routing), Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli claude --disable-smart-routing), Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli claude --smart-routing-status), Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli codex --enable-smart-routing), Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli codex --disable-smart-routing), Bash("$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli codex --smart-routing-status)
metadata:
version: "1.2.0"
version: "1.3.0"
---

# Smart Router
Expand All @@ -15,13 +16,17 @@ Keep its path quoted and do not substitute `ug` or `python` from PATH.
`"$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli claude --disable-smart-routing` for `off`.
- In Codex, run exactly `"$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli codex --enable-smart-routing` for `on` or
`"$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli codex --disable-smart-routing` for `off`.
- For `status`, run exactly `"$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli claude --smart-routing-status`
in Claude Code or `"$UCODE_SMART_ROUTER_PYTHON" -m ucode.cli codex --smart-routing-status` in Codex.
Return the current session's on/off status without changing routing or orchestration.
Claude's native mod handles this locally and renders an ON/OFF badge; the CLI supplies text in other sessions.

In PowerShell, use `& "$env:UCODE_SMART_ROUTER_PYTHON"` in place of
`"$UCODE_SMART_ROUTER_PYTHON"`; keep the remaining arguments the same.
If `UCODE_SMART_ROUTER_PYTHON` or `UCODE_SESSION_ENV_FILE` is unset, ask the user
to restart through an updated Unity Gateway with smart routing enabled.

With no argument, explain that only `on` and `off` are accepted. Do not edit the state file.
With no argument, explain that `on`, `off`, and `status` are accepted. Do not edit the state file.
This affects subsequent subagent model selection in the current session, not the root model
or first prompt. In sessions launched with `ENABLE_SMART_ROUTER_ORCHESTRATOR=1`, it also controls Smart Router
Orchestrator. When turned off, earlier Smart Router Orchestrator instructions
Expand Down
1 change: 1 addition & 0 deletions src/ucode/agents/claude_mods/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const CLI_MODULE = 'ucode.cli';
export const CLI_AGENT = 'claude';
export const ENABLE_ROUTING_FLAG = '--enable-smart-routing';
export const DISABLE_ROUTING_FLAG = '--disable-smart-routing';
export const ROUTING_STATUS_FLAG = '--smart-routing-status';
40 changes: 35 additions & 5 deletions src/ucode/agents/claude_mods/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DISABLED_RECIPE,
ENABLE_ROUTING_FLAG,
RECIPE_HEADER,
ROUTING_STATUS_FLAG,
} from './constants.ts';

type Environment = Record<string, string | undefined>;
Expand All @@ -28,19 +29,19 @@ async function writeEnvironment($: EngineInterface, env: Environment): Promise<v
}

// The session-control file stays Python's to write: the CLI validates it and replaces it
// atomically. The mod only requests the toggle it just applied to the environment.
async function writeSessionControls($: EngineInterface, enabled: boolean): Promise<void> {
// atomically. The mod runs that CLI for toggles and read-only status.
async function runSessionControl($: EngineInterface, flag: string): Promise<string> {
// Claude's capability scanner requires literal environment-variable names.
const python = await $.env.get('UCODE_SMART_ROUTER_PYTHON');
const sessionFile = await $.env.get('UCODE_SESSION_ENV_FILE');
if (!python || !sessionFile) {
throw new Error('Smart Router session controls are unavailable for this session.');
}
const flag = enabled ? ENABLE_ROUTING_FLAG : DISABLE_ROUTING_FLAG;
const result = await $.process.run([python, '-m', CLI_MODULE, CLI_AGENT, flag]);
if (result.exitCode !== 0) {
throw new Error(`Smart Router controls exited ${result.exitCode}: ${result.stderr.trim()}`);
}
return result.stdout.trim();
}

async function setRouting(
Expand Down Expand Up @@ -70,7 +71,7 @@ async function setRouting(
// An explicit toggle must also replace the CLI/skill's saved session overrides,
// which the routing hooks apply on top of the environment and would otherwise win.
if (syncSessionControls) {
await writeSessionControls($, enabled);
await runSessionControl($, enabled ? ENABLE_ROUTING_FLAG : DISABLE_ROUTING_FLAG);
}
} catch (error) {
// Restore earlier successful writes if a later environment write is denied.
Expand All @@ -96,7 +97,15 @@ export function register(on: On, options: PluginOptions): void {
// The installed smart-router skill already supplies the slash command.
on('command.run', {command: 'smart-router'}, async ($, e) => {
const action = e.args.trim();
if (action !== 'on' && action !== 'off') return {text: 'Usage: /smart-router on|off'};
if (action === 'status') {
if (!launchedEnabled) return {text: 'Smart Router is off for this session.'};
try {
return {text: await runSessionControl($, ROUTING_STATUS_FLAG)};
} catch {
return {text: 'Could not read Smart Router status. Check session controls and process permissions; retry or restart through UG.'};
}
}
if (action !== 'on' && action !== 'off') return {text: 'Usage: /smart-router on|off|status'};
if (!launchedEnabled) return {text: 'Smart Router was not enabled for this session.'};
try {
await setRouting($, options, action === 'on', true);
Expand All @@ -108,4 +117,25 @@ export function register(on: On, options: PluginOptions): void {
return {text: 'Could not update Smart Router. Check the recipe, ANTHROPIC_CUSTOM_HEADERS, and environment permissions; retry or restart through UG.'};
}
});

on('ui.render', {component: 'CommandOutput'}, ($, e, next) => {
if (e.props.command !== 'smart-router') return next(e);
const status = /^Smart Router is (on|off) for this session\.$/.exec(e.props.text);
if (!status) return next(e);
const { Box, Text } = $.ui.resolve(e);
const enabled = status[1] === 'on';
return Box({
flexDirection: 'row',
columnGap: 1,
children: [
Text({bold: true, children: ['⚡ Unity Gateway Smart Router']}),
Text({
bold: true,
color: 'black',
backgroundColor: enabled ? 'green' : 'gray',
children: [enabled ? ' ON ' : ' OFF '],
}),
],
});
});
}
32 changes: 32 additions & 0 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2614,6 +2614,20 @@ def _smart_routing_v2_flag(enabled: bool | None, *, agent: str) -> Iterator[None
smart_routing_v2.restore_smart_routing_env(previous)


def _print_smart_routing_status(agent: str, enabled: bool | None) -> None:
try:
if enabled is not None:
raise RuntimeError("Use --smart-routing-status separately from routing toggle flags.")
session_env_path()
routing_enabled = smart_routing_v2.smart_routing_enabled(
effective_environment(agent=agent, strict=True), default=False, agent=agent
)
except RuntimeError as exc:
print_err(str(exc))
raise typer.Exit(1) from None
console.print(f"Smart Router is {'on' if routing_enabled else 'off'} for this session.")


def _toggle_current_smart_routing_session(enabled: bool | None) -> bool:
if enabled is None:
return False
Expand Down Expand Up @@ -3542,8 +3556,17 @@ def codex_cmd(
help="Enable or disable AI Gateway model routing for this Codex launch or session.",
),
] = None,
smart_routing_status: Annotated[
bool,
typer.Option(
"--smart-routing-status", help="Show whether Smart Router is on or off in this session."
),
] = False,
) -> None:
"""Launch Codex via Databricks."""
if smart_routing_status:
_print_smart_routing_status(AGENT_CODEX, enable_smart_routing_flag)
return
if _toggle_current_smart_routing_session(enable_smart_routing_flag):
return
try:
Expand Down Expand Up @@ -3630,8 +3653,17 @@ def claude_cmd(
help="Enable or disable AI Gateway model routing for this Claude Code launch or session.",
),
] = None,
smart_routing_status: Annotated[
bool,
typer.Option(
"--smart-routing-status", help="Show whether Smart Router is on or off in this session."
),
] = False,
) -> None:
"""Launch Claude Code via Databricks."""
if smart_routing_status:
_print_smart_routing_status(AGENT_CLAUDE, enable_smart_routing_flag)
return
if _toggle_current_smart_routing_session(enable_smart_routing_flag):
return
try:
Expand Down
6 changes: 5 additions & 1 deletion src/ucode/smart_routing/session_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def _read(path: Path) -> dict[str, str]:
return _validate(json.loads(path.read_text(encoding="utf-8")))


def effective_environment(env: Mapping[str, str] | None = None, *, agent: str) -> dict[str, str]:
def effective_environment(
env: Mapping[str, str] | None = None, *, agent: str, strict: bool = False
) -> dict[str, str]:
"""Overlay the latest session controls on the hook process environment."""
effective = resolve_environment(env, agent=agent)
try:
Expand All @@ -62,6 +64,8 @@ def effective_environment(env: Mapping[str, str] | None = None, *, agent: str) -
try:
effective.update(_read(path))
except (OSError, UnicodeError, ValueError) as exc:
if strict:
raise RuntimeError(f"Smart Router session controls at {path} are invalid.") from exc
print(
f"Smart Router could not read session controls at {path} ({exc}); "
"using the inherited environment.",
Expand Down
4 changes: 4 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ version is not executed here.
first in PATH and a launching interpreter path containing spaces. Both agents' toggles
must update the session controls through the launching installation. Launch tests check
that Claude settings and Codex's shell policy carry the interpreter and session marker.
Status checks cover both agents' presets and session toggles without launching an agent
or writing state, and reject invalid controls and combined status/toggle flags.
Native Claude component tests check status without inference and the ON/OFF badge trees
on terminal and desktop, with ordinary rendering preserved for errors and other commands.
These are component checks; they do not establish native skill permission matching or
PowerShell execution.

Expand Down
4 changes: 4 additions & 0 deletions tests/integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,10 @@ PATH conflicts for the Smart Router skill have subprocess/component coverage in
`ug` first in PATH. The live journeys above do not inject a second installation or
establish PowerShell command execution.

Read-only `/smart-router status` has CLI component coverage in `../test_smart_router.py`
and native command/render-tree coverage in `../native_claude/test_mod.py`, including session
overrides, ON/OFF badges, and error rendering. Live interactive status rendering is not covered.

The toggle journeys retain the legacy routing-only and orchestration cases and add each of
the three subagent-only `SMART_ROUTER_CONFIG_VERSION` presets. They require only `smart-router`
for routing-only cases, and both `smart-router-orchestrator` and `smart-router` when orchestration
Expand Down
44 changes: 44 additions & 0 deletions tests/native_claude/mod_tests/register.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
import { expect, test } from 'claude-code/testing';

test('status is off when routing was not enabled at launch', async ($) => {
expect(await $.command.run({command: 'smart-router', args: 'status'})).toEqual({
text: 'Smart Router is off for this session.',
});
});

test('status output renders ON and OFF badges on both surfaces', async ($) => {
for (const surface of ['terminal', 'desktop'] as const) {
for (const enabled of [true, false]) {
const ui = await $.ui.mount({
plugin: 'ug-smart-router',
component: 'CommandOutput',
surface,
requestId: 'router-status',
props: {
command: 'smart-router',
text: `Smart Router is ${enabled ? 'on' : 'off'} for this session.`,
},
});
expect(await ui.find({type: 'Text', text: '⚡ Unity Gateway Smart Router'})).toBeDefined();
const badge = await ui.find({type: 'Text', text: enabled ? ' ON ' : ' OFF '});
expect(badge.props).toMatchObject({
bold: true, color: 'black', backgroundColor: enabled ? 'green' : 'gray',
});
await ui.unmount();
}
}
});

test('other command output and status errors retain the original rendering', async ($, on) => {
on('ui.render', () => ({type: 'Text', props: {}, children: ['original output']}));
for (const props of [
{command: 'other', text: 'Smart Router is on for this session.'},
{command: 'smart-router', text: 'Could not read Smart Router status.'},
]) {
const ui = await $.ui.mount({
plugin: 'ug-smart-router',
component: 'CommandOutput', surface: 'terminal', requestId: 'other-output', props,
});
expect(await ui.find({type: 'Text', text: 'original output'})).toBeDefined();
await ui.unmount();
}
});

test('session start forwards the event and result', async ($, on) => {
const event = { surface: 'terminal', isInteractive: true, cwd: '/work' } as const;
let calls = 0;
Expand Down
27 changes: 27 additions & 0 deletions tests/native_claude/test_mod.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

import pytest
Expand Down Expand Up @@ -55,3 +57,28 @@ def test_native_options_are_launch_local(native_binary, tmp_path):
assert a.observe()["options"] == _claude_mod_launch_options(env)
assert b.observe()["options"] == _claude_mod_launch_options({})
assert not (a.plugin / "hooks/launch.json").exists()


def test_status_reads_session_overrides_without_inference(native_binary, tmp_path):
session_file = tmp_path / "env.json"
env = {
"SMART_ROUTER_CONFIG_VERSION": config.SUBAGENT_ONLY_V0,
"UCODE_SESSION_ENV_FILE": str(session_file),
"UCODE_SMART_ROUTER_PYTHON": sys.executable,
}
with Session(native_binary, tmp_path / "status", env) as session:
for overrides, expected in [
({}, "on"),
({"ENABLE_SMART_ROUTING_SUBAGENT_ONLY": "0"}, "off"),
({}, "on"),
]:
session_file.write_text(json.dumps(overrides))
before = session_file.read_bytes()
result = session.send("/smart-router status")
assert f"Smart Router is {expected} for this session." in result["result"]
assert result["num_turns"] == 0 and result["usage"]["output_tokens"] == 0
assert session_file.read_bytes() == before
session_file.write_text("invalid json")
result = session.send("/smart-router status")
assert "Could not read Smart Router status" in result["result"]
assert session_file.read_text() == "invalid json"
Loading
Loading