fix(routing): apply the glm arm under the gateway's model route

The task_v1 codex arm `glm-5-2` resolved to the catalog's
`databricks-glm-5-2`, which the codex turn then failed to serve: that
serving endpoint advertises chat-completions only and 400s on
`/codex/v1`. Probes on staging and prod (2026-08-01) show the Responses
API does serve GLM — but only under the gateway model route
`system.ai.glm-5-2`. GLM appears in no discovery listing, so the working
name can only be pinned, not discovered.

Add a per-model servable-alias map next to the arm tables and consult it
when an arm resolves to a servable id, so the codex apply layer writes
`system.ai.glm-5-2`. Subagent candidates are offered under the same
spelling, so a rewrite spawns with the id routing resolves to. The
router's arm id stays `glm-5-2`, and the alias strips to the same bare id
so decision records show no substitution.

Co-authored-by: Isaac
This commit is contained in:
Bryan Qiu
2026-08-01 12:10:13 -07:00
parent 6d4cb367c7
commit 907f8886a9
5 changed files with 107 additions and 15 deletions
+10 -2
View File
@@ -418,7 +418,11 @@ def candidate_models(
catalog has no row for.
:returns: Harness → model ids, cheapest first, empty entries dropped.
"""
from omnigent.server.smart_routing import catalog_models_for_harness, infer_models
from omnigent.server.smart_routing import (
apply_servable_alias,
catalog_models_for_harness,
infer_models,
)
offered = (harness, _COUNTERPART_HARNESS.get(harness)) if cross_harness else (harness,)
result: dict[str, list[str]] = {}
@@ -433,8 +437,12 @@ def candidate_models(
# ``model_family_mismatch`` at dispatch.
family = harness_family(candidate)
models = [m for m in models if model_in_family(family, m)]
# Offer each model under the spelling the gateway serves, so the
# spawn's model matches what routing resolves to; dedupe when the
# catalog carries both spellings.
models = list(dict.fromkeys(apply_servable_alias(m) for m in models))
if models:
result[candidate] = list(models)
result[candidate] = models
return result
+33 -5
View File
@@ -636,6 +636,28 @@ _ARM_SUBSTITUTES: Mapping[str, tuple[str, ...]] = MappingProxyType(
}
)
# Per-model overrides for the id a resolved arm is actually applied under, when
# the catalog's spelling is not the one the gateway serves. Bare id → exact id
# to apply. Not a general prefix rule: each entry is a probed fact about one
# model.
#
# ``glm-5-2``: probed 2026-08-01 on staging and prod — the gateway serves GLM on
# the Responses API only as the model route ``system.ai.glm-5-2``. The serving
# endpoint ``databricks-glm-5-2`` the catalog offers advertises chat-completions
# only and 400s on ``/codex/v1``, and GLM appears in no discovery listing, so the
# working spelling has to be named here.
_SERVABLE_ALIASES: Mapping[str, str] = MappingProxyType({"glm-5-2": "system.ai.glm-5-2"})
def apply_servable_alias(model: str, prefixes: Sequence[str] | None = None) -> str:
"""Map a resolved model id onto the spelling this gateway actually serves.
:param model: A servable catalog id, in either vocabulary.
:param prefixes: Catalog prefixes to strip before comparing ids.
:returns: The aliased id, or *model* unchanged when no alias applies.
"""
return _SERVABLE_ALIASES.get(_bare_id(model, prefixes), model)
def task_v1_claude_arms() -> tuple[str, ...]:
"""Return the frozen Claude-family arms task_v1 may select.
@@ -799,7 +821,8 @@ def substitute_model(
:param prefixes: Catalog prefixes to strip before comparing ids; ``None``
uses this deployment's configured ones.
:param barred: Bare ids to skip, e.g. models the harness's gateway 400s on.
:returns: A servable candidate id, or ``None`` when nothing fits.
:returns: A servable candidate id (through :func:`apply_servable_alias`),
or ``None`` when nothing fits.
"""
local: dict[str, str] = {}
for candidate in candidates:
@@ -809,7 +832,7 @@ def substitute_model(
if substitute in skip:
continue
if substitute in local:
return local[substitute]
return apply_servable_alias(local[substitute], prefixes)
family = _model_family(model)
ranked = [
(index, candidate)
@@ -821,8 +844,9 @@ def substitute_model(
target = _cost_position(model, candidates, prefixes)
if target is None:
# Nothing places the pick on the cost curve; stay at the cheap end.
return ranked[0][1]
return min(ranked, key=lambda entry: (abs(entry[0] - target), entry[0]))[1]
return apply_servable_alias(ranked[0][1], prefixes)
nearest = min(ranked, key=lambda entry: (abs(entry[0] - target), entry[0]))[1]
return apply_servable_alias(nearest, prefixes)
def natural_harness_for_model(
@@ -1028,7 +1052,11 @@ class TaskV1RouteOptionSource:
local = substitute_model(raw, pool, prefixes=self._model_prefixes)
if local is None:
return None
return ResolvedRoute(model=local, harness=harness, raw_model=raw)
return ResolvedRoute(
model=apply_servable_alias(local, self._model_prefixes),
harness=harness,
raw_model=raw,
)
def _is_menu_arm(
self,
@@ -34,6 +34,8 @@ ROUTED_MODEL = "databricks-claude-opus-4-8"
LLM_PICKED_MODEL = "databricks-claude-sonnet-4-6"
GPT_MODEL = "databricks-gpt-5-5"
GLM_MODEL = "databricks-glm-5-2"
# The spelling the gateway serves GLM under; see ``_SERVABLE_ALIASES``.
GLM_SERVABLE = "system.ai.glm-5-2"
pytestmark = pytest.mark.asyncio
@@ -475,7 +477,9 @@ async def test_codex_session_keeps_glm_candidates_and_applies_a_glm_pick(
GLM serves on the same Responses wire codex speaks, so the live catalog
row must reach the router and the resulting pick must pass the dispatch
family gate a real spawn goes through.
family gate a real spawn goes through. The row is offered — and the pick
applied — under the gateway's own ``system.ai.glm-5-2`` model route, which
is the only name that serves.
"""
from omnigent.model_override import model_family_mismatch
from omnigent.server import smart_routing as smart_routing_module
@@ -487,7 +491,7 @@ async def test_codex_session_keeps_glm_candidates_and_applies_a_glm_pick(
subagent_routing="on",
)
routing_client = FakeRoutingClient(
RoutingResult(model=GLM_MODEL, rationale="delegate arm", harness="codex")
RoutingResult(model=GLM_SERVABLE, rationale="delegate arm", harness="codex")
)
live_catalog = {"self": [GPT_MODEL, GLM_MODEL]}
@@ -509,12 +513,12 @@ async def test_codex_session_keeps_glm_candidates_and_applies_a_glm_pick(
assert resp.status_code == 200, resp.text
# The GLM row reached the router as a codex candidate.
assert routing_client.offered[0] == {"codex-native": [GPT_MODEL, GLM_MODEL]}
assert routing_client.offered[0] == {"codex-native": [GPT_MODEL, GLM_SERVABLE]}
body = resp.json()
assert body["action"] == "rewrite"
assert body["model"] == GLM_MODEL
assert body["model"] == GLM_SERVABLE
# And the applied pick is one the dispatch gate accepts on codex.
assert model_family_mismatch("codex-native", GLM_MODEL) is None
assert model_family_mismatch("codex-native", GLM_SERVABLE) is None
async def test_auto_session_and_its_children_keep_cross_harness_picks(
+40 -2
View File
@@ -1797,12 +1797,13 @@ _CLAUDE_TIERS = (
("gpt-5-6-luna", ("gpt-5-nano",), "codex", "gpt-5-nano"),
# glm-5-2 has no local endpoint, and its chain escalates like sol's.
("glm-5-2", _UCODE_CODEX_CATALOG, "codex", "gpt-5-5"),
# A workspace that serves GLM resolves the glm arm to that endpoint.
# A workspace that serves GLM resolves the glm arm to that endpoint,
# under the gateway's own model-route spelling.
(
"glm-5-2",
(*_UCODE_CODEX_CATALOG, "databricks-glm-5-2"),
"codex",
"databricks-glm-5-2",
"system.ai.glm-5-2",
),
# The prefix-restore path is untouched: a servable arm is applied as-is.
(
@@ -1872,6 +1873,43 @@ def test_unservable_arm_substitutes_from_its_own_chain(
assert _substitute(arm, catalog, harness) == expected
@pytest.mark.parametrize(
"catalog",
[
("databricks-gpt-5-5", "databricks-glm-5-2"),
("databricks-gpt-5-5", "system.ai.glm-5-2"),
("databricks-gpt-5-5", "databricks-glm-5-2", "system.ai.glm-5-2"),
# The picker's dotted spelling still keys to the dashed arm.
("databricks-gpt-5-5", "databricks-glm-5.2"),
],
)
def test_glm_arm_applies_the_gateway_model_route_spelling(catalog: Sequence[str]) -> None:
"""The glm arm applies as ``system.ai.glm-5-2`` whatever the catalog spells."""
assert _substitute("glm-5-2", catalog) == "system.ai.glm-5-2"
def test_servable_alias_leaves_other_arms_alone() -> None:
from omnigent.server.smart_routing import apply_servable_alias
for model in ("databricks-gpt-5-5", "system.ai.claude-opus-4-8", "databricks-kimi-k2-6"):
assert apply_servable_alias(model) == model
def test_aliased_glm_pick_reports_no_substitution() -> None:
"""The alias is a spelling, so the decision must not show a swap arrow."""
from omnigent.server.smart_routing import RoutePick, TaskV1RouteOptionSource, _bare_id
source = TaskV1RouteOptionSource(model_prefixes=["databricks-", "system.ai."])
resolved = source.resolve_selection(
RoutePick(model="glm-5-2"),
["codex"],
{"codex": ["databricks-gpt-5-5", "databricks-glm-5-2"]},
)
assert resolved is not None
assert resolved.model == "system.ai.glm-5-2"
assert _bare_id(resolved.model) == _bare_id(resolved.raw_model) == "glm-5-2"
def test_substitution_is_independent_of_catalog_order() -> None:
import random
+15 -1
View File
@@ -43,6 +43,8 @@ from tests.server.helpers import FakeCaps, FakeRoutingClient
CLAUDE_MODEL = "databricks-claude-opus-4-8"
GPT_MODEL = "databricks-gpt-5-5"
GLM_MODEL = "databricks-glm-5-2"
# The spelling the gateway actually serves GLM under; see ``_SERVABLE_ALIASES``.
GLM_SERVABLE = "system.ai.glm-5-2"
KIMI_MODEL = "databricks-kimi-k2-6"
PARENT_MODEL = "databricks-claude-sonnet-4-6"
@@ -116,11 +118,23 @@ def test_candidate_models_applies_the_family_constraint_to_catalog_rows() -> Non
"""
catalog = {"self": [GPT_MODEL, GLM_MODEL, KIMI_MODEL, CLAUDE_MODEL]}
candidates = candidate_models("codex-native", catalog=catalog)
assert candidates == {"codex-native": [GPT_MODEL, GLM_MODEL, KIMI_MODEL]}
assert candidates == {"codex-native": [GPT_MODEL, GLM_SERVABLE, KIMI_MODEL]}
assert model_in_family(harness_family("codex-native"), GLM_MODEL) is True
assert model_in_family(harness_family("claude-native"), GLM_MODEL) is False
def test_candidate_models_offers_glm_under_its_servable_alias() -> None:
"""Both catalog spellings collapse to the one the gateway serves.
The offered id is what a rewrite spawns with, so it has to match the id
routing resolves the ``glm-5-2`` arm to.
"""
catalog = {"self": [GPT_MODEL, GLM_MODEL, GLM_SERVABLE]}
assert candidate_models("codex-native", catalog=catalog) == {
"codex-native": [GPT_MODEL, GLM_SERVABLE]
}
def test_candidate_models_drops_a_harness_with_nothing_servable() -> None:
catalog = {"self": [CLAUDE_MODEL]}
assert candidate_models("codex-native", catalog=catalog) == {}