From dc9200a12c0c6166fa2e6db3e68c53ac9078eacc Mon Sep 17 00:00:00 2001 From: Will Miao Date: Sat, 8 Aug 2026 21:50:33 +0800 Subject: [PATCH] fix(recipes): match recipe-format lora cache item by autov2/autov3 hash --- py/recipes/parsers/recipe_format.py | 10 +- tests/services/test_recipe_format_parser.py | 251 ++++++++++++++++++++ 2 files changed, 260 insertions(+), 1 deletion(-) diff --git a/py/recipes/parsers/recipe_format.py b/py/recipes/parsers/recipe_format.py index b97eaa60..5ece07a7 100644 --- a/py/recipes/parsers/recipe_format.py +++ b/py/recipes/parsers/recipe_format.py @@ -91,7 +91,15 @@ class RecipeFormatParser(RecipeMetadataParser): exists_locally = lora_scanner.has_hash(lora['hash']) if exists_locally: lora_cache = await lora_scanner.get_cached_data() - lora_item = next((item for item in lora_cache.raw_data if item['sha256'].lower() == lora['hash'].lower()), None) + # Cascade match: full sha256, stored autov3, or autov2 (sha256[:10]). + h = (lora.get('hash') or '').lower() + lora_item = next( + (item for item in lora_cache.raw_data + if (item.get("sha256") or "").lower() == h + or (item.get("autov3") or "").lower() == h + or (item.get("sha256") or "")[:10].lower() == h), + None + ) if lora_item: lora_entry['existsLocally'] = True lora_entry['inLibrary'] = True diff --git a/tests/services/test_recipe_format_parser.py b/tests/services/test_recipe_format_parser.py index 141c49c7..d1bb3e41 100644 --- a/tests/services/test_recipe_format_parser.py +++ b/tests/services/test_recipe_format_parser.py @@ -7,6 +7,47 @@ from py.recipes.parsers.recipe_format import RecipeFormatParser from py.config import config +class _FakeCache: + def __init__(self, entries, version_index=None): + self.raw_data = entries + self.version_index = version_index or {} + + +class _FakeLoraScanner: + def __init__(self, entries, version_index=None, has_hash_result=True): + self._cache = _FakeCache(entries, version_index) + self._has_hash_result = has_hash_result + + def has_hash(self, sha256): + return self._has_hash_result + + async def get_cached_data(self): + return self._cache + + +class _FakeRecipeScanner: + def __init__(self, lora_scanner): + self._lora_scanner = lora_scanner + + +async def _noop_metadata_provider(): + class Provider: + async def get_model_version_info(self, version_id): + return None, None + + return Provider() + + +def _parse(monkeypatch, recipe_metadata, recipe_scanner): + monkeypatch.setattr( + "py.recipes.parsers.recipe_format.get_default_metadata_provider", + _noop_metadata_provider, + ) + parser = RecipeFormatParser() + metadata_text = f"Recipe metadata: {json.dumps(recipe_metadata)}" + return parser.parse_metadata(metadata_text, recipe_scanner=recipe_scanner) + + @pytest.mark.asyncio async def test_recipe_format_parser_populates_checkpoint(monkeypatch): checkpoint_info = { @@ -144,3 +185,213 @@ async def test_recipe_format_parser_marks_lora_in_library_by_version(monkeypatch assert lora_entry["thumbnailUrl"] == config.get_preview_static_url( cached_entry["preview_url"] ) + + +@pytest.mark.asyncio +async def test_recipe_format_parser_matches_lora_by_autov3_hash(monkeypatch): + # Cache item is matched by its stored 12-char autov3 hash, even when its + # full sha256 differs from the recipe hash. + cached_entry: Dict[str, Any] = { + "file_path": "/loras/autov3.safetensors", + "file_name": "AutoV3 LoRA", + "size": 4096, + "sha256": "f" * 64, + "autov3": "AbCdEf123456", + "preview_url": "/previews/autov3.png", + } + + recipe_metadata = { + "title": "Autov3", + "base_model": "Illustrious", + "loras": [ + { + "modelVersionId": 9001, + "modelName": "AutoV3 LoRA", + "modelVersionName": "V1", + "strength": 0.7, + "hash": "abcdef123456", + } + ], + "gen_params": {"steps": 29}, + "tags": [], + } + + result = await _parse( + monkeypatch, + recipe_metadata, + recipe_scanner=_FakeRecipeScanner(_FakeLoraScanner([cached_entry])), + ) + + lora_entry = result["loras"][0] + assert lora_entry["existsLocally"] is True + assert lora_entry["localPath"] == cached_entry["file_path"] + assert lora_entry["thumbnailUrl"] == config.get_preview_static_url( + cached_entry["preview_url"] + ) + + +@pytest.mark.asyncio +async def test_recipe_format_parser_matches_lora_by_autov2_prefix(monkeypatch): + # 10-char autov2 recipe hash matches the sha256[:10] prefix of the cache item. + sha256 = "abcdef0123456789" + "0" * 48 + cached_entry: Dict[str, Any] = { + "file_path": "/loras/autov2.safetensors", + "file_name": "AutoV2 LoRA", + "size": 8192, + "sha256": sha256, + "preview_url": "/previews/autov2.png", + } + + recipe_metadata = { + "title": "Autov2", + "base_model": "Illustrious", + "loras": [ + { + "modelVersionId": 9002, + "modelName": "AutoV2 LoRA", + "modelVersionName": "V1", + "strength": 0.5, + "hash": sha256[:10], + } + ], + "gen_params": {"steps": 20}, + "tags": [], + } + + result = await _parse( + monkeypatch, + recipe_metadata, + recipe_scanner=_FakeRecipeScanner(_FakeLoraScanner([cached_entry])), + ) + + lora_entry = result["loras"][0] + assert lora_entry["existsLocally"] is True + assert lora_entry["localPath"] == cached_entry["file_path"] + + +@pytest.mark.asyncio +async def test_recipe_format_parser_matches_lora_by_full_sha256(monkeypatch): + # Full 64-char sha256 recipe hash matches exactly as before the cascade change. + sha256 = "0123456789abcdef" * 4 + cached_entry: Dict[str, Any] = { + "file_path": "/loras/sha256.safetensors", + "file_name": "Sha256 LoRA", + "size": 4096, + "sha256": sha256, + "preview_url": "/previews/sha256.png", + } + + recipe_metadata = { + "title": "Sha256", + "base_model": "Illustrious", + "loras": [ + { + "modelVersionId": 9003, + "modelName": "Sha256 LoRA", + "modelVersionName": "V1", + "strength": 0.9, + "hash": sha256.upper(), + } + ], + "gen_params": {"steps": 25}, + "tags": [], + } + + result = await _parse( + monkeypatch, + recipe_metadata, + recipe_scanner=_FakeRecipeScanner(_FakeLoraScanner([cached_entry])), + ) + + lora_entry = result["loras"][0] + assert lora_entry["existsLocally"] is True + assert lora_entry["localPath"] == cached_entry["file_path"] + + +@pytest.mark.asyncio +async def test_recipe_format_parser_no_hash_match_falls_back_to_version_index(monkeypatch): + # No hash-form match: falls through to modelVersionId lookup as today. + version_entry: Dict[str, Any] = { + "file_path": "/loras/versioned.safetensors", + "file_name": "Versioned LoRA", + "size": 4096, + "sha256": "a" * 64, + "preview_url": "/previews/versioned.png", + } + cache_entry: Dict[str, Any] = { + "file_path": "/loras/other.safetensors", + "file_name": "Other LoRA", + "size": 2048, + "sha256": "b" * 64, + "preview_url": "/previews/other.png", + } + + recipe_metadata = { + "title": "Versioned", + "base_model": "Illustrious", + "loras": [ + { + "modelVersionId": 9004, + "modelName": "Versioned LoRA", + "modelVersionName": "V1", + "strength": 1.0, + "hash": "c" * 64, + } + ], + "gen_params": {"steps": 20}, + "tags": [], + } + + result = await _parse( + monkeypatch, + recipe_metadata, + recipe_scanner=_FakeRecipeScanner( + _FakeLoraScanner( + [cache_entry], + version_index={9004: version_entry}, + has_hash_result=False, + ) + ), + ) + + lora_entry = result["loras"][0] + assert lora_entry["existsLocally"] is True + assert lora_entry["localPath"] == version_entry["file_path"] + assert lora_entry["file_name"] == version_entry["file_name"] + assert lora_entry["size"] == version_entry["size"] + + +@pytest.mark.asyncio +async def test_recipe_format_parser_sha256_less_cache_item_no_keyerror(monkeypatch): + # A cache item without a sha256 field must not raise KeyError in the lookup. + cache_entry: Dict[str, Any] = { + "file_path": "/loras/nohash.safetensors", + "file_name": "NoHash LoRA", + "size": 4096, + } + + recipe_metadata = { + "title": "NoHash", + "base_model": "Illustrious", + "loras": [ + { + "modelVersionId": 9005, + "modelName": "NoHash LoRA", + "modelVersionName": "V1", + "strength": 1.0, + "hash": "d" * 64, + } + ], + "gen_params": {"steps": 20}, + "tags": [], + } + + result = await _parse( + monkeypatch, + recipe_metadata, + recipe_scanner=_FakeRecipeScanner(_FakeLoraScanner([cache_entry])), + ) + + lora_entry = result["loras"][0] + assert lora_entry["existsLocally"] is False + assert lora_entry["localPath"] is None