Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 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")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The list of weight extensions here (.gguf, .safetensors, .bin) is slightly inconsistent with other parts of the codebase (e.g., line 634 includes .pt and .pth). While these three are the most common for this application, ensuring a single source of truth for supported weight formats would be beneficial.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The list of weight extensions here (.gguf, .safetensors, .bin) is slightly inconsistent with other parts of the codebase (e.g., line 634 includes .pt and .pth). While these three are the most common for this application, ensuring a single source of truth for supported weight formats would be beneficial.

The extensions here (.gguf, .safetensors, .bin) are intentionally scoped to the formats relevant to local model detection. .pt and .pth at line 634 serve a different purpose (training checkpoint detection). A unified constant could help but is out of scope for this PR.

for f in models_dir.iterdir()
if f.is_file()
)
Comment on lines +158 to +162
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Using models_dir.iterdir() here and then iterating over the directory again at line 183 is slightly inefficient. While not critical for most model directories, you could consider listing the children once if performance becomes an issue, or using glob patterns which are used elsewhere in the file for similar checks.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Using models_dir.iterdir() here and then iterating over the directory again at line 183 is slightly inefficient. While not critical for most model directories, you could consider listing the children once if performance becomes an issue, or using glob patterns which are used elsewhere in the file for similar checks.

The iterdir() at line 162 only runs when the directory itself is a model (early return path). When it's not, the second iterdir() at line 183 runs instead. They don't both execute for the same directory, so there's no redundant scan.

_is_self_model = _has_config and _has_weights
Comment on lines +155 to +163
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for detecting if a directory is a model (checking for config.json or adapter_config.json and weight files) is repeated multiple times in this file (lines 155, 190, 283, 314). Consider refactoring this into a helper function to improve maintainability and ensure consistency across different scan methods.

Copy link
Copy Markdown

@JYYYYYT JYYYYYT Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for detecting if a directory is a model (checking for config.json or adapter_config.json and weight files) is repeated multiple times in this file (lines 155, 190, 283, 314). Consider refactoring this into a helper function to improve maintainability and ensure consistency across different scan methods.

Good suggestion. The model detection logic does appear in multiple places with intentionally different criteria (e.g. _is_self_model requires config AND weights, while Phase 1 uses a broader OR check). Refactoring into a shared helper would require careful parameterization to preserve these distinctions. I think this is better suited as a follow-up PR to keep this fix focused.

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,17 @@ 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:
pass

found: List[LocalModelInfo] = []
for child in lm_dir.iterdir():
try:
Expand All @@ -263,6 +307,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