Skip to content

Commit d5e3f62

Browse files
committed
Add opt-in Codex intelligent routing
1 parent 4740381 commit d5e3f62

7 files changed

Lines changed: 1130 additions & 2 deletions

File tree

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

src/ucode/agents/codex.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import os
66
import re
7+
import shlex
8+
import subprocess
79
from pathlib import Path
810

911
from ucode.agent_updates import available_npm_package_update
@@ -33,6 +35,10 @@
3335
CODEX_MODEL_PROVIDER_NAME = "ucode-databricks"
3436
MINIMUM_CODEX_VERSION = (0, 134, 0)
3537
MINIMUM_CODEX_VERSION_TEXT = "0.134.0"
38+
MINIMUM_ROUTING_CODEX_VERSION = (0, 145, 0)
39+
MINIMUM_ROUTING_CODEX_VERSION_TEXT = "0.145.0"
40+
INTELLIGENT_ROUTING_STATE_KEY = "codex_intelligent_routing_enabled"
41+
ROUTING_HOOK_COMMAND_MARKER = "codex-router-hook"
3642

3743

3844
SPEC: ToolSpec = {
@@ -318,6 +324,11 @@ def write_tool_config(state: dict, model: str | None = None, provider: str | Non
318324
databricks_profile = state.get("profile")
319325

320326
if _use_legacy_layout():
327+
if intelligent_routing_enabled(state) and provider is None:
328+
raise RuntimeError(
329+
"Codex intelligent routing requires Codex "
330+
f"{MINIMUM_ROUTING_CODEX_VERSION_TEXT} or newer."
331+
)
321332
# Codex < 0.134.0 only reads ~/.codex/config.toml. Write the shared
322333
# config with [profiles.ucode] + shared [model_providers.ucode-databricks]
323334
# and skip the per-profile-file cleanup that would normally strip
@@ -358,6 +369,11 @@ def write_tool_config(state: dict, model: str | None = None, provider: str | Non
358369
# deep_merge can't drop keys, so clear a `model` pinned by an earlier
359370
# non-provider run that the provider overlay omits.
360371
doc.pop("model", None)
372+
_sync_intelligent_routing_hooks(
373+
doc,
374+
state,
375+
enabled=intelligent_routing_enabled(state) and provider is None,
376+
)
361377
write_toml_file(CODEX_CONFIG_PATH, doc)
362378
state = mark_tool_managed(state, "codex", MANAGED_KEYS)
363379
save_state(state)
@@ -399,6 +415,181 @@ def launch(state: dict, tool_args: list[str]) -> None:
399415
exec_or_spawn([binary, "--profile", CODEX_PROFILE_NAME, *tool_args])
400416

401417

418+
def intelligent_routing_enabled(state: dict) -> bool:
419+
"""Return whether the current workspace opted into Codex routing."""
420+
return state.get(INTELLIGENT_ROUTING_STATE_KEY) is True
421+
422+
423+
def enable_intelligent_routing(state: dict) -> dict:
424+
"""Persist the current workspace's Codex intelligent-routing opt-in."""
425+
parsed = _parse_version(agent_version(SPEC["binary"]))
426+
if parsed is not None and parsed < MINIMUM_ROUTING_CODEX_VERSION:
427+
raise RuntimeError(
428+
"Codex intelligent routing requires Codex "
429+
f"{MINIMUM_ROUTING_CODEX_VERSION_TEXT} or newer; found "
430+
f"{agent_version(SPEC['binary'])}."
431+
)
432+
state[INTELLIGENT_ROUTING_STATE_KEY] = True
433+
return state
434+
435+
436+
def disable_intelligent_routing(state: dict) -> bool:
437+
"""Disable routing and remove only ucode's Codex routing hooks."""
438+
state.pop(INTELLIGENT_ROUTING_STATE_KEY, None)
439+
if state.get("workspace"):
440+
save_state(state)
441+
changed = False
442+
for path in (CODEX_CONFIG_PATH, LEGACY_CODEX_CONFIG_PATH):
443+
if not path.exists():
444+
continue
445+
doc = read_toml_safe(path)
446+
if _remove_intelligent_routing_hooks(doc):
447+
write_toml_file(path, doc)
448+
changed = True
449+
from ucode.codex_routing import clear_routing_artifacts
450+
451+
clear_routing_artifacts()
452+
return changed
453+
454+
455+
def route_launch_model(state: dict, tool_args: list[str]):
456+
"""Route a root Codex launch before the Codex process starts."""
457+
from ucode.codex_routing import request_routing_decision
458+
459+
workspace = state.get("workspace")
460+
models = state.get("codex_models")
461+
if not isinstance(workspace, str) or not isinstance(models, list):
462+
return None, "workspace model metadata is unavailable"
463+
try:
464+
token = get_databricks_token(workspace, state.get("profile"))
465+
except RuntimeError as exc:
466+
return None, f"could not authenticate the routing request: {exc}"
467+
task = _launch_routing_task(tool_args)
468+
return request_routing_decision(workspace, token, task, models)
469+
470+
471+
def _launch_routing_task(tool_args: list[str]) -> str:
472+
if "exec" in tool_args:
473+
prompt_parts = tool_args[tool_args.index("exec") + 1 :]
474+
if prompt_parts:
475+
return " ".join(prompt_parts)
476+
if tool_args:
477+
return "Start a Codex session with options: " + " ".join(tool_args)
478+
return f"Start an interactive Codex coding session in {Path.cwd().name}."
479+
480+
481+
def _sync_intelligent_routing_hooks(doc: dict, state: dict, *, enabled: bool) -> None:
482+
_remove_intelligent_routing_hooks(doc)
483+
if not enabled:
484+
return
485+
hooks = doc.setdefault("hooks", {})
486+
for event, groups in _routing_hook_groups(state).items():
487+
existing = hooks.get(event)
488+
if not isinstance(existing, list):
489+
existing = []
490+
hooks[event] = [*existing, *groups]
491+
492+
493+
def _routing_hook_groups(state: dict) -> dict[str, list[dict]]:
494+
route_argv = _routing_hook_argv(state, "route-subagent")
495+
session_argv = _routing_hook_argv(state, "session-start")
496+
subagent_argv = _routing_hook_argv(state, "record-subagent")
497+
return {
498+
"PreToolUse": [
499+
{
500+
"matcher": "Agent|.*spawn_agent$",
501+
"hooks": [_routing_command_hook(route_argv, status="Routing subagent model")],
502+
}
503+
],
504+
"SessionStart": [
505+
{
506+
"matcher": "startup|resume|clear",
507+
"hooks": [_routing_command_hook(session_argv)],
508+
}
509+
],
510+
"SubagentStart": [
511+
{
512+
"hooks": [_routing_command_hook(subagent_argv)],
513+
}
514+
],
515+
}
516+
517+
518+
def _routing_hook_argv(state: dict, event: str) -> list[str]:
519+
workspace = str(state.get("workspace") or "")
520+
argv = [
521+
build_auth_token_argv(workspace, state.get("profile"), use_pat=bool(state.get("use_pat")))[
522+
0
523+
],
524+
ROUTING_HOOK_COMMAND_MARKER,
525+
event,
526+
]
527+
if event != "route-subagent":
528+
return argv
529+
argv += ["--host", workspace]
530+
profile = state.get("profile")
531+
if isinstance(profile, str) and profile:
532+
argv += ["--profile", profile]
533+
if state.get("use_pat"):
534+
argv.append("--use-pat")
535+
for model in state.get("codex_models") or []:
536+
if isinstance(model, str) and model:
537+
argv += ["--model", model]
538+
return argv
539+
540+
541+
def _routing_command_hook(argv: list[str], *, status: str | None = None) -> dict:
542+
hook = {
543+
"type": "command",
544+
"command": shlex.join(argv),
545+
"command_windows": subprocess.list2cmdline(argv),
546+
"timeout": 35,
547+
}
548+
if status:
549+
hook["statusMessage"] = status
550+
return hook
551+
552+
553+
def _remove_intelligent_routing_hooks(doc: dict) -> bool:
554+
hooks = doc.get("hooks")
555+
if not isinstance(hooks, dict):
556+
return False
557+
changed = False
558+
for event in list(hooks):
559+
groups = hooks.get(event)
560+
if not isinstance(groups, list):
561+
continue
562+
kept_groups = []
563+
for group in groups:
564+
if not isinstance(group, dict):
565+
kept_groups.append(group)
566+
continue
567+
handlers = group.get("hooks")
568+
if not isinstance(handlers, list):
569+
kept_groups.append(group)
570+
continue
571+
kept_handlers = [
572+
handler
573+
for handler in handlers
574+
if not (
575+
isinstance(handler, dict)
576+
and ROUTING_HOOK_COMMAND_MARKER in str(handler.get("command") or "")
577+
)
578+
]
579+
if len(kept_handlers) != len(handlers):
580+
changed = True
581+
if kept_handlers:
582+
group["hooks"] = kept_handlers
583+
kept_groups.append(group)
584+
if kept_groups:
585+
hooks[event] = kept_groups
586+
else:
587+
hooks.pop(event, None)
588+
if not hooks:
589+
doc.pop("hooks", None)
590+
return changed
591+
592+
402593
def validate_cmd(binary: str) -> list[str]:
403594
return [
404595
binary,

0 commit comments

Comments
 (0)