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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) == {}
|
||||
|
||||
Reference in New Issue
Block a user