2828 build_tool_base_url ,
2929 get_databricks_token ,
3030)
31+ from ucode .intelligent_routing .claude_hooks import (
32+ remove_intelligent_routing_hooks ,
33+ sync_intelligent_routing_hooks ,
34+ )
3135from ucode .launcher import exec_or_spawn
3236from ucode .state import mark_tool_managed , save_state
3337from ucode .telemetry import agent_version , ucode_version
4650 "backup_path" : CLAUDE_BACKUP_PATH ,
4751}
4852
53+ # Per-workspace opt-in flag for Claude Code intelligent routing (state key).
54+ INTELLIGENT_ROUTING_STATE_KEY = "claude_intelligent_routing_enabled"
55+ # Claude Code settings.json hook events ucode manages when routing is enabled;
56+ # marked managed so they're tracked/reverted with the rest of ucode's config.
57+ CLAUDE_ROUTING_HOOK_EVENTS = ("PreToolUse" , "SessionStart" , "SubagentStart" )
58+
4959
5060def is_update_available () -> tuple [str , str ] | None :
5161 return available_npm_package_update (SPEC ["package" ])
@@ -161,6 +171,7 @@ def render_overlay(
161171 fable_enabled : bool = False ,
162172 relayed : bool = False ,
163173 relayed_base_url : str | None = None ,
174+ route_root_model : str | None = None ,
164175) -> tuple [dict , list [list [str ]]]:
165176 """Return (overlay, managed_key_paths) for Claude settings.json.
166177
@@ -212,13 +223,20 @@ def render_overlay(
212223 "ENABLE_TOOL_SEARCH" : "1" ,
213224 "CLAUDE_CODE_USE_GATEWAY" : "1" ,
214225 }
215- # Intentionally NOT setting ANTHROPIC_MODEL. Setting it produces a duplicate
216- # catalog row in Claude Code's /model picker (e.g. "Opus 4.8 (1M context) ✓")
217- # on top of the family-alias row from ANTHROPIC_DEFAULT_OPUS_MODEL. Without
218- # it, Default resolves through the pinned family alias and the picker shows
219- # only one row per model. `ucode claude -- --model X` still overrides for a
220- # single session via Claude Code's own --model flag.
226+ # Intentionally NOT setting ANTHROPIC_MODEL by default. Setting it produces a
227+ # duplicate catalog row in Claude Code's /model picker (e.g. "Opus 4.8 (1M
228+ # context) ✓") on top of the family-alias row from ANTHROPIC_DEFAULT_OPUS_MODEL.
229+ # Without it, Default resolves through the pinned family alias and the picker
230+ # shows only one row per model. `ucode claude -- --model X` still overrides for
231+ # a single session via Claude Code's own --model flag.
232+ #
233+ # The one exception is intelligent routing: `route_root_model` pins the
234+ # router's per-launch pick for the root session as ANTHROPIC_MODEL. The
235+ # duplicate-picker-row cost is acceptable because the whole point is to launch
236+ # on the routed model rather than the family default.
221237 _ = model # API stability; no longer pinned via env.
238+ if route_root_model :
239+ env ["ANTHROPIC_MODEL" ] = route_root_model
222240 # A Bedrock-backed provider needs its provider-side ids pinned verbatim
223241 # (Claude Code's canonical names aren't routable there). These come from the
224242 # service's targets, already de-duped to one id per family upstream.
@@ -336,12 +354,41 @@ def _unregister_web_search_mcp() -> None:
336354 pass
337355
338356
357+ def intelligent_routing_enabled (state : dict ) -> bool :
358+ """Return whether the current workspace opted into Claude Code routing."""
359+ return state .get (INTELLIGENT_ROUTING_STATE_KEY ) is True
360+
361+
362+ def enable_intelligent_routing (state : dict ) -> dict :
363+ """Persist the current workspace's Claude Code intelligent-routing opt-in."""
364+ state [INTELLIGENT_ROUTING_STATE_KEY ] = True
365+ return state
366+
367+
368+ def disable_intelligent_routing (state : dict ) -> bool :
369+ """Disable routing and remove only ucode's Claude Code routing hooks."""
370+ state .pop (INTELLIGENT_ROUTING_STATE_KEY , None )
371+ if state .get ("workspace" ):
372+ save_state (state )
373+ changed = False
374+ if CLAUDE_SETTINGS_PATH .exists ():
375+ doc = read_json_safe (CLAUDE_SETTINGS_PATH )
376+ if remove_intelligent_routing_hooks (doc ):
377+ write_json_file (CLAUDE_SETTINGS_PATH , doc )
378+ changed = True
379+ from ucode .intelligent_routing .claude_routing import clear_routing_artifacts
380+
381+ clear_routing_artifacts ()
382+ return changed
383+
384+
339385def write_tool_config (
340386 state : dict ,
341387 model : str | None ,
342388 provider : str | None = None ,
343389 provider_models : dict [str , str ] | None = None ,
344390 relayed : bool = False ,
391+ route_root_model : str | None = None ,
345392) -> dict :
346393 backup_existing_file (CLAUDE_SETTINGS_PATH , CLAUDE_BACKUP_PATH )
347394 web_search_model = _resolve_web_search_model (state )
@@ -360,6 +407,7 @@ def write_tool_config(
360407 fable_enabled = bool (state .get ("fable_enabled" )),
361408 relayed = relayed ,
362409 relayed_base_url = relayed_base_url ,
410+ route_root_model = route_root_model ,
363411 )
364412 tracing_env_vars = tracing_env (state , "claude" )
365413 stop_hook_command = claude_tracing_stop_hook_command () if tracing_env_vars else None
@@ -404,6 +452,13 @@ def write_tool_config(
404452 if isinstance (merged_env , dict ):
405453 for key in CLAUDE_REMOVED_ENV_KEYS :
406454 merged_env .pop (key , None )
455+ # Intelligent-routing hooks: install ucode's PreToolUse/SessionStart/
456+ # SubagentStart hooks when routing is enabled (and not under a provider,
457+ # which pins no Databricks model), else surgically strip only ucode's own.
458+ routing_enabled = intelligent_routing_enabled (state ) and provider is None
459+ sync_intelligent_routing_hooks (merged , state , enabled = routing_enabled )
460+ if routing_enabled :
461+ managed_keys = managed_keys + [["hooks" , event ] for event in CLAUDE_ROUTING_HOOK_EVENTS ]
407462 write_json_file (CLAUDE_SETTINGS_PATH , merged )
408463
409464 if web_search_model :
0 commit comments