Files
unslothai--unsloth/studio/backend/hub/services/models/common.py
T
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

614 lines
20 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
"""Shared model inventory helpers for the Hub service layer."""
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import List, Literal, Optional
from urllib.parse import quote
from hub.schemas.inventory import (
LocalModelCapabilities,
LocalModelInfo,
ModelFormat,
ModelRuntime,
)
from hub.utils.gguf import (
extract_quant_label,
is_gguf_filename as _is_gguf_filename,
is_mmproj_filename as _is_mmproj_filename,
is_mtp_drafter_path as _is_mtp_drafter_path,
)
from hub.utils.paths import is_valid_repo_id as _is_valid_repo_id
ModelType = Literal["text", "vision", "audio", "embeddings"]
LocalModelSource = Literal["models_dir", "hf_cache", "lmstudio", "ollama", "custom"]
def _safe_is_dir(path) -> bool:
# Py >= 3.12 propagates PermissionError (EACCES) from is_dir(); folder scans
# probe root-owned system dirs, so treat un-stat-able paths as not-a-dir.
try:
return Path(path).is_dir()
except OSError:
return False
_LOCAL_CHECKPOINT_EXTENSIONS = (
".bin",
".pt",
".pth",
".ckpt",
".h5",
".msgpack",
".npz",
)
_LOCAL_BASE_MODEL_PREFIXES = {
"checkpoint",
"checkpoints",
"export",
"exports",
"model",
"models",
"output",
"outputs",
"run",
"runs",
"train",
}
_HF_CACHE_MODEL_FILE_PROBE_LIMIT = 2000
def _is_model_directory(d: Path) -> bool:
"""True when *d* has a config plus real weights; excludes mmproj GGUFs and non-weight ``.bin`` files (``tokenizer.bin``) to avoid false positives."""
def _is_weight_file(f: Path) -> bool:
suffix = f.suffix.lower()
if suffix == ".safetensors":
return True
if suffix == ".gguf":
return "mmproj" not in f.name.lower() and not _is_mtp_drafter_path(f.name)
if suffix == ".bin":
name = f.name.lower()
return (
name.startswith("pytorch_model")
or name.startswith("model")
or name.startswith("adapter_model")
or name.startswith("consolidated")
)
return False
try:
has_config = (d / "config.json").exists() or (d / "adapter_config.json").exists()
if not has_config:
return False
return any(_is_weight_file(f) for f in d.iterdir() if f.is_file())
except OSError:
return False
def _local_inventory_id(
source: str,
model_format: ModelFormat,
semantic_id: str,
variant: Optional[str] = None,
) -> str:
parts = [
source,
model_format,
quote(semantic_id, safe = ""),
]
if variant:
parts.append(quote(variant, safe = ""))
return ":".join(parts)
def _runtime_for_format(model_format: ModelFormat) -> ModelRuntime:
if model_format == "gguf":
return "llama_cpp"
if model_format == "adapter":
return "adapter"
if model_format in {"safetensors", "checkpoint"}:
return "transformers"
return "unknown"
def _capabilities_for_format(
model_format: ModelFormat,
source: str,
*,
partial: bool = False,
requires_variant: bool = False,
) -> LocalModelCapabilities:
is_complete = not partial
can_chat = model_format in {"gguf", "safetensors", "adapter", "checkpoint"}
can_train = model_format in {"safetensors", "checkpoint"} and is_complete
return LocalModelCapabilities(
can_train = can_train,
can_chat = can_chat and is_complete,
can_delete = source == "hf_cache",
can_download = False,
requires_variant = requires_variant,
supports_lora = model_format in {"safetensors", "checkpoint"} and is_complete,
supports_vision = False,
)
def _prefer_complete_larger(
candidate_partial: bool,
candidate_size_bytes: int,
existing_partial: bool,
existing_size_bytes: int,
) -> bool:
if candidate_partial != existing_partial:
return not candidate_partial
return candidate_size_bytes > existing_size_bytes
def _gguf_variant_state_summary(repo_id: str) -> tuple[bool, int]:
"""Whether GGUF variant-scoped state exists and its expected size; a cancelled/in-progress variant may have only manifests/markers/`.incomplete` blobs, which inventory needs to avoid a generic fallback row."""
from hub.utils import download_manifest
variant_keys: set[str] = set()
size_by_variant: dict[str, int] = {}
for variant, _path in download_manifest.iter_variant_manifests(
"model",
repo_id,
):
key = variant.lower()
variant_keys.add(key)
manifest = download_manifest.read_manifest("model", repo_id, variant)
if manifest is None:
continue
size_by_variant[key] = max(
size_by_variant.get(key, 0),
sum(max(0, int(file.size or 0)) for file in manifest.expected_files),
)
for variant, _path in download_manifest.iter_variant_markers(
"model",
repo_id,
):
variant_keys.add(variant.lower())
return bool(variant_keys), sum(size_by_variant.values())
def _apply_format_aware_partial(
rows: List[LocalModelInfo],
*,
snapshot_partial: bool,
gguf_partial: bool,
snapshot_partial_transport: Optional[str] = None,
) -> List[LocalModelInfo]:
"""Rewrite each row's partial flag with format-aware predicates so a hybrid (gguf + safetensors) repo's broken format doesn't taint the clean one; capabilities are recomputed from the new flag."""
rewritten: List[LocalModelInfo] = []
for row in rows:
target = gguf_partial if row.model_format == "gguf" else snapshot_partial
if not target:
rewritten.append(row)
continue
# GGUF row-level transport is ambiguous (variants may differ); per-variant
# detail lives on GgufVariantDetail.partial_transport via the variants endpoint.
partial_transport = None if row.model_format == "gguf" else snapshot_partial_transport
rewritten.append(
row.model_copy(
update = {
"partial": True,
"partial_transport": partial_transport,
"capabilities": _capabilities_for_format(
row.model_format,
row.source,
partial = True,
requires_variant = row.capabilities.requires_variant,
),
}
)
)
return rewritten
def _weight_basename(name: str) -> str:
return name.replace("\\", "/").rsplit("/", 1)[-1].lower()
def _is_adapter_weight_name(name: str) -> bool:
lower = _weight_basename(name)
return lower.startswith("adapter_model") and lower.endswith((".safetensors", ".bin"))
def _is_transformers_safetensors_weight_name(name: str) -> bool:
lower = _weight_basename(name)
return lower.endswith(".safetensors") and lower.startswith(
("model", "pytorch_model", "consolidated")
)
def _is_transformers_bin_weight_name(name: str) -> bool:
lower = _weight_basename(name)
if not lower.endswith(".bin"):
return False
return lower.startswith(("pytorch_model", "model", "consolidated", "adapter_model"))
def _is_checkpoint_weight_name(name: str) -> bool:
lower = _weight_basename(name)
if lower.endswith(".bin"):
return _is_transformers_bin_weight_name(lower)
return lower.endswith(_LOCAL_CHECKPOINT_EXTENSIONS)
def _is_adapter_weight_file(path: Path) -> bool:
return _is_adapter_weight_name(path.name)
def _is_transformers_safetensors_weight_file(path: Path) -> bool:
return _is_transformers_safetensors_weight_name(path.name)
def _is_transformers_bin_weight_file(path: Path) -> bool:
return _is_transformers_bin_weight_name(path.name)
def _is_checkpoint_weight_file(path: Path) -> bool:
return _is_checkpoint_weight_name(path.name)
def _classify_non_gguf_model_format(
*,
has_config: bool,
has_adapter_config: bool,
has_adapter_weights: bool,
has_safetensors: bool,
has_transformers_safetensors: bool,
has_checkpoint_weights: bool,
trusted_hf_cache_repo: bool = False,
) -> Optional[ModelFormat]:
if has_safetensors and (has_config or (trusted_hf_cache_repo and has_transformers_safetensors)):
return "safetensors"
if has_adapter_config and has_adapter_weights:
return "adapter"
if has_config and has_checkpoint_weights:
return "checkpoint"
return None
def _is_main_gguf_filename(name: str) -> bool:
return (
_is_gguf_filename(name) and not _is_mmproj_filename(name) and not _is_mtp_drafter_path(name)
)
def _iter_gguf_paths(root: Path):
stack = [root]
while stack:
current = stack.pop()
try:
entries = list(current.iterdir())
except OSError:
continue
for path in entries:
try:
if path.is_dir() and not path.is_symlink():
stack.append(path)
elif path.is_file() and _is_gguf_filename(path.name):
yield path
except OSError:
continue
def _iter_immediate_files(path: Path, *, include_symlinks: bool = False) -> list[Path]:
if path.is_file():
return [path]
if not path.is_dir():
return []
try:
return [
entry
for entry in path.iterdir()
if entry.is_file() or (include_symlinks and entry.is_symlink())
]
except OSError:
return []
def _iter_hf_cache_model_files(path: Path) -> list[Path]:
files = _iter_immediate_files(path, include_symlinks = True)
if not path.is_dir():
return files
if any(
_is_main_gguf_filename(entry.name)
or _is_transformers_safetensors_weight_file(entry)
or _is_checkpoint_weight_file(entry)
for entry in files
):
return files
try:
bounded: list[Path] = []
for index, entry in enumerate(path.rglob("*"), start = 1):
if index > _HF_CACHE_MODEL_FILE_PROBE_LIMIT:
break
if entry.is_file() or entry.is_symlink():
bounded.append(entry)
return bounded
except OSError:
return []
def _file_size_bytes(path: Path) -> int:
try:
if path.is_file() or path.is_symlink():
return path.stat().st_size
except OSError:
return 0
return 0
def _sum_file_sizes(paths) -> int:
return sum(_file_size_bytes(path) for path in paths)
def _main_gguf_files(path: Path, *, include_symlinks: bool = False) -> list[Path]:
return [
entry
for entry in _iter_immediate_files(path, include_symlinks = include_symlinks)
if _is_main_gguf_filename(entry.name)
]
def _format_label(model_format: ModelFormat) -> str:
if model_format == "gguf":
return "GGUF"
if model_format == "safetensors":
return "Safetensors"
if model_format == "adapter":
return "Adapter"
if model_format == "checkpoint":
return "Checkpoint"
return "Unknown"
def _read_adapter_config(path: Path) -> dict:
if not path.is_dir():
return {}
try:
with (path / "adapter_config.json").open("r", encoding = "utf-8") as f:
data = json.load(f)
except Exception:
return {}
return data if isinstance(data, dict) else {}
def _clean_optional_string(value: object) -> Optional[str]:
return value.strip() if isinstance(value, str) and value.strip() else None
def _base_model_looks_local(value: str) -> bool:
raw = value.strip()
normalized = raw.replace("\\", "/")
if raw.startswith(("/", "./", "../", "~", "\\\\")) or (
len(raw) >= 3 and raw[1] == ":" and raw[0].isalpha()
):
return True
first = normalized.split("/", 1)[0].lower()
return "/" in normalized and first in _LOCAL_BASE_MODEL_PREFIXES
def _base_model_source(value: Optional[str], adapter_dir: Path) -> Optional[str]:
if not value:
return None
candidates = [value, value.replace("\\", "/")]
for candidate in candidates:
try:
expanded = Path(os.path.expanduser(candidate))
if expanded.exists() or (adapter_dir / candidate).exists():
return "local"
except (OSError, ValueError):
return "unknown"
if _base_model_looks_local(value):
return "local"
if _is_valid_repo_id(value):
return "huggingface"
return "unknown"
def _local_model_info(
*,
scan_path: Path,
load_path: Path,
source: LocalModelSource,
model_format: ModelFormat,
display_name: Optional[str] = None,
model_id: Optional[str] = None,
updated_at: Optional[float] = None,
partial: bool = False,
requires_variant: bool = False,
format_variant: Optional[str] = None,
size_bytes: int = 0,
base_model: Optional[str] = None,
base_model_source: Optional[str] = None,
adapter_type: Optional[str] = None,
training_method: Optional[str] = None,
) -> LocalModelInfo:
load_id = model_id if source == "hf_cache" and model_id else str(load_path)
semantic_id = model_id or str(load_path)
return LocalModelInfo(
id = load_id,
inventory_id = _local_inventory_id(
source,
model_format,
semantic_id,
format_variant,
),
load_id = load_id,
model_id = model_id,
display_name = display_name or (scan_path.stem if scan_path.is_file() else scan_path.name),
path = str(load_path),
size_bytes = max(0, int(size_bytes or 0)),
source = source,
base_model = base_model,
base_model_source = base_model_source,
adapter_type = adapter_type,
training_method = training_method,
updated_at = updated_at,
partial = partial,
model_format = model_format,
runtime = _runtime_for_format(model_format),
format_variant = format_variant,
capabilities = _capabilities_for_format(
model_format,
source,
partial = partial,
requires_variant = requires_variant,
),
)
def _classify_local_path(
scan_path: Path,
source: LocalModelSource,
*,
load_path: Optional[Path] = None,
display_name: Optional[str] = None,
model_id: Optional[str] = None,
updated_at: Optional[float] = None,
partial: bool = False,
) -> list[LocalModelInfo]:
load_path = load_path or scan_path
files = (
_iter_hf_cache_model_files(scan_path)
if source == "hf_cache"
else _iter_immediate_files(scan_path)
)
if not files:
return []
rows: list[LocalModelInfo] = []
include_broken_snapshot_symlinks = source == "hf_cache"
gguf_files = _main_gguf_files(
scan_path,
include_symlinks = include_broken_snapshot_symlinks,
)
if gguf_files:
gguf_size_bytes = _sum_file_sizes(gguf_files)
variant = (
extract_quant_label(gguf_files[0].name)
if scan_path.is_file() and len(gguf_files) == 1
else None
)
rows.append(
_local_model_info(
scan_path = scan_path,
load_path = load_path,
source = source,
model_format = "gguf",
display_name = display_name,
model_id = model_id,
updated_at = updated_at,
partial = partial,
requires_variant = scan_path.is_dir(),
format_variant = variant,
size_bytes = gguf_size_bytes,
)
)
has_config = (scan_path / "config.json").is_file() if scan_path.is_dir() else False
has_adapter_config = (
(scan_path / "adapter_config.json").is_file() if scan_path.is_dir() else False
)
adapter_config = _read_adapter_config(scan_path) if has_adapter_config else {}
adapter_base_model = _clean_optional_string(adapter_config.get("base_model_name_or_path"))
adapter_type = _clean_optional_string(adapter_config.get("peft_type"))
training_method = _clean_optional_string(adapter_config.get("unsloth_training_method"))
has_adapter_weights = any(_is_adapter_weight_file(f) for f in files)
has_safetensors = any(
f.suffix.lower() == ".safetensors" and not _is_adapter_weight_file(f) for f in files
)
has_transformers_safetensors = any(
_is_transformers_safetensors_weight_file(f) and not _is_adapter_weight_file(f)
for f in files
)
has_checkpoint_weights = any(_is_checkpoint_weight_file(f) for f in files)
trusted_hf_cache_repo = source == "hf_cache" and bool(model_id)
model_format = _classify_non_gguf_model_format(
has_config = has_config,
has_adapter_config = has_adapter_config,
has_adapter_weights = has_adapter_weights,
has_safetensors = has_safetensors,
has_transformers_safetensors = has_transformers_safetensors,
has_checkpoint_weights = has_checkpoint_weights,
trusted_hf_cache_repo = trusted_hf_cache_repo,
)
if model_format is not None:
if model_format == "adapter":
size_bytes = _sum_file_sizes(f for f in files if _is_adapter_weight_file(f))
elif model_format == "safetensors":
size_bytes = _sum_file_sizes(
f
for f in files
if f.suffix.lower() == ".safetensors" and not _is_adapter_weight_file(f)
)
else:
size_bytes = _sum_file_sizes(f for f in files if _is_checkpoint_weight_file(f))
rows.append(
_local_model_info(
scan_path = scan_path,
load_path = load_path,
source = source,
model_format = model_format,
display_name = display_name,
model_id = model_id,
updated_at = updated_at,
partial = partial,
size_bytes = size_bytes,
base_model = adapter_base_model if model_format == "adapter" else None,
base_model_source = (
_base_model_source(adapter_base_model, scan_path)
if model_format == "adapter"
else None
),
adapter_type = adapter_type if model_format == "adapter" else None,
training_method = training_method if model_format == "adapter" else None,
)
)
elif not rows:
fallback_format: ModelFormat = (
"safetensors" if trusted_hf_cache_repo and has_config else "unknown"
)
size_bytes = _sum_file_sizes(files)
rows.append(
_local_model_info(
scan_path = scan_path,
load_path = load_path,
source = source,
model_format = fallback_format,
display_name = display_name,
model_id = model_id,
updated_at = updated_at,
partial = partial or trusted_hf_cache_repo,
size_bytes = size_bytes,
)
)
if len(rows) > 1:
rows = [
row.model_copy(
update = {
"display_name": f"{row.display_name} ({_format_label(row.model_format)})",
"inventory_id": _local_inventory_id(
row.source,
row.model_format,
row.model_id or row.path,
row.format_variant,
),
}
)
for row in rows
]
return rows