fix: route GGUF-only Gemma models safely

This commit is contained in:
Marcel Hofer
2026-08-21 09:33:18 +02:00
parent ac6de2611d
commit a2bbcd92fa
5 changed files with 292 additions and 85 deletions
+92
View File
@@ -366,6 +366,46 @@
"defaults": {
"context_length": 131072
}
},
"Gemma-4-E4B-Uncensored-Aggressive-GGUF [Q2:4.4GB|Q3:4.7-4.9GB|Q4:5.1-5.4GB|Q5:5.8GB|Q6:6.3GB|Q8:8.1GB|VRAM:~7GB]": {
"author": "HauhauCS",
"repo_name": "Gemma-4-E4B-Uncensored-HauhauCS-Aggressive",
"repo_id": "HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive",
"commercial_status": "unclear",
"model_files": [
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-IQ3_M.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q2_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q3_K_M.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q3_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q5_K_M.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q5_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q6_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q8_K_P.gguf"
],
"defaults": {
"context_length": 131072
}
},
"Gemma-4-E2B-Uncensored-Aggressive-GGUF [Q2:3GB|Q3:3.1GB|Q4:3.4GB|Q5:3.7GB|Q6:3.9GB|Q8:5GB|VRAM:~5GB]": {
"author": "HauhauCS",
"repo_name": "Gemma-4-E2B-Uncensored-HauhauCS-Aggressive",
"repo_id": "HauhauCS/Gemma-4-E2B-Uncensored-HauhauCS-Aggressive",
"commercial_status": "unclear",
"model_files": [
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-IQ3_M.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q2_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q3_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q4_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q5_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q6_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q8_K_P.gguf"
],
"defaults": {
"context_length": 131072
}
}
},
"qwenVL_model": {
@@ -611,6 +651,58 @@
"pool_size": 4194304
}
},
"Gemma-4-E4B-Uncensored-Aggressive-GGUF [Q2:4.4GB|Q3:4.7-4.9GB|Q4:5.1-5.4GB|Q5:5.8GB|Q6:6.3GB|Q8:8.1GB|VRAM:~7GB]": {
"author": "HauhauCS",
"repo_name": "Gemma-4-E4B-Uncensored-HauhauCS-Aggressive",
"repo_id": "HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive",
"commercial_status": "unclear",
"mmproj_file": "mmproj-Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-f16.gguf",
"model_files": [
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-IQ3_M.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q2_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q3_K_M.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q3_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q5_K_M.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q5_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q6_K_P.gguf",
"Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q8_K_P.gguf"
],
"defaults": {
"context_length": 131072,
"image_max_tokens": 4096,
"n_batch": 512,
"gpu_layers": -1,
"top_k": 64,
"pool_size": 4194304
}
},
"Gemma-4-E2B-Uncensored-Aggressive-GGUF [Q2:3GB|Q3:3.1GB|Q4:3.4GB|Q5:3.7GB|Q6:3.9GB|Q8:5GB|VRAM:~5GB]": {
"author": "HauhauCS",
"repo_name": "Gemma-4-E2B-Uncensored-HauhauCS-Aggressive",
"repo_id": "HauhauCS/Gemma-4-E2B-Uncensored-HauhauCS-Aggressive",
"commercial_status": "unclear",
"mmproj_file": "mmproj-Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-f16.gguf",
"model_files": [
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-IQ3_M.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q2_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q3_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q4_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q5_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q6_K_P.gguf",
"Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q8_K_P.gguf"
],
"defaults": {
"context_length": 131072,
"image_max_tokens": 4096,
"n_batch": 512,
"gpu_layers": -1,
"top_k": 64,
"pool_size": 4194304
}
},
"Qwen3-VL-4B-Instruct-c_abliterated-v2-GGUF [Q4:2.4GB|Q5:2.8GB|Q6:3.2GB|Q8:4.2GB|F16:8GB|VRAM:~4GB]": {
"author": "mradermacher",
"repo_name": "Qwen3-VL-4B-Instruct-c_abliterated-v2-GGUF",
-44
View File
@@ -209,28 +209,6 @@
"8bit": 22.0,
"4bit": 14.0
}
},
"Gemma-4-E4B-Uncensored-Aggressive [DL: 16GB, VRAM: 7.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive",
"commercial_status": "unclear",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 7.0,
"8bit": 4.5,
"4bit": 2.5
}
},
"Gemma-4-E2B-Uncensored-Aggressive [DL: 10.3GB, VRAM: 4.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E2B-Uncensored-HauhauCS-Aggressive",
"commercial_status": "unclear",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 4.0,
"8bit": 2.5,
"4bit": 1.5
}
}
},
"hf_text_models": {
@@ -397,28 +375,6 @@
"8bit": 22.0,
"4bit": 14.0
}
},
"Gemma-4-E4B-Uncensored-Aggressive [DL: 16GB, VRAM: 7.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive",
"commercial_status": "unclear",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 7.0,
"8bit": 4.5,
"4bit": 2.5
}
},
"Gemma-4-E2B-Uncensored-Aggressive [DL: 10.3GB, VRAM: 4.0GB]": {
"repo_id": "HauhauCS/Gemma-4-E2B-Uncensored-HauhauCS-Aggressive",
"commercial_status": "unclear",
"default": false,
"quantized": false,
"vram_requirement": {
"full": 4.0,
"8bit": 2.5,
"4bit": 1.5
}
}
}
}
+132 -1
View File
@@ -106,6 +106,18 @@ def build_loader_test_stubs() -> dict[str, types.ModuleType]:
),
"huggingface_hub": build_stub_module(
"huggingface_hub",
HfApi=type(
"HfApi",
(),
{
"__init__": lambda self, *args, **kwargs: None,
"list_repo_files": lambda self, *args, **kwargs: [
"config.json",
"model.safetensors",
"processor_config.json",
],
},
),
hf_hub_download=lambda *args, **kwargs: "",
snapshot_download=lambda *args, **kwargs: "",
),
@@ -759,7 +771,11 @@ class TestModelRecommendations(unittest.TestCase):
def test_gemma4_e2b_text_catalog_uses_current_hugging_face_files(self):
payload = json.loads((PKG / "gguf_models.json").read_text(encoding="utf-8"))
entries = [entry for key, entry in (payload.get("Qwen_model") or {}).items() if "Gemma-4-E2B" in key]
entries = [
entry
for key, entry in (payload.get("Qwen_model") or {}).items()
if "Gemma-4-E2B" in key and "Aggressive" not in key
]
self.assertTrue(entries)
for entry in entries:
@@ -770,6 +786,33 @@ class TestModelRecommendations(unittest.TestCase):
"google_gemma-4-E2B-it-bf16.gguf",
])
def test_hauhaucs_gemma4_gguf_only_models_are_not_in_transformers_catalogs(self):
hf_payload = json.loads((PKG / "hf_models.json").read_text(encoding="utf-8"))
gguf_payload = json.loads((PKG / "gguf_models.json").read_text(encoding="utf-8"))
expected = {
"HauhauCS/Gemma-4-E4B-Uncensored-HauhauCS-Aggressive": {
"model": "Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf",
"mmproj": "mmproj-Gemma-4-E4B-Uncensored-HauhauCS-Aggressive-f16.gguf",
},
"HauhauCS/Gemma-4-E2B-Uncensored-HauhauCS-Aggressive": {
"model": "Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-Q4_K_P.gguf",
"mmproj": "mmproj-Gemma-4-E2B-Uncensored-HauhauCS-Aggressive-f16.gguf",
},
}
for hf_section in ("hf_vl_models", "hf_text_models"):
repo_ids = {entry.get("repo_id") for entry in (hf_payload.get(hf_section) or {}).values()}
self.assertTrue(expected.keys().isdisjoint(repo_ids))
for repo_id, filenames in expected.items():
text_entries = [entry for entry in (gguf_payload.get("Qwen_model") or {}).values() if entry.get("repo_id") == repo_id]
vision_entries = [entry for entry in (gguf_payload.get("qwenVL_model") or {}).values() if entry.get("repo_id") == repo_id]
self.assertEqual(1, len(text_entries))
self.assertEqual(1, len(vision_entries))
self.assertIn(filenames["model"], text_entries[0]["model_files"])
self.assertIn(filenames["model"], vision_entries[0]["model_files"])
self.assertEqual(filenames["mmproj"], vision_entries[0]["mmproj_file"])
def test_qwen3_asr_gguf_catalog_is_removed(self):
gguf_payload = json.loads((PKG / "gguf_models.json").read_text(encoding="utf-8"))
vision_catalog = gguf_payload.get("qwenVL_model") or {}
@@ -2103,6 +2146,94 @@ class TestHuggingFaceTokenSupport(unittest.TestCase):
self.assertIn("<redacted HF token>", message)
self.assertIn("access was rejected", message)
def test_hf_snapshot_rejects_gguf_only_repo_before_download_and_preserves_target(self):
module = self._load_qwenvl_with_stubs()
access_globals = module._download_model_snapshot.__globals__
snapshot_download_mock = mock.Mock(side_effect=AssertionError("GGUF-only repository must not be downloaded as Transformers"))
class FakeHfApi:
def __init__(self, *args, **kwargs):
pass
def list_repo_files(self, **kwargs):
return ["README.md", "model-Q4_K_M.gguf", "mmproj-f16.gguf"]
with tempfile.TemporaryDirectory() as temp_dir:
target = Path(temp_dir) / "existing-model"
target.mkdir()
sentinel = target / "user-file.gguf"
sentinel.write_bytes(b"preserve me")
with mock.patch.dict(access_globals, {"HfApi": FakeHfApi, "snapshot_download": snapshot_download_mock}):
with self.assertRaisesRegex(RuntimeError, "ThinkingLLM GGUF node"):
module._download_model_snapshot("example/gguf-only", target)
self.assertEqual(b"preserve me", sentinel.read_bytes())
snapshot_download_mock.assert_not_called()
def test_hf_snapshot_stages_validation_and_preserves_existing_files(self):
module = self._load_qwenvl_with_stubs()
access_globals = module._download_model_snapshot.__globals__
download_targets = []
class FakeHfApi:
def __init__(self, *args, **kwargs):
pass
def list_repo_files(self, **kwargs):
return ["config.json", "model.safetensors", "processor_config.json"]
def fake_snapshot_download(**kwargs):
staging = Path(kwargs["local_dir"])
download_targets.append(staging)
staging.mkdir(parents=True)
(staging / "config.json").write_text("{}", encoding="utf-8")
(staging / "processor_config.json").write_text("{}", encoding="utf-8")
(staging / "model.safetensors").write_bytes(b"weights")
with tempfile.TemporaryDirectory() as temp_dir:
target = Path(temp_dir) / "existing-model"
target.mkdir()
sentinel = target / "user-note.txt"
sentinel.write_text("preserve me", encoding="utf-8")
with mock.patch.dict(access_globals, {"HfApi": FakeHfApi, "snapshot_download": mock.Mock(side_effect=fake_snapshot_download)}):
module._download_model_snapshot("example/transformers", target, require_processor=True)
self.assertEqual("preserve me", sentinel.read_text(encoding="utf-8"))
self.assertEqual(b"weights", (target / "model.safetensors").read_bytes())
self.assertEqual(1, len(download_targets))
self.assertNotEqual(target, download_targets[0])
def test_hf_snapshot_validation_failure_does_not_modify_existing_target(self):
module = self._load_qwenvl_with_stubs()
access_globals = module._download_model_snapshot.__globals__
class FakeHfApi:
def __init__(self, *args, **kwargs):
pass
def list_repo_files(self, **kwargs):
return ["config.json", "model.safetensors"]
def fake_incomplete_download(**kwargs):
staging = Path(kwargs["local_dir"])
staging.mkdir(parents=True)
(staging / "config.json").write_text("{}", encoding="utf-8")
with tempfile.TemporaryDirectory() as temp_dir:
target = Path(temp_dir) / "existing-model"
target.mkdir()
sentinel = target / "user-note.txt"
sentinel.write_text("preserve me", encoding="utf-8")
with mock.patch.dict(
access_globals,
{"HfApi": FakeHfApi, "snapshot_download": mock.Mock(side_effect=fake_incomplete_download)},
):
with self.assertRaisesRegex(RuntimeError, "existing target was preserved"):
module._download_model_snapshot("example/incomplete", target)
self.assertEqual([sentinel], list(target.iterdir()))
self.assertEqual("preserve me", sentinel.read_text(encoding="utf-8"))
def test_hf_file_download_failure_raises_redacted_message(self):
module = self._load_qwenvl_with_stubs()
access_globals = module.download_hf_file_to_path.__globals__
+1 -11
View File
@@ -689,22 +689,12 @@ def ensure_model(model_name, require_processor=False, node_id=None, progress_lab
_download_model_snapshot(
repo_id,
target,
require_processor=require_processor,
node_id=node_id,
progress_label=progress_label or f"QwenVL HF Download: {model_name}",
hf_token=hf_token,
)
if not _model_snapshot_has_required_files(target, require_processor=require_processor):
print(f"[QwenVL] Refreshed snapshot is still incomplete, retrying clean download: {target}")
_download_model_snapshot(
repo_id,
target,
force_clean_target=True,
node_id=node_id,
progress_label=progress_label or f"QwenVL HF Download: {model_name}",
hf_token=hf_token,
)
if not _model_snapshot_has_required_files(target, require_processor=require_processor):
missing = []
if not (target / "config.json").exists():
+67 -29
View File
@@ -3,13 +3,15 @@
import inspect
import os
import shutil
import tempfile
import time
from pathlib import Path
try:
from huggingface_hub import hf_hub_download, snapshot_download
from huggingface_hub import HfApi, hf_hub_download, snapshot_download
except ImportError:
from huggingface_hub import snapshot_download
HfApi = None
hf_hub_download = None
try:
from tqdm.auto import tqdm
@@ -37,6 +39,12 @@ MODEL_STATUS_PRESENTATION = {
}
MODEL_INSTALLED_SUFFIX = " [installed]"
MODEL_LOCAL_PREFIX = "[local] "
_PROCESSOR_CONFIG_FILENAMES = (
"preprocessor_config.json",
"processor_config.json",
"image_processor_config.json",
"video_preprocessor_config.json",
)
def _format_download_size(num_bytes) -> str:
@@ -403,43 +411,73 @@ def _model_snapshot_has_required_files(target: Path, require_processor: bool = F
return False
if not (target / "config.json").exists():
return False
if require_processor:
processor_files = (
"preprocessor_config.json",
"processor_config.json",
"image_processor_config.json",
"video_preprocessor_config.json",
)
if not any((target / name).exists() for name in processor_files):
return False
if require_processor and not any((target / name).exists() for name in _PROCESSOR_CONFIG_FILENAMES):
return False
return True
def _download_model_snapshot(repo_id: str, target: Path, *, force_clean_target: bool = False, node_id=None, progress_label: str | None = None, hf_token: str | None = None) -> None:
def _validate_transformers_repo_files(repo_id: str, repo_files: list[str], *, require_processor: bool = False) -> None:
root_files = {Path(str(filename)).as_posix() for filename in repo_files if filename}
missing = []
if "config.json" not in root_files:
missing.append("config.json")
if not any("/" not in filename and filename.endswith((".safetensors", ".bin")) for filename in root_files):
missing.append("Transformers model weights")
if require_processor and not any(filename in root_files for filename in _PROCESSOR_CONFIG_FILENAMES):
missing.append("processor metadata")
if not missing:
return
gguf_hint = " The repository contains GGUF files; select it through a ThinkingLLM GGUF node instead." if any(
filename.lower().endswith(".gguf") for filename in root_files
) else ""
raise ValueError(
f"[QwenVL] {repo_id} is not a compatible Transformers snapshot: missing {', '.join(missing)}.{gguf_hint}"
)
def _download_model_snapshot(repo_id: str, target: Path, *, require_processor: bool = False, node_id=None, progress_label: str | None = None, hf_token: str | None = None) -> None:
if COMMERCIAL_RELEASE:
raise RuntimeError("[QwenVL] Commercial release mode only permits preinstalled, hash-locked models.")
reporter = _DownloadProgressReporter(node_id=node_id, label=progress_label or "QwenVL HF Download", repo_id=repo_id)
token_for_download = _clean_hf_token(hf_token)
download_kwargs = {}
if force_clean_target and target.exists() and target.is_dir():
print(f"[QwenVL] Removing incomplete model snapshot before retry: {target}")
reporter.stage("Cleaning incomplete snapshot", detail=target.name)
shutil.rmtree(target, ignore_errors=False)
reporter.stage("Preparing download", detail=target.name)
target.parent.mkdir(parents=True, exist_ok=True)
reporter.stage("Checking repository", detail=target.name)
try:
download_kwargs = {
"repo_id": repo_id,
"local_dir": str(target),
"ignore_patterns": ["*.md", ".git*"],
"force_download": force_clean_target,
}
if token_for_download:
download_kwargs["token"] = token_for_download
tqdm_cls = reporter.make_tqdm_class()
if tqdm_cls is not None:
download_kwargs["tqdm_class"] = tqdm_cls
snapshot_download(**download_kwargs)
_cleanup_unneeded_snapshot_weights(target)
if HfApi is None:
raise RuntimeError("huggingface_hub.HfApi is unavailable; update huggingface-hub before downloading models")
api = HfApi(token=token_for_download) if token_for_download else HfApi()
repo_files = api.list_repo_files(repo_id=repo_id, repo_type="model")
_validate_transformers_repo_files(repo_id, repo_files, require_processor=require_processor)
with tempfile.TemporaryDirectory(prefix=f".{target.name}.download-", dir=target.parent) as temp_dir:
staging_target = Path(temp_dir) / "snapshot"
reporter.stage("Preparing download", detail=target.name)
download_kwargs = {
"repo_id": repo_id,
"local_dir": str(staging_target),
"ignore_patterns": ["*.md", ".git*"],
}
if token_for_download:
download_kwargs["token"] = token_for_download
tqdm_cls = reporter.make_tqdm_class()
if tqdm_cls is not None:
download_kwargs["tqdm_class"] = tqdm_cls
snapshot_download(**download_kwargs)
_cleanup_unneeded_snapshot_weights(staging_target)
if not _model_snapshot_has_required_files(staging_target, require_processor=require_processor):
raise FileNotFoundError(
f"[QwenVL] Downloaded model snapshot is incomplete for {repo_id}; the existing target was preserved"
)
reporter.stage("Installing validated snapshot", detail=target.name)
if target.exists():
if not target.is_dir():
raise FileExistsError(f"[QwenVL] Model target exists and is not a directory: {target}")
shutil.copytree(staging_target, target, dirs_exist_ok=True)
else:
shutil.move(str(staging_target), str(target))
except Exception as exc:
reporter.fail(_hf_download_error_message(exc, repo_id=repo_id, hf_token=token_for_download))
raise RuntimeError(_hf_download_error_message(exc, repo_id=repo_id, hf_token=token_for_download)) from exc