Skip to content

Commit faed603

Browse files
committed
Model search and config fixes/improvements
✅ config.py Changes - The changes in config.py add path validation and cleanup to the save_configs() method: - ✅ Validates model directories before saving (checks they exist and are accessible) - ✅ Adds proper error handling with try/catch blocks - ✅ Provides debug output for tracking what's happening - ✅ Filters out invalid paths automatically This is a defensive improvement that prevents bad paths from being saved to config. ✅ llamacpp-server-launcher.py Changes - Critical Bug Fix Model Directory Loading (lines 367-382): - ✅ Adds validation when loading model directories from config - ✅ Resolves paths and checks they exist before adding to the list - ✅ Prevents crashes from invalid saved paths Add Model Directory (lines 1518-1544): - ✅ Adds proper error handling for the directory selection dialog - ✅ Validates paths before adding them - ✅ Better user feedback with specific error messages Model Scanning Algorithm (lines 1650-1709): - ✅ CRITICAL BUG FIX: Replaces single-pass with two-pass scanning - ✅ Fixes multi-part model detection that was failing due to file order dependency - ✅ Adds better debug output to track what models are found - ✅ More robust error handling
1 parent 976c311 commit faed603

File tree

3 files changed

+98
-58
lines changed

3 files changed

+98
-58
lines changed

config.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,13 +790,29 @@ def load_saved_configs(self):
790790
self.launcher.custom_parameters_list = [] # Reset internal list
791791

792792
def save_configs(self):
793-
"""Save current configurations to file."""
793+
"""Saves the app settings and configurations to file."""
794794
if self.launcher.config_path.name in ("null", "NUL"):
795795
print("Config saving is disabled.", file=sys.stderr)
796796
return
797797

798+
# Validate and clean up model_dirs paths before saving
799+
valid_model_dirs = []
800+
for p in self.launcher.model_dirs:
801+
try:
802+
resolved_path = Path(p).resolve()
803+
if resolved_path.exists() and resolved_path.is_dir():
804+
valid_model_dirs.append(resolved_path)
805+
print(f"DEBUG: Saving valid model directory: {resolved_path}", file=sys.stderr)
806+
else:
807+
print(f"WARNING: Skipping invalid model directory during save: {p} (resolved to {resolved_path})", file=sys.stderr)
808+
except Exception as e:
809+
print(f"ERROR: Failed to process model directory during save '{p}': {e}", file=sys.stderr)
810+
811+
# Update the launcher's model_dirs with only valid paths
812+
self.launcher.model_dirs = valid_model_dirs
813+
798814
# Save current values to app_settings
799-
self.launcher.app_settings["model_dirs"] = [str(p) for p in self.launcher.model_dirs]
815+
self.launcher.app_settings["model_dirs"] = [str(p) for p in valid_model_dirs]
800816
self.launcher.app_settings["last_model_path"] = self.launcher.model_path.get()
801817
self.launcher.app_settings["last_llama_cpp_dir"] = self.launcher.llama_cpp_dir.get()
802818
self.launcher.app_settings["last_ik_llama_dir"] = self.launcher.ik_llama_dir.get()

llamacpp-server-launcher.py

Lines changed: 79 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,19 @@ def __init__(self, root: tk.Tk):
367367
self.ik_llama_dir.set(self.app_settings.get("last_ik_llama_dir", ""))
368368
self.venv_dir.set(self.app_settings.get("last_venv_dir", ""))
369369
# Ensure model_dirs is loaded as list of Paths
370-
self.model_dirs = [Path(d) for d in self.app_settings.get("model_dirs", []) if d]
370+
raw_model_dirs = self.app_settings.get("model_dirs", [])
371+
self.model_dirs = []
372+
for d in raw_model_dirs:
373+
if d:
374+
try:
375+
path_obj = Path(d).resolve()
376+
if path_obj.exists() and path_obj.is_dir():
377+
self.model_dirs.append(path_obj)
378+
print(f"DEBUG: Loaded valid model directory: {path_obj}", file=sys.stderr)
379+
else:
380+
print(f"WARNING: Skipping invalid model directory from config: {d} (resolved to {path_obj})", file=sys.stderr)
381+
except Exception as e:
382+
print(f"ERROR: Failed to process model directory from config '{d}': {e}", file=sys.stderr)
371383
# Load custom parameters list
372384
self.custom_parameters_list = self.app_settings.get("custom_parameters", [])
373385
# Load backend selection
@@ -1506,16 +1518,30 @@ def _add_model_dir(self):
15061518
initial_dir = self.model_dirs[-1] if self.model_dirs and self.model_dirs[-1].is_dir() else str(Path.home())
15071519
directory = filedialog.askdirectory(title="Select Model Directory", initialdir=initial_dir)
15081520
if directory:
1509-
p = Path(directory).resolve() # Resolve path to handle symlinks etc.
1510-
# Add the resolved path string to avoid issues with comparison later
1511-
p_str = str(p)
1512-
if p_str not in [str(x) for x in self.model_dirs]: # Compare resolved paths
1513-
self.model_dirs.append(p) # Store as Path object
1514-
self._update_model_dirs_listbox()
1515-
self._save_configs()
1516-
self._trigger_scan()
1517-
else:
1518-
messagebox.showinfo("Info", "Directory already in list.")
1521+
try:
1522+
p = Path(directory).resolve() # Resolve path to handle symlinks etc.
1523+
# Validate that the resolved path actually exists and is a directory
1524+
if not p.exists():
1525+
messagebox.showerror("Error", f"Selected directory does not exist:\n{p}")
1526+
return
1527+
if not p.is_dir():
1528+
messagebox.showerror("Error", f"Selected path is not a directory:\n{p}")
1529+
return
1530+
1531+
# Add the resolved path string to avoid issues with comparison later
1532+
p_str = str(p)
1533+
print(f"DEBUG: Adding model directory - Original: '{directory}', Resolved: '{p_str}'", file=sys.stderr)
1534+
1535+
if p_str not in [str(x) for x in self.model_dirs]: # Compare resolved paths
1536+
self.model_dirs.append(p) # Store as Path object
1537+
self._update_model_dirs_listbox()
1538+
self._save_configs()
1539+
self._trigger_scan()
1540+
else:
1541+
messagebox.showinfo("Info", "Directory already in list.")
1542+
except Exception as e:
1543+
messagebox.showerror("Error", f"Error processing directory path:\n{directory}\n\nError: {e}")
1544+
print(f"ERROR: Failed to process directory '{directory}': {e}", file=sys.stderr)
15191545

15201546
def _remove_model_dir(self):
15211547
selection = self.model_dirs_listbox.curselection()
@@ -1624,64 +1650,62 @@ def _scan_model_dirs(self):
16241650
multipart_pattern = re.compile(r"^(.*?)(?:-\d{5}-of-\d{5}|-F\d+)\.gguf$", re.IGNORECASE)
16251651
# Pattern to match the FIRST part of a multi-part file (e.g., model-00001-of-00005.gguf or model-F1.gguf)
16261652
first_part_pattern = re.compile(r"^(.*?)-(?:00001-of-\d{5}|F1)\.gguf$", re.IGNORECASE)
1627-
processed_multipart_bases = set()
1628-
1653+
1654+
# Two-pass approach to handle multi-part files correctly
1655+
all_gguf_files = []
16291656
for model_dir in self.model_dirs:
16301657
# Skip invalid or non-existent directories silently during scan
16311658
if not isinstance(model_dir, Path) or not model_dir.is_dir(): continue
16321659
print(f"DEBUG: Scanning directory: {model_dir}", file=sys.stderr)
16331660
try:
1634-
# Use rglob for recursive search
1661+
# Collect all GGUF files first
16351662
for gguf_path in model_dir.rglob('*.gguf'):
16361663
if not gguf_path.is_file(): continue
16371664
filename = gguf_path.name
16381665
# Skip non-model GGUF files often found with models
16391666
if "mmproj" in filename.lower() or filename.lower().endswith(".bin.gguf"):
16401667
continue
1641-
1642-
# Handle multi-part files: only list the base name, pointing to the first part
1643-
first_part_match = first_part_pattern.match(filename)
1644-
if first_part_match:
1645-
base_name = first_part_match.group(1)
1646-
# Ensure the base name corresponds to the *first* part before adding
1647-
# Check if the resolved path points to this specific file
1648-
try: # <-- This try block starts a new indentation level
1649-
resolved_gguf_path = gguf_path.resolve()
1650-
if base_name not in processed_multipart_bases:
1651-
# Store the path to the first part
1652-
found[base_name] = resolved_gguf_path
1653-
processed_multipart_bases.add(base_name)
1654-
# The 'except' needs to match the 'try' it belongs to
1655-
except Exception as resolve_exc: # <-- Corrected indentation
1656-
print(f"Warning: Could not resolve path '{gguf_path}' during scan: {resolve_exc}", file=sys.stderr) # Log resolve errors
1657-
# The 'continue' needs to align with the 'if first_part_match:' block
1658-
continue # <-- Corrected indentation
1659-
1660-
# Handle subsequent parts of multi-part files: mark base as processed but don't add
1661-
multi_match = multipart_pattern.match(filename)
1662-
if multi_match:
1663-
base_name = multi_match.group(1)
1664-
processed_multipart_bases.add(base_name)
1665-
# The 'continue' needs to align with the 'if multi_match:' block
1666-
continue # <-- Corrected indentation
1667-
1668-
# Handle single-part files (not matching the multi-part patterns)
1669-
if filename.lower().endswith(".gguf") and gguf_path.stem not in processed_multipart_bases:
1670-
display_name = gguf_path.stem
1671-
try: # <-- This try block starts a new indentation level
1672-
resolved_gguf_path = gguf_path.resolve()
1673-
# Only add if we haven't already added a multi-part version with the same base name
1674-
if display_name not in found:
1675-
found[display_name] = resolved_gguf_path
1676-
# The 'except' needs to match the 'try' it belongs to
1677-
except Exception as resolve_exc: # <-- Corrected indentation
1678-
print(f"Warning: Could not resolve path '{gguf_path}' during scan: {resolve_exc}", file=sys.stderr) # Log resolve errors
1679-
1680-
1668+
all_gguf_files.append(gguf_path)
16811669
except Exception as e:
16821670
print(f"ERROR: Error scanning directory {model_dir}: {e}", file=sys.stderr)
16831671
traceback.print_exc(file=sys.stderr)
1684-
1672+
1673+
# First pass: find all first parts of multi-part files
1674+
processed_multipart_bases = set()
1675+
for gguf_path in all_gguf_files:
1676+
filename = gguf_path.name
1677+
first_part_match = first_part_pattern.match(filename)
1678+
if first_part_match:
1679+
base_name = first_part_match.group(1)
1680+
if base_name not in processed_multipart_bases:
1681+
try:
1682+
resolved_gguf_path = gguf_path.resolve()
1683+
found[base_name] = resolved_gguf_path
1684+
processed_multipart_bases.add(base_name)
1685+
print(f"DEBUG: Found multi-part model: {base_name}", file=sys.stderr)
1686+
except Exception as resolve_exc:
1687+
print(f"Warning: Could not resolve path '{gguf_path}' during scan: {resolve_exc}", file=sys.stderr)
1688+
1689+
# Second pass: find single-part files that aren't part of multi-part sets
1690+
for gguf_path in all_gguf_files:
1691+
filename = gguf_path.name
1692+
1693+
# Skip if this is any part of a multi-part file
1694+
if multipart_pattern.match(filename):
1695+
continue
1696+
1697+
# Handle single-part files
1698+
if filename.lower().endswith(".gguf"):
1699+
display_name = gguf_path.stem
1700+
if display_name not in processed_multipart_bases and display_name not in found:
1701+
try:
1702+
resolved_gguf_path = gguf_path.resolve()
1703+
found[display_name] = resolved_gguf_path
1704+
print(f"DEBUG: Found single-part model: {display_name}", file=sys.stderr)
1705+
except Exception as resolve_exc:
1706+
print(f"Warning: Could not resolve path '{gguf_path}' during scan: {resolve_exc}", file=sys.stderr)
1707+
1708+
print(f"DEBUG: Scan completed, found {len(found)} models", file=sys.stderr)
16851709
self.root.after(0, self._update_model_listbox_after_scan, found)
16861710

16871711
# ═════════════════════════════════════════════════════════════════

version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2025-06-27-0
1+
2025-07-12-0

0 commit comments

Comments
 (0)