Skip to content
59 changes: 59 additions & 0 deletions studio/backend/routes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,39 @@ def _scan_models_dir(
if not models_dir.exists() or not models_dir.is_dir():
return []

# Check if the directory itself IS a model (has a model config AND
# weight files). Both conditions are required: a bare directory with
# only loose .gguf files (no config) might be a mixed collection that
# should list files individually, and a config.json alone (no weights)
# does not make a model directory.
try:
_has_config = (models_dir / "config.json").exists() or (
models_dir / "adapter_config.json"
).exists()
_has_weights = any(
f.suffix.lower() in (".gguf", ".safetensors", ".bin")
for f in models_dir.iterdir()
if f.is_file()
)
_is_self_model = _has_config and _has_weights
except OSError:
_is_self_model = False

if _is_self_model:
try:
updated_at = models_dir.stat().st_mtime
except OSError:
updated_at = None
return [
LocalModelInfo(
id = str(models_dir),
display_name = models_dir.name,
path = str(models_dir),
source = "models_dir",
updated_at = updated_at,
),
]

found: List[LocalModelInfo] = []
for child in models_dir.iterdir():
if limit is not None and len(found) >= limit:
Expand Down Expand Up @@ -243,6 +276,19 @@ def _scan_lmstudio_dir(lm_dir: Path) -> List[LocalModelInfo]:
if not lm_dir.exists() or not lm_dir.is_dir():
return []

# If the directory itself is a model directory (has config files),
# it is not an LM Studio publisher structure -- _scan_models_dir
# already handles it.
try:
if (lm_dir / "config.json").exists() or (
lm_dir / "adapter_config.json"
).exists():
return []
except OSError:
# If we cannot stat the config files (permissions, broken symlink,
# etc.), treat the directory as a normal LM Studio root and continue.
pass

found: List[LocalModelInfo] = []
for child in lm_dir.iterdir():
try:
Expand All @@ -263,6 +309,19 @@ def _scan_lmstudio_dir(lm_dir: Path) -> List[LocalModelInfo]:
)
continue

# If the child directory itself looks like a model (has config
# or model weight files), skip it -- _scan_models_dir already
# handles it. Only treat it as a publisher directory otherwise.
_child_is_model = (
(child / "config.json").exists()
or (child / "adapter_config.json").exists()
or any(child.glob("*.safetensors"))
or any(child.glob("*.bin"))
or any(child.glob("*.gguf"))
)
if _child_is_model:
continue

# child is a publisher directory -- scan its sub-directories
for model_dir in child.iterdir():
try:
Expand Down
14 changes: 12 additions & 2 deletions studio/backend/utils/models/model_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1047,8 +1047,13 @@ def list_local_gguf_variants(
(variants, has_vision): list of non-mmproj GGUF variants + vision flag.
"""
p = Path(directory)
# If a file path was passed (e.g. a standalone .gguf entry), scan its
# parent directory instead so we still find the correct variants.
if not p.is_dir():
return [], False
if p.is_file() and p.suffix.lower() == ".gguf":
p = p.parent
else:
return [], False

quant_totals: dict[str, int] = {}
quant_first_file: dict[str, str] = {}
Expand Down Expand Up @@ -1088,8 +1093,13 @@ def _find_local_gguf_by_variant(directory: str, variant: str) -> Optional[str]:
Returns the resolved absolute path, or ``None`` if no match.
"""
p = Path(directory)
# If a file path was passed (e.g. a standalone .gguf entry), use its
# parent directory so variant lookup still works.
if not p.is_dir():
return None
if p.is_file() and p.suffix.lower() == ".gguf":
p = p.parent
else:
return None

matches = sorted(
f
Expand Down
Loading