fix(console-ui): align coordinator placeholder fallback with session_factory

Two Copilot-review followups on /v1/api/models default resolution.

- console/server.py: coordinator_default_alias now mirrors the full
  fallback chain in console/session_factory.py:109-110 — explicit
  coordinator.model_alias → model.default_alias → registry.default.
  The registry tier was missing, so the home composer placeholder went
  blank whenever an operator never set model.default_alias in the admin
  UI even though new coordinator sessions still launch on
  registry.default (loaded from config.toml [model].default by
  load_model_registry).  Two new tests cover the registry-default
  branch and the disabled-alias guard.
- console/static/app.js: _resolveModelLabel returns "" (not the bare
  alias) when the alias isn't found in the dropdown's model list, so
  callers can rely on the documented "fall back to neutral placeholder"
  contract.  Matches the existing doc comment.
This commit is contained in:
Patrick Buckley
2026-05-09 15:26:31 -07:00
parent e8e15d583a
commit 389400c879
3 changed files with 53 additions and 8 deletions
+35
View File
@@ -67,6 +67,7 @@ def _make_client(
storage: SQLiteBackend,
*,
settings: dict[str, str] | None = None,
registry_default: str | None = None,
) -> TestClient:
app = Starlette(
routes=[Route("/v1/api/models", list_available_models)],
@@ -74,6 +75,12 @@ def _make_client(
)
app.state.auth_storage = storage
app.state.config_store = _FakeConfigStore(dict(settings or {}))
if registry_default is not None:
# Mimic the live console's ``coord_registry`` shape — the handler
# only reads ``.default``, so a SimpleNamespace is enough.
from types import SimpleNamespace
app.state.coord_registry = SimpleNamespace(default=registry_default)
client = TestClient(app)
client.headers.update({"X-Test-User": "admin", "X-Test-Perms": ""})
return client
@@ -165,6 +172,34 @@ def test_coordinator_set_to_unknown_alias_falls_back_to_default(
assert body["coordinator_default_alias"] == "primary"
def test_coordinator_falls_back_to_registry_default_when_config_store_empty(
storage: SQLiteBackend,
) -> None:
"""Match ``console/session_factory.py:109-110``: when both
``coordinator.model_alias`` and ``model.default_alias`` are unset, new
coordinator sessions run on ``registry.default`` (loaded from
config.toml ``[model].default``). The placeholder must report the
same alias rather than going blank — otherwise the home composer
advertises "Default model" while sessions actually launch on a
concrete alias."""
_seed_model(storage, definition_id="m1", alias="primary")
body = _get_models(_make_client(storage, registry_default="primary"))
assert body["default_alias"] == ""
assert body["coordinator_default_alias"] == "primary"
assert body["judge_default_alias"] == "primary"
def test_coordinator_skips_registry_default_when_alias_disabled(
storage: SQLiteBackend,
) -> None:
"""Registry default points at an alias that's been disabled in the DB
— the placeholder stays blank rather than advertising a model that
workstream creation would refuse to use."""
_seed_model(storage, definition_id="m1", alias="legacy", enabled=False)
body = _get_models(_make_client(storage, registry_default="legacy"))
assert body["coordinator_default_alias"] == ""
# ---------------------------------------------------------------------------
# Judge resolution
# ---------------------------------------------------------------------------
+17 -7
View File
@@ -1684,15 +1684,25 @@ async def list_available_models(request: Request) -> JSONResponse:
default_alias = ""
if channel_default_alias and channel_default_alias not in enabled_aliases:
channel_default_alias = ""
# Coordinator falls back to model.default_alias when its setting is
# empty or points at a disabled/removed alias (matches
# console/session_factory.py). Judge falls back to the resolved
# coordinator alias when judge.model is empty *or* not a registered
# alias — judge.model is alias-only (matches IntentJudge.__init__),
# so an unknown value is operator misconfiguration that the judge
# itself silently inherits the session model on.
# Coordinator fallback chain mirrors console/session_factory.py:109-110:
# explicit ``coordinator.model_alias`` → ``model.default_alias``
# (admin-managed) → ``registry.default`` (the config.toml default that
# session_factory falls through to via ``registry.default`` when both
# ConfigStore keys are unset). Without the registry tier the
# placeholder lies whenever an operator never set ``model.default_alias``
# in the admin UI — new coordinator sessions will still run on
# ``registry.default``. Judge falls back to the resolved coordinator
# alias when ``judge.model`` is empty *or* not a registered alias —
# judge.model is alias-only (matches IntentJudge.__init__), so an
# unknown value is operator misconfiguration that the judge itself
# silently inherits the session model on.
if not coordinator_default_alias or coordinator_default_alias not in enabled_aliases:
coordinator_default_alias = default_alias
if not coordinator_default_alias:
coord_registry = getattr(request.app.state, "coord_registry", None)
registry_default = getattr(coord_registry, "default", "") if coord_registry else ""
if registry_default and registry_default in enabled_aliases:
coordinator_default_alias = registry_default
if not judge_default_alias or judge_default_alias not in enabled_aliases:
judge_default_alias = coordinator_default_alias
return JSONResponse(
+1 -1
View File
@@ -1813,7 +1813,7 @@ function _resolveModelLabel(alias, models) {
return m.alias === m.model ? m.alias : m.alias + " (" + m.model + ")";
}
}
return alias;
return "";
}
// Populate Model + Judge Model dropdowns from /v1/api/models — same