fix(download): accept newer CivitAI file types for primary file selection

Downloads failed with "No suitable file found in metadata" for models whose
only file uses newer CivitAI file types (e.g. 'Enhancement LoRA' for
Anima/AIR image-editing LoRAs) because the primary-file allowlist only
covered legacy types.

- unify the weights-type allowlist as MODEL_WEIGHT_FILE_TYPES
  (py/utils/constants.py) and apply it across download, recipe and
  metadata-refresh lookups
- mirror CivitAI's getPrimaryFile() semantics: prefer weights-type primary,
  fall back to weights files, then trust CivitAI's primary flag (excluding
  non-downloadable artifacts like Config/Archive/Workflow)
- mirror the allowlist in the frontend via shared isModelWeightFile() helper
- add regression tests for the Enhancement LoRA primary-file download,
  primary-flag fallback and weights-over-non-weights-primary preference
This commit is contained in:
Will Miao
2026-08-12 21:14:23 +08:00
parent c2f16784b3
commit 303cca0d85
10 changed files with 267 additions and 21 deletions
+9 -2
View File
@@ -21,6 +21,7 @@ from .model_metadata_provider import (
from .downloader import get_downloader
from .errors import RateLimitError, ResourceNotFoundError
from ..utils.civitai_utils import resolve_license_payload
from ..utils.constants import MODEL_WEIGHT_FILE_TYPES
logger = logging.getLogger(__name__)
@@ -538,10 +539,16 @@ class CivitaiClient:
return model_versions[0]
def _extract_primary_model_hash(self, version_entry: Dict[str, Any]) -> Optional[str]:
# Prefer the generic "Model" file (most reliable version identity);
# fall back to any other weights-type primary.
for file_info in version_entry.get("files", []):
if file_info.get("type") == "Model" and file_info.get("primary"):
hashes = file_info.get("hashes", {})
model_hash = hashes.get("SHA256")
model_hash = (file_info.get("hashes", {}) or {}).get("SHA256")
if model_hash:
return model_hash
for file_info in version_entry.get("files", []):
if file_info.get("type") in MODEL_WEIGHT_FILE_TYPES and file_info.get("primary"):
model_hash = (file_info.get("hashes", {}) or {}).get("SHA256")
if model_hash:
return model_hash
return None
+41 -4
View File
@@ -18,6 +18,7 @@ from ..utils.models import LoraMetadata, CheckpointMetadata, EmbeddingMetadata
from ..utils.constants import (
CARD_PREVIEW_WIDTH,
DIFFUSION_MODEL_BASE_MODELS,
MODEL_WEIGHT_FILE_TYPES,
SUPPORTED_DOWNLOAD_SKIP_BASE_MODELS,
VALID_LORA_TYPES,
)
@@ -46,6 +47,11 @@ CIVITAI_DOWNLOAD_URL_PREFIXES = (
)
# File types that are never the intended download target even when CivitAI
# marks them primary — configs/archives/workflows are auxiliary artifacts.
NON_DOWNLOADABLE_PRIMARY_TYPES = ("Config", "Archive", "Workflow", "Training Data")
class DownloadManager:
_instance = None
_lock = asyncio.Lock()
@@ -1500,7 +1506,7 @@ class DownloadManager:
f
for f in files
if f.get("primary")
and f.get("type") in ("Model", "Negative", "Diffusion Model", "UNet")
and f.get("type") in MODEL_WEIGHT_FILE_TYPES
),
None,
)
@@ -1540,21 +1546,52 @@ class DownloadManager:
# Fallback to primary file if no match found
if not file_info:
logger.debug("[download] Looking for primary file as fallback")
# Prefer a weights-type file CivitAI marked primary; then any
# weights-type file (providers without primary flags, e.g.
# civarchive); then trust CivitAI's primary flag regardless of
# type — newer types like 'Enhancement LoRA' are valid primary
# files. Weights files are preferred over non-weights primary
# files so a Config/Archive primary never replaces a Model.
file_info = next(
(
f
for f in files
if f.get("primary") and f.get("type") in ("Model", "Negative", "Diffusion Model", "UNet")
if f.get("primary") and f.get("type") in MODEL_WEIGHT_FILE_TYPES
),
None,
)
if file_info:
logger.debug(
"[download] Fallback primary file selected: id=%s, name=%s",
"[download] Fallback primary file selected (primary + weights): id=%s, name=%s",
file_info.get("id"), file_info.get("name"),
)
else:
logger.debug("[download] No primary file found in fallback lookup")
file_info = next(
(f for f in files if f.get("type") in MODEL_WEIGHT_FILE_TYPES),
None,
)
if file_info:
logger.debug(
"[download] Fallback primary file selected (weights type, no primary flag): id=%s, name=%s",
file_info.get("id"), file_info.get("name"),
)
else:
file_info = next(
(
f
for f in files
if f.get("primary")
and f.get("type") not in NON_DOWNLOADABLE_PRIMARY_TYPES
),
None,
)
if file_info:
logger.debug(
"[download] Fallback primary file selected (trusting CivitAI primary flag): id=%s, name=%s, type=%s",
file_info.get("id"), file_info.get("name"), file_info.get("type"),
)
else:
logger.debug("[download] No primary file found in fallback lookup")
if not file_info:
return {"success": False, "error": "No suitable file found in metadata"}