3030 get_databricks_token ,
3131)
3232from 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+ )
3337from ucode .state import mark_tool_managed , save_state
3438from ucode .telemetry import agent_version , ucode_version
3539from ucode .tracing import tracing_env
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
5164def 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+
392441def 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 :
0 commit comments