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
This commit is contained in:
Dariusz L
2026-08-25 02:05:45 +02:00
parent ddc5333bd2
commit 9c24d92151
7 changed files with 129 additions and 14 deletions
+2 -2
View File
@@ -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)?",
+23 -11
View File
@@ -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"
+1
View File
@@ -155,6 +155,7 @@ MODEL_VARIANT_SUFFIXES = (
"conversion",
"compressed",
"conv",
"convrot",
)
+27
View File
@@ -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()
@@ -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')})`);
+40
View File
@@ -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"
@@ -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() {