From 171380888fa031cd73e5cb831e676712432b994c Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Mon, 31 Aug 2026 21:05:38 +0000 Subject: [PATCH 1/8] Map V2 Codex routing to bundled GPT slugs --- src/ucode/smart_routing/codex_interposer.py | 2 ++ src/ucode/smart_routing/codex_routing.py | 10 ++++++++-- tests/test_codex_routing.py | 15 +++++++++++++++ tests/test_codex_smart_routing_v2.py | 8 +++++--- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/ucode/smart_routing/codex_interposer.py b/src/ucode/smart_routing/codex_interposer.py index 2841b8f6..e3c7a941 100644 --- a/src/ucode/smart_routing/codex_interposer.py +++ b/src/ucode/smart_routing/codex_interposer.py @@ -7,6 +7,7 @@ import time import uuid from collections.abc import Callable +from dataclasses import replace from pathlib import Path from websockets.asyncio.client import connect @@ -90,6 +91,7 @@ def on_tui_frame(self, raw: str) -> str: if decision is None: self.log(f"[ROUTE] selection failed; keeping current model: {reason}") return raw + decision = replace(decision, model=codex_routing.codex_model_id(decision.model)) self.target = decision.model if self.switch_message_fn is not None: self.switch_message = self.switch_message_fn(decision.model, decision.rationale) diff --git a/src/ucode/smart_routing/codex_routing.py b/src/ucode/smart_routing/codex_routing.py index 87906f2a..e1237adf 100644 --- a/src/ucode/smart_routing/codex_routing.py +++ b/src/ucode/smart_routing/codex_routing.py @@ -159,7 +159,7 @@ def record(payload, task, decision, requested): workspace, token, task, available_models, timeout=timeout ), default_task_label="Codex subagent task", - model_id_mapper=_codex_model_id, + model_id_mapper=codex_model_id, record_decision=record, ) @@ -203,7 +203,13 @@ def _model_strength(model: str) -> tuple[int, int, int, int]: return major, minor, patch, 1 if not suffix else 0 -def _codex_model_id(model: str) -> str: +def codex_model_id(model: str) -> str: + """Map a UC GPT service ID to Codex's bundled catalog slug. + + Codex's bundled GPT catalog owns the model metadata for these aliases, + while the AI Gateway resolves them back to the matching ``system.ai`` service. + Leave non-GPT models unchanged because their metadata comes from the gateway catalog. + """ tail = model.rsplit("/", 1)[-1] if tail in {"databricks-gpt-5-2-codex", "databricks-gpt-5-4-nano"}: return tail diff --git a/tests/test_codex_routing.py b/tests/test_codex_routing.py index 8973eaf1..8a41e2a6 100644 --- a/tests/test_codex_routing.py +++ b/tests/test_codex_routing.py @@ -204,6 +204,21 @@ def test_spawn_rewrite_uses_codex_model_id_for_uc_endpoint(monkeypatch): assert output["hookSpecificOutput"]["updatedInput"]["model"] == "gpt-5.6-luna" +def test_codex_model_id_maps_uc_gpt_models_to_codex_slugs(): + expected = { + "system.ai.gpt-5-2": "gpt-5.2", + "system.ai.gpt-5-4": "gpt-5.4", + "system.ai.gpt-5-4-mini": "gpt-5.4-mini", + "system.ai.gpt-5-5": "gpt-5.5", + "system.ai.gpt-5-6-luna": "gpt-5.6-luna", + "system.ai.gpt-5-6-sol": "gpt-5.6-sol", + "system.ai.gpt-5-6-terra": "gpt-5.6-terra", + } + assert {model: codex_routing.codex_model_id(model) for model in expected} == expected + assert codex_routing.codex_model_id("system.ai.gpt-5-6-experimental") == "gpt-5.6-experimental" + assert codex_routing.codex_model_id("system.ai.glm-5-2") == "system.ai.glm-5-2" + + def test_spawn_glm_decision_applies_glm_model(monkeypatch): # GLM is no longer skipped for Codex subagents: a GLM routing decision is # applied like any other arm. diff --git a/tests/test_codex_smart_routing_v2.py b/tests/test_codex_smart_routing_v2.py index 0ba0c548..af53ccd6 100644 --- a/tests/test_codex_smart_routing_v2.py +++ b/tests/test_codex_smart_routing_v2.py @@ -368,7 +368,7 @@ def select(prompt): assert json.loads(output)["params"]["model"] == "claude-opus-4-8" assert "Task classified as bugfix." in sess.switch_message - def test_shows_routing_notice_when_selected_model_is_already_active(self): + def test_maps_selected_uc_gpt_model_and_shows_routing_notice(self): def select(_prompt): return ( codex_interposer.routing.RoutingDecision( @@ -387,14 +387,16 @@ def select(_prompt): ) frame = self._turn_start("system.ai.gpt-5-6-luna") - assert sess.on_tui_frame(frame) == frame + output = sess.on_tui_frame(frame) + assert json.loads(output)["params"]["model"] == "gpt-5.6-luna" injected = sess.on_engine_frame(self._turn_started("turn-1")) assert [message["method"] for message in injected] == [ + codex_interposer.SETTINGS_UPDATED, codex_interposer.ITEM_STARTED, codex_interposer.ITEM_COMPLETED, ] - assert "Selected Model : system.ai.gpt-5-6-luna" in (injected[0]["params"]["item"]["text"]) + assert "Selected Model : gpt-5.6-luna" in (injected[1]["params"]["item"]["text"]) def test_routes_first_prompt_to_oss_model(self): def select(_prompt): From 25c8a5616598712e2325088010647cbcb196640e Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Mon, 31 Aug 2026 21:40:07 +0000 Subject: [PATCH 2/8] update --- src/ucode/agents/codex.py | 17 +++++++++++++++- src/ucode/smart_routing/v2.py | 6 +++--- tests/test_agent_codex.py | 19 ++++++++++++++++++ tests/test_codex_smart_routing_v2.py | 29 +++++++++++++++++++++++++--- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index e6da7255..07240b7e 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -49,6 +49,7 @@ # Shared across agents: one opt-in enables smart routing for every routing-capable # tool (codex, claude), so a workspace turns it on once. SMART_ROUTING_STATE_KEY = "smart_routing_enabled" +V2_BOOTSTRAP_MODEL = "gpt-5.6-luna" SPEC: ToolSpec = { "binary": "codex", @@ -420,6 +421,20 @@ def default_model(state: dict) -> str | None: return None +def starting_model(state: dict) -> str | None: + """Return the model V2 uses to bootstrap the Codex app-server.""" + managed_model = default_model(state) + if managed_model: + return managed_model + for key in ("codex_models", "oss_models"): + models = state.get(key) + if isinstance(models, list): + for model in models: + if isinstance(model, str) and model: + return model + return V2_BOOTSTRAP_MODEL + + def clear_model_preferences(state: dict) -> bool: """Remove ucode profile model preferences so Codex selects its default.""" if isinstance(state.get("codex_default_model"), str): @@ -460,7 +475,7 @@ def launch(state: dict, tool_args: list[str]) -> None: state, tool_args, binary=binary, - start_model=default_model(state), + start_model=starting_model(state), render_overlay=render_overlay, ) if workspace: diff --git a/src/ucode/smart_routing/v2.py b/src/ucode/smart_routing/v2.py index c4f99ebc..a73122f7 100644 --- a/src/ucode/smart_routing/v2.py +++ b/src/ucode/smart_routing/v2.py @@ -468,9 +468,9 @@ def launch_codex( os.environ[OAUTH_TOKEN_ENV_VAR] = get_databricks_token(workspace, profile) available_models = _cached_routing_models(state) if not available_models: - raise RuntimeError( - "Smart routing v2 has no cached Unity Catalog model services; " - "run `ucode configure codex` to refresh them." + print_note( + "Smart routing model metadata is unavailable; starting Codex on gpt-5.6-luna " + "without automatic model switching. Run `ucode configure codex` to enable routing." ) overlay = render_overlay( workspace, diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index 948ff400..c2cecf6a 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -592,6 +592,25 @@ def test_managed_default_model_takes_priority(self): } assert codex.default_model(state) == "admin-chosen-default" + def test_starting_model_uses_cached_codex_model(self): + state = { + "codex_models": ["system.ai.gpt-5-6-luna"], + "oss_models": ["system.ai.glm-5-2"], + } + + assert codex.starting_model(state) == "system.ai.gpt-5-6-luna" + + def test_starting_model_prefers_managed_default(self): + state = { + "codex_default_model": "admin-chosen-default", + "codex_models": ["system.ai.gpt-5-6-luna"], + } + + assert codex.starting_model(state) == "admin-chosen-default" + + def test_starting_model_falls_back_to_codex_catalog_default(self): + assert codex.starting_model({}) == "gpt-5.6-luna" + class TestCodexValidateCmd: def test_starts_with_binary(self): diff --git a/tests/test_codex_smart_routing_v2.py b/tests/test_codex_smart_routing_v2.py index af53ccd6..491e650b 100644 --- a/tests/test_codex_smart_routing_v2.py +++ b/tests/test_codex_smart_routing_v2.py @@ -53,6 +53,7 @@ def test_codex_launch_dispatches_when_flag_enabled(self, monkeypatch): calls = [] monkeypatch.setenv(v2.ENV_VAR, "1") monkeypatch.setattr(codex, "default_model", lambda state: "gpt-start") + monkeypatch.setattr(codex, "clear_model_preferences", lambda state: False) def launch_v2(state, tool_args, **kwargs): calls.append((state, tool_args, kwargs)) @@ -233,18 +234,40 @@ def test_v2_pre_tool_hook_replaces_existing_ucode_hook(self, tmp_path, monkeypat assert "--model system.ai.gpt-5-6-sol" in routing_commands[0] assert "--model old" not in routing_commands[0] - def test_missing_cached_models_blocks_launch(self, monkeypatch): + def test_missing_cached_models_starts_with_bootstrap_model(self, monkeypatch): monkeypatch.setattr(v2, "get_databricks_token", lambda workspace, profile: "token") + monkeypatch.setattr(v2, "_free_port", lambda: 41001) + monkeypatch.setattr(v2, "_wait_for_app_server", lambda port, timeout: True) + monkeypatch.setattr( + v2.subprocess, + "Popen", + lambda *args, **kwargs: type( + "Process", + (), + { + "wait": lambda self, timeout=None: 0, + "terminate": lambda self: None, + "kill": lambda self: None, + }, + )(), + ) + monkeypatch.setattr( + codex_interposer, + "start_interposer_thread", + lambda *args, **kwargs: (41002, lambda: None), + ) - with pytest.raises(RuntimeError, match="ucode configure codex"): + with pytest.raises(SystemExit) as exc: v2.launch_codex( {"workspace": WS}, [], binary="codex", - start_model="gpt-start", + start_model="gpt-5.6-luna", render_overlay=codex.render_overlay, ) + assert exc.value.code == 0 + def test_interposer_startup_failure_is_propagated(monkeypatch): async def fail_to_serve(*args, **kwargs): From b03238895f37f0a8e023e25e756ab0d4f613a917 Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Mon, 31 Aug 2026 21:43:12 +0000 Subject: [PATCH 3/8] fix --- src/ucode/agents/codex.py | 3 ++- tests/test_agent_codex.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index 07240b7e..476008c0 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -31,6 +31,7 @@ remove_smart_routing_hooks, sync_smart_routing_hooks, ) +from ucode.smart_routing.codex_routing import codex_model_id from ucode.state import mark_tool_managed, save_state from ucode.telemetry import agent_version, ucode_version from ucode.ui import print_warning_err @@ -431,7 +432,7 @@ def starting_model(state: dict) -> str | None: if isinstance(models, list): for model in models: if isinstance(model, str) and model: - return model + return codex_model_id(model) return V2_BOOTSTRAP_MODEL diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index c2cecf6a..aa1c0141 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -598,7 +598,7 @@ def test_starting_model_uses_cached_codex_model(self): "oss_models": ["system.ai.glm-5-2"], } - assert codex.starting_model(state) == "system.ai.gpt-5-6-luna" + assert codex.starting_model(state) == "gpt-5.6-luna" def test_starting_model_prefers_managed_default(self): state = { From 206d793bf1786f004535c5d62a5ce112406a8a6c Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Mon, 31 Aug 2026 21:49:47 +0000 Subject: [PATCH 4/8] update --- src/ucode/agents/codex.py | 28 +++++++++++++--------------- tests/test_agent_codex.py | 20 -------------------- tests/test_codex_smart_routing_v2.py | 20 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index 476008c0..f2409d8f 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -422,20 +422,6 @@ def default_model(state: dict) -> str | None: return None -def starting_model(state: dict) -> str | None: - """Return the model V2 uses to bootstrap the Codex app-server.""" - managed_model = default_model(state) - if managed_model: - return managed_model - for key in ("codex_models", "oss_models"): - models = state.get(key) - if isinstance(models, list): - for model in models: - if isinstance(model, str) and model: - return codex_model_id(model) - return V2_BOOTSTRAP_MODEL - - def clear_model_preferences(state: dict) -> bool: """Remove ucode profile model preferences so Codex selects its default.""" if isinstance(state.get("codex_default_model"), str): @@ -472,11 +458,23 @@ def launch(state: dict, tool_args: list[str]) -> None: # path so flag-off launches retain their existing dependencies and behavior. from ucode.smart_routing import v2 as smart_routing_v2 + def starting_model() -> str: + managed_model = default_model(state) + if managed_model: + return managed_model + for key in ("codex_models", "oss_models"): + models = state.get(key) + if isinstance(models, list): + for model in models: + if isinstance(model, str) and model: + return codex_model_id(model) + return V2_BOOTSTRAP_MODEL + smart_routing_v2.launch_codex( state, tool_args, binary=binary, - start_model=starting_model(state), + start_model=starting_model(), render_overlay=render_overlay, ) if workspace: diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index aa1c0141..32b587d2 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -592,26 +592,6 @@ def test_managed_default_model_takes_priority(self): } assert codex.default_model(state) == "admin-chosen-default" - def test_starting_model_uses_cached_codex_model(self): - state = { - "codex_models": ["system.ai.gpt-5-6-luna"], - "oss_models": ["system.ai.glm-5-2"], - } - - assert codex.starting_model(state) == "gpt-5.6-luna" - - def test_starting_model_prefers_managed_default(self): - state = { - "codex_default_model": "admin-chosen-default", - "codex_models": ["system.ai.gpt-5-6-luna"], - } - - assert codex.starting_model(state) == "admin-chosen-default" - - def test_starting_model_falls_back_to_codex_catalog_default(self): - assert codex.starting_model({}) == "gpt-5.6-luna" - - class TestCodexValidateCmd: def test_starts_with_binary(self): cmd = codex.validate_cmd("codex") diff --git a/tests/test_codex_smart_routing_v2.py b/tests/test_codex_smart_routing_v2.py index 491e650b..ebe76e91 100644 --- a/tests/test_codex_smart_routing_v2.py +++ b/tests/test_codex_smart_routing_v2.py @@ -78,6 +78,26 @@ def launch_v2(state, tool_args, **kwargs): ) ] + def test_codex_launch_normalizes_cached_bootstrap_model(self, monkeypatch): + calls = [] + monkeypatch.setenv(v2.ENV_VAR, "1") + monkeypatch.setattr(codex, "clear_model_preferences", lambda state: False) + monkeypatch.setattr(codex, "default_model", lambda state: None) + + def launch_v2(state, tool_args, **kwargs): + calls.append(kwargs) + raise SystemExit(0) + + monkeypatch.setattr(v2, "launch_codex", launch_v2) + + with pytest.raises(SystemExit): + codex.launch( + {"workspace": WS, "codex_models": ["system.ai.gpt-5-6-luna"]}, + [], + ) + + assert calls[0]["start_model"] == "gpt-5.6-luna" + def test_owns_app_server_interposer_and_tui_lifecycle(self, monkeypatch): processes = [] interposer_args = {} From 73c624ad21d892ff6ff75d4811290c0dcf439290 Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Mon, 31 Aug 2026 21:56:02 +0000 Subject: [PATCH 5/8] update --- src/ucode/agents/codex.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index f2409d8f..72d7d552 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -50,7 +50,7 @@ # Shared across agents: one opt-in enables smart routing for every routing-capable # tool (codex, claude), so a workspace turns it on once. SMART_ROUTING_STATE_KEY = "smart_routing_enabled" -V2_BOOTSTRAP_MODEL = "gpt-5.6-luna" +APP_SERVER_SMART_ROUTING_STARTING_MODEL = "gpt-5.6-luna" SPEC: ToolSpec = { "binary": "codex", @@ -468,7 +468,7 @@ def starting_model() -> str: for model in models: if isinstance(model, str) and model: return codex_model_id(model) - return V2_BOOTSTRAP_MODEL + return APP_SERVER_SMART_ROUTING_STARTING_MODEL smart_routing_v2.launch_codex( state, From b3a694ddff327186dec82655e00cd99b1049e034 Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Mon, 31 Aug 2026 21:57:49 +0000 Subject: [PATCH 6/8] update --- src/ucode/agents/codex.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ucode/agents/codex.py b/src/ucode/agents/codex.py index 72d7d552..c056541c 100644 --- a/src/ucode/agents/codex.py +++ b/src/ucode/agents/codex.py @@ -29,6 +29,7 @@ from ucode.managed_files import OS, current_os, write_managed_file from ucode.smart_routing.codex_hooks import ( remove_smart_routing_hooks, + routing_models, sync_smart_routing_hooks, ) from ucode.smart_routing.codex_routing import codex_model_id @@ -458,23 +459,20 @@ def launch(state: dict, tool_args: list[str]) -> None: # path so flag-off launches retain their existing dependencies and behavior. from ucode.smart_routing import v2 as smart_routing_v2 - def starting_model() -> str: + def _app_server_start_model() -> str: managed_model = default_model(state) if managed_model: return managed_model - for key in ("codex_models", "oss_models"): - models = state.get(key) - if isinstance(models, list): - for model in models: - if isinstance(model, str) and model: - return codex_model_id(model) + models = routing_models(state) + if models: + return codex_model_id(models[0]) return APP_SERVER_SMART_ROUTING_STARTING_MODEL smart_routing_v2.launch_codex( state, tool_args, binary=binary, - start_model=starting_model(), + start_model=_app_server_start_model(), render_overlay=render_overlay, ) if workspace: From ae1e7c86bc50988a6469fa7b7d263d9af74976b8 Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Mon, 31 Aug 2026 22:00:03 +0000 Subject: [PATCH 7/8] fix --- tests/test_codex_smart_routing_v2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_codex_smart_routing_v2.py b/tests/test_codex_smart_routing_v2.py index ebe76e91..2b909480 100644 --- a/tests/test_codex_smart_routing_v2.py +++ b/tests/test_codex_smart_routing_v2.py @@ -256,6 +256,7 @@ def test_v2_pre_tool_hook_replaces_existing_ucode_hook(self, tmp_path, monkeypat def test_missing_cached_models_starts_with_bootstrap_model(self, monkeypatch): monkeypatch.setattr(v2, "get_databricks_token", lambda workspace, profile: "token") + monkeypatch.setattr(codex, "agent_version", lambda binary: "unknown") monkeypatch.setattr(v2, "_free_port", lambda: 41001) monkeypatch.setattr(v2, "_wait_for_app_server", lambda port, timeout: True) monkeypatch.setattr( From 0b1eacc8945e3c5fa985be45e9a7da00cd539c8e Mon Sep 17 00:00:00 2001 From: Lilly Luo Date: Mon, 31 Aug 2026 22:37:55 +0000 Subject: [PATCH 8/8] ruff --- tests/test_agent_codex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_agent_codex.py b/tests/test_agent_codex.py index 32b587d2..948ff400 100644 --- a/tests/test_agent_codex.py +++ b/tests/test_agent_codex.py @@ -592,6 +592,7 @@ def test_managed_default_model_takes_priority(self): } assert codex.default_model(state) == "admin-chosen-default" + class TestCodexValidateCmd: def test_starts_with_binary(self): cmd = codex.validate_cmd("codex")