diff --git a/__init__.py b/__init__.py
index e937ef8..6aa945f 100644
--- a/__init__.py
+++ b/__init__.py
@@ -258,6 +258,7 @@ class ModelResolverExtension:
search_lora_manager_archive_for_file,
clear_search_cache as clear_lora_manager_archive_search_cache,
)
+ from .core.sources import clear_all_search_caches
download_available = True
except ImportError as e:
@@ -3152,10 +3153,7 @@ class ModelResolverExtension:
@json_api_endpoint("Clear search cache")
async def clear_search_cache_route(request):
"""Clear backend search caches after token/settings changes."""
- clear_huggingface_search_cache()
- clear_civitai_search_cache()
- clear_civarchive_search_cache()
- clear_lora_manager_archive_search_cache()
+ clear_all_search_caches()
reload_popular_databases()
reload_model_list()
invalidate_model_files_cache()
@@ -3236,10 +3234,7 @@ class ModelResolverExtension:
async def model_list_update_route(request):
"""Download latest ComfyUI-Manager model-list.json."""
result = await asyncio.to_thread(update_model_list_from_remote)
- clear_huggingface_search_cache()
- clear_civitai_search_cache()
- clear_civarchive_search_cache()
- clear_lora_manager_archive_search_cache()
+ clear_all_search_caches()
self.search_result_timestamps.clear()
return web.json_response(result)
diff --git a/core/resolver.py b/core/resolver.py
index da013c7..8434184 100644
--- a/core/resolver.py
+++ b/core/resolver.py
@@ -696,7 +696,7 @@ def extract_workflow_urls(workflow_json: Dict[str, Any]) -> Dict[str, Dict[str,
return url_map
-def parse_huggingface_url(url: str) -> Tuple[Optional[str], Optional[str]]:
+def get_huggingface_repo_and_file_from_url(url: str) -> Tuple[Optional[str], Optional[str]]:
"""
Extract HuggingFace repo and path from URL.
diff --git a/core/sources/__init__.py b/core/sources/__init__.py
index 4ec901d..345cb90 100644
--- a/core/sources/__init__.py
+++ b/core/sources/__init__.py
@@ -12,8 +12,14 @@ from .huggingface import (
search_huggingface,
search_huggingface_for_file,
get_huggingface_download_url,
+ clear_search_cache as clear_huggingface_search_cache,
+)
+from .civitai import (
+ search_civitai,
+ search_civitai_for_file,
+ get_civitai_download_url,
+ clear_search_cache as clear_civitai_search_cache,
)
-from .civitai import search_civitai, search_civitai_for_file, get_civitai_download_url
from .civarchive import (
clear_search_cache as clear_civarchive_search_cache,
is_civarchive_available,
@@ -30,6 +36,13 @@ from .lora_manager_archive import (
search_lora_manager_archive_for_file,
)
+def clear_all_search_caches() -> None:
+ """Clear search caches for all external sources."""
+ clear_huggingface_search_cache()
+ clear_civitai_search_cache()
+ clear_civarchive_search_cache()
+ clear_lora_manager_archive_search_cache()
+
__all__ = [
"search_popular_models",
"get_popular_model_url",
@@ -52,4 +65,5 @@ __all__ = [
"is_lora_manager_archive_available",
"search_lora_manager_archive",
"search_lora_manager_archive_for_file",
+ "clear_all_search_caches",
]
diff --git a/core/sources/civarchive.py b/core/sources/civarchive.py
index b9e24a8..7bb7d60 100644
--- a/core/sources/civarchive.py
+++ b/core/sources/civarchive.py
@@ -34,6 +34,7 @@ from ..type_utils import (
parse_size_header as _parse_size_header,
parse_content_range_size as _parse_content_range_size,
extract_response_file_size as _response_file_size,
+ parse_civitai_model_path,
)
from ..progress import report_progress, get_progress_reporter
from ..log_system.log_funcs import create_module_logger
@@ -509,16 +510,7 @@ def parse_civarchive_url(url: str) -> Optional[Dict[str, Any]]:
if sha_match:
return {"sha256": sha_match.group(1).lower()}
- model_match = re.search(r"/models/(\d+)", parsed.path)
- if model_match:
- result: Dict[str, Any] = {"model_id": int(model_match.group(1))}
- query = parse_qs(parsed.query)
- version_id = query.get("modelVersionId", [None])[0]
- if version_id:
- result["version_id"] = _coerce_int(version_id)
- return result
-
- return None
+ return parse_civitai_model_path(parsed.path, parsed.query)
def _extract_sha256(value: str) -> Optional[str]:
diff --git a/core/sources/civitai.py b/core/sources/civitai.py
index 232cf18..46e92d3 100644
--- a/core/sources/civitai.py
+++ b/core/sources/civitai.py
@@ -28,6 +28,7 @@ from ..type_utils import (
get_version_sort_key,
check_credential_http,
DEFAULT_BROWSER_USER_AGENT,
+ parse_civitai_model_path,
)
from ..progress import report_progress, get_progress_reporter
from ..path_utils import calculate_file_sha256, get_filename_from_path
@@ -684,15 +685,7 @@ def parse_civitai_url(url: str) -> Optional[Dict[str, Any]]:
if match:
return {"version_id": int(match.group(1))}
- match = re.search(r"/models/(\d+)", parsed.path)
- if match:
- result = {"model_id": int(match.group(1))}
- query = parse_qs(parsed.query)
- if "modelVersionId" in query:
- result["version_id"] = int(query["modelVersionId"][0])
- return result
-
- return None
+ return parse_civitai_model_path(parsed.path, parsed.query)
def get_civitai_download_url(version_id: int, api_key: Optional[str] = None) -> str:
diff --git a/core/type_utils.py b/core/type_utils.py
index 8c843f6..7cc4d1a 100644
--- a/core/type_utils.py
+++ b/core/type_utils.py
@@ -503,3 +503,26 @@ def extract_response_file_size(response: Any) -> Optional[int]:
return size
return None
+
+
+def parse_civitai_model_path(path: str, query_string: str) -> Optional[Dict[str, int]]:
+ """
+ Common helper to parse `/models/{model_id}?modelVersionId={version_id}` URLs
+ for CivitAI and CivArchive.
+ """
+ import re
+ from urllib.parse import parse_qs
+
+ model_match = re.search(r"/models/(\d+)", path)
+ if model_match:
+ result = {"model_id": int(model_match.group(1))}
+ query = parse_qs(query_string)
+ version_id_list = query.get("modelVersionId")
+ if version_id_list:
+ try:
+ result["version_id"] = int(version_id_list[0])
+ except (ValueError, TypeError):
+ pass
+ return result
+ return None
+
diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js
index 06bf059..835ecc0 100644
--- a/web/resolver/actions/queue_methods.js
+++ b/web/resolver/actions/queue_methods.js
@@ -2296,7 +2296,7 @@ export const queueMethods = {
text = text.split('/').pop() || text;
}
if (stem) {
- text = text.replace(/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)$/i, '');
+ text = this.stripModelExtension(text);
}
return text;
},
diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js
index fdbba59..d85c67d 100644
--- a/web/resolver/search/search_panel.js
+++ b/web/resolver/search/search_panel.js
@@ -150,7 +150,7 @@ export const searchPanelMethods = {
.split(/[\\/]+/)
.map(part => part.trim())
.filter(Boolean);
- const directoryParts = parts.filter(part => !/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)$/i.test(part));
+ const directoryParts = parts.filter(part => !this.hasModelExtension(part));
for (const part of directoryParts) {
const exact = this.resolveBaseModelAliasExact(part);
@@ -311,7 +311,7 @@ export const searchPanelMethods = {
if (/^urn:/i.test(value.trim())) {
return;
}
- if (/\.(safetensors|ckpt|pt|pth|bin|gguf|onnx)\b/i.test(value) || /model|checkpoint|unet|diffusion/i.test(key)) {
+ if (this.hasModelExtension(value) || /model|checkpoint|unet|diffusion/i.test(key)) {
values.push(value);
}
return;
diff --git a/web/resolver/utils/render_format_methods.js b/web/resolver/utils/render_format_methods.js
index 4296661..59b14ef 100644
--- a/web/resolver/utils/render_format_methods.js
+++ b/web/resolver/utils/render_format_methods.js
@@ -335,5 +335,26 @@ export const renderFormatMethods = {
normalizePathToBackward(value) {
return String(value || '').trim().replace(/\//g, '\\');
+ },
+
+ stripModelExtension(value) {
+ return stripModelExtension(value);
+ },
+
+ hasModelExtension(value) {
+ return hasModelExtension(value);
}
};
+
+export const MODEL_EXTENSIONS_REGEX = /\.(safetensors|ckpt|pt2?|bin|pth|pkl|sft|onnx|gguf)(?:$|[?#])/i;
+
+export function stripModelExtension(value) {
+ if (!value) return '';
+ return String(value).replace(MODEL_EXTENSIONS_REGEX, '');
+}
+
+export function hasModelExtension(value) {
+ if (!value) return false;
+ return MODEL_EXTENSIONS_REGEX.test(value);
+}
+
diff --git a/web/resolver/views/model_info_methods.js b/web/resolver/views/model_info_methods.js
index 5492cb7..2462c4b 100644
--- a/web/resolver/views/model_info_methods.js
+++ b/web/resolver/views/model_info_methods.js
@@ -1056,7 +1056,7 @@ export const modelInfoMethods = {
}
} catch (e) {
// Fall back to direct search
- const searchName = name.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
+ const searchName = this.stripModelExtension(name);
const searchUrl = `https://civitai.com/search?q=${encodeURIComponent(searchName)}`;
window.open(searchUrl, '_blank');
}
@@ -1154,7 +1154,7 @@ export const modelInfoMethods = {
},
createInfoDialog(loraName, modelData) {
- const loraDisplayName = loraName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
+ const loraDisplayName = this.stripModelExtension(loraName);
const dialog = document.createElement('div');
dialog.className = 'mr-info-dialog-backdrop';
@@ -1917,7 +1917,7 @@ export const modelInfoMethods = {
buildLocalInfoDialogData(loraName, modelData = {}) {
const filename = loraName || modelData?.filename || modelData?.name || '';
- const modelName = String(filename || '').replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
+ const modelName = this.stripModelExtension(filename);
const resolvedPath = this.getModelInfoResolvedPath(modelData);
const categoryTypeMap = {
checkpoints: 'checkpoint',
diff --git a/web/resolver/views/tabs_loaded_methods.js b/web/resolver/views/tabs_loaded_methods.js
index fade9f9..3a571cd 100644
--- a/web/resolver/views/tabs_loaded_methods.js
+++ b/web/resolver/views/tabs_loaded_methods.js
@@ -168,6 +168,8 @@ export const tabsLoadedMethods = {
const activeCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.active.length, 0);
const inactiveCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.inactive.length, 0);
+ const stripModelExtension = (value) => this.stripModelExtension(value || 'Unknown');
+
const buildCategoryStrings = (filter) => {
const result = {};
for (const [category, modelsObj] of Object.entries(byCategory)) {
@@ -175,10 +177,7 @@ export const tabsLoadedMethods = {
const models = filter === 'active' ? modelsObj.active : filter === 'inactive' ? modelsObj.inactive : [...modelsObj.active, ...modelsObj.inactive];
const parts = models.map(model => {
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
- let name = fullName;
- if (fullName.match(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i)) {
- name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
- }
+ const name = stripModelExtension(fullName);
const strength = model.strength !== null && model.strength !== undefined
? model.strength.toFixed(2)
: '1.00';
@@ -194,7 +193,6 @@ export const tabsLoadedMethods = {
const allString = buildCategoryStrings('all');
const copyIcon = getSvgIcon('copy', 'currentColor', 'mr-copy-btn-icon');
- const stripModelExtension = (value) => String(value || 'Unknown').replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const modelToken = (model, category) => {
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
const name = stripModelExtension(fullName);
@@ -253,7 +251,7 @@ export const tabsLoadedMethods = {
for (const model of modelsObj.active) {
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
- const name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
+ const name = this.stripModelExtension(fullName);
const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null;
html += `${this.escapeHtml(name)}${strength !== null ? `${this.escapeHtml(strength)}` : ''}`;
}
@@ -272,7 +270,7 @@ export const tabsLoadedMethods = {
for (const model of modelsObj.inactive) {
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
- const name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
+ const name = this.stripModelExtension(fullName);
const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null;
html += `${this.escapeHtml(name)}${strength !== null ? `${this.escapeHtml(strength)}` : ''}`;
}