From 9c24d92151fb3825e092b4ed5bf20a7545cb8bb4 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Tue, 25 Aug 2026 02:05:45 +0200 Subject: [PATCH] feat: support HuggingFace string identifiers, convrot variants, and mixed/scaled precision tokens - Preserve HuggingFace textual model/version IDs in download metadata instead of coercing to integers - Add convrot as a recognized model variant suffix - Recognize numeric precision tokens with optional mixed/scaled suffixes (e.g. int8mixed, fp8scaled) - Report partial batch search failures as informational notifications rather than errors --- core/matcher.py | 4 +- core/services/download_service.py | 34 +++++++++++----- core/type_utils.py | 1 + tests/test_download_routes.py | 27 +++++++++++++ tests/test_downloads_tab_workflow_route.mjs | 35 ++++++++++++++++ tests/test_matcher.py | 40 +++++++++++++++++++ .../actions/resolve_download_methods.js | 2 +- 7 files changed, 129 insertions(+), 14 deletions(-) diff --git a/core/matcher.py b/core/matcher.py index d7965db..edbfa09 100644 --- a/core/matcher.py +++ b/core/matcher.py @@ -300,7 +300,7 @@ _TECHNICAL_VARIANT_TOKENS = { *TECHNICAL_MODEL_SUFFIXES, } _TECHNICAL_VARIANT_TOKEN_RE = re.compile( - r"^(?:(?:bf|bfloat|fp|float|int|uint|f|i)\d+|" + r"^(?:(?:bf|bfloat|fp|float|int|uint|f|i)\d+(?:mixed|scaled)?|" r"(?:i?q|tq)\d+|(?:mxfp|nvfp)\d+|e\d+m\d+(?:fnuz|fn)?|bnb\d+)$", re.IGNORECASE, ) @@ -544,7 +544,7 @@ def build_filename_search_queries(filename: str) -> List[str]: + "|".join( [ *literal_suffixes, - r"(?:bf|bfloat|fp|float|int|uint|f|i)\d+", + r"(?:bf|bfloat|fp|float|int|uint|f|i)\d+(?:mixed|scaled)?", r"(?:i?q|tq)\d+", r"(?:mxfp|nvfp)\d+", r"e\d+m\d+(?:fnuz|fn)?", diff --git a/core/services/download_service.py b/core/services/download_service.py index f12b651..632da22 100644 --- a/core/services/download_service.py +++ b/core/services/download_service.py @@ -177,16 +177,6 @@ class DownloadService: contract_name="Download path metadata", ) - model_id = coerce_integer_identifier( - raw_model_id, - "model_id", - contract_name="Download metadata", - ) - version_id = coerce_integer_identifier( - raw_version_id, - "version_id", - contract_name="Download metadata", - ) except (TypeError, ValueError) as exc: return self.web.json_response({"error": str(exc)}, status=400) settings = self.load_resolver_settings() @@ -243,6 +233,29 @@ class DownloadService: elif self.host_matches_domain(download_host, "huggingface.co"): inferred_source = "huggingface" + source_name = str(metadata_source or inferred_source).strip().lower() + source_token = source_name.replace("-", "_").replace(" ", "_") + try: + if source_token in {"huggingface", "hugging_face"}: + # Hugging Face identifies repositories and revisions with + # strings such as "owner/repo" and "main". Civitai-style + # numeric coercion would reject valid HF download metadata. + model_id = raw_model_id + version_id = raw_version_id + else: + model_id = coerce_integer_identifier( + raw_model_id, + "model_id", + contract_name="Download metadata", + ) + version_id = coerce_integer_identifier( + raw_version_id, + "version_id", + contract_name="Download metadata", + ) + except (TypeError, ValueError) as exc: + return self.web.json_response({"error": str(exc)}, status=400) + download_metadata.setdefault("filename", filename) download_metadata.setdefault("category", category) download_metadata.setdefault("download_url", url) @@ -252,7 +265,6 @@ class DownloadService: "source", metadata_source or inferred_source, ) - source_name = (metadata_source or inferred_source).lower() try: if ( source_name == "civitai" diff --git a/core/type_utils.py b/core/type_utils.py index 28f328e..e8b43b5 100644 --- a/core/type_utils.py +++ b/core/type_utils.py @@ -155,6 +155,7 @@ MODEL_VARIANT_SUFFIXES = ( "conversion", "compressed", "conv", + "convrot", ) diff --git a/tests/test_download_routes.py b/tests/test_download_routes.py index ceeac4a..b12137f 100644 --- a/tests/test_download_routes.py +++ b/tests/test_download_routes.py @@ -252,6 +252,33 @@ async def test_download_route_supports_huggingface_headers_and_optional_inputs() values["get_default_root_for_category"].assert_not_called() +@pytest.mark.asyncio +async def test_download_route_preserves_huggingface_textual_identifiers(): + handlers, values = _build_download_routes() + handler = handlers[("POST", "/model_resolver/download")] + + response = await handler( + _request( + { + "url": "https://huggingface.co/org/repo/resolve/main/model.safetensors", + "filename": "model.safetensors", + "download_metadata": { + "details_source": "huggingface", + "model_id": "org/repo", + "version_id": "main", + }, + } + ) + ) + + assert response.status == 200 + download_call = values["start_background_download"].call_args.kwargs + assert download_call["metadata"]["model_id"] == "org/repo" + assert download_call["metadata"]["version_id"] == "main" + values["get_civitai_model_details"].assert_not_called() + values["get_civarchive_model_details"].assert_not_called() + + @pytest.mark.asyncio async def test_download_route_rejects_invalid_download_metadata(): handlers, values = _build_download_routes() diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 58a1ab7..6487689 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -3601,6 +3601,41 @@ function extractMethod(source, methodName, paramsPattern = '[^)]*') { throw new Error(`Could not parse ${methodName}`); } +test('completed batch search with failed items uses an informational notification', async () => { + const searchMissingBatch = eval(`(${extractMethod(resolveDownloadMethodsSource, 'searchMissingBatch')})`); + const targets = [{ id: 1 }, { id: 2 }]; + const notifications = []; + const dialog = { + batchSearchRunning: false, + batchSearchCancelRequested: false, + getBatchSearchTargets() { + return targets; + }, + getWorkflowScopedQueueKey() { + return 'workflow-1'; + }, + getSearchStateForWorkflow() { + return {}; + }, + async searchOnline(target) { + if (target === targets[0]) throw new Error('search unavailable'); + }, + showNotification(message, type) { + notifications.push([message, type]); + }, + updateBatchFooterButtons() {}, + closeFooterMenus() {}, + persistSearchStateForActiveWorkflow() {} + }; + + await searchMissingBatch.call(dialog, 'all', 'all'); + + assert.deepEqual(notifications, [ + ['Searching 2 missing models...', 'info'], + ['Finished search for 2 models, 1 failed.', 'info'] + ]); +}); + test('model info hash readers preserve field precedence and casing', () => { const getInfoDialogHash = eval(`(${extractMethod(modelInfoMethodsSource, 'getInfoDialogHash')})`); const getSourceModelFileHash = eval(`(${extractMethod(modelInfoMethodsSource, 'getSourceModelFileHash')})`); diff --git a/tests/test_matcher.py b/tests/test_matcher.py index 3472611..afed5e8 100644 --- a/tests/test_matcher.py +++ b/tests/test_matcher.py @@ -97,6 +97,7 @@ class MatcherTests(unittest.TestCase): "bnb4", "ema-only", "pruned", + "convrot", ] for suffix in suffixes: @@ -108,6 +109,45 @@ class MatcherTests(unittest.TestCase): ), ) + def test_model_family_normalization_handles_compact_precision_variants(self): + self.assertIn( + "Krea2_Turbo", + build_filename_search_queries( + "Krea2_Turbo_convrot_int8mixed.safetensors" + ), + ) + self.assertEqual( + "krea2 turbo", + normalize_model_family_filename( + "Krea2_Turbo_convrot_int8mixed.safetensors" + ), + ) + self.assertEqual( + "krea2 raw", + normalize_model_family_filename( + "Krea2_Raw_convrot_int8mixed.safetensors" + ), + ) + + def test_find_matches_includes_krea2_conversion_variants(self): + target = "krea2_turbo_bf16.safetensors" + candidates = [ + {"filename": "Krea2_Raw_convrot_int8mixed.safetensors"}, + {"filename": "Krea2_Turbo_convrot_int8mixed.safetensors"}, + ] + + matches = find_matches(target, candidates, threshold=0.7) + + self.assertEqual( + [ + "Krea2_Turbo_convrot_int8mixed.safetensors", + "Krea2_Raw_convrot_int8mixed.safetensors", + ], + [match.filename for match in matches], + ) + self.assertEqual(94.0, matches[0].confidence) + self.assertEqual(70.0, matches[1].confidence) + def test_filename_confidence_prioritizes_family_and_size_over_precision(self): target = "qwen3vl-4b-abliterated_fp8_e4m3fn.safetensors" diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index b6defd1..44c7d2a 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -245,7 +245,7 @@ export const resolveDownloadMethods = { if (hasBatchError) { throw batchError; } - this.showNotification(`Finished search for ${completed} model${completed === 1 ? '' : 's'}${suffix}.`, failed ? 'error' : 'success'); + this.showNotification(`Finished search for ${completed} model${completed === 1 ? '' : 's'}${suffix}.`, failed ? 'info' : 'success'); }, stopBatchSearch() {