From 0c753aba3054907bf418086e587a38081dc71081 Mon Sep 17 00:00:00 2001 From: JYYYYYT Date: Sun, 5 Apr 2026 14:54:26 +0800 Subject: [PATCH 1/4] fix(studio): scan custom folder as model when it contains config + weights When a custom scan folder points directly at a model directory (e.g. gemma-4-e2b-it-gguf/ containing config.json and .gguf files), _scan_models_dir previously skipped the directory itself and listed individual .gguf files as standalone models. The gguf-variants endpoint then received file paths instead of directory paths, causing list_local_gguf_variants to return an empty list ("No GGUF variants found"). Three fixes: 1. _scan_models_dir: detect when the scanned directory itself is a model (has BOTH a config file AND weight files) and return it as a single entry. Both conditions are required to avoid false positives on bare .gguf collections or config-only directories. 2. _scan_lmstudio_dir: early-return when the directory has config files (not a publisher structure), and skip child directories that are model directories rather than treating them as publisher folders. 3. list_local_gguf_variants: fall back to the parent directory when a .gguf file path is passed instead of a directory. --- studio/backend/routes/models.py | 55 +++++++++++++++++++++ studio/backend/utils/models/model_config.py | 7 ++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index 1e31a91e26..46a420a37d 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -146,6 +146,40 @@ 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(models_dir.glob("*.gguf")) + or any(models_dir.glob("*.safetensors")) + or any(models_dir.glob("*.bin")) + ) + _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: @@ -243,6 +277,15 @@ 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: @@ -263,6 +306,18 @@ 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")) + ) + if _child_is_model: + continue + # child is a publisher directory -- scan its sub-directories for model_dir in child.iterdir(): try: diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index f97ea993eb..b1649a8e10 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -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] = {} From d955ee22c702070e5e0d785d1dcae23a632ad759 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 08:23:49 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/routes/models.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index 46a420a37d..2a86b36b7b 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -152,10 +152,9 @@ def _scan_models_dir( # 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_config = (models_dir / "config.json").exists() or ( + models_dir / "adapter_config.json" + ).exists() _has_weights = ( any(models_dir.glob("*.gguf")) or any(models_dir.glob("*.safetensors")) @@ -281,7 +280,9 @@ def _scan_lmstudio_dir(lm_dir: Path) -> List[LocalModelInfo]: # 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(): + if (lm_dir / "config.json").exists() or ( + lm_dir / "adapter_config.json" + ).exists(): return [] except OSError: pass From 19f06ffa8162dd160d77fc9691f46965232d2bc4 Mon Sep 17 00:00:00 2001 From: JYYYYYT Date: Sun, 5 Apr 2026 18:00:44 +0800 Subject: [PATCH 3/4] address review: optimize glob, add .gguf to child check, add variant lookup fallback --- studio/backend/routes/models.py | 13 ++++++++----- studio/backend/utils/models/model_config.py | 7 ++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index 46a420a37d..c78bb54df0 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -156,10 +156,10 @@ def _scan_models_dir( (models_dir / "config.json").exists() or (models_dir / "adapter_config.json").exists() ) - _has_weights = ( - any(models_dir.glob("*.gguf")) - or any(models_dir.glob("*.safetensors")) - or any(models_dir.glob("*.bin")) + _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: @@ -281,7 +281,9 @@ def _scan_lmstudio_dir(lm_dir: Path) -> List[LocalModelInfo]: # 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(): + if (lm_dir / "config.json").exists() or ( + lm_dir / "adapter_config.json" + ).exists(): return [] except OSError: pass @@ -314,6 +316,7 @@ def _scan_lmstudio_dir(lm_dir: Path) -> List[LocalModelInfo]: 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 diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index b1649a8e10..4a6b565a1a 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -1093,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 From a4e5673ec487df636323c4738c869dcf4d141894 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 10:02:40 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/routes/models.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py index c78bb54df0..79c03c8fec 100644 --- a/studio/backend/routes/models.py +++ b/studio/backend/routes/models.py @@ -152,10 +152,9 @@ def _scan_models_dir( # 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_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()