From 70c150bd80a7a262704ea6d281a7ef2a7ea8920a Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 17 Mar 2026 14:20:23 +0800 Subject: [PATCH] fix(services): implement stable sorting for model and recipe caches Add file_path as a tie-breaker for all sort modes in ModelCache, BaseModelService, LoraService, and RecipeCache to ensure deterministic ordering when primary keys are identical. Resolves issue #859. --- py/services/base_model_service.py | 6 +++++- py/services/lora_service.py | 10 ++++++++-- py/services/model_cache.py | 28 ++++++++++++++++++++-------- py/services/recipe_cache.py | 3 ++- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/py/services/base_model_service.py b/py/services/base_model_service.py index c08e0dad..681ed325 100644 --- a/py/services/base_model_service.py +++ b/py/services/base_model_service.py @@ -208,7 +208,11 @@ class BaseModelService(ABC): reverse = sort_params.order == "desc" annotated.sort( - key=lambda x: (x.get("usage_count", 0), x.get("model_name", "").lower()), + key=lambda x: ( + x.get("usage_count", 0), + x.get("model_name", "").lower(), + x.get("file_path", "").lower() + ), reverse=reverse, ) return annotated diff --git a/py/services/lora_service.py b/py/services/lora_service.py index 6b22209c..740a6d22 100644 --- a/py/services/lora_service.py +++ b/py/services/lora_service.py @@ -516,12 +516,18 @@ class LoraService(BaseModelService): if sort_by == "model_name": available_loras = sorted( available_loras, - key=lambda x: (x.get("model_name") or x.get("file_name", "")).lower() + key=lambda x: ( + (x.get("model_name") or x.get("file_name", "")).lower(), + x.get("file_path", "").lower() + ) ) else: # Default to filename available_loras = sorted( available_loras, - key=lambda x: x.get("file_name", "").lower() + key=lambda x: ( + x.get("file_name", "").lower(), + x.get("file_path", "").lower() + ) ) # Return minimal data needed for cycling diff --git a/py/services/model_cache.py b/py/services/model_cache.py index ac0f273c..9b396634 100644 --- a/py/services/model_cache.py +++ b/py/services/model_cache.py @@ -221,33 +221,45 @@ class ModelCache: start_time = time.perf_counter() reverse = (order == 'desc') if sort_key == 'name': - # Natural sort by configured display name, case-insensitive + # Natural sort by configured display name, case-insensitive, with file_path as tie-breaker result = natsorted( data, - key=lambda x: self._get_display_name(x).lower(), + key=lambda x: ( + self._get_display_name(x).lower(), + x.get('file_path', '').lower() + ), reverse=reverse ) elif sort_key == 'date': - # Sort by modified timestamp (use .get() with default to handle missing fields) + # Sort by modified timestamp, fallback to name and path for stability result = sorted( data, - key=lambda x: x.get('modified', 0.0), + key=lambda x: ( + x.get('modified', 0.0), + self._get_display_name(x).lower(), + x.get('file_path', '').lower() + ), reverse=reverse ) elif sort_key == 'size': - # Sort by file size (use .get() with default to handle missing fields) + # Sort by file size, fallback to name and path for stability result = sorted( data, - key=lambda x: x.get('size', 0), + key=lambda x: ( + x.get('size', 0), + self._get_display_name(x).lower(), + x.get('file_path', '').lower() + ), reverse=reverse ) elif sort_key == 'usage': - # Sort by usage count, fallback to 0, then name for stability + # Sort by usage count, fallback to 0, then name and path for stability return sorted( data, key=lambda x: ( x.get('usage_count', 0), - self._get_display_name(x).lower() + self._get_display_name(x).lower(), + x.get('file_path', '').lower() ), reverse=reverse ) diff --git a/py/services/recipe_cache.py b/py/services/recipe_cache.py index 602795f8..7c0c499c 100644 --- a/py/services/recipe_cache.py +++ b/py/services/recipe_cache.py @@ -135,7 +135,8 @@ class RecipeCache: """Sort cached views. Caller must hold ``_lock``.""" self.sorted_by_name = natsorted( - self.raw_data, key=lambda x: x.get("title", "").lower() + self.raw_data, + key=lambda x: (x.get("title", "").lower(), x.get("file_path", "").lower()), ) if not name_only: self.sorted_by_date = sorted(