* Studio: hide infra models from the hub cached inventory The hub inventory scans behind /api/hub/cached-gguf and /api/hub/cached-models returned the llama.cpp install validation probe (ggml-org/models) and the RAG embedder (unsloth/bge-small-en-v1.5[-GGUF]) as on-device models. Share the hidden-model check from routes/models.py via utils/models/hidden_models.py and apply it in both scans. A GGUF infra repo stays visible when the user explicitly downloaded a variant through the Hub, since variant manifests only exist for user-initiated downloads. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio: make On Device trust the hub inventory, match repo ids exactly, lighten the hidden-model import Follow-up on the hub cached-inventory hidden-model change, addressing the review. On Device now trusts the Hub inventory API for cached rows. The backend already hides the RAG embedder and the llama.cpp probe and re-includes a GGUF infra repo once the user downloads a variant through the Hub, but the frontend was re-hiding it by repo id, so the user-downloaded variant never appeared in the On Device list or the count. isVisibleInventoryRow now short-circuits cached rows (kind === "cache") to visible and keeps client-side needle hiding only for local filesystem rows and Discover. is_hidden_model matches Hub repo ids exactly (case-insensitive) against the probe plus the effective embedder and its GGUF companion, instead of substring matching the configured-embedder basename. A custom embedder with a generic basename like org/model no longer hides unrelated cached repos such as user/model-chat or org/model-instruct. The probe filename and local-path embedders keep exact matching. The helper moves to utils/hidden_models.py and is imported at module scope in the hub cache scanner, so it no longer pulls in utils/models/__init__ (the eager model-config/checkpoint stack) and a broken import fails at startup instead of being swallowed per-repo and silently emptying the inventory. routes.models keeps the _is_hidden_model and _safe_resolve aliases and drops the unused _HF_REPO_ID_RE re-export that was failing source lint. Tests: exact repo-id matching with a custom embedder, the cached-models scan keeping an unrelated repo, and a clean-interpreter check that the helper imports without the model-config stack. * Studio: match the llama.cpp probe filename on both path separators The hidden-model check compared the probe's on-disk filename with Path(value).name, which on a POSIX interpreter does not split a Windows-style path ("...\stories260K.gguf") and would let the probe through. Split on both separators so the probe is matched regardless of which OS produced the path, matching the tolerance of the previous substring check. Adds a Windows-path assertion to the probe test. * Studio: harden hidden infra model handling * Fix hidden cache row confirmation * Fix hidden local rows and confirmed hint merges * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Handle snapshot-configured hidden models * Hide basename-only default embedders * Fix dynamic embedder inventory filtering * Studio: hide the configured RAG embedder from Discover and feed rows --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: danielhanchen <unslothshared@gmail.com> Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com>
142 lines
5.7 KiB
Python
142 lines
5.7 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Infra-only model detection shared by the model routes and the hub
|
|
inventory. Lives directly under ``utils`` (not ``utils.models``) so the hub
|
|
cache scanner can import it without pulling in ``utils/models/__init__.py``,
|
|
which eagerly loads the model-config/checkpoint stack, and without importing
|
|
``routes.models`` (import-time side effects, would cycle)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
# Hub repo id shape ("owner/name", no leading separator); anything else is
|
|
# treated as a local filesystem path.
|
|
_HF_REPO_ID_RE = re.compile(r"^[A-Za-z0-9][\w.\-]*/[\w.\-]+$")
|
|
|
|
# The llama.cpp install-validation probe repo. Always hidden.
|
|
_PROBE_REPO_ID = "ggml-org/models"
|
|
# The probe's on-disk filename. Carries the ".gguf" so it stays specific and
|
|
# does not hide unrelated repos like ``user/stories260K-finetune-GGUF``.
|
|
_PROBE_FILENAME = "stories260k.gguf"
|
|
# Keep previously cached defaults hidden after settings changes.
|
|
_DEFAULT_EMBEDDING_REPO_IDS = {
|
|
"unsloth/bge-small-en-v1.5",
|
|
"unsloth/bge-small-en-v1.5-GGUF",
|
|
}
|
|
# Local copies do not always retain the repo id. Keep a narrow basename
|
|
# fallback for Studio's static default embedder only; configured custom repos
|
|
# remain exact-match-only.
|
|
_DEFAULT_EMBEDDING_PATH_BASENAMES = {"bge-small-en-v1.5"}
|
|
|
|
|
|
def _safe_resolve(path: Path) -> Optional[str]:
|
|
"""resolve() to a string, or None when the path is inaccessible."""
|
|
try:
|
|
return str(path.resolve())
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def _existing_resolved_path(value: str) -> Optional[str]:
|
|
"""Resolve an existing local path."""
|
|
path = Path(value).expanduser()
|
|
try:
|
|
if not path.exists():
|
|
return None
|
|
except OSError:
|
|
return None
|
|
return _safe_resolve(path)
|
|
|
|
|
|
def _path_contains_repo_id(value: str, repo_ids: set[str]) -> bool:
|
|
"""Match exact repo-derived path segments."""
|
|
parts = [part for part in value.lower().replace("\\", "/").split("/") if part]
|
|
for repo_id in repo_ids:
|
|
owner, name = repo_id.split("/", 1)
|
|
if f"models--{owner}--{name}" in parts:
|
|
return True
|
|
if any(
|
|
parts[index] == owner and parts[index + 1] == name for index in range(len(parts) - 1)
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _path_basename_is_default_embedder(value: str) -> bool:
|
|
"""Match a default embedder folder or a suffixed local weight filename."""
|
|
normalized = value.lower().replace("\\", "/").rstrip("/")
|
|
basename = normalized.rsplit("/", 1)[-1]
|
|
return any(
|
|
basename == needle
|
|
or any(basename.startswith(f"{needle}{separator}") for separator in ("-", "_", "."))
|
|
for needle in _DEFAULT_EMBEDDING_PATH_BASENAMES
|
|
)
|
|
|
|
|
|
def is_hidden_model(*values: str | None) -> bool:
|
|
"""True if any id/path is the RAG embedding model (the effective embedder
|
|
or its GGUF companion repo) 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.
|
|
|
|
Hub repo ids are matched EXACTLY (case-insensitive full "owner/name"), so a
|
|
custom embedder with a generic basename like "org/model" cannot substring
|
|
hide unrelated cached repos such as "user/model-chat" or "org/model-GGUF".
|
|
Existing paths take precedence over the identical ``owner/name`` repo
|
|
shape. Cache and LM Studio paths use exact repo-derived segments. Local
|
|
copies of the static default embedder also use a boundary-aware basename
|
|
fallback; configured custom repos never do."""
|
|
from core.rag import config as rag_config
|
|
|
|
hidden_repo_ids = {
|
|
_PROBE_REPO_ID.lower(),
|
|
*(repo_id.lower() for repo_id in _DEFAULT_EMBEDDING_REPO_IDS),
|
|
}
|
|
exact_paths: list[str] = []
|
|
for model in {
|
|
rag_config.EMBEDDING_MODEL,
|
|
rag_config.default_gguf_repo(),
|
|
rag_config.effective_embedding_model(),
|
|
rag_config.effective_gguf_repo(),
|
|
}:
|
|
existing_path = _existing_resolved_path(model)
|
|
if existing_path:
|
|
exact_paths.append(existing_path.lower())
|
|
elif _HF_REPO_ID_RE.match(model):
|
|
hidden_repo_ids.add(model.lower())
|
|
else:
|
|
resolved = _safe_resolve(Path(model).expanduser())
|
|
if resolved:
|
|
exact_paths.append(resolved.lower())
|
|
for v in values:
|
|
if not v:
|
|
continue
|
|
low = v.lower()
|
|
if _HF_REPO_ID_RE.match(v):
|
|
# A repo id ("owner/name"): match the hidden set exactly. It is
|
|
# never a filesystem path, so skip the path/filename checks.
|
|
if low in hidden_repo_ids:
|
|
return True
|
|
continue
|
|
# Anything else is treated as a filesystem path (the cached snapshot
|
|
# path, or a local model id). Match the probe by its exact filename and
|
|
# any configured local-path embedder by exact resolved path. Split on
|
|
# both separators so a Windows-style path ("...\\stories260K.gguf") is
|
|
# matched even when this runs on a POSIX interpreter (and vice versa).
|
|
if low.replace("\\", "/").rsplit("/", 1)[-1] == _PROBE_FILENAME:
|
|
return True
|
|
if _path_basename_is_default_embedder(v):
|
|
return True
|
|
if _path_contains_repo_id(v, hidden_repo_ids):
|
|
return True
|
|
if exact_paths:
|
|
resolved = _safe_resolve(Path(v).expanduser())
|
|
if resolved and resolved.lower() in exact_paths:
|
|
return True
|
|
return False
|