read the backend from the file that declares it, and stop advertising parked models (v0.5.4)

Both of these were found by running the previous commit rather than trusting
it, which is the only reason they are not still in.

The audio backend was read from the manager's per-model config map. That map
holds RUNTIME kwargs, and a whisper-server model has no row in it at all --
it lives in `whisper_servers` -- so the lookup answered "no backend" for
exactly the models whose backend decides the question, and the pick that was
supposed to avoid them chose `whisper2`. The entry is now read from
models.json, where `backend` is actually written, matched on id, alias, path
or basename because an audio model is referred to by any of them and the same
alias appears twice in a real catalogue. Verified against this install's
sixteen audio models: every one resolves, and the pick is now the model
measured to return fourteen word timings rather than the one measured to
return none.

The new warning is what caught it: the render reported "'whisper2' returned no
word timings" instead of quietly estimating. That is the whole point of it.

And `enabled: false` was hidden behind `all_engines`, which is the wrong seam.
The node-wide list the system worker serves -- the one the front hands clients
-- MUST pass all_engines=True, because that worker has no engine of its own
and would otherwise list nothing. So the filter was off on the only path that
mattered, and /v1/models offered LTX-2.5, Krea-2-Turbo and Krea-2-Edit: three
models configured ahead of a diffusers version that does not exist yet, each
of which answers 500. `include_disabled` is now a separate question, asked
only by the admin config view, because that is where a parked model gets
re-enabled.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
parent 2754079b
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.5.3" __version__ = "0.5.4"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -375,7 +375,10 @@ async def api_list_models(username: str = Depends(require_admin)): ...@@ -375,7 +375,10 @@ async def api_list_models(username: str = Depends(require_admin)):
# all_engines=True: the admin config view must show every configured model, # all_engines=True: the admin config view must show every configured model,
# including ones pinned to a secondary engine (e.g. whisper-servers on the # including ones pinned to a secondary engine (e.g. whisper-servers on the
# radeon engine) which the per-engine assignment filter would otherwise hide. # radeon engine) which the per-engine assignment filter would otherwise hide.
return multi_model_manager.list_models(all_engines=True) # include_disabled=True: and the parked ones, since this page is where
# they get re-enabled. No other caller asks for those.
return multi_model_manager.list_models(all_engines=True,
include_disabled=True)
except Exception: except Exception:
return [] return []
......
...@@ -705,7 +705,49 @@ def _stt_words(loop, audio_path: str, model: str, language: Optional[str]) -> Li ...@@ -705,7 +705,49 @@ def _stt_words(loop, audio_path: str, model: str, language: Optional[str]) -> Li
def _stt_entry(model_id: str) -> dict: def _stt_entry(model_id: str) -> dict:
"""An audio model's own models.json entry, without loading the model.""" """An audio model's own models.json entry, without loading the model.
Read from models.json rather than from the manager's per-model config map:
that map holds RUNTIME kwargs, keyed by registration id, and a
whisper-server model has no entry in it at all -- it lives in
``whisper_servers``. Asking it therefore answered "no backend" for exactly
the models whose backend matters, which picked `whisper2` for a render that
needed word timings (seen on a real render, 2026-10-26).
Matched on id, alias, path or basename, because an audio model is referred
to by any of them and the same alias may appear twice -- one entry naming
``path``, another ``model_path``. An entry that names a backend wins over
one that does not.
"""
want = str(model_id or "").strip().lower()
if not want:
return {}
tail = want.rstrip("/").split("/")[-1]
entries = []
try:
from codai.admin.routes import config_manager
md = getattr(config_manager, "models_data", {}) or {}
entries = [e for e in (md.get("audio_models") or []) if isinstance(e, dict)]
except Exception:
entries = []
found = {}
for e in entries:
names = set()
for key in ("id", "alias", "name", "path", "model_path"):
val = str(e.get(key) or "").strip().lower()
if val:
names.add(val)
names.add(val.rstrip("/").split("/")[-1])
if want in names or tail in names:
if e.get("backend") or not found:
found = e
if e.get("backend"):
break
if found:
return found
# No models.json in reach (a pod seeds its catalogue differently): fall back
# to the manager's map, which carries the entry under `_raw_cfg` when it has
# one.
try: try:
from codai.models.manager import multi_model_manager from codai.models.manager import multi_model_manager
cfg = multi_model_manager._config_for_model(model_id) or {} cfg = multi_model_manager._config_for_model(model_id) or {}
......
...@@ -5915,13 +5915,23 @@ class MultiModelManager: ...@@ -5915,13 +5915,23 @@ class MultiModelManager:
return info.id return info.id
return None return None
def list_models(self, all_engines: bool = False) -> List[ModelInfo]: def list_models(self, all_engines: bool = False,
include_disabled: bool = False) -> List[ModelInfo]:
"""List all available models (configured + runtime aliases) with type/capability metadata. """List all available models (configured + runtime aliases) with type/capability metadata.
all_engines=True bypasses the per-engine assignment filter so the admin all_engines=True bypasses the per-engine assignment filter so the admin
config UI sees every configured model — otherwise, on a multi-engine node, config UI sees every configured model — otherwise, on a multi-engine node,
models pinned to a secondary engine (e.g. whisper-servers on `radeon`) are models pinned to a secondary engine (e.g. whisper-servers on `radeon`) are
hidden from the primary's list, which serves /admin/api/models.""" hidden from the primary's list, which serves /admin/api/models.
include_disabled=True additionally lists entries parked as
``enabled: false``. It is a SEPARATE question from all_engines, found
out the hard way: hiding them behind all_engines meant they stayed
hidden only on a per-engine list, while the one list clients actually
read -- the node-wide one the system worker serves, which must pass
all_engines=True because it has no engine of its own -- advertised
every parked model and answered 500 for it. Only the admin config view
asks for them, because that is where they get re-enabled."""
from codai.models.capabilities import detect_model_capabilities, ModelCapabilities from codai.models.capabilities import detect_model_capabilities, ModelCapabilities
models = [] models = []
...@@ -6041,9 +6051,8 @@ class MultiModelManager: ...@@ -6041,9 +6051,8 @@ class MultiModelManager:
# else, so an entry configured ahead of a blocker -- # else, so an entry configured ahead of a blocker --
# weights downloaded, settings written, waiting on a # weights downloaded, settings written, waiting on a
# dependency -- was offered to clients and answered # dependency -- was offered to clients and answered
# 500. The admin view (all_engines) still shows it, # 500. Only the admin config view asks for them.
# because that is where it gets re-enabled. if (not include_disabled and isinstance(m, dict)
if (not all_engines and isinstance(m, dict)
and m.get("enabled") is False): and m.get("enabled") is False):
continue continue
if isinstance(m, str): if isinstance(m, str):
......
...@@ -224,18 +224,27 @@ def _audio_catalogue(monkeypatch, entries): ...@@ -224,18 +224,27 @@ def _audio_catalogue(monkeypatch, entries):
import codai.api.compose as comp import codai.api.compose as comp
from codai.models import manager as mgr from codai.models import manager as mgr
def _raw(name): rows = []
spec = entries.get(name, "") for name, spec in entries.items():
backend, langs = spec if isinstance(spec, tuple) else (spec, None) backend, langs = spec if isinstance(spec, tuple) else (spec, None)
entry = {"backend": backend} row = {"id": name, "alias": name, "backend": backend}
if langs is not None: if langs is not None:
entry["languages"] = langs row["languages"] = langs
return {"_raw_cfg": entry} rows.append(row)
from codai.admin import routes as admin_routes
class _ConfigManager:
models_data = {"audio_models": rows}
monkeypatch.setattr(mgr.multi_model_manager, "list_models", monkeypatch.setattr(mgr.multi_model_manager, "list_models",
lambda *a, **k: [_Model(i) for i in entries], raising=False) lambda *a, **k: [_Model(i) for i in entries], raising=False)
monkeypatch.setattr(mgr.multi_model_manager, "_config_for_model", # The entry comes from models.json, which is where the backend is written:
_raw, raising=False) # the manager's config map holds runtime kwargs and has no row at all for a
# whisper-server model. (`config_manager` is None until a server boots, so
# the whole object is replaced, not one attribute of it.)
monkeypatch.setattr(admin_routes, "config_manager", _ConfigManager(),
raising=False)
return comp return comp
...@@ -409,3 +418,42 @@ def test_a_whisper_checkpoint_still_gets_its_generate_kwargs(monkeypatch, tmp_pa ...@@ -409,3 +418,42 @@ def test_a_whisper_checkpoint_still_gets_its_generate_kwargs(monkeypatch, tmp_pa
assert out["words"] assert out["words"]
assert pipe.calls[0]["generate_kwargs"] == {"task": "transcribe", assert pipe.calls[0]["generate_kwargs"] == {"task": "transcribe",
"language": "it"} "language": "it"}
def test_a_whisper_server_model_is_recognised_from_models_json(monkeypatch):
"""The live miss: its backend is in models.json and NOWHERE else.
A whisper-server model is not in the manager's per-model config map at all
(it lives in `whisper_servers`), so asking that map answered "no backend"
and the model looked able. Same alias twice -- one row naming `path`, one
naming `model_path` -- is also how the real catalogue stores it.
"""
import codai.api.compose as comp
from codai.admin import routes as admin_routes
from codai.models import manager as mgr
rows = [
{"id": "whisper2", "alias": "whisper2", "backend": "whisper-server",
"model_path": "/AI/guffcache/whisper-large-v3-q8_0.gguf"},
{"id": None, "alias": "whisper2", "backend": "whisper-server",
"path": "/AI/guffcache/whisper-large-v3-q8_0.gguf"},
{"id": "whisper3-it", "alias": "whisper3-it", "backend": "crisperwhisper",
"path": "LocalAI-io/whisper-large-v3-turbo-it"},
]
class _ConfigManager:
models_data = {"audio_models": rows}
monkeypatch.setattr(admin_routes, "config_manager", _ConfigManager(),
raising=False)
# The map the old code asked: it knows nothing about either model.
monkeypatch.setattr(mgr.multi_model_manager, "_config_for_model",
lambda name: {}, raising=False)
monkeypatch.setattr(mgr.multi_model_manager, "list_models",
lambda *a, **k: [_Model("whisper2"), _Model("whisper3-it")],
raising=False)
assert comp._stt_backend("whisper2") == "whisper-server"
assert comp._stt_backend("whisper3-it") == "crisperwhisper"
from codai.api.compose import CaptionSpec
assert comp._pick_stt_model(CaptionSpec(enabled=True), ["it"]) == "whisper3-it"
"""A model parked as `enabled: false` must not be advertised to clients.
Measured on a running install (2026-10-26): `/v1/models` offered `LTX-2.5`,
`Krea-2-Turbo` and `Krea-2-Edit` -- three entries configured ahead of a
diffusers version that does not exist yet, whose weights are downloaded and
whose settings are written, and which answer 500 if anyone asks for them.
The first attempt hid them behind `all_engines`, which was the wrong seam: the
node-wide list the system worker serves -- the one the front hands clients --
MUST pass all_engines=True, because that worker has no engine of its own and
would otherwise list nothing. So the filter was off on the only path that
mattered. Hence a separate `include_disabled`, which only the admin config view
asks for, because that is where a parked model gets re-enabled.
"""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from codai.models.manager import multi_model_manager
CATALOGUE = {
"image_models": [
{"id": None, "alias": "Z-Image", "path": "Tongyi-MAI/Z-Image-Turbo",
"enabled": True},
{"id": None, "alias": "Krea-2-Turbo", "path": "unsloth/Krea-2-Turbo",
"enabled": False},
],
"video_models": [
{"id": None, "alias": "LTX-2.3-10Eros",
"path": "diffusers/LTX-2.3-Diffusers", "enabled": True},
{"id": None, "alias": "LTX-2.5",
"path": "Lightricks/LTX-2.5-Diffusers", "enabled": False},
],
}
@pytest.fixture
def catalogue(monkeypatch):
from codai.admin import routes as admin_routes
class _ConfigManager:
models_data = CATALOGUE
config = None
monkeypatch.setattr(admin_routes, "config_manager", _ConfigManager(),
raising=False)
# No per-engine restriction, so the assignment filter is not what is being
# measured here.
monkeypatch.setattr(multi_model_manager, "_assigned_model_keys", None,
raising=False)
return CATALOGUE
def _ids(**kw):
return {m.id for m in multi_model_manager.list_models(**kw)}
def test_the_node_wide_list_clients_read_hides_a_parked_model(catalogue):
"""all_engines=True is what the system worker passes. Clients see this."""
ids = _ids(all_engines=True)
assert "Z-Image" in ids and "LTX-2.3-10Eros" in ids
for parked in ("Krea-2-Turbo", "LTX-2.5"):
assert parked not in ids, f"{parked} is parked and must not be offered"
# Nor under its path or its basename, which are advertised as aliases too.
assert "unsloth/Krea-2-Turbo" not in ids
assert "Lightricks/LTX-2.5-Diffusers" not in ids
assert "LTX-2.5-Diffusers" not in ids
def test_the_per_engine_list_hides_it_too(catalogue):
assert "LTX-2.5" not in _ids()
def test_the_admin_view_still_shows_it(catalogue):
"""It is the page where a parked model gets re-enabled."""
ids = _ids(all_engines=True, include_disabled=True)
assert {"Krea-2-Turbo", "LTX-2.5"} <= ids
def test_the_admin_route_asks_for_the_parked_ones(catalogue):
"""The caller, not just the switch: /admin/api/models must pass both."""
import inspect
from codai.admin import routes
src = inspect.getsource(routes.api_list_models)
assert "include_disabled=True" in src
assert "all_engines=True" in src
def test_the_system_worker_does_not(catalogue):
"""The one list clients read must not ask for parked models."""
import inspect
import codai.system_app as system_app
src = inspect.getsource(system_app)
assert "list_models(all_engines=True)" in src
assert "include_disabled" not in src
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment