Studio: hide the RAG embedder and llama.cpp probe from the hub cached inventory (#7018)

* 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>
This commit is contained in:
Michael Han 2026-07-19 03:20:56 -07:00 committed by GitHub
commit 74d1a284eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 899 additions and 100 deletions

View file

@ -201,7 +201,11 @@ def _build_index() -> dict[str, _LocalGgufEntry]:
continue
# Skip what Unsloth hides from its pickers (validation probe, RAG embed
# weights): not chat models, so never an auto-switch target.
if _is_hidden_model(raw_id, getattr(info, "path", None)):
if _is_hidden_model(
raw_id,
getattr(info, "model_id", None),
getattr(info, "path", None),
):
continue
# Advertise a client-facing alias, not an absolute filesystem path.
loader_id = _advertised_loader_id(info)

View file

@ -87,6 +87,22 @@ def _names_gguf(model: str) -> bool:
return "gguf" in re.split(r"[^a-z0-9]+", model.lower())
def gguf_repo_for_embedding_model(model: str) -> str:
"""GGUF repo for ``model``, honoring an explicit companion override."""
if "RAG_EMBED_GGUF_REPO" in os.environ:
return EMBED_GGUF_REPO
if model == DEFAULT_EMBEDDING_MODEL:
return EMBED_GGUF_REPO
if _names_gguf(model):
return model
return f"{model}-GGUF"
def default_gguf_repo() -> str:
"""GGUF companion for the env/default embedding model."""
return gguf_repo_for_embedding_model(EMBEDDING_MODEL)
def effective_gguf_repo() -> str:
"""GGUF repo for the llama-server backend, tracking the effective model.
@ -95,14 +111,7 @@ def effective_gguf_repo() -> str:
``-GGUF`` companion repo (the unsloth convention the default pair follows),
or is used as-is when it already names a GGUF repo.
"""
if "RAG_EMBED_GGUF_REPO" in os.environ:
return EMBED_GGUF_REPO
model = effective_embedding_model()
if model == DEFAULT_EMBEDDING_MODEL:
return EMBED_GGUF_REPO
if _names_gguf(model):
return model
return f"{model}-GGUF"
return gguf_repo_for_embedding_model(effective_embedding_model())
# llama-server backend only. F16 over Q8_0: faster (no per-block dequant for this

View file

@ -37,6 +37,13 @@ from hub.services.models.common import (
_runtime_for_format,
)
# Imported at module scope (not inside the per-repo scan loop) so a broken
# import surfaces at startup instead of silently emptying the inventory: the
# scan loop swallows per-repo exceptions and would drop every repo. Lives under
# ``utils`` (not ``utils.models``) to avoid the eager model-config/checkpoint
# imports in ``utils/models/__init__.py``.
from utils.hidden_models import is_hidden_model
logger = get_logger(__name__)
_repo_size_cache: "OrderedDict[tuple[str, str, str], tuple[int, frozenset[str], float]]" = (
@ -243,6 +250,13 @@ def invalidate_hf_cache_scans() -> None:
hf_cache_scan.invalidate_hf_cache_scans()
def _is_hidden_infra_repo(*values: str | None) -> bool:
"""True for infra-only repos (the RAG embedder and the llama.cpp install
validation probe) that are cached as a side effect of Studio itself and are
not usable chat models."""
return is_hidden_model(*values)
def _scan_cached_gguf() -> list[dict]:
"""Synchronous HF-cache disk walk for GGUF repos; runs in a worker thread."""
cache_scans = all_hf_cache_scans()
@ -254,13 +268,24 @@ def _scan_cached_gguf() -> list[dict]:
if str(repo_info.repo_type) != "model":
continue
repo_id = repo_info.repo_id
repo_path = Path(repo_info.repo_path)
snapshot_path = _cached_model_snapshot_path(repo_path)
total_size = _repo_gguf_size_bytes(repo_info)
has_variant_state, variant_state_size = _gguf_variant_state_summary(repo_id)
is_hidden_infra = _is_hidden_infra_repo(
repo_id,
str(repo_path),
str(snapshot_path) if snapshot_path is not None else None,
)
# Hide infra repos unless the user downloaded a variant via
# the Hub; variant state only exists for user downloads.
if is_hidden_infra and not has_variant_state:
continue
if total_size == 0 and not has_variant_state:
continue
partial = hf_cache_scan.is_gguf_repo_partial(
repo_id,
Path(repo_info.repo_path),
repo_path,
)
if total_size == 0 and not partial:
continue
@ -283,6 +308,9 @@ def _scan_cached_gguf() -> list[dict]:
requires_variant = True,
)
)
# Visible infra variants remain management-only.
if is_hidden_infra:
row["capabilities"]["can_chat"] = False
if _prefer_cache_row(row, existing):
seen_lower[key] = row
except Exception as e:
@ -475,6 +503,15 @@ def _scan_cached_models() -> list[dict]:
if str(repo_info.repo_type) != "model":
continue
repo_id = repo_info.repo_id
repo_path = Path(repo_info.repo_path)
snapshot_path = _cached_model_snapshot_path(repo_path)
# The non-GGUF embedder has no variant downloads; always hide.
if _is_hidden_infra_repo(
repo_id,
str(repo_path),
str(snapshot_path) if snapshot_path is not None else None,
):
continue
has_main_gguf = _repo_has_gguf_files(repo_info)
payload = _repo_non_gguf_model_payload(repo_info)
if payload.size_bytes == 0:
@ -486,7 +523,6 @@ def _scan_cached_models() -> list[dict]:
continue
key = repo_id.lower()
existing = seen_lower.get(key)
repo_path = Path(repo_info.repo_path)
snapshot_partial = hf_cache_scan.is_snapshot_partial(
"model",
repo_id,

View file

@ -36,6 +36,7 @@ from hub.utils.paths import (
)
from hub.services.models import common as model_common
from hub.services.models.ollama import scan_ollama_dir
from utils.hidden_models import is_hidden_model
logger = get_logger(__name__)
_MAX_MODELS_PER_CUSTOM_FOLDER = 200
@ -623,6 +624,20 @@ def _dedupe_local_models(local_models: List[LocalModelInfo]) -> list[LocalModelI
)
def _filter_hidden_models(local_models: List[LocalModelInfo]) -> list[LocalModelInfo]:
"""Remove infrastructure-only models from the shared local inventory."""
visible: list[LocalModelInfo] = []
for model in local_models:
resolved_cache_path = (
hf_cache_scan.resolve_hf_cache_realpath(Path(model.path))
if model.source == "hf_cache"
else None
)
if not is_hidden_model(model.id, model.model_id, model.path, resolved_cache_path):
visible.append(model)
return visible
async def list_local_models_response(models_dir: str = "./models") -> LocalModelListResponse:
"""List local model candidates from every supported on-device source."""
hf_cache_dir = _resolve_hf_cache_dir()
@ -653,7 +668,7 @@ async def list_local_models_response(models_dir: str = "./models") -> LocalModel
ollama_dirs,
)
local_models += await _collect_models_from_custom_folders()
models = _dedupe_local_models(local_models)
models = _dedupe_local_models(_filter_hidden_models(local_models))
return LocalModelListResponse(
models_dir = str(models_root),

View file

@ -439,6 +439,287 @@ def test_cached_gguf_scan_includes_variant_state_without_completed_gguf(monkeypa
assert row["capabilities"]["requires_variant"] is True
def test_cached_gguf_scan_hides_infra_repos_without_user_downloads(monkeypatch, tmp_path):
probe = _repo(
"ggml-org/models",
[_file("tinyllamas/stories260K.gguf", 1_200_000)],
tmp_path / "probe",
)
embedder = _repo(
"unsloth/bge-small-en-v1.5-GGUF",
[_file("bge-small-en-v1.5-f16.gguf", 60_000_000)],
tmp_path / "embedder",
)
chat = _repo("Org/Chat-GGUF", [_file("Q4_K_M.gguf", 100)], tmp_path / "chat")
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
lambda: [SimpleNamespace(repos = [probe, embedder, chat])],
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_gguf_repo_partial",
lambda _repo_id, _path: False,
)
result = {"cached": cache_inventory._scan_cached_gguf()}
assert [row["repo_id"] for row in result["cached"]] == ["Org/Chat-GGUF"]
def test_cached_gguf_scan_keeps_infra_repo_with_user_downloaded_variant(monkeypatch, tmp_path):
monkeypatch.setattr(state_dir, "cache_root", lambda: tmp_path / "state")
embedder = _repo(
"unsloth/bge-small-en-v1.5-GGUF",
[
_file("bge-small-en-v1.5-f16.gguf", 60_000_000),
_file("bge-small-en-v1.5-Q8_0.gguf", 35_000_000),
],
tmp_path / "embedder",
)
# Variant manifests only exist for user Hub downloads, not auto-downloads.
assert download_manifest.write_manifest(
"model",
"unsloth/bge-small-en-v1.5-GGUF",
"Q8_0",
[download_manifest.ExpectedFile(path = "bge-small-en-v1.5-Q8_0.gguf", size = 35_000_000)],
"http",
)
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
lambda: [SimpleNamespace(repos = [embedder])],
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_gguf_repo_partial",
lambda _repo_id, _path: False,
)
result = {"cached": cache_inventory._scan_cached_gguf()}
assert [row["repo_id"] for row in result["cached"]] == ["unsloth/bge-small-en-v1.5-GGUF"]
assert result["cached"][0]["capabilities"]["can_chat"] is False
def test_cached_models_scan_hides_non_gguf_embedder(monkeypatch, tmp_path):
embedder_path = tmp_path / "hub" / "models--unsloth--bge-small-en-v1.5"
embedder_path.mkdir(parents = True)
embedder = _repo(
"unsloth/bge-small-en-v1.5",
[_file("config.json", 12), _file("model.safetensors", 130_000_000)],
embedder_path,
)
chat_path = tmp_path / "hub" / "models--Org--Chat"
chat_path.mkdir(parents = True)
chat = _repo(
"Org/Chat",
[_file("config.json", 12), _file("model.safetensors", 100)],
chat_path,
)
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
lambda: [SimpleNamespace(repos = [embedder, chat])],
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_snapshot_partial",
lambda _kind, _repo_id, _path: False,
)
result = {"cached": cache_inventory._scan_cached_models()}
assert [row["repo_id"] for row in result["cached"]] == ["Org/Chat"]
def test_cached_scans_hide_embedders_configured_by_cache_path(monkeypatch, tmp_path):
from core.rag import config as rag_config
gguf_path = tmp_path / "hub" / "models--Org--PathEmbedder-GGUF"
gguf_path.mkdir(parents = True)
gguf = _repo(
"Org/PathEmbedder-GGUF",
[_file("model-F16.gguf", 60_000_000)],
gguf_path,
)
model_path = tmp_path / "hub" / "models--Org--PathEmbedder"
model_path.mkdir(parents = True)
model = _repo(
"Org/PathEmbedder",
[_file("config.json", 12), _file("model.safetensors", 130_000_000)],
model_path,
)
monkeypatch.setattr(
rag_config,
"effective_embedding_model",
lambda: str(model_path),
)
monkeypatch.setattr(
rag_config,
"effective_gguf_repo",
lambda: str(gguf_path),
)
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
lambda: [SimpleNamespace(repos = [gguf, model])],
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_gguf_repo_partial",
lambda _repo_id, _path: False,
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_snapshot_partial",
lambda _kind, _repo_id, _path: False,
)
assert cache_inventory._scan_cached_gguf() == []
assert cache_inventory._scan_cached_models() == []
def test_cached_scans_hide_embedders_configured_by_snapshot_path(monkeypatch, tmp_path):
from core.rag import config as rag_config
gguf_path = tmp_path / "hub" / "models--Org--SnapshotEmbedder-GGUF"
gguf_snapshot = gguf_path / "snapshots" / "gguf-revision"
gguf_snapshot.mkdir(parents = True)
gguf = _repo(
"Org/SnapshotEmbedder-GGUF",
[_file("model-F16.gguf", 60_000_000)],
gguf_path,
)
model_path = tmp_path / "hub" / "models--Org--SnapshotEmbedder"
model_snapshot = model_path / "snapshots" / "model-revision"
model_snapshot.mkdir(parents = True)
model = _repo(
"Org/SnapshotEmbedder",
[_file("config.json", 12), _file("model.safetensors", 130_000_000)],
model_path,
)
monkeypatch.setattr(
rag_config,
"effective_embedding_model",
lambda: str(model_snapshot),
)
monkeypatch.setattr(
rag_config,
"effective_gguf_repo",
lambda: str(gguf_snapshot),
)
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
lambda: [SimpleNamespace(repos = [gguf, model])],
)
def _resolve_snapshot(repo_path):
return str(
{
gguf_path: gguf_snapshot,
model_path: model_snapshot,
}.get(Path(repo_path), Path(repo_path))
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"resolve_hf_cache_realpath",
_resolve_snapshot,
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_gguf_repo_partial",
lambda _repo_id, _path: False,
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_snapshot_partial",
lambda _kind, _repo_id, _path: False,
)
assert cache_inventory._scan_cached_gguf() == []
assert cache_inventory._scan_cached_models() == []
def test_cached_models_scan_keeps_unrelated_repo_with_custom_generic_embedder(
monkeypatch, tmp_path
):
# A custom embedder with a generic basename ("org/model") must be hidden by
# EXACT repo-id match only. An unrelated cached chat model whose id merely
# contains "model" (e.g. "user/model-chat") must stay on device: substring
# basename matching used to drop real chat models from the inventory.
from core.rag import config as rag_config
monkeypatch.setattr(rag_config, "effective_embedding_model", lambda: "org/model")
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/model-GGUF")
def _model_repo(repo_id: str):
path = tmp_path / "hub" / f"models--{repo_id.replace('/', '--')}"
path.mkdir(parents = True)
return _repo(
repo_id,
[_file("config.json", 12), _file("model.safetensors", 100)],
path,
)
embedder = _model_repo("org/model")
chat = _model_repo("user/model-chat")
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
lambda: [SimpleNamespace(repos = [embedder, chat])],
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_snapshot_partial",
lambda _kind, _repo_id, _path: False,
)
result = {"cached": cache_inventory._scan_cached_models()}
assert [row["repo_id"] for row in result["cached"]] == ["user/model-chat"]
def test_cached_scans_hide_stale_default_embedder_after_custom_setting(monkeypatch, tmp_path):
from core.rag import config as rag_config
monkeypatch.setattr(rag_config, "effective_embedding_model", lambda: "org/custom")
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/custom-GGUF")
gguf = _repo(
"unsloth/bge-small-en-v1.5-GGUF",
[_file("bge-small-en-v1.5-f16.gguf", 60_000_000)],
tmp_path / "default-gguf",
)
weights_path = tmp_path / "hub" / "models--unsloth--bge-small-en-v1.5"
weights_path.mkdir(parents = True)
weights = _repo(
"unsloth/bge-small-en-v1.5",
[_file("config.json", 12), _file("model.safetensors", 130_000_000)],
weights_path,
)
monkeypatch.setattr(
cache_inventory,
"all_hf_cache_scans",
lambda: [SimpleNamespace(repos = [gguf, weights])],
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_gguf_repo_partial",
lambda _repo_id, _path: False,
)
monkeypatch.setattr(
cache_inventory.hf_cache_scan,
"is_snapshot_partial",
lambda _kind, _repo_id, _path: False,
)
assert cache_inventory._scan_cached_gguf() == []
assert cache_inventory._scan_cached_models() == []
def test_gguf_variant_requirements_include_split_files_and_preferred_mmproj():
requirements = gguf_variants._build_gguf_variant_requirements(
[
@ -1610,6 +1891,63 @@ def test_hf_cache_scan_uses_gguf_partial_row_for_variant_state(monkeypatch, tmp_
assert rows[0].capabilities.requires_variant is True
def test_local_inventory_filters_custom_embedder_hf_cache_row(monkeypatch, tmp_path):
from core.rag import config as rag_config
monkeypatch.setattr(rag_config, "effective_embedding_model", lambda: "org/embedder")
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/embedder-GGUF")
def _row(repo_id: str):
repo_path = tmp_path / f"models--{repo_id.replace('/', '--')}"
return model_common._local_model_info(
scan_path = repo_path,
load_path = repo_path,
source = "hf_cache",
model_format = "safetensors",
model_id = repo_id,
)
rows = local_inventory._filter_hidden_models([_row("org/embedder"), _row("org/chat-model")])
assert [row.model_id for row in rows] == ["org/chat-model"]
def test_local_inventory_filters_embedder_configured_by_snapshot_path(monkeypatch, tmp_path):
from core.rag import config as rag_config
embedder_path = tmp_path / "hub" / "models--org--embedder"
embedder_snapshot = embedder_path / "snapshots" / "revision"
embedder_snapshot.mkdir(parents = True)
chat_path = tmp_path / "hub" / "models--org--chat-model"
chat_path.mkdir(parents = True)
monkeypatch.setattr(
rag_config,
"effective_embedding_model",
lambda: str(embedder_snapshot),
)
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/embedder-GGUF")
monkeypatch.setattr(
local_inventory.hf_cache_scan,
"resolve_hf_cache_realpath",
lambda path: str(embedder_snapshot) if Path(path) == embedder_path else str(path),
)
def _row(repo_id: str, repo_path: Path):
return model_common._local_model_info(
scan_path = repo_path,
load_path = repo_path,
source = "hf_cache",
model_format = "safetensors",
model_id = repo_id,
)
rows = local_inventory._filter_hidden_models(
[_row("org/embedder", embedder_path), _row("org/chat-model", chat_path)]
)
assert [row.model_id for row in rows] == ["org/chat-model"]
def test_model_download_job_helpers_preserve_idle_shape():
key = downloads._download_job_key("Org/Model", None)
status = downloads._job_status(key)

View file

@ -59,59 +59,12 @@ def _safe_is_dir(path) -> bool:
return False
# 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.\-]+$")
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) 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. A local-path embedder is matched by exact resolved
path only: a generic basename like "model" must not substring-hide
unrelated chat models."""
from core.rag import config as rag_config
needles = [
# 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",
]
exact_paths: list[str] = []
for model in (
rag_config.effective_embedding_model(),
rag_config.effective_gguf_repo(),
):
if _HF_REPO_ID_RE.match(model):
needles.append(model.split("/")[-1].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 any(n in low for n in needles):
return True
if exact_paths:
resolved = _safe_resolve(Path(v).expanduser())
if resolved and resolved.lower() in exact_paths:
return True
return False
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
# Shared with the hub inventory scans; keep the private aliases so existing
# importers (core.inference.local_model_resolver, tests) stay valid.
from utils.hidden_models import (
_safe_resolve,
is_hidden_model as _is_hidden_model,
)
backend_path = Path(__file__).parent.parent.parent
@ -853,7 +806,7 @@ def collect_local_models(models_root: Path) -> List[LocalModelInfo]:
key = lambda item: (item.updated_at or 0),
reverse = True,
)
return [m for m in models if not _is_hidden_model(m.id, m.path)]
return [m for m in models if not _is_hidden_model(m.id, m.model_id, m.path)]
@router.get("/local", response_model = LocalModelListResponse)

View file

@ -10,6 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator
from auth.authentication import get_current_subject
from auth.storage import rotate_preview_link_secret
from core.rag.config import default_gguf_repo, effective_gguf_repo
from loggers import get_logger
from utils.utils import safe_error_detail, log_and_http_error
from utils.personalization_settings import (
@ -263,14 +264,18 @@ class EmbeddingModelPayload(BaseModel):
class EmbeddingModelResponse(BaseModel):
embedding_model: str
embedding_gguf_repo: str
default_embedding_model: str
default_embedding_gguf_repo: str
is_custom: bool
def _embedding_model_response() -> EmbeddingModelResponse:
return EmbeddingModelResponse(
embedding_model = get_rag_embedding_model(),
embedding_gguf_repo = effective_gguf_repo(),
default_embedding_model = default_embedding_model(),
default_embedding_gguf_repo = default_gguf_repo(),
is_custom = get_stored_embedding_model() is not None,
)

View file

@ -120,12 +120,151 @@ def test_is_hidden_model_hides_validation_probe_everywhere():
assert models_route._is_hidden_model(
None, "/hf/models--ggml-org--models/snapshots/abc/tinyllamas/stories260K.gguf"
)
# A Windows-style snapshot path must match too, even on a POSIX interpreter
# (the filename check splits on both separators).
assert models_route._is_hidden_model(
r"C:\Users\u\.cache\huggingface\hub\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_is_hidden_model_matches_repo_ids_exactly(monkeypatch):
"""A custom embedder with a generic basename is hidden by EXACT repo-id
match only, so unrelated cached repos that merely contain the basename stay
visible. Regression: substring basename matching hid real chat models like
``user/model-chat`` from the On Device inventory."""
from core.rag import config as rag_config
monkeypatch.setattr(rag_config, "effective_embedding_model", lambda: "org/model")
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/model-GGUF")
# The exact embedder repo and its GGUF companion are hidden.
assert models_route._is_hidden_model("org/model")
assert models_route._is_hidden_model("org/model-GGUF")
# Unrelated repos that merely contain "model" must NOT be hidden.
assert not models_route._is_hidden_model("user/model-chat")
assert not models_route._is_hidden_model("org/model-instruct")
assert not models_route._is_hidden_model("acme/remodelled-chat")
# The validation probe stays hidden regardless of embedder config.
assert models_route._is_hidden_model("ggml-org/models")
def test_is_hidden_model_matches_repo_derived_local_paths(monkeypatch):
"""Match exact repo-derived cache and LM Studio paths."""
from core.rag import config as rag_config
monkeypatch.setattr(rag_config, "effective_embedding_model", lambda: "org/model")
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/model-GGUF")
assert models_route._is_hidden_model(
"/cache/models--org--model/snapshots/abc/model.safetensors"
)
assert models_route._is_hidden_model(
r"C:\Users\u\.cache\huggingface\hub\models--org--model-GGUF\snapshots\abc"
)
assert models_route._is_hidden_model("/lm-studio/org/model-GGUF/model-Q8_0.gguf")
assert not models_route._is_hidden_model("/lm-studio/user/model-chat/model-Q8_0.gguf")
assert not models_route._is_hidden_model("/cache/models--org--model-instruct")
def test_is_hidden_model_prefers_existing_relative_path(monkeypatch, tmp_path):
"""Prefer an existing relative path over repo-id syntax."""
from core.rag import config as rag_config
embedder = tmp_path / "models" / "embedder"
embedder.mkdir(parents = True)
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(rag_config, "effective_embedding_model", lambda: "models/embedder")
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/embedder-GGUF")
assert models_route._is_hidden_model(str(embedder))
def test_is_hidden_model_keeps_stale_default_embedder_hidden(monkeypatch):
"""Keep default embedders hidden after a settings change."""
from core.rag import config as rag_config
monkeypatch.setattr(rag_config, "effective_embedding_model", lambda: "org/custom")
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/custom-GGUF")
assert models_route._is_hidden_model("unsloth/bge-small-en-v1.5")
assert models_route._is_hidden_model("unsloth/bge-small-en-v1.5-GGUF")
assert models_route._is_hidden_model("/models/bge-small-en-v1.5")
assert models_route._is_hidden_model("/models/bge-small-en-v1.5-F16.gguf")
assert models_route._is_hidden_model(r"C:\models\bge-small-en-v1.5-Q8_0.gguf")
# Repo IDs still use exact matching, and similar local basenames must have
# a real separator after the static default name.
assert not models_route._is_hidden_model("user/bge-small-en-v1.5-chat")
assert not models_route._is_hidden_model("/models/bge-small-en-v1.50")
def test_is_hidden_model_keeps_env_default_hidden_after_override(monkeypatch):
"""A persisted override must not expose the deployment's env default."""
from core.rag import config as rag_config
monkeypatch.delenv("RAG_EMBED_GGUF_REPO", raising = False)
monkeypatch.setattr(rag_config, "EMBEDDING_MODEL", "org/env-default")
monkeypatch.setattr(rag_config, "effective_embedding_model", lambda: "org/custom")
monkeypatch.setattr(rag_config, "effective_gguf_repo", lambda: "org/custom-GGUF")
assert models_route._is_hidden_model("org/env-default")
assert models_route._is_hidden_model("org/env-default-GGUF")
assert models_route._is_hidden_model("org/custom")
assert models_route._is_hidden_model("org/custom-GGUF")
assert not models_route._is_hidden_model("org/env-default-chat")
def test_hidden_models_importable_without_heavy_model_stack():
"""The hub cache scanner imports ``is_hidden_model`` at module scope, so it
must not drag in ``utils/models/__init__`` (the model-config + checkpoint
stack). Verify in a clean interpreter that importing the helper touches
neither ``utils.models`` nor those heavy submodules, and still classifies
the probe."""
import os
import subprocess
import textwrap
backend = Path(__file__).resolve().parents[1]
code = textwrap.dedent(
"""
import sys
class _Blocker:
_blocked = (
"utils.models",
"utils.models.model_config",
"utils.models.checkpoints",
)
def find_spec(self, name, path=None, target=None):
if name in self._blocked:
raise ImportError("blocked heavy import: " + name)
return None
sys.meta_path.insert(0, _Blocker())
from utils.hidden_models import is_hidden_model
loaded = sorted(m for m in sys.modules if m.startswith("utils.models"))
assert not loaded, loaded
assert is_hidden_model("ggml-org/models") is True
assert is_hidden_model("unsloth/gemma-3-270m-it-GGUF") is False
print("HIDDEN_MODELS_IMPORT_OK")
"""
)
env = dict(os.environ, PYTHONPATH = str(backend))
proc = subprocess.run(
[sys.executable, "-c", code],
capture_output = True,
text = True,
env = env,
)
assert proc.returncode == 0, proc.stderr
assert "HIDDEN_MODELS_IMPORT_OK" in proc.stdout
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.

View file

@ -52,6 +52,16 @@ def client(monkeypatch):
monkeypatch.setattr(settings, "_resolves_as_local_gguf", lambda m: False)
monkeypatch.setattr(settings, "get_rag_embedding_model", lambda: saved.get("model", ""))
monkeypatch.setattr(settings, "get_stored_embedding_model", lambda: saved.get("model"))
monkeypatch.setattr(
settings,
"effective_gguf_repo",
lambda: f"{saved.get('model', 'unsloth/default-embed')}-GGUF",
)
monkeypatch.setattr(
settings,
"default_gguf_repo",
lambda: "unsloth/default-embed-GGUF",
)
app = FastAPI()
app.include_router(settings.router)
@ -257,6 +267,13 @@ def test_clean_repo_saves_under_force(client, monkeypatch):
r = c.put("/embedding-model", json = {"embedding_model": "acme/clean-embed", "force": True})
assert r.status_code == 200
assert saved.get("model") == "acme/clean-embed"
assert r.json() == {
"embedding_model": "acme/clean-embed",
"embedding_gguf_repo": "acme/clean-embed-GGUF",
"default_embedding_model": "unsloth/default-embed",
"default_embedding_gguf_repo": "unsloth/default-embed-GGUF",
"is_custom": True,
}
def test_load_sink_refuses_flagged_model(monkeypatch):

View file

@ -53,3 +53,10 @@ def test_custom_model_overrides_default_and_derives_gguf(settings_store, monkeyp
assert ems.reset_rag_embedding_model() == rag_config.EMBEDDING_MODEL
assert ems.get_stored_embedding_model() is None
def test_env_default_derives_its_gguf_companion(monkeypatch):
monkeypatch.delenv("RAG_EMBED_GGUF_REPO", raising = False)
monkeypatch.setattr(rag_config, "EMBEDDING_MODEL", "org/env-default-embedder")
assert rag_config.default_gguf_repo() == "org/env-default-embedder-GGUF"

View file

@ -697,6 +697,10 @@ def test_index_excludes_hidden_models(tmp_path, monkeypatch):
normal.write_bytes(b"x" * 32)
probe = tmp_path / "stories260K.gguf" # llama.cpp install-validation probe
probe.write_bytes(b"x" * 32)
embedder = tmp_path / "embedding-Q8_0.gguf"
embedder.write_bytes(b"x" * 32)
local_default_embedder = tmp_path / "bge-small-en-v1.5-F16.gguf"
local_default_embedder.write_bytes(b"x" * 32)
def _info(mid, path):
return SimpleNamespace(id = mid, path = str(path), model_id = mid, display_name = mid)
@ -704,7 +708,22 @@ def test_index_excludes_hidden_models(tmp_path, monkeypatch):
monkeypatch.setattr(
models_route,
"_scan_models_dir",
lambda *a, **k: [_info("org/Normal-GGUF", normal), _info("ggml-org/models", probe)],
lambda *a, **k: [
_info("org/Normal-GGUF", normal),
_info("ggml-org/models", probe),
SimpleNamespace(
id = str(embedder),
path = str(embedder),
model_id = "unsloth/bge-small-en-v1.5-GGUF",
display_name = "embedding-Q8_0",
),
SimpleNamespace(
id = str(local_default_embedder),
path = str(local_default_embedder),
model_id = None,
display_name = local_default_embedder.name,
),
],
)
monkeypatch.setattr(models_route, "_scan_hf_cache", lambda *a, **k: [])
monkeypatch.setattr(models_route, "_resolve_hf_cache_dir", lambda: tmp_path)
@ -713,6 +732,8 @@ def test_index_excludes_hidden_models(tmp_path, monkeypatch):
index = resolver._index()
assert "org/normal-gguf" in index # keys are normalized to lowercase
assert "ggml-org/models" not in index
assert "unsloth/bge-small-en-v1.5-gguf" not in index
assert str(local_default_embedder).lower() not in index
# And the hidden probe cannot be auto-switched to by name.
resolver._scan = (0.0, {})
assert resolver.resolve_local_gguf("ggml-org/models") is None
@ -1729,6 +1750,8 @@ def test_index_advertises_alias_not_filesystem_path(tmp_path, monkeypatch):
# host path in /v1/models, yet the model stays resolvable by that path too.
from types import SimpleNamespace
import routes.models as models_route
from storage import studio_db
import utils.paths as paths
gguf = tmp_path / "model-Q4_K_M.gguf"
gguf.write_bytes(b"x" * 32)
@ -1742,6 +1765,8 @@ def test_index_advertises_alias_not_filesystem_path(tmp_path, monkeypatch):
monkeypatch.setattr(models_route, "_scan_hf_cache", lambda *a, **k: [])
monkeypatch.setattr(models_route, "_resolve_hf_cache_dir", lambda: tmp_path)
monkeypatch.setattr(models_route, "_is_hidden_model", lambda *a, **k: False)
monkeypatch.setattr(paths, "lmstudio_model_dirs", lambda: [])
monkeypatch.setattr(studio_db, "list_scan_folders", lambda: [])
resolver._scan = (0.0, {})
# The advertised id is the alias, never the absolute path.

View file

@ -0,0 +1,142 @@
# 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

View file

@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { loadEmbeddingModelSettings } from "@/features/settings";
import { useEffect, useState } from "react";
import { useInventoryVersion } from "../stores/inventory-events";
/** Backend-resolved embedding repos that optimistic inventory rows must hide. */
export function useHiddenEmbeddingModelIds(
enabled: boolean,
): ReadonlySet<string> {
const inventoryVersion = useInventoryVersion();
const [hiddenIds, setHiddenIds] = useState<ReadonlySet<string>>(
() => new Set(),
);
// biome-ignore lint/correctness/useExhaustiveDependencies: inventory invalidation must reload backend-resolved embedder ids
useEffect(() => {
if (!enabled) {
return;
}
let cancelled = false;
loadEmbeddingModelSettings()
.then((settings) => {
if (cancelled) {
return;
}
setHiddenIds(
new Set(
[
settings.embeddingModel,
settings.embeddingGgufRepo,
settings.defaultEmbeddingModel,
settings.defaultEmbeddingGgufRepo,
].map((value) => value.trim().toLowerCase()),
),
);
})
.catch(() => undefined);
return () => {
cancelled = true;
};
}, [enabled, inventoryVersion]);
return hiddenIds;
}

View file

@ -63,6 +63,7 @@ import { useDiscoverSearch } from "./hooks/use-discover-search";
import { useFeedWriteBack } from "./hooks/use-feed-write-back";
import { useHubFeed } from "./hooks/use-hub-feed";
import { useHubModelVram } from "./hooks/use-hub-model-vram";
import { useHiddenEmbeddingModelIds } from "./hooks/use-hidden-embedding-models";
import { useModelsSelection } from "./hooks/use-models-selection";
import {
CHANNEL_TO_SECTION,
@ -73,7 +74,10 @@ import {
SECTION_TO_CHANNEL,
findChannel,
} from "./lib/channels";
import { isHiddenModelId } from "./lib/hidden-models";
import {
isConfiguredHiddenModelId,
isHiddenModelId,
} from "./lib/hidden-models";
import { inventoryRowMatches, tokenizeQuery } from "./lib/inventory-search";
import { resolveOwnerProviderLogo } from "./lib/provider-logos";
import {
@ -386,6 +390,7 @@ export function ModelsPage() {
useState<ModelFormatFilter>("all");
const isDiscoverTab = tab === "discover";
const isDatasetMode = resourceType === "datasets";
const hiddenEmbeddingModelIds = useHiddenEmbeddingModelIds(!isDatasetMode);
const urlSection = hubSearch.section ?? null;
const isModelDiscover = isDiscoverTab && !isDatasetMode;
const sectionChannelId: ChannelId | null = urlSection
@ -700,6 +705,7 @@ export function ModelsPage() {
return discoverRows.filter(
(row) =>
!isHiddenModelId(row.id) &&
!isConfiguredHiddenModelId(hiddenEmbeddingModelIds, row.id) &&
// The default feed only shows models with a provider logo.
(!isFeedMode ||
resolveOwnerProviderLogo(row.owner, row.repo) !== null) &&
@ -714,6 +720,7 @@ export function ModelsPage() {
);
}, [
discoverRows,
hiddenEmbeddingModelIds,
isDatasetMode,
isFeedMode,
effectiveDiscoverFormat,
@ -739,7 +746,11 @@ export function ModelsPage() {
effectiveCachedRows,
effectiveLocalRows,
)
.filter((row) => !isHiddenModelId(row.id))
.filter(
(row) =>
!isHiddenModelId(row.id) &&
!isConfiguredHiddenModelId(hiddenEmbeddingModelIds, row.id),
)
.filter((row) => matchesFormat(row.result.isGguf, "gguf"))
// Same fit filter as the main Discover list, so the feed carousel
// honors the toggle too.
@ -751,6 +762,7 @@ export function ModelsPage() {
),
[
hubFeed.trending.results,
hiddenEmbeddingModelIds,
modelDiscoveryInventorySignature,
fitOnDeviceOnly,
gpu,
@ -778,22 +790,29 @@ export function ModelsPage() {
() => (isDiscoverTab ? [] : tokenizeQuery(deferredDebouncedQuery)),
[isDiscoverTab, deferredDebouncedQuery],
);
// Hide infra models (e.g. the RAG embedder bge-small-en-v1.5) from the On
// Device list like Discover, but reveal a row when a query matches it so the
// user can confirm it is already downloaded.
// Server cache rows already apply variant-aware infra hiding. Optimistic
// rows are not server-confirmed, so apply the client filter first.
const isVisibleInventoryRow = useCallback(
(row: CachedInventoryRow | LocalInventoryRow) =>
// Local rows can have a null repoId and an id that is a hash rather than
// the file path/name, so also check path/title (the backend's
// _is_hidden_model checks the on-disk path for the same reason).
!isHiddenModelId(
row.id,
row.repoId,
row.kind !== "cache" ? row.path : undefined,
row.kind !== "cache" ? row.title : undefined,
) ||
(inventoryTokens.length > 0 && inventoryRowMatches(row, inventoryTokens)),
[inventoryTokens],
(row: CachedInventoryRow | LocalInventoryRow) => {
if (row.kind === "cache") {
return (
!row.optimistic ||
(!isHiddenModelId(row.id, row.repoId, row.cachePath) &&
!isConfiguredHiddenModelId(
hiddenEmbeddingModelIds,
row.id,
row.repoId,
row.cachePath,
))
);
}
// Local rows may lack a repo id, so also check path and title.
return (
!isHiddenModelId(row.id, row.repoId, row.path, row.title) ||
(inventoryTokens.length > 0 && inventoryRowMatches(row, inventoryTokens))
);
},
[hiddenEmbeddingModelIds, inventoryTokens],
);
// Format filter is a deliberate scope narrowing, so hard-filter it out. The
// text query instead drives dim-not-filter on On Device (see ModelsCatalog) so

View file

@ -2,6 +2,7 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
export { cancelStagedModelDownload } from "./download-manager";
export { bumpInventoryVersion } from "./stores/inventory-events";
export {
getHfToken,
mirrorHfTokenInto,

View file

@ -12,6 +12,7 @@ export type InventoryHintRow = {
repo_id: string;
size_bytes: number;
partial?: boolean;
optimistic?: boolean;
};
export type InventoryHintReconciliation = {
@ -41,6 +42,7 @@ function optimisticRow(hint: InventoryHint): InventoryHintRow {
repo_id: hint.repoId,
size_bytes: hint.bytes ?? 0,
partial: false,
optimistic: true,
};
}
@ -101,9 +103,14 @@ function mergeInventoryHint(
if (idx === -1) {
return [...rows, seed];
}
const serverRow = rows[idx];
const merged = {
...rows[idx],
...seed,
...serverRow,
// A completed hint may arrive before a partial server scan catches up. In
// that case keep the synthetic row non-runnable. A complete server row is
// already authoritative even when its runnable-weight size is smaller than
// the hint's full-snapshot byte count, so do not mark that merge optimistic.
...(serverRow.partial ? seed : { optimistic: false }),
size_bytes: Math.max(rowSizeBytes(rows[idx]), rowSizeBytes(seed)),
};
return [...rows.slice(0, idx), merged, ...rows.slice(idx + 1)];

View file

@ -54,6 +54,7 @@ export interface CachedInventoryRow {
libraryName?: string | null;
quantMethod?: string | null;
liveDownload?: boolean;
optimistic?: boolean;
}
export interface LocalInventoryRow {

View file

@ -204,6 +204,7 @@ function liveDownloadInventoryRows(
size_bytes: job.displayBytes,
partial: true,
partial_transport: null,
optimistic: true,
},
modelFormat,
),

View file

@ -176,6 +176,7 @@ export function buildCachedInventoryRow(
runtime?: string | null;
format_variant?: string | null;
capabilities?: BackendModelCapabilities | null;
optimistic?: boolean;
},
fallbackFormat: ModelInventoryFormat,
): CachedInventoryRow {
@ -185,6 +186,15 @@ export function buildCachedInventoryRow(
const inferredFromEndpoint =
rawModelFormat === "unknown" && modelFormat !== "unknown";
const requiresVariant = modelFormat === "gguf";
const capabilities = normalizeCapabilities(
inferredFromEndpoint ? null : row.capabilities,
modelFormat,
row.partial ?? false,
requiresVariant,
);
if (row.optimistic) {
capabilities.canChat = false;
}
return {
kind: "cache",
id:
@ -202,12 +212,7 @@ export function buildCachedInventoryRow(
modelFormat,
),
formatVariant: row.format_variant ?? null,
capabilities: normalizeCapabilities(
inferredFromEndpoint ? null : row.capabilities,
modelFormat,
row.partial ?? false,
requiresVariant,
),
capabilities,
bytes: row.size_bytes,
cachePath: row.cache_path ?? null,
partial: row.partial ?? false,
@ -216,6 +221,7 @@ export function buildCachedInventoryRow(
tags: row.tags,
libraryName: row.library_name ?? null,
quantMethod: row.quant_method ?? null,
optimistic: row.optimistic,
};
}

View file

@ -1,11 +1,13 @@
// 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 models hidden from every browse/preview list (Hub discover and the chat
// model selector). Mirrors the backend `_is_hidden_model`: the RAG embedding
// model and the llama.cpp validation probe are not usable chat models. Per-repo
// file/download views are NOT filtered, so a reinstall still shows the model as
// already downloaded.
// Infra models hidden from browse/preview lists (Hub Discover, the chat model
// selector, and local on-device rows). Mirrors the backend
// `utils.hidden_models`: the RAG embedding model and the llama.cpp validation
// probe are not usable chat models. Server-confirmed cache rows are trusted
// because the backend applies variant-aware filtering. Optimistic cache rows
// still use these needles until the server confirms them. Per-repo views are
// not filtered, so reinstall flows still show downloaded files.
const HIDDEN_NEEDLES = [
"bge-small-en-v1.5", // RAG embedder: unsloth/bge-small-en-v1.5[-GGUF]
"ggml-org/models", // llama.cpp validation probe repo
@ -17,8 +19,20 @@ export function isHiddenModelId(
...values: (string | null | undefined)[]
): boolean {
return values.some((v) => {
if (!v) return false;
if (!v) {
return false;
}
const lower = v.toLowerCase();
return HIDDEN_NEEDLES.some((needle) => lower.includes(needle));
});
}
/** Exact-match configured infra repos without hiding similarly named models. */
export function isConfiguredHiddenModelId(
configuredIds: ReadonlySet<string>,
...values: (string | null | undefined)[]
): boolean {
return values.some(
(value) => value != null && configuredIds.has(value.trim().toLowerCase()),
);
}

View file

@ -2,11 +2,14 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { authFetch } from "@/features/auth";
import { bumpInventoryVersion } from "@/features/hub";
import { readFastApiError } from "@/lib/format-fastapi-error";
export type EmbeddingModelSettings = {
embeddingModel: string;
embeddingGgufRepo: string;
defaultEmbeddingModel: string;
defaultEmbeddingGgufRepo: string;
isCustom: boolean;
};
@ -14,8 +17,12 @@ type ApiEmbeddingModelSettings = {
// biome-ignore lint/style/useNamingConvention: API schema
embedding_model: string;
// biome-ignore lint/style/useNamingConvention: API schema
embedding_gguf_repo: string;
// biome-ignore lint/style/useNamingConvention: API schema
default_embedding_model: string;
// biome-ignore lint/style/useNamingConvention: API schema
default_embedding_gguf_repo: string;
// biome-ignore lint/style/useNamingConvention: API schema
is_custom: boolean;
};
@ -30,7 +37,9 @@ export class EmbeddingModelBlockedError extends Error {}
function fromApi(settings: ApiEmbeddingModelSettings): EmbeddingModelSettings {
return {
embeddingModel: settings.embedding_model,
embeddingGgufRepo: settings.embedding_gguf_repo,
defaultEmbeddingModel: settings.default_embedding_model,
defaultEmbeddingGgufRepo: settings.default_embedding_gguf_repo,
isCustom: settings.is_custom,
};
}
@ -75,7 +84,9 @@ export async function updateEmbeddingModelSettings(
await readFastApiError(res, "Failed to save embedding model"),
);
}
return fromApi(await res.json());
const settings = fromApi(await res.json());
bumpInventoryVersion();
return settings;
}
export async function resetEmbeddingModelSettings(): Promise<EmbeddingModelSettings> {
@ -87,5 +98,7 @@ export async function resetEmbeddingModelSettings(): Promise<EmbeddingModelSetti
await readFastApiError(res, "Failed to reset embedding model"),
);
}
return fromApi(await res.json());
const settings = fromApi(await res.json());
bumpInventoryVersion();
return settings;
}

View file

@ -2,6 +2,7 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
export { SettingsDialog } from "./settings-dialog";
export { loadEmbeddingModelSettings } from "./api/embedding-model";
export {
loadPersonalization,
savePersonalization,