feat: Enhance Qwen3.8 model integration and duration handling

- Added Qwen3.8-27B-ABLITERATED-GGUF model to the model catalog with detailed specifications and context length.
- Improved duration widget behavior to ensure it remains hidden for non-video presets and preserves last valid values.
- Updated documentation to clarify duration handling and widget visibility.
- Expanded test coverage for model catalog labels and Qwen3.8 model presence in both text and vision catalogs.
- Refactored model access functions to better manage installed models and local paths.
- Enhanced tooltips and recommendations for model selection, emphasizing installed and local models.
- Fixed issues with dynamic duration-widget serialization and visibility in the frontend.
This commit is contained in:
Marcel Hofer
2026-08-16 11:11:05 +02:00
parent 6a46a78deb
commit ec7cd7f84d
11 changed files with 505 additions and 81 deletions
+5 -14
View File
@@ -60,6 +60,7 @@ from thinkingllm_core.media import (
from thinkingllm_core.model_access import (
download_hf_file_to_path,
enforce_model_access,
mark_catalog_files_installed,
model_catalog_options,
normalize_commercial_status,
resolve_model_catalog_name,
@@ -181,7 +182,7 @@ def _quant_from_filename(filename: str) -> str | None:
"""Extract quantization level from a GGUF filename stem.
E.g. 'gemma-4-E4B-it-Q4_K_M.gguf' -> 'Q4'."""
name = Path(filename).stem.upper()
for q in ('BF16', 'F16', 'F32', 'Q8_0', 'Q6_K', 'Q5_K_M', 'Q5_K_S', 'Q4_K_M', 'Q4_K_S', 'Q3_K_M', 'Q2_K'):
for q in ('BF16', 'F16', 'F32', 'Q8_0', 'Q6_K', 'Q5_K_M', 'Q5_K_S', 'Q4_K_M', 'Q4_K_S', 'Q3_K_M', 'Q3_K_S', 'Q2_K'):
if q in name:
return _QUANT_CANONICAL.get(q, q)
return None
@@ -191,7 +192,7 @@ _QUANT_CANONICAL = {
'Q8_0': 'Q8', 'Q6_K': 'Q6',
'Q5_K_M': 'Q5', 'Q5_K_S': 'Q5',
'Q4_K_M': 'Q4', 'Q4_K_S': 'Q4',
'Q3_K_M': 'Q3', 'Q2_K': 'Q2',
'Q3_K_M': 'Q3', 'Q3_K_S': 'Q3', 'Q2_K': 'Q2',
}
import re
@@ -582,17 +583,7 @@ def _load_gguf_vl_catalog():
# Mark catalog entries already present on disk, then add uncatalogued local models.
search_dirs = _gguf_search_dirs(base_dir)
installed_filenames: set[str] = set()
for directory in search_dirs:
try:
installed_filenames.update(path.name for path in directory.rglob("*.gguf") if path.is_file())
except (OSError, PermissionError):
pass
for display, entry in list(flattened.items()):
filenames = _filename_search_candidates(entry.get("filename", ""), entry.get("local_filenames"))
if any(filename in installed_filenames for filename in filenames):
flattened.pop(display)
flattened[f"{display} [installed]"] = {**entry, "catalog_display_name": display}
mark_catalog_files_installed(flattened, search_dirs)
existing_filenames = {Path(e.get("filename", "")).name for e in flattened.values() if e.get("filename")}
for scan_dir in search_dirs:
@@ -607,7 +598,7 @@ def _load_gguf_vl_catalog():
GGUF_VL_CATALOG = _load_gguf_vl_catalog()
GGUF_TOOLTIPS = {
"model_name": "GGUF vision model from gguf_models.json or auto-detected local files. [installed] means the catalog model file was found on disk; [local] means an uncatalogued local model. Missing GGUF or mmproj files are downloaded on first use.",
"model_name": "GGUF vision model from gguf_models.json or auto-detected local files. [installed] means the catalog model was found in a configured GGUF/LLM location; [local] means an uncatalogued local model. Missing GGUF or mmproj files are downloaded on first use.",
"audio_model_name": "Gemma 4 audio-capable GGUF model. Only Gemma 4 E2B, E4B, and 12B are listed; 26B/31B variants are image/text-only for this purpose.",
"audio_file_path": "Optional local audio file path. M4A, MP3, WAV, FLAC, and other FFmpeg-readable files are decoded to 16 kHz mono WAV before inference.",
"device": "auto prefers CUDA when PyTorch sees an NVIDIA GPU. If RAW_TRACE says GPU offload is no or unknown, verify your llama-cpp-python CUDA wheel before blaming the model.",
+7 -4
View File
@@ -46,6 +46,7 @@ from thinkingllm_core.hf_models import ensure_cuda_vram_headroom
from thinkingllm_core.model_access import (
download_hf_file_to_path,
enforce_model_access,
mark_catalog_files_installed,
model_catalog_options,
resolve_model_catalog_name,
)
@@ -85,7 +86,7 @@ from AILab_QwenVL_GGUF import (
GGUF_PROMPT_TOOLTIPS = {
"model_name": "GGUF text model from config or auto-detected local files. First run downloads the selected GGUF when it is not already on disk.",
"model_name": "GGUF text model from config or auto-detected local files. [installed] means the catalog model was found in a configured GGUF/LLM location; [local] means an uncatalogued local model. Missing GGUF files download on first use.",
"prompt_text": "Prompt text to enhance. Leave blank to emit the selected preset instruction as the base prompt.",
"preset_system_prompt": "Preset enhancement style. Use Custom Only when you want custom_system_prompt to fully control the instruction.",
"custom_system_prompt": "Optional extra instruction. Required when using Custom Only; otherwise it is prepended to the selected style.",
@@ -126,7 +127,7 @@ def _quant_from_filename(filename: str) -> str | None:
E.g. 'gemma-4-E4B-it-Q4_K_M.gguf' -> 'Q4'."""
from pathlib import Path
name = Path(filename).stem.upper()
for q in ('BF16', 'F16', 'F32', 'Q8_0', 'Q6_K', 'Q5_K_M', 'Q5_K_S', 'Q4_K_M', 'Q4_K_S', 'Q3_K_M', 'Q2_K'):
for q in ('BF16', 'F16', 'F32', 'Q8_0', 'Q6_K', 'Q5_K_M', 'Q5_K_S', 'Q4_K_M', 'Q4_K_S', 'Q3_K_M', 'Q3_K_S', 'Q2_K'):
if q in name:
return _QUANT_CANONICAL.get(q, q)
return None
@@ -136,7 +137,7 @@ _QUANT_CANONICAL = {
'Q8_0': 'Q8', 'Q6_K': 'Q6',
'Q5_K_M': 'Q5', 'Q5_K_S': 'Q5',
'Q4_K_M': 'Q4', 'Q4_K_S': 'Q4',
'Q3_K_M': 'Q3', 'Q2_K': 'Q2',
'Q3_K_M': 'Q3', 'Q3_K_S': 'Q3', 'Q2_K': 'Q2',
}
_EMPTY_THINK_RE = re.compile(r"<think[^>]*>\s*</think>", flags=re.IGNORECASE | re.DOTALL)
@@ -531,8 +532,10 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
# Scan filesystem for locally available models not in JSON config
# Collect all directories to scan: the configured base_dir + any extra LLM paths from ComfyUI
search_dirs = _gguf_search_dirs(base_dir)
mark_catalog_files_installed(models, search_dirs)
existing_filenames = {Path(e.get("filename", "")).name for e in models.values() if e.get("filename")}
for scan_dir in _gguf_search_dirs(base_dir):
for scan_dir in search_dirs:
local_models = ThinkingLLM_QwenVL_GGUF_PromptEnhancer._scan_local_gguf_text_models(scan_dir, existing_filenames)
models.update(local_models)
existing_filenames.update(Path(e.get("filename", "")).name for e in local_models.values() if e.get("filename"))
+7 -1
View File
@@ -118,7 +118,7 @@ HF tokenizer note: some Gemma and Qwen variants need `sentencepiece` or `tiktoke
#### Shared ComfyUI text encoders
Compatible single-file checkpoints already in `ComfyUI/models/text_encoders` appear in the model selector with a `[ComfyUI]` prefix. ThinkingLLM loads them through ComfyUI, so it does not download or duplicate a Transformers snapshot.
Compatible single-file checkpoints already in `ComfyUI/models/text_encoders` appear as `[local] filename (text_encoders)` in every applicable model selector. ThinkingLLM loads them through ComfyUI, so it does not download or duplicate a Transformers snapshot. Older workflows saved with the former `[ComfyUI]` label remain compatible.
- Qwen3-VL 4B and 8B `.safetensors`, including `qwen3vl_8b_fp8_scaled.safetensors`
- Gemma 3 12B `.safetensors`, including `gemma-3-12b-it-heretic-v2_int8.safetensors`
@@ -134,6 +134,8 @@ The MiniMax H3/Qwen3-VL 32B conditioning checkpoint is intentionally excluded be
The GGUF path requires a multimodal-capable `llama-cpp-python` build. The normal PyPI package may not include the chat handlers needed for Qwen vision, Gemma 4, or MTMD audio. On Linux, ThinkingLLM auto-checks this at first GGUF use and attempts an automatic install of a matching JamePeng backend. Use the setup notes in [docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md](docs/LLAMA_CPP_PYTHON_VISION_INSTALL.md) if you need to override the wheel/package source.
Model labels are consistent across the standard, Advanced, audio, and Prompt Enhancer selectors: `[installed]` marks a catalog model found in any configured `LLM`, `GGUF`, or `Qwen-VL` location, while `[local]` marks an uncatalogued local model. These labels are display-only and do not change saved model identities.
## Workflow tips
- **Pre-process input images** — use a resize or scale node before ThinkingLLM so large images don't blow up the context window.
@@ -145,6 +147,8 @@ The GGUF path requires a multimodal-capable `llama-cpp-python` build. The normal
The four vision nodes and both Prompt Enhancer nodes expose an optional `duration_seconds` input. It becomes visible only for registered LTX 2.3 and MiniMax H3 video presets; all other presets ignore it. Connect the same requested duration value to ThinkingLLM and your downstream video generator so prompt timing and generated length stay aligned.
Switching from a duration-aware preset back to `Custom Only` or another non-video preset hides the control without changing its ComfyUI serialization slot. The last valid duration is preserved; if an older workflow contains a corrupted non-numeric value in that slot, the frontend repairs it to the last valid value or `5.0` seconds. After updating ThinkingLLM, restart ComfyUI and use `Ctrl+F5` once so the corrected frontend code is loaded.
- LTX 2.3 uses the requested value directly and returns one chronological paragraph with a natural total-duration phrase.
- MiniMax H3 mirrors ComfyUI's 24 fps `17k+5` frame grid. For example, `5.0` seconds becomes 124 frames (`5.17` seconds), `8.0` stays 192 frames (`8.00` seconds), and `12.0` becomes 294 frames (`12.25` seconds). The effective duration is internal planning context: T2VA, I2VA, and full-reference outputs do not get an invented `Target duration:` field. FL2VA and L2VA use it only in the official image-alignment line.
- The duration input overrides conflicting duration wording in the free prompt for duration-aware presets. Older saved nodes that do not contain the optional input use `5.0` seconds.
@@ -180,6 +184,7 @@ Commercial-release model dropdowns group entries by rights status and accept onl
- Qwen2.5-VL 3B / 7B
### Qwen — Vision-language (GGUF)
- Qwen3.8 27B Blackfrost Abliterated (Q2 through Q8, image/video)
- Qwen3.5 4B / 9B / 27B (Q4 K M through BF16)
- Qwen3-VL 4B / 8B / 32B (Instruct, abliterated, thinking)
- Qwen2.5-VL 3B / 7B
@@ -189,6 +194,7 @@ Commercial-release model dropdowns group entries by rights status and accept onl
- Qwen3.5 4B / 9B (base + heretic + uncensored)
### Qwen — Text-only (GGUF)
- Qwen3.8 27B Blackfrost Abliterated (Q2 through Q8)
- Qwen3 4B / 8B (abliterated, Josiefied, base)
- Qwen3.5 4B / 9B / 27B (base + uncensored HauhauCS)
+3 -2
View File
@@ -13,7 +13,7 @@ The optional `duration_seconds` input is available on:
- `ThinkingLLM Prompt Enhancer`
- `ThinkingLLM Prompt Enhancer (GGUF)`
The widget is shown only for registered LTX 2.3 and MiniMax H3 video presets. It remains hidden and has no effect for image, analysis, tagging, and other non-video presets. Older workflows without the input use `5.0` seconds for a duration-aware preset.
The widget is shown only for registered LTX 2.3 and MiniMax H3 video presets. It remains hidden and has no effect for `Custom Only`, image, analysis, tagging, and other non-video presets. Hiding the widget does not change its type or serialization position, so switching presets cannot move the read-only recommendation text into `duration_seconds`. The last valid duration is preserved. Older workflows without the input use `5.0` seconds for a duration-aware preset, and workflows containing a corrupted non-numeric duration are repaired to the last valid value or `5.0` seconds by the frontend.
Connect the same requested duration to ThinkingLLM and the downstream video generator. No separate enable switch is required.
@@ -99,6 +99,7 @@ ThinkingLLM does **not** enforce exact field count or order, speaker IDs, dialog
## Recommended settings
- The read-only info box shows the active video preset's recommendation without changing node values. See the [complete video preset settings table](VIDEO_PRESET_SETTINGS.md).
- You can switch from LTX or MiniMax back to `Custom Only` and queue immediately; the hidden duration remains numeric and is ignored by the non-video preset.
- Connect the same requested duration to ThinkingLLM and the H3 generator.
- Start with `enable_thinking=false` for direct prompt conversion.
- Start with `max_tokens=2048` for Base/T2VA, I2VA, FL2VA, and L2VA prompts.
@@ -107,7 +108,7 @@ ThinkingLLM does **not** enforce exact field count or order, speaker IDs, dialog
- Increase the token limit only when a useful response is genuinely truncated.
- Inspect `RAW_TRACE` for model reasoning and connect the main output to Show Text or Show Anything to inspect the exact downstream string.
After updating the custom node, restart ComfyUI completely and refresh the browser with `Ctrl+F5` so Python node definitions and frontend widget visibility are both reloaded.
After updating the custom node, restart ComfyUI completely and refresh the browser with `Ctrl+F5` so Python node definitions and frontend widget visibility are both reloaded. If an old browser tab still reports `Failed to convert ... duration_seconds` followed by recommendation text, that tab is still using the stale frontend script; close it or perform another hard refresh.
## Official references
+51
View File
@@ -224,6 +224,26 @@
"context_length": 131072
}
},
"Qwen3.8-27B-ABLITERATED-GGUF [Q2:10.7GB|Q3:12.1-13.3GB|Q4:15.6-16.5GB|Q5:18.7-19.2GB|Q6:22.1GB|Q8:28.6GB|VRAM:~20GB+]": {
"author": "Blackfrost-AI",
"repo_name": "Qwen3.8-27B-ABLITERATED-GGUF",
"repo_id": "Blackfrost-AI/Qwen3.8-27B-ABLITERATED-GGUF",
"commercial_status": "unclear",
"model_files": [
"Qwen3.8-27B-ABLITERATED-Q2_K.gguf",
"Qwen3.8-27B-ABLITERATED-Q3_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q3_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q4_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q4_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q5_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q5_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q6_K.gguf",
"Qwen3.8-27B-ABLITERATED-Q8_0.gguf"
],
"defaults": {
"context_length": 262144
}
},
"Gemma-4-E2B-it-GGUF [Q4:3.2GB|Q8:4.7GB|BF16:8.7GB|VRAM:~5GB]": {
"author": "bartowski",
"repo_name": "google_gemma-4-E2B-it-GGUF",
@@ -440,6 +460,37 @@
"pool_size": 4194304
}
},
"Qwen3.8-27B-ABLITERATED-GGUF [Q2:10.7GB|Q3:12.1-13.3GB|Q4:15.6-16.5GB|Q5:18.7-19.2GB|Q6:22.1GB|Q8:28.6GB|VRAM:~20GB+]": {
"author": "Blackfrost-AI",
"repo_name": "Qwen3.8-27B-ABLITERATED-GGUF",
"repo_id": "Blackfrost-AI/Qwen3.8-27B-ABLITERATED-GGUF",
"commercial_status": "unclear",
"mmproj_file": "mmproj-Qwen3.8-27B-ABLITERATED-Q8_0.gguf",
"mmproj_local_filenames": {
"mmproj-Qwen3.8-27B-ABLITERATED-Q8_0.gguf": [
"mmproj-Qwen3.8-27B-ABLITERATED-F16.gguf"
]
},
"model_files": [
"Qwen3.8-27B-ABLITERATED-Q2_K.gguf",
"Qwen3.8-27B-ABLITERATED-Q3_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q3_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q4_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q4_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q5_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q5_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q6_K.gguf",
"Qwen3.8-27B-ABLITERATED-Q8_0.gguf"
],
"defaults": {
"context_length": 262144,
"image_max_tokens": 4096,
"n_batch": 512,
"gpu_layers": -1,
"top_k": 20,
"pool_size": 4194304
}
},
"Qwen3.5-4B [Q4:2.4GB|Q6:3.2GB|Q8:4.2GB|BF16:8GB|VRAM:~4GB]": {
"author": "unsloth",
"repo_name": "Qwen3.5-4B-GGUF",
+214 -28
View File
@@ -519,6 +519,48 @@ class TestModelRecommendations(unittest.TestCase):
self.assertEqual("unclear", module.normalize_commercial_status({}))
self.assertEqual("unclear", module.normalize_commercial_status({"commercial_status": "invalid"}))
def test_model_catalog_labels_share_installed_and_local_conventions(self):
module = load_module_from_file(
"thinkingllm_core/model_access.py",
"thinkingllm_shared_model_label_test",
)
catalog = {
"catalog model": {"is_installed": True},
"remote model": {},
"[local] custom.gguf": {"is_local": True},
"[ComfyUI] encoder.safetensors": {
"is_local": True,
"local_display_name": "encoder.safetensors",
"storage_label": "text_encoders",
},
}
self.assertEqual(
[
"catalog model [installed]",
"remote model",
"[local] custom.gguf",
"[local] encoder.safetensors (text_encoders)",
],
module.model_catalog_options(catalog),
)
self.assertEqual(
"catalog model",
module.resolve_model_catalog_name(catalog, "catalog model [installed]"),
)
self.assertEqual(
"catalog model",
module.resolve_model_catalog_name(catalog, "catalog model"),
)
self.assertEqual(
"[ComfyUI] encoder.safetensors",
module.resolve_model_catalog_name(catalog, "[local] encoder.safetensors (text_encoders)"),
)
self.assertEqual(
"[ComfyUI] encoder.safetensors",
module.resolve_model_catalog_name(catalog, "[ComfyUI] encoder.safetensors"),
)
def test_qwenvl_facade_preserves_exports_and_shared_object_identity(self):
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_facade_compatibility_test")
@@ -585,7 +627,8 @@ class TestModelRecommendations(unittest.TestCase):
)
self.assertIn("only permits preinstalled, hash-locked models", access_source)
self.assertIn("if custom.exists() and not COMMERCIAL_RELEASE", hf_models_source)
self.assertIn("if not COMMERCIAL_RELEASE:\n _scan_local_hf_models()", hf_models_source)
self.assertIn("if COMMERCIAL_RELEASE:\n _mark_installed_hf_catalog_models()", hf_models_source)
self.assertIn("else:\n _scan_local_hf_models()", hf_models_source)
self.assertIn('"local_files_only": COMMERCIAL_RELEASE', hf_runtime_source)
self.assertIn("trust_remote_code=not COMMERCIAL_RELEASE", hf_runtime_source)
self.assertIn('"local_files_only": COMMERCIAL_RELEASE', enhancer_source)
@@ -631,6 +674,71 @@ class TestModelRecommendations(unittest.TestCase):
self.assertGreaterEqual(len(text_catalog[text_key].get("model_files") or []), 11)
self.assertGreaterEqual(len(vision_catalog[vision_key].get("model_files") or []), 11)
def test_qwen38_blackfrost_exists_in_text_and_vision_catalogs(self):
payload = json.loads((PKG / "gguf_models.json").read_text(encoding="utf-8"))
text_catalog = payload.get("Qwen_model") or {}
vision_catalog = payload.get("qwenVL_model") or {}
expected_files = [
"Qwen3.8-27B-ABLITERATED-Q2_K.gguf",
"Qwen3.8-27B-ABLITERATED-Q3_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q3_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q4_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q4_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q5_K_S.gguf",
"Qwen3.8-27B-ABLITERATED-Q5_K_M.gguf",
"Qwen3.8-27B-ABLITERATED-Q6_K.gguf",
"Qwen3.8-27B-ABLITERATED-Q8_0.gguf",
]
text_key = next((key for key in text_catalog if "Qwen3.8-27B-ABLITERATED" in key), None)
vision_key = next((key for key in vision_catalog if "Qwen3.8-27B-ABLITERATED" in key), None)
self.assertIsNotNone(text_key)
self.assertIsNotNone(vision_key)
for catalog, key in ((text_catalog, text_key), (vision_catalog, vision_key)):
entry = catalog[key]
self.assertEqual(entry["repo_id"], "Blackfrost-AI/Qwen3.8-27B-ABLITERATED-GGUF")
self.assertEqual(entry["model_files"], expected_files)
self.assertEqual(entry["defaults"]["context_length"], 262144)
self.assertEqual(
vision_catalog[vision_key]["mmproj_file"],
"mmproj-Qwen3.8-27B-ABLITERATED-Q8_0.gguf",
)
self.assertEqual(
vision_catalog[vision_key]["mmproj_local_filenames"],
{
"mmproj-Qwen3.8-27B-ABLITERATED-Q8_0.gguf": [
"mmproj-Qwen3.8-27B-ABLITERATED-F16.gguf"
]
},
)
def test_qwen38_blackfrost_expands_in_both_gguf_node_catalogs(self):
stubs = build_loader_test_stubs()
with mock.patch.dict(sys.modules, stubs, clear=False):
vision_module = load_module_from_file(
"AILab_QwenVL_GGUF.py",
"thinkingllm_qwen38_vision_catalog_test",
)
with mock.patch.dict(sys.modules, {"AILab_QwenVL_GGUF": vision_module}, clear=False):
prompt_module = load_module_from_file(
"AILab_QwenVL_GGUF_PromptEnhancer.py",
"thinkingllm_qwen38_prompt_catalog_test",
)
vision_models = vision_module.GGUF_VL_CATALOG.get("models") or {}
prompt_models = prompt_module.ThinkingLLM_QwenVL_GGUF_PromptEnhancer.load_gguf_models().get("models") or {}
q3_name = "Qwen3.8-27B-ABLITERATED-Q3_K_S.gguf [~12.1-13.3GB]"
q4_name = "Qwen3.8-27B-ABLITERATED-Q4_K_M.gguf [~15.6-16.5GB]"
self.assertIn(q3_name, vision_models)
self.assertIn(q4_name, vision_models)
self.assertIn(q3_name, prompt_models)
self.assertIn(q4_name, prompt_models)
resolved = vision_module._resolve_model_entry(q4_name)
self.assertEqual(resolved.mmproj_filename, "mmproj-Qwen3.8-27B-ABLITERATED-Q8_0.gguf")
self.assertIn("mmproj-Qwen3.8-27B-ABLITERATED-F16.gguf", resolved.mmproj_local_filenames)
def test_gemma4_12b_is_available_for_hf_and_gguf_vision_and_text(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"))
@@ -1401,6 +1509,20 @@ class TestModelRecommendations(unittest.TestCase):
self.assertIn("Info only: your saved widget values are not changed.", source)
self.assertIn('["model_name", "enable_thinking", ...PRESET_WIDGET_NAMES]', source)
def test_duration_visibility_preserves_widget_type_and_serialization_slot(self):
source = read_source("web/js/appearance.js")
visibility_block = source[
source.index("function setWidgetVisible("):
source.index("function readComboValue(")
]
self.assertNotIn("widget.type =", visibility_block)
self.assertNotIn("widget.draw =", visibility_block)
self.assertIn("widget.computedHeight = 0", visibility_block)
self.assertIn("delete widget.computedHeight", visibility_block)
self.assertIn("repairDurationWidgetValue(durationWidget)", source)
self.assertIn("DEFAULT_DURATION_SECONDS", source)
def test_hf_default_flag_controls_default_model(self):
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
package = load_thinkingllm_loader_subset(["AILab_QwenVL.py"])
@@ -1666,28 +1788,39 @@ class TestSharedComfyUITextEncoders(unittest.TestCase):
with mock.patch.dict(sys.modules, stub_modules, clear=False):
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_native_text_encoder_test")
qwen_name = "[ComfyUI] qwen3vl_8b_fp8_scaled.safetensors"
gemma_name = "[ComfyUI] gemma-3-12b-it-heretic-v2_int8.safetensors"
hf_folder_paths = module.load_model_configs.__globals__["folder_paths"]
with mock.patch.object(
hf_folder_paths,
"get_filename_list",
side_effect=lambda category: filenames if category == "text_encoders" else [],
), mock.patch.object(
hf_folder_paths,
"get_full_path_or_raise",
side_effect=lambda category, filename: str(PKG / filename),
):
module.load_model_configs()
qwen_name = "[ComfyUI] qwen3vl_8b_fp8_scaled.safetensors"
gemma_name = "[ComfyUI] gemma-3-12b-it-heretic-v2_int8.safetensors"
self.assertIn("[ComfyUI] qwen3vl_4b_convrot.safetensors", module.HF_VL_MODELS)
self.assertIn(qwen_name, module.HF_VL_MODELS)
self.assertIn(gemma_name, module.HF_VL_MODELS)
self.assertNotIn("[ComfyUI] qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors", module.HF_VL_MODELS)
self.assertNotIn("[ComfyUI] qwen3vl_32b_minimax_h3_nvfp4_awq.safetensor", module.HF_VL_MODELS)
self.assertIn("[ComfyUI] qwen3vl_4b_convrot.safetensors", module.HF_VL_MODELS)
self.assertIn(qwen_name, module.HF_VL_MODELS)
self.assertIn(gemma_name, module.HF_VL_MODELS)
self.assertNotIn("[ComfyUI] qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors", module.HF_VL_MODELS)
self.assertNotIn("[ComfyUI] qwen3vl_32b_minimax_h3_nvfp4_awq.safetensor", module.HF_VL_MODELS)
node = module.QwenVLBase()
node.load_model(qwen_name, "unused", "auto", False, "auto", True)
text, raw = node.generate("hello", None, None, 4, 32, 0.7, 0.9, 1, 1.1, model_name=qwen_name, seed=7)
node = module.QwenVLBase()
node.load_model(qwen_name, "unused", "auto", False, "auto", True)
text, raw = node.generate("hello", None, None, 4, 32, 0.7, 0.9, 1, 1.1, model_name=qwen_name, seed=7)
self.assertEqual(("native answer", "native answer"), (text, raw))
self.assertEqual([str(PKG / "qwen3vl_8b_fp8_scaled.safetensors")], calls["load"]["ckpt_paths"])
self.assertTrue(calls["tokenize"][1]["thinking"])
self.assertEqual(7, calls["generate"][1]["seed"])
calls["decoded_text"] = " "
with self.assertRaisesRegex(RuntimeError, "without producing text"):
node.generate("hello", None, None, 4, 32, 0.7, 0.9, 1, 1.1, model_name=qwen_name, seed=8)
with self.assertRaisesRegex(ValueError, "not video"):
node.generate("hello", None, object(), 4, 32, 0.7, 0.9, 1, 1.1, model_name=gemma_name)
self.assertEqual(("native answer", "native answer"), (text, raw))
self.assertEqual([str(PKG / "qwen3vl_8b_fp8_scaled.safetensors")], calls["load"]["ckpt_paths"])
self.assertTrue(calls["tokenize"][1]["thinking"])
self.assertEqual(7, calls["generate"][1]["seed"])
calls["decoded_text"] = " "
with self.assertRaisesRegex(RuntimeError, "without producing text"):
node.generate("hello", None, None, 4, 32, 0.7, 0.9, 1, 1.1, model_name=qwen_name, seed=8)
with self.assertRaisesRegex(ValueError, "not video"):
node.generate("hello", None, object(), 4, 32, 0.7, 0.9, 1, 1.1, model_name=gemma_name)
class TestHuggingFaceTokenSupport(unittest.TestCase):
@@ -1794,6 +1927,44 @@ class TestHuggingFaceTokenSupport(unittest.TestCase):
self.assertEqual([("private/repo", "hf_test_secret")], captured)
def test_hf_catalog_model_is_labeled_and_reused_from_extra_llm_path(self):
module = self._load_qwenvl_with_stubs()
models_globals = module.ensure_model.__globals__
model_folder_paths = models_globals["folder_paths"]
entry = {"repo_id": "example/example-model", "commercial_status": "unclear"}
with tempfile.TemporaryDirectory() as temp_dir:
temp_root = Path(temp_dir)
shared_llm = temp_root / "shared" / "LLM"
target = shared_llm / "Qwen-VL" / "example-model"
target.mkdir(parents=True)
(target / "config.json").write_text("{}", encoding="utf-8")
(target / "model.safetensors").write_bytes(b"weights")
with mock.patch.dict(models_globals, {"HF_ALL_MODELS": {"Example model": entry}}), mock.patch.object(
model_folder_paths,
"models_dir",
str(temp_root / "dev" / "models"),
), mock.patch.object(
model_folder_paths,
"folder_names_and_paths",
{"LLM": ([str(shared_llm)], set())},
), mock.patch.object(
model_folder_paths,
"get_folder_paths",
side_effect=lambda name: [str(shared_llm)] if name == "LLM" else [],
), mock.patch.dict(
models_globals,
{"_download_model_snapshot": mock.Mock(side_effect=AssertionError("installed model must not download"))},
):
models_globals["_mark_installed_hf_catalog_models"]()
self.assertTrue(entry["is_installed"])
self.assertEqual(
["Example model [installed]"],
module.model_catalog_options({"Example model": entry}),
)
self.assertEqual(str(target), module.ensure_model("Example model [installed]"))
def test_gated_gguf_stops_before_download_when_token_is_missing(self):
hf_module = self._load_qwenvl_with_stubs()
stubs = build_loader_test_stubs()
@@ -2365,12 +2536,16 @@ class TestGGUFAdvancedWorkflowCompatibility(unittest.TestCase):
with mock.patch.object(module.folder_paths, "models_dir", str(models_dir)):
catalog = module._load_gguf_vl_catalog()
self.assertIn("model.gguf [installed]", catalog["models"])
self.assertIn("model.gguf", catalog["models"])
self.assertTrue(catalog["models"]["model.gguf"]["is_installed"])
self.assertNotIn("[local] model.gguf", catalog["models"])
self.assertIn("model.gguf [installed]", module.model_catalog_options(catalog["models"]))
with mock.patch.object(module, "GGUF_VL_CATALOG", catalog):
resolved = module._resolve_model_entry("model.gguf")
resolved_from_label = module._resolve_model_entry("model.gguf [installed]")
self.assertEqual(resolved.model_filename, "model.gguf")
self.assertEqual(resolved_from_label.model_filename, "model.gguf")
def test_gguf_vision_advanced_allows_larger_reasoning_output_budget(self):
node_cls = self._load_gguf_advanced_class()
@@ -2934,16 +3109,22 @@ class TestGGUFWindllRuntimeFallback(unittest.TestCase):
)
with mock.patch.dict(sys.modules, stubs, clear=False):
module = load_module_from_file(
"AILab_QwenVL_GGUF_PromptEnhancer.py",
"thinkingllm_gguf_prompt_extra_llm_reuse_test",
vision_module = load_module_from_file(
"AILab_QwenVL_GGUF.py",
"thinkingllm_gguf_prompt_extra_llm_vision_dependency_test",
)
with mock.patch.dict(sys.modules, {"AILab_QwenVL_GGUF": vision_module}, clear=False):
module = load_module_from_file(
"AILab_QwenVL_GGUF_PromptEnhancer.py",
"thinkingllm_gguf_prompt_extra_llm_reuse_test",
)
model_name = next(
name
for name in module.ThinkingLLM_QwenVL_GGUF_PromptEnhancer.load_gguf_models()["models"]
for name in module.ThinkingLLM_QwenVL_GGUF_PromptEnhancer.INPUT_TYPES()["required"]["model_name"][0]
if name.startswith(model_filename)
)
self.assertTrue(model_name.endswith(" [installed]"))
loader = module.ThinkingLLM_QwenVL_GGUF_PromptEnhancer()
with mock.patch.dict(sys.modules, stubs, clear=False), mock.patch.object(
@@ -3206,10 +3387,15 @@ class TestGGUFWindllRuntimeFallback(unittest.TestCase):
)
with mock.patch.dict(sys.modules, stubs, clear=False):
module = load_module_from_file(
"AILab_QwenVL_GGUF_PromptEnhancer.py",
"thinkingllm_gguf_prompt_google_gemma_alias_reuse_test",
vision_module = load_module_from_file(
"AILab_QwenVL_GGUF.py",
"thinkingllm_gguf_prompt_google_gemma_vision_dependency_test",
)
with mock.patch.dict(sys.modules, {"AILab_QwenVL_GGUF": vision_module}, clear=False):
module = load_module_from_file(
"AILab_QwenVL_GGUF_PromptEnhancer.py",
"thinkingllm_gguf_prompt_google_gemma_alias_reuse_test",
)
model_name = "google_gemma-4-E4B-it-Q8_0.gguf [~8GB]"
loader = module.ThinkingLLM_QwenVL_GGUF_PromptEnhancer()
+76 -21
View File
@@ -51,7 +51,7 @@ PRESET_PROMPTS: list[str] = [NO_PRESET_PROMPT, "Describe this image in detail."]
TOOLTIPS = {
"model_name": "Pick the checkpoint. [ComfyUI] entries reuse compatible models/text_encoders files; other entries download into models/LLM/Qwen-VL on first use.",
"model_name": "Pick the checkpoint. [installed] means a catalog model is available in a configured LLM location; [local] means an uncatalogued local model, including compatible ComfyUI text_encoders. Missing catalog models download on first use.",
"quantization": "Precision vs VRAM. FP16 gives the best quality if memory allows; 8-bit suits 8–16 GB GPUs; 4-bit fits 6 GB or lower but is slower.",
"attention_mode": "auto tries SageAttention → FlashAttention 2 → SDPA in order. SDPA is stable and recommended. Only override when debugging attention backends.",
"preset_prompt": "Built-in instruction describing how Qwen-VL should analyze the media input.",
@@ -70,6 +70,63 @@ TOOLTIPS = {
}
def _configured_llm_roots() -> list[Path]:
roots: list[Path] = []
try:
if "LLM" in folder_paths.folder_names_and_paths:
roots.extend(Path(path) for path in folder_paths.get_folder_paths("LLM"))
except (AttributeError, KeyError, TypeError):
pass
default_root = Path(folder_paths.models_dir) / "LLM"
if default_root not in roots:
roots.append(default_root)
return roots
def _hf_qwen_vl_dirs() -> list[Path]:
return [root / "Qwen-VL" for root in _configured_llm_roots()]
def _hf_catalog_search_dirs() -> list[Path]:
search_dirs: list[Path] = []
for root in _configured_llm_roots():
for directory in (root / "Qwen-VL", root):
if directory not in search_dirs:
search_dirs.append(directory)
return search_dirs
def _find_installed_hf_model_dir(info: dict, *, require_processor: bool = False) -> Path | None:
repo_id = info.get("repo_id") if isinstance(info, dict) else None
if not isinstance(repo_id, str) or not repo_id.strip():
return None
directory_name = repo_id.rstrip("/").rsplit("/", 1)[-1]
installed_path = info.get("installed_path")
candidates = [Path(installed_path)] if installed_path else []
candidates.extend(directory / directory_name for directory in _hf_catalog_search_dirs())
seen: set[Path] = set()
for candidate in candidates:
if candidate in seen:
continue
seen.add(candidate)
if _model_snapshot_has_required_files(candidate, require_processor=require_processor):
return candidate
return None
def _mark_installed_hf_catalog_models() -> None:
for info in HF_ALL_MODELS.values():
if not isinstance(info, dict) or info.get("is_local"):
continue
installed_path = _find_installed_hf_model_dir(info)
if installed_path is None:
info.pop("is_installed", None)
info.pop("installed_path", None)
continue
info["is_installed"] = True
info["installed_path"] = str(installed_path)
def ensure_cuda_vram_headroom(module_name="QwenVL", min_free_gb=1.0, min_free_ratio=0.08):
if not torch.cuda.is_available():
return True
@@ -226,24 +283,18 @@ def load_model_configs():
SYSTEM_PROMPTS.update(next_system_prompts)
PRESET_PROMPTS[:] = next_preset_prompts
# Commercial releases expose only the two entries in the reviewed catalog.
if not COMMERCIAL_RELEASE:
# Commercial releases expose only reviewed catalog entries, but still show
# whether those entries are available in any configured LLM location.
if COMMERCIAL_RELEASE:
_mark_installed_hf_catalog_models()
else:
_scan_local_hf_models()
def _scan_local_hf_models():
"""Scan local HF directories and compatible ComfyUI text encoders."""
# Collect all Qwen-VL directories to scan from ComfyUI's multi-path system
scan_dirs: list[Path] = []
llm_paths = folder_paths.get_folder_paths("LLM") if "LLM" in folder_paths.folder_names_and_paths else []
for llm_path in llm_paths:
qwen_dir = Path(llm_path) / "Qwen-VL"
if qwen_dir not in scan_dirs:
scan_dirs.append(qwen_dir)
# Always include the default location as fallback
default_dir = Path(folder_paths.models_dir) / "LLM" / "Qwen-VL"
if default_dir not in scan_dirs:
scan_dirs.append(default_dir)
_mark_installed_hf_catalog_models()
scan_dirs = _hf_qwen_vl_dirs()
# Collect known local directory names from JSON config
known_dirs = set()
@@ -302,6 +353,8 @@ def _scan_local_hf_models():
"comfy_text_encoder": filename,
"repo_id": None,
"is_local": True,
"local_display_name": filename,
"storage_label": "text_encoders",
"commercial_status": "local",
"quantized": True,
"native_family": family,
@@ -599,6 +652,13 @@ def ensure_model(model_name, require_processor=False, node_id=None, progress_lab
if not repo_id:
raise ValueError(f"Model '{model_name}' has no repo_id or local_path")
installed_target = _find_installed_hf_model_dir(info, require_processor=require_processor)
if installed_target is not None:
info["is_installed"] = True
info["installed_path"] = str(installed_target)
enforce_model_access(info, model_name, local_exists=True, hf_token=hf_token)
return str(installed_target)
if COMMERCIAL_RELEASE:
enforce_model_access(info, model_name, local_exists=False, hf_token=hf_token)
target = Path(folder_paths.models_dir) / "LLM" / repo_id.split("/")[-1]
@@ -609,13 +669,8 @@ def ensure_model(model_name, require_processor=False, node_id=None, progress_lab
f"[QwenVL] Locked commercial model is missing or incomplete: {target}"
)
# Use ComfyUI's multi-path system if available
llm_paths = folder_paths.get_folder_paths("LLM") if "LLM" in folder_paths.folder_names_and_paths else []
if llm_paths:
models_dir = Path(llm_paths[0]) / "Qwen-VL"
else:
# Fallback to default behavior
models_dir = Path(folder_paths.models_dir) / "LLM" / "Qwen-VL"
# Download into ComfyUI's first configured LLM location.
models_dir = _hf_qwen_vl_dirs()[0]
models_dir.mkdir(parents=True, exist_ok=True)
target = models_dir / repo_id.split("/")[-1]
+83 -3
View File
@@ -35,6 +35,8 @@ MODEL_STATUS_PRESENTATION = {
"unclear": (3, "⚠ Rights unclear | "),
"noncommercial": (4, "🚫 Non-commercial | "),
}
MODEL_INSTALLED_SUFFIX = " [installed]"
MODEL_LOCAL_PREFIX = "[local] "
def _format_download_size(num_bytes) -> str:
@@ -196,10 +198,25 @@ def normalize_commercial_status(info: dict | None) -> str:
return status if status in VALID_COMMERCIAL_STATUSES else "unclear"
def _model_display_name(name: str, info: dict | None) -> str:
info = info if isinstance(info, dict) else {}
local_display_name = str(info.get("local_display_name") or "").strip()
if info.get("is_local") and local_display_name:
label = f"{MODEL_LOCAL_PREFIX}{local_display_name}"
storage_label = str(info.get("storage_label") or "").strip()
if storage_label:
label = f"{label} ({storage_label})"
return label
if info.get("is_installed") and not info.get("is_local") and not name.endswith(MODEL_INSTALLED_SUFFIX):
return f"{name}{MODEL_INSTALLED_SUFFIX}"
return name
def model_catalog_label(name: str, info: dict | None) -> str:
display_name = _model_display_name(name, info)
if not COMMERCIAL_RELEASE:
return name
return f"{MODEL_STATUS_PRESENTATION[normalize_commercial_status(info)][1]}{name}"
return display_name
return f"{MODEL_STATUS_PRESENTATION[normalize_commercial_status(info)][1]}{display_name}"
def model_catalog_options(models: dict[str, dict], predicate=None) -> list[str]:
@@ -222,11 +239,74 @@ def resolve_model_catalog_name(models: dict[str, dict], selected: str) -> str:
if selected in models:
return selected
for name, info in models.items():
if selected == f"{MODEL_STATUS_PRESENTATION[normalize_commercial_status(info)][1]}{name}":
prefix = MODEL_STATUS_PRESENTATION[normalize_commercial_status(info)][1]
if selected in {model_catalog_label(name, info), f"{prefix}{name}"}:
return name
undecorated = selected
for _order, prefix in MODEL_STATUS_PRESENTATION.values():
if undecorated.startswith(prefix):
undecorated = undecorated[len(prefix):]
break
if undecorated.endswith(MODEL_INSTALLED_SUFFIX):
legacy_name = undecorated[:-len(MODEL_INSTALLED_SUFFIX)]
if legacy_name in models:
return legacy_name
return selected
def _catalog_filename_candidates(info: dict) -> set[str]:
candidates: set[str] = set()
for key in ("filename", "local_filenames", "local_model_files"):
value = info.get(key)
if isinstance(value, str):
values = [value]
elif isinstance(value, (list, tuple, set)):
values = value
elif isinstance(value, dict):
values = [*value.keys(), *value.values()]
else:
values = []
for candidate in values:
if isinstance(candidate, (list, tuple, set)):
nested = candidate
else:
nested = [candidate]
for item in nested:
filename = Path(str(item)).name.casefold()
if filename:
candidates.add(filename)
return candidates
def mark_catalog_files_installed(models: dict[str, dict], search_dirs) -> None:
"""Mark catalog entries found in any configured directory without changing their keys."""
installed_files: dict[str, Path] = {}
for directory in search_dirs:
directory = Path(directory)
if not directory.exists() or not directory.is_dir():
continue
try:
for path in directory.rglob("*.gguf"):
if path.is_file():
installed_files.setdefault(path.name.casefold(), path)
except (OSError, PermissionError):
continue
for info in models.values():
if not isinstance(info, dict) or info.get("is_local"):
continue
installed_path = next(
(installed_files[name] for name in _catalog_filename_candidates(info) if name in installed_files),
None,
)
if installed_path is None:
info.pop("is_installed", None)
info.pop("installed_path", None)
continue
info["is_installed"] = True
info["installed_path"] = str(installed_path)
def enforce_model_access(info: dict | None, model_name: str, *, local_exists: bool, hf_token: str | None = None) -> None:
status = normalize_commercial_status(info)
if COMMERCIAL_RELEASE:
+2 -1
View File
@@ -9,11 +9,12 @@
- Refactored the QwenVL implementation into focused shared modules for model access, runtime configuration, prompt state, prompt contracts, and media handling while preserving compatibility exports.
- Improved streaming so reasoning stays separate from final content, unfinished thinking traces trigger a direct finalization retry, and terminal-only punctuation remains safe on legacy Windows code pages.
- Expanded preset recommendations, tooltips, system prompts, and documentation for video token budgets, thinking behavior, reference roles, dialogue fidelity, and duration handling.
- Fixed dynamic duration-widget hiding so switching from LTX or MiniMax back to `Custom Only` cannot serialize the read-only recommendation text as `duration_seconds`; existing corrupted values are repaired to the last valid duration or `5.0` seconds.
### 2.4.0 Validation
- Expanded regression coverage for compatibility, downloads, caching, duration-aware prompting, MiniMax structures, streaming, and node registration.
- All 131 isolated regression tests pass in the bundled ComfyUI Python runtime.
- All 136 isolated regression tests pass in the bundled ComfyUI Python runtime, including duration-widget serialization coverage.
## Version 2.3.24 (2026/08/08) - System Prompt Preset Utility
+21 -7
View File
@@ -84,6 +84,7 @@ const RECOMMENDATIONS_URL = new URL("../model_recommendations.json", import.meta
const PRESET_TOOLTIPS_URL = new URL("../preset_tooltips.json", import.meta.url);
const RECOMMENDATION_WIDGET_NAME = "recommended_settings";
const RECOMMENDATION_PLACEHOLDER = "Select a model to see provider/community recommended settings. This note never changes your saved widget values.";
const DEFAULT_DURATION_SECONDS = 5.0;
const RECOMMENDATION_FIELDS = ["temperature", "top_p", "top_k", "max_tokens", "ctx_min", "n_batch", "image_max_tokens"];
const AUDIO_UNSUPPORTED_MESSAGE = "No curated audio support for the selected model. Use Gemma 4 Audio for audio understanding or Whisper ASR for transcription.";
const PRESET_WIDGET_NAMES = ["preset_prompt", "enhancement_style", "preset_system_prompt"];
@@ -159,27 +160,39 @@ function setWidgetVisible(node, widget, visible) {
if (!widget) { return; }
if (!widget._thinkingllmVisibilityState) {
widget._thinkingllmVisibilityState = {
type: widget.type,
computeSize: widget.computeSize,
draw: widget.draw,
hidden: widget.hidden,
};
}
const original = widget._thinkingllmVisibilityState;
if (visible) {
widget.type = original.type;
widget.computeSize = original.computeSize;
widget.draw = original.draw;
widget.hidden = original.hidden;
delete widget.computedHeight;
} else {
widget.hidden = true;
widget.type = `thinkingllm_hidden_${original.type || "widget"}`;
widget.computeSize = () => [0, 0];
widget.draw = () => {};
widget.computeSize = () => [0, -3.3];
widget.computedHeight = 0;
}
resizeNode(node);
}
function repairDurationWidgetValue(widget) {
if (!widget) { return; }
const numericValue = typeof widget.value === "number"
? widget.value
: Number(String(widget.value ?? "").trim());
const minimum = Number(widget.options?.min ?? 0.2);
const maximum = Number(widget.options?.max ?? 150.0);
if (Number.isFinite(numericValue) && numericValue >= minimum && numericValue <= maximum) {
widget.value = numericValue;
widget._thinkingllmLastValidDuration = numericValue;
return;
}
const previousValue = Number(widget._thinkingllmLastValidDuration);
widget.value = Number.isFinite(previousValue) ? previousValue : DEFAULT_DURATION_SECONDS;
}
function readComboValue(widget) {
const value = widget?.value;
if (typeof value === "string") {
@@ -228,6 +241,7 @@ function updateDurationVisibility(node, payload) {
}
const selectedPreset = selectedPresetName(node);
const durationWidget = findWidget(node, "duration_seconds");
repairDurationWidgetValue(durationWidget);
const { metadataAvailable, metadata } = findVideoPresetMetadata(payload, selectedPreset);
if (!metadataAvailable) {
// Keep the backend-provided control usable if the auxiliary payload is stale or unavailable.
+36
View File
@@ -1,6 +1,42 @@
{
"version": 1,
"rules": [
{
"id": "qwen38_blackfrost_27b",
"priority": 110,
"match_any": [
"Qwen3.8-27B-ABLITERATED"
],
"family_label": "Qwen3.8 27B Blackfrost abliterated",
"sources": [
"Blackfrost-AI Qwen3.8 27B Abliterated GGUF model card",
"Qwen3.8 27B official model card"
],
"support_notes": {
"hf": "This catalog entry is GGUF-only. Use a GGUF node for this repository.",
"gguf": "Hard toggle via enable_thinking in the Qwen3.8 chat template. Vision requires the matching mmproj file and a current llama.cpp build.",
"soft_switch": "Qwen3.8 uses chat-template thinking controls; no /think or /no_think soft switch is documented."
},
"thinking_on": {
"temperature": 1.0,
"top_p": 0.95,
"top_k": 20,
"max_tokens": 32768,
"ctx_min": 262144
},
"thinking_off": {
"temperature": 0.7,
"top_p": 0.8,
"top_k": 20,
"max_tokens": 32768,
"ctx_min": 262144
},
"notes": [
"Thinking is enabled by default; disable it for direct prompt rewrites and keep it enabled for deeper scene planning.",
"Q4_K_M is the model author's balanced default; use the compact Q8 projector for image or video input.",
"The model supports 262K context architecturally, but large llama.cpp contexts consume substantial KV-cache memory; lower ctx for ordinary prompt work."
]
},
{
"id": "qwen36_hauhau_multimodal",
"priority": 100,