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:
+2
-2
@@ -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)?",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -155,6 +155,7 @@ MODEL_VARIANT_SUFFIXES = (
|
||||
"conversion",
|
||||
"compressed",
|
||||
"conv",
|
||||
"convrot",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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')})`);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user