Skip to content

Commit c19ffbb

Browse files
author
Anthony Ivan
committed
Merge remote-tracking branch 'origin/main' into pr-257-resolve
Signed-off-by: Anthony Ivan <anthony.ivan@databricks.com> # Conflicts: # src/ucode/cli.py
2 parents 7256f01 + e87e2d4 commit c19ffbb

19 files changed

Lines changed: 2558 additions & 14 deletions

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ ucode codex --full-auto
4040

4141
All agents route through Databricks AI Gateway using your workspace credentials — no API keys required.
4242

43+
Codex intelligent routing is opt-in. Enabling it asks the AI Gateway router to select the
44+
root-session model before launch and installs profile-scoped hooks that route future
45+
`spawn_agent` calls. Codex may require one-time review of the installed hooks through `/hooks`.
46+
47+
```bash
48+
ucode codex --enable-intelligent-routing
49+
```
50+
51+
The setting persists for the current workspace. Disable it and remove only ucode's routing
52+
hooks with:
53+
54+
```bash
55+
ucode codex --disable-intelligent-routing
56+
```
57+
4358
To configure all tools at once:
4459

4560
```bash
@@ -162,6 +177,8 @@ you to run `ucode <agent>` (existing agent sessions need a restart before the MC
162177
| `ucode configure --workspaces https://first.databricks.com,https://second.databricks.com` | Configure workspaces without the interactive picker |
163178
| `ucode configure --profiles DEFAULT` | Configure using existing Databricks CLI profiles (hosts come from `~/.databrickscfg`) |
164179
| `ucode configure --profiles DEFAULT --use-pat` | Authenticate with the profile's personal access token — no browser login |
180+
| `ucode codex --enable-intelligent-routing` | Enable AI Gateway routing for Codex sessions and subagents |
181+
| `ucode codex --disable-intelligent-routing` | Disable routing and remove ucode's Codex routing hooks |
165182
| `ucode configure --skip-validate` | Write configs without sending a test message through each agent |
166183
| `ucode configure --agents claude --mcp system.ai.slack` | Configure an agent and register its Databricks MCP server(s) in one command |
167184
| `ucode configure skills` | Register the skills MCP connection (utility tools only); no skills download |

src/ucode/agents/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def configure_tool(
312312
provider: str | None = None,
313313
provider_models: dict[str, str] | None = None,
314314
relayed: bool = False,
315+
route_root_model: str | None = None,
315316
) -> dict:
316317
result: dict | tuple[dict, str]
317318
if tool == "codex":
@@ -322,7 +323,12 @@ def configure_tool(
322323
if not model and not provider:
323324
raise RuntimeError(f"A {tool} model must be selected before configuration.")
324325
result = claude.write_tool_config(
325-
state, model, provider=provider, provider_models=provider_models, relayed=relayed
326+
state,
327+
model,
328+
provider=provider,
329+
provider_models=provider_models,
330+
relayed=relayed,
331+
route_root_model=route_root_model,
326332
)
327333
else:
328334
# provider routing is claude/codex-only; every other tool needs a model.

src/ucode/agents/claude.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
get_databricks_token,
3131
)
3232
from ucode.launcher import exec_or_spawn
33+
from ucode.smart_routing.claude_hooks import (
34+
remove_smart_routing_hooks,
35+
sync_smart_routing_hooks,
36+
)
3337
from ucode.state import mark_tool_managed, save_state
3438
from ucode.telemetry import agent_version, ucode_version
3539
from ucode.tracing import tracing_env
@@ -47,6 +51,15 @@
4751
"backup_path": CLAUDE_BACKUP_PATH,
4852
}
4953

54+
# Per-workspace opt-in flag for Claude Code smart routing (state key).
55+
# Shared across agents: one opt-in enables smart routing for every routing-capable
56+
# tool (codex, claude), so a workspace turns it on once. Kept identical to
57+
# codex.SMART_ROUTING_STATE_KEY on purpose.
58+
SMART_ROUTING_STATE_KEY = "smart_routing_enabled"
59+
# Claude Code settings.json hook events ucode manages when routing is enabled;
60+
# marked managed so they're tracked/reverted with the rest of ucode's config.
61+
CLAUDE_ROUTING_HOOK_EVENTS = ("PreToolUse", "SessionStart", "SubagentStart")
62+
5063

5164
def is_update_available() -> tuple[str, str] | None:
5265
return available_npm_package_update(SPEC["package"])
@@ -214,6 +227,7 @@ def render_overlay(
214227
fable_enabled: bool = False,
215228
relayed: bool = False,
216229
relayed_base_url: str | None = None,
230+
route_root_model: str | None = None,
217231
) -> tuple[dict, list[list[str]]]:
218232
"""Return (overlay, managed_key_paths) for Claude settings.json.
219233
@@ -265,13 +279,20 @@ def render_overlay(
265279
"ENABLE_TOOL_SEARCH": "1",
266280
"CLAUDE_CODE_USE_GATEWAY": "1",
267281
}
268-
# Intentionally NOT setting ANTHROPIC_MODEL. Setting it produces a duplicate
269-
# catalog row in Claude Code's /model picker (e.g. "Opus 4.8 (1M context) ✓")
270-
# on top of the family-alias row from ANTHROPIC_DEFAULT_OPUS_MODEL. Without
271-
# it, Default resolves through the pinned family alias and the picker shows
272-
# only one row per model. `ucode claude -- --model X` still overrides for a
273-
# single session via Claude Code's own --model flag.
282+
# Intentionally NOT setting ANTHROPIC_MODEL by default. Setting it produces a
283+
# duplicate catalog row in Claude Code's /model picker (e.g. "Opus 4.8 (1M
284+
# context) ✓") on top of the family-alias row from ANTHROPIC_DEFAULT_OPUS_MODEL.
285+
# Without it, Default resolves through the pinned family alias and the picker
286+
# shows only one row per model. `ucode claude -- --model X` still overrides for
287+
# a single session via Claude Code's own --model flag.
288+
#
289+
# The one exception is smart routing: `route_root_model` pins the
290+
# router's per-launch pick for the root session as ANTHROPIC_MODEL. The
291+
# duplicate-picker-row cost is acceptable because the whole point is to launch
292+
# on the routed model rather than the family default.
274293
_ = model # API stability; no longer pinned via env.
294+
if route_root_model:
295+
env["ANTHROPIC_MODEL"] = route_root_model
275296
# A Bedrock-backed provider needs its provider-side ids pinned verbatim
276297
# (Claude Code's canonical names aren't routable there). These come from the
277298
# service's targets, already de-duped to one id per family upstream.
@@ -389,12 +410,41 @@ def _unregister_web_search_mcp() -> None:
389410
pass
390411

391412

413+
def smart_routing_enabled(state: dict) -> bool:
414+
"""Return whether the current workspace opted into Claude Code routing."""
415+
return state.get(SMART_ROUTING_STATE_KEY) is True
416+
417+
418+
def enable_smart_routing(state: dict) -> dict:
419+
"""Persist the current workspace's Claude Code smart-routing opt-in."""
420+
state[SMART_ROUTING_STATE_KEY] = True
421+
return state
422+
423+
424+
def disable_smart_routing(state: dict) -> bool:
425+
"""Disable routing and remove only ucode's Claude Code routing hooks."""
426+
state.pop(SMART_ROUTING_STATE_KEY, None)
427+
if state.get("workspace"):
428+
save_state(state)
429+
changed = False
430+
if CLAUDE_SETTINGS_PATH.exists():
431+
doc = read_json_safe(CLAUDE_SETTINGS_PATH)
432+
if remove_smart_routing_hooks(doc):
433+
write_json_file(CLAUDE_SETTINGS_PATH, doc)
434+
changed = True
435+
from ucode.smart_routing.claude_routing import clear_routing_artifacts
436+
437+
clear_routing_artifacts()
438+
return changed
439+
440+
392441
def write_tool_config(
393442
state: dict,
394443
model: str | None,
395444
provider: str | None = None,
396445
provider_models: dict[str, str] | None = None,
397446
relayed: bool = False,
447+
route_root_model: str | None = None,
398448
) -> dict:
399449
backup_existing_file(CLAUDE_SETTINGS_PATH, CLAUDE_BACKUP_PATH)
400450
web_search_model = _resolve_web_search_model(state)
@@ -413,6 +463,7 @@ def write_tool_config(
413463
fable_enabled=bool(state.get("fable_enabled")),
414464
relayed=relayed,
415465
relayed_base_url=relayed_base_url,
466+
route_root_model=route_root_model,
416467
)
417468
tracing_env_vars = tracing_env(state, "claude")
418469
stop_hook_command = claude_tracing_stop_hook_command() if tracing_env_vars else None
@@ -457,6 +508,13 @@ def write_tool_config(
457508
if isinstance(merged_env, dict):
458509
for key in CLAUDE_REMOVED_ENV_KEYS:
459510
merged_env.pop(key, None)
511+
# Smart-routing hooks: install ucode's PreToolUse/SessionStart/
512+
# SubagentStart hooks when routing is enabled (and not under a provider,
513+
# which pins no Databricks model), else surgically strip only ucode's own.
514+
routing_enabled = smart_routing_enabled(state) and provider is None
515+
sync_smart_routing_hooks(merged, state, enabled=routing_enabled)
516+
if routing_enabled:
517+
managed_keys = managed_keys + [["hooks", event] for event in CLAUDE_ROUTING_HOOK_EVENTS]
460518
write_json_file(CLAUDE_SETTINGS_PATH, merged)
461519

462520
if web_search_model:

src/ucode/agents/codex.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
get_databricks_token,
2222
)
2323
from ucode.launcher import exec_or_spawn
24+
from ucode.smart_routing.codex_hooks import (
25+
remove_smart_routing_hooks,
26+
sync_smart_routing_hooks,
27+
)
2428
from ucode.state import mark_tool_managed, save_state
2529
from ucode.telemetry import agent_version, ucode_version
2630

@@ -33,6 +37,11 @@
3337
CODEX_MODEL_PROVIDER_NAME = "ucode-databricks"
3438
MINIMUM_CODEX_VERSION = (0, 134, 0)
3539
MINIMUM_CODEX_VERSION_TEXT = "0.134.0"
40+
MINIMUM_ROUTING_CODEX_VERSION = (0, 145, 0)
41+
MINIMUM_ROUTING_CODEX_VERSION_TEXT = "0.145.0"
42+
# Shared across agents: one opt-in enables smart routing for every routing-capable
43+
# tool (codex, claude), so a workspace turns it on once.
44+
SMART_ROUTING_STATE_KEY = "smart_routing_enabled"
3645

3746

3847
SPEC: ToolSpec = {
@@ -318,6 +327,10 @@ def write_tool_config(state: dict, model: str | None = None, provider: str | Non
318327
databricks_profile = state.get("profile")
319328

320329
if _use_legacy_layout():
330+
if smart_routing_enabled(state) and provider is None:
331+
raise RuntimeError(
332+
f"Codex smart routing requires Codex {MINIMUM_ROUTING_CODEX_VERSION_TEXT} or newer."
333+
)
321334
# Codex < 0.134.0 only reads ~/.codex/config.toml. Write the shared
322335
# config with [profiles.ucode] + shared [model_providers.ucode-databricks]
323336
# and skip the per-profile-file cleanup that would normally strip
@@ -358,6 +371,11 @@ def write_tool_config(state: dict, model: str | None = None, provider: str | Non
358371
# deep_merge can't drop keys, so clear a `model` pinned by an earlier
359372
# non-provider run that the provider overlay omits.
360373
doc.pop("model", None)
374+
sync_smart_routing_hooks(
375+
doc,
376+
state,
377+
enabled=smart_routing_enabled(state) and provider is None,
378+
)
361379
write_toml_file(CODEX_CONFIG_PATH, doc)
362380
state = mark_tool_managed(state, "codex", MANAGED_KEYS)
363381
save_state(state)
@@ -399,6 +417,43 @@ def launch(state: dict, tool_args: list[str]) -> None:
399417
exec_or_spawn([binary, "--profile", CODEX_PROFILE_NAME, *tool_args])
400418

401419

420+
def smart_routing_enabled(state: dict) -> bool:
421+
"""Return whether the current workspace opted into Codex routing."""
422+
return state.get(SMART_ROUTING_STATE_KEY) is True
423+
424+
425+
def enable_smart_routing(state: dict) -> dict:
426+
"""Persist the current workspace's Codex smart-routing opt-in."""
427+
parsed = _parse_version(agent_version(SPEC["binary"]))
428+
if parsed is not None and parsed < MINIMUM_ROUTING_CODEX_VERSION:
429+
raise RuntimeError(
430+
"Codex smart routing requires Codex "
431+
f"{MINIMUM_ROUTING_CODEX_VERSION_TEXT} or newer; found "
432+
f"{agent_version(SPEC['binary'])}."
433+
)
434+
state[SMART_ROUTING_STATE_KEY] = True
435+
return state
436+
437+
438+
def disable_smart_routing(state: dict) -> bool:
439+
"""Disable routing and remove only ucode's Codex routing hooks."""
440+
state.pop(SMART_ROUTING_STATE_KEY, None)
441+
if state.get("workspace"):
442+
save_state(state)
443+
changed = False
444+
for path in (CODEX_CONFIG_PATH, LEGACY_CODEX_CONFIG_PATH):
445+
if not path.exists():
446+
continue
447+
doc = read_toml_safe(path)
448+
if remove_smart_routing_hooks(doc):
449+
write_toml_file(path, doc)
450+
changed = True
451+
from ucode.smart_routing.codex_routing import clear_routing_artifacts
452+
453+
clear_routing_artifacts()
454+
return changed
455+
456+
402457
def validate_cmd(binary: str) -> list[str]:
403458
return [
404459
binary,

0 commit comments

Comments
 (0)