From 950da4cba827f7d9a2efb8a1da136f5526497efc Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 26 Jul 2026 11:21:50 +0000 Subject: [PATCH] Keep curated models listed, guard the video companion repo, pin diffusers Three review findings: - The picker filtered every catalog member out of Recommended and Hub search on the way to canonical group rows, but nothing renders those rows yet (catalogGroupFitsDevice and groupMatchesQuery are imported and unused). A task-scoped picker's models list is catalogToModelOptions(), i.e. group members exclusively, so both lists came back empty and no curated model could be discovered or downloaded. Keep the artifacts listed until the grouped UI exists. - The video delete guard compared only repo_id, so deleting the companion base of a loaded GGUF video model was allowed even though it supplies the VAE and text encoders. Compare base_repo too, matching what the images guard already does for its companions. - diffusers was declared unversioned while the diffusion stack requires 0.39 (Krea2Pipeline, the cache_context child registries, the Flux2 and Z-Image pipelines), so an upgrade could keep an older release and selecting an advertised model failed until the user upgraded by hand. --- pyproject.toml | 6 ++++- .../backend/hub/services/models/deletion.py | 11 ++++++--- studio/backend/tests/test_video_routes.py | 23 +++++++++++++++++++ .../components/model-selector/pickers.tsx | 11 +++++---- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0f57ecf4df..a89d2c2a51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,11 @@ huggingfacenotorch = [ "peft>=0.18.0,!=0.11.0", "huggingface_hub>=0.34.0", "hf_transfer", - "diffusers", + # 0.39 is the floor the diffusion stack already assumes: Krea2Pipeline (the Krea 2 + # workflow raises without it), the cache_context child-registry behavior, and the + # Flux2 / Z-Image pipelines. An unversioned entry let an upgrade keep an older + # diffusers, so advertised models failed to load until the user upgraded by hand. + "diffusers>=0.39.0", "transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,!=5.0.0,!=5.1.0,<=5.5.0", "trl>=0.18.2,!=0.19.0,<=0.24.0", "sentence-transformers", diff --git a/studio/backend/hub/services/models/deletion.py b/studio/backend/hub/services/models/deletion.py index efc5013b61..8ef6f0e174 100644 --- a/studio/backend/hub/services/models/deletion.py +++ b/studio/backend/hub/services/models/deletion.py @@ -629,9 +629,14 @@ def _video_blocks_delete(repo_id: str) -> Optional[str]: logger.debug(f"Video backend unavailable during delete guard for {repo_id}: {e}") return None status = backend.status() - if status.get("loaded") and status.get("repo_id"): - if _loaded_id_matches_repo(str(status["repo_id"]), repo_id): - return "Unload the model before deleting" + if status.get("loaded"): + # repo_id names the checkpoint; for a GGUF / single-file load the companion base + # supplies the VAE and text encoders and is just as much part of the live model, + # so refuse it too (the Images guard above does the same via loaded_repo_ids). + for key in ("repo_id", "base_repo"): + held = status.get(key) + if held and _loaded_id_matches_repo(str(held), repo_id): + return "Unload the model before deleting" for lid in getattr(backend, "loading_repo_ids", tuple)(): if _loaded_id_matches_repo(str(lid), repo_id): return "A Video model load is using this repo; wait for it to finish" diff --git a/studio/backend/tests/test_video_routes.py b/studio/backend/tests/test_video_routes.py index c127bc0cbe..3e7d0bb66a 100644 --- a/studio/backend/tests/test_video_routes.py +++ b/studio/backend/tests/test_video_routes.py @@ -734,3 +734,26 @@ def test_export_endpoint_validation(client, monkeypatch): resp = client.get(f"/api/inference/video/gallery/{video['id']}/export?format=webm") assert resp.status_code == 501 assert "PyAV" in resp.json()["detail"] + + +def test_delete_guard_protects_the_loaded_video_companion_base(monkeypatch): + # For a GGUF / single-file video load the companion base supplies the VAE and text + # encoders, so it is as much part of the live model as the checkpoint. Deleting it used + # to sail past the guard, which only compared repo_id. + from hub.services.models import deletion + + class _Backend: + def status(self): + return { + "loaded": True, + "repo_id": "unsloth/LTX-2.3-GGUF", + "base_repo": "unsloth/LTX-2.3", + } + + def loading_repo_ids(self): + return () + + monkeypatch.setattr(video_module, "get_video_backend", lambda: _Backend()) + assert deletion._video_blocks_delete("unsloth/LTX-2.3-GGUF") is not None + assert deletion._video_blocks_delete("unsloth/LTX-2.3") is not None + assert deletion._video_blocks_delete("unsloth/something-else") is None diff --git a/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx b/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx index d13d84362c..9a5e972db3 100644 --- a/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx +++ b/studio/frontend/src/features/model-picker/components/model-selector/pickers.tsx @@ -1921,8 +1921,11 @@ export function HubModelPicker({ ? isKnownGgufRepo(id) : !chatOnly || isRecommendableFormat(id, isKnownGgufRepo(id), isMac), ) - // Member repos of a catalog group collapse into the canonical group row. - .filter((id) => !catalog || !groupForRepoId(id, catalog)) + // Member repos of a catalog group would collapse into the canonical group row -- + // but nothing renders those rows yet (catalogGroupFitsDevice / groupMatchesQuery are + // imported and unused), and a task-scoped picker's `models` is catalogToModelOptions(), + // i.e. group members exclusively. Suppressing them here emptied Recommended on the + // Images and Video pages, so keep the artifacts listed until the grouped UI lands. .filter((id) => !/-FP8[-.]|FP8-Dynamic/i.test(id)); // Sort: GGUFs first, then hub models const gguf: string[] = []; @@ -1970,8 +1973,8 @@ export function HubModelPicker({ : rows.filter((r) => matchesFormatFilter(r.id, r.isGguf, formatFilter)); // Task-scoped pages load single-file GGUF only. if (task) rows = rows.filter((r) => r.isGguf); - // Members already render under their canonical group row. - if (catalog) rows = rows.filter((r) => !groupForRepoId(r.id, catalog)); + // Members would render under their canonical group row, but that row does not exist + // yet (see recommendedIds): filtering here removed curated models from Hub search too. // The "recommended" sort always applies the device-fit filter; the shared // "Fits on device" tick extends it to the other sorts too. if (recommendedSort !== "recommended" && !fitOnDeviceOnly) return rows;