diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index 6835126e62..c05522e64a 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -41,12 +41,21 @@ def _safe_is_dir(path) -> bool: def _is_hidden_model(*values: str | None) -> bool: """True if any id/path is the RAG embedding model (EMBEDDING_MODEL or - EMBED_GGUF_REPO basename), so pickers hide it (GGUF and non-GGUF).""" + EMBED_GGUF_REPO basename) or the llama.cpp install validation probe + (ggml-org/models / stories260K), so pickers hide them (GGUF and non-GGUF). + None are usable chat models; the probe can be cached as a side effect of + installing the prebuilt llama-server and otherwise sorts smallest, so it + would be auto-selected.""" from core.rag import config as rag_config needles = ( rag_config.EMBEDDING_MODEL.split("/")[-1].lower(), rag_config.EMBED_GGUF_REPO.split("/")[-1].lower(), + # The validation probe's repo (matches the cached repo id) and its exact + # filename (matches the on-disk path). The filename carries the .gguf so + # it does not hide unrelated repos like ``user/stories260K-finetune-GGUF``. + "ggml-org/models", + "stories260k.gguf", ) return any(v and any(n in v.lower() for n in needles) for v in values) diff --git a/studio/backend/tests/test_cached_gguf_routes.py b/studio/backend/tests/test_cached_gguf_routes.py index 6409aef11e..03662e7b08 100644 --- a/studio/backend/tests/test_cached_gguf_routes.py +++ b/studio/backend/tests/test_cached_gguf_routes.py @@ -107,6 +107,48 @@ def test_list_cached_gguf_matches_extension_case_insensitively(monkeypatch, tmp_ ] +def test_is_hidden_model_hides_validation_probe_everywhere(): + """Every picker (model list, local, cached GGUF, cached models) gates on + _is_hidden_model, so hiding the probe here hides it in the search menu too. + Cover both forms callers pass: the reconstructed repo id and the on-disk + snapshot path.""" + assert models_route._is_hidden_model("ggml-org/models") + assert models_route._is_hidden_model("ggml-org/models/tinyllamas/stories260K.gguf") + assert models_route._is_hidden_model( + None, "/hf/models--ggml-org--models/snapshots/abc/tinyllamas/stories260K.gguf" + ) + assert not models_route._is_hidden_model("unsloth/gemma-3-270m-it-GGUF") + # The exact-filename needle must not hide a real repo that merely + # references stories260K in its name. + assert not models_route._is_hidden_model("user/stories260K-finetune-GGUF") + + +def test_list_cached_gguf_hides_llama_validation_probe(monkeypatch, tmp_path): + """The ggml-org/models / stories260K install validation probe can land in + the HF cache as a side effect of installing the prebuilt llama-server. + It is not a chat model (it sorts smallest and would be auto-selected), so + pickers must hide it while keeping real cached models.""" + probe = _repo( + "ggml-org/models", + [_file("tinyllamas/stories260K.gguf", 1_000)], + tmp_path / "models--ggml-org--models", + ) + real = _repo( + "unsloth/gemma-3-270m-it-GGUF", + [_file("gemma-3-270m-it-UD-Q4_K_XL.gguf", 200_000)], + tmp_path / "models--unsloth--gemma-3-270m-it-GGUF", + ) + monkeypatch.setattr( + models_route, "_all_hf_cache_scans", lambda: [SimpleNamespace(repos = [probe, real])] + ) + + result = asyncio.run(models_route.list_cached_gguf(current_subject = "test-user")) + + repo_ids = [c["repo_id"] for c in result["cached"]] + assert "ggml-org/models" not in repo_ids + assert "unsloth/gemma-3-270m-it-GGUF" in repo_ids + + def test_list_cached_gguf_skips_repos_without_positive_gguf_size(monkeypatch, tmp_path): missing = _repo( "Org/ReadmeOnly",