Disable automatic GGUF finalization retries
This commit is contained in:
+46
-17
@@ -333,6 +333,23 @@ def _resolve_base_dir(base_dir_value: str) -> Path:
|
||||
return Path(folder_paths.models_dir) / base_dir
|
||||
|
||||
|
||||
def _gguf_search_dirs(base_dir_value: str) -> list[Path]:
|
||||
search_dirs: list[Path] = [_resolve_base_dir(base_dir_value)]
|
||||
try:
|
||||
if "LLM" in folder_paths.folder_names_and_paths:
|
||||
for llm_path in folder_paths.get_folder_paths("LLM"):
|
||||
gguf_dir = Path(llm_path) / "GGUF"
|
||||
if gguf_dir not in search_dirs:
|
||||
search_dirs.append(gguf_dir)
|
||||
|
||||
llm_dir = Path(llm_path)
|
||||
if llm_dir not in search_dirs:
|
||||
search_dirs.append(llm_dir)
|
||||
except Exception:
|
||||
pass
|
||||
return search_dirs
|
||||
|
||||
|
||||
def _safe_dirname(value: str) -> str:
|
||||
value = (value or "").strip()
|
||||
if not value:
|
||||
@@ -495,20 +512,7 @@ def _load_gguf_vl_catalog():
|
||||
# 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
|
||||
existing_filenames = {Path(e.get("filename", "")).name for e in flattened.values() if e.get("filename")}
|
||||
scan_dirs: list[Path] = [_resolve_base_dir(base_dir)]
|
||||
try:
|
||||
if "LLM" in folder_paths.folder_names_and_paths:
|
||||
for llm_path in folder_paths.get_folder_paths("LLM"):
|
||||
gguf_dir = Path(llm_path) / "GGUF"
|
||||
if gguf_dir not in scan_dirs:
|
||||
scan_dirs.append(gguf_dir)
|
||||
# Also scan the LLM path itself (users may put GGUFs directly there)
|
||||
llm_p = Path(llm_path)
|
||||
if llm_p not in scan_dirs:
|
||||
scan_dirs.append(llm_p)
|
||||
except Exception:
|
||||
pass
|
||||
for scan_dir in scan_dirs:
|
||||
for scan_dir in _gguf_search_dirs(base_dir):
|
||||
local_models = _scan_local_gguf_models(scan_dir, existing_filenames)
|
||||
flattened.update(local_models)
|
||||
# Update existing_filenames so we don't add duplicates across directories
|
||||
@@ -986,6 +990,14 @@ def _find_existing_local_file(base_dir: Path, filename: str) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def _find_existing_local_file_in_dirs(search_dirs: list[Path], filename: str) -> Path | None:
|
||||
for search_dir in search_dirs:
|
||||
found = _find_existing_local_file(search_dir, filename)
|
||||
if found is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_model_entry(model_name: str) -> GGUFVLResolved:
|
||||
all_models = GGUF_VL_CATALOG.get("models") or {}
|
||||
entry = all_models.get(model_name) or {}
|
||||
@@ -1171,7 +1183,9 @@ class QwenVLGGUFBase:
|
||||
if mmproj_path is not None and not mmproj_path.exists():
|
||||
raise FileNotFoundError(f"[QwenVL] Local mmproj not found: {mmproj_path}")
|
||||
else:
|
||||
base_dir = _resolve_base_dir(GGUF_VL_CATALOG.get("base_dir") or "llm/GGUF")
|
||||
base_dir_value = GGUF_VL_CATALOG.get("base_dir") or "llm/GGUF"
|
||||
search_dirs = _gguf_search_dirs(base_dir_value)
|
||||
base_dir = search_dirs[0]
|
||||
|
||||
author_dir = _safe_dirname(resolved.author or "")
|
||||
repo_dir = _safe_dirname(resolved.repo_dirname)
|
||||
@@ -1180,7 +1194,7 @@ class QwenVLGGUFBase:
|
||||
model_path = target_dir / Path(resolved.model_filename).name
|
||||
mmproj_path = target_dir / Path(resolved.mmproj_filename).name if resolved.mmproj_filename else None
|
||||
|
||||
existing_model = _find_existing_local_file(base_dir, resolved.model_filename)
|
||||
existing_model = _find_existing_local_file_in_dirs(search_dirs, resolved.model_filename)
|
||||
if existing_model is not None:
|
||||
model_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not model_path.exists():
|
||||
@@ -1192,7 +1206,7 @@ class QwenVLGGUFBase:
|
||||
model_path = model_path
|
||||
|
||||
if mmproj_path is not None:
|
||||
existing_mmproj = _find_existing_local_file(base_dir, resolved.mmproj_filename)
|
||||
existing_mmproj = _find_existing_local_file_in_dirs(search_dirs, resolved.mmproj_filename)
|
||||
if existing_mmproj is not None:
|
||||
mmproj_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not mmproj_path.exists():
|
||||
@@ -1427,6 +1441,7 @@ class QwenVLGGUFBase:
|
||||
model_name: str = "",
|
||||
stream_to_terminal: bool = False,
|
||||
enable_thinking: bool = True,
|
||||
auto_finalization_retry: bool = False,
|
||||
top_k: int | None = None,
|
||||
):
|
||||
"""Returns (cleaned_text, raw_text) tuple."""
|
||||
@@ -1630,6 +1645,8 @@ class QwenVLGGUFBase:
|
||||
best_cleaned = cleaned_text
|
||||
if _answer_output_is_usable(best_cleaned):
|
||||
return best_cleaned, "\n\n".join(raw_trace_parts)
|
||||
if not auto_finalization_retry:
|
||||
return best_cleaned or "", "\n\n".join(raw_trace_parts)
|
||||
|
||||
current_raw = raw_text
|
||||
for attempt_number in range(2, _GGUF_MAX_FINALIZATION_ATTEMPTS + 1):
|
||||
@@ -1739,6 +1756,7 @@ class QwenVLGGUFBase:
|
||||
node_class="QwenVLGGUF",
|
||||
stream_to_terminal=False,
|
||||
enable_thinking=True,
|
||||
auto_finalization_retry=False,
|
||||
hf_token="",
|
||||
):
|
||||
print(f"[QwenVL GGUF DEBUG] Starting run with seed={seed}")
|
||||
@@ -1769,6 +1787,7 @@ class QwenVLGGUFBase:
|
||||
offload_kqv=bool(offload_kqv),
|
||||
ctx_checkpoints=ctx_checkpoints,
|
||||
enable_thinking=bool(enable_thinking),
|
||||
auto_finalization_retry=bool(auto_finalization_retry),
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
@@ -1918,6 +1937,7 @@ class QwenVLGGUFBase:
|
||||
model_name=model_name,
|
||||
stream_to_terminal=stream_to_terminal,
|
||||
enable_thinking=effective_thinking,
|
||||
auto_finalization_retry=auto_finalization_retry,
|
||||
top_k=top_k,
|
||||
)
|
||||
if self.last_backend_trace:
|
||||
@@ -1995,6 +2015,7 @@ class ThinkingLLM_QwenVL_GGUF(QwenVLGGUFBase):
|
||||
"seed": ("INT", {"default": 1, "min": 1, "max": 2**32 - 1, "tooltip": GGUF_TOOLTIPS["seed"]}),
|
||||
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": GGUF_TOOLTIPS["stream_tokens_to_terminal"]}),
|
||||
"enable_thinking": ("BOOLEAN", {"default": True, "tooltip": "Enable model reasoning/thinking when the backend supports it: True=allow thinking, False=force direct answer. Even when enabled, easy prompts may still get a direct answer, and this node automatically disables thinking when there is not enough output budget left for useful reasoning. For non-Qwen GGUF models this is advisory and may not be honored by the backend."}),
|
||||
"auto_finalization_retry": ("BOOLEAN", {"default": False, "tooltip": "If enabled, runs an extra LLM completion when the first output is empty or reasoning-only. Disabled by default so one node execution performs one generation pass."}),
|
||||
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": GGUF_TOOLTIPS["hf_token"]}),
|
||||
},
|
||||
"optional": {
|
||||
@@ -2030,6 +2051,7 @@ class ThinkingLLM_QwenVL_GGUF(QwenVLGGUFBase):
|
||||
extra_pnginfo=None,
|
||||
stream_tokens_to_terminal=False,
|
||||
enable_thinking=True,
|
||||
auto_finalization_retry=False,
|
||||
hf_token="",
|
||||
):
|
||||
result = self.run(
|
||||
@@ -2065,6 +2087,7 @@ class ThinkingLLM_QwenVL_GGUF(QwenVLGGUFBase):
|
||||
node_class="ThinkingLLM_QwenVL_GGUF",
|
||||
stream_to_terminal=stream_tokens_to_terminal,
|
||||
enable_thinking=enable_thinking,
|
||||
auto_finalization_retry=auto_finalization_retry,
|
||||
hf_token=hf_token,
|
||||
)
|
||||
hf_token = ""
|
||||
@@ -2086,6 +2109,7 @@ class ThinkingLLM_Gemma4_Audio_GGUF(QwenVLGGUFBase):
|
||||
"seed": ("INT", {"default": 1, "min": 1, "max": 2**32 - 1, "tooltip": GGUF_TOOLTIPS["seed"]}),
|
||||
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": GGUF_TOOLTIPS["stream_tokens_to_terminal"]}),
|
||||
"enable_thinking": ("BOOLEAN", {"default": False, "tooltip": "Gemma 4 can reason, but audio transcription and short analysis are usually clearer with thinking disabled."}),
|
||||
"auto_finalization_retry": ("BOOLEAN", {"default": False, "tooltip": "If enabled, runs an extra LLM completion when the first output is empty or reasoning-only. Disabled by default so one node execution performs one generation pass."}),
|
||||
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": GGUF_TOOLTIPS["hf_token"]}),
|
||||
},
|
||||
"optional": {
|
||||
@@ -2112,6 +2136,7 @@ class ThinkingLLM_Gemma4_Audio_GGUF(QwenVLGGUFBase):
|
||||
seed,
|
||||
stream_tokens_to_terminal=False,
|
||||
enable_thinking=False,
|
||||
auto_finalization_retry=False,
|
||||
hf_token="",
|
||||
audio=None,
|
||||
audio_file_path="",
|
||||
@@ -2151,6 +2176,7 @@ class ThinkingLLM_Gemma4_Audio_GGUF(QwenVLGGUFBase):
|
||||
node_class="ThinkingLLM_Gemma4_Audio_GGUF",
|
||||
stream_to_terminal=stream_tokens_to_terminal,
|
||||
enable_thinking=enable_thinking,
|
||||
auto_finalization_retry=auto_finalization_retry,
|
||||
hf_token=hf_token,
|
||||
)
|
||||
hf_token = ""
|
||||
@@ -2201,6 +2227,7 @@ class ThinkingLLM_QwenVL_GGUF_Advanced(QwenVLGGUFBase):
|
||||
"ctx_checkpoints": ("INT", {"default": 0, "min": 0, "max": 32, "tooltip": GGUF_TOOLTIPS["ctx_checkpoints"]}),
|
||||
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": GGUF_TOOLTIPS["stream_tokens_to_terminal"]}),
|
||||
"enable_thinking": ("BOOLEAN", {"default": True, "tooltip": "Enable model reasoning/thinking when the backend supports it: True=allow thinking, False=force direct answer. Even when enabled, easy prompts may still get a direct answer, and this node automatically disables thinking when there is not enough output budget left for useful reasoning. For non-Qwen GGUF models this is advisory."}),
|
||||
"auto_finalization_retry": ("BOOLEAN", {"default": False, "tooltip": "If enabled, runs an extra LLM completion when the first output is empty or reasoning-only. Disabled by default so one node execution performs one generation pass."}),
|
||||
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": GGUF_TOOLTIPS["hf_token"]}),
|
||||
},
|
||||
"optional": {
|
||||
@@ -2255,6 +2282,7 @@ class ThinkingLLM_QwenVL_GGUF_Advanced(QwenVLGGUFBase):
|
||||
extra_pnginfo=None,
|
||||
stream_tokens_to_terminal=False,
|
||||
enable_thinking=True,
|
||||
auto_finalization_retry=False,
|
||||
hf_token="",
|
||||
):
|
||||
_ = legacy_seed_mode
|
||||
@@ -2292,6 +2320,7 @@ class ThinkingLLM_QwenVL_GGUF_Advanced(QwenVLGGUFBase):
|
||||
node_class="ThinkingLLM_QwenVL_GGUF_Advanced",
|
||||
stream_to_terminal=stream_tokens_to_terminal,
|
||||
enable_thinking=enable_thinking,
|
||||
auto_finalization_retry=auto_finalization_retry,
|
||||
hf_token=hf_token,
|
||||
)
|
||||
hf_token = ""
|
||||
|
||||
@@ -79,6 +79,7 @@ GGUF_PROMPT_TOOLTIPS = {
|
||||
"keep_last_prompt": "Return the saved per-node prompt instead of generating a new one when available.",
|
||||
"stream_tokens_to_terminal": "Show clean wrapped generated tokens in the ComfyUI terminal. Streaming bypasses fixed-seed reuse for a fresh run.",
|
||||
"enable_thinking": "Enable model reasoning/thinking when supported. The node still returns the cleaned final prompt, so reasoning may be hidden or empty.",
|
||||
"auto_finalization_retry": "If enabled, runs an extra LLM completion when the first output is empty or reasoning-only. Disabled by default so one node execution performs one generation pass.",
|
||||
"hf_token": "Optional Hugging Face access token for private or gated GGUF downloads. It is passed only to the download call, never logged or cached, and the in-memory copy is dropped after the download attempt. Clear this field before saving or sharing workflows.",
|
||||
}
|
||||
|
||||
@@ -481,6 +482,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
|
||||
"keep_last_prompt": ("BOOLEAN", {"default": False, "tooltip": GGUF_PROMPT_TOOLTIPS["keep_last_prompt"]}),
|
||||
"stream_tokens_to_terminal": ("BOOLEAN", {"default": False, "tooltip": GGUF_PROMPT_TOOLTIPS["stream_tokens_to_terminal"]}),
|
||||
"enable_thinking": ("BOOLEAN", {"default": True, "tooltip": GGUF_PROMPT_TOOLTIPS["enable_thinking"]}),
|
||||
"auto_finalization_retry": ("BOOLEAN", {"default": False, "tooltip": GGUF_PROMPT_TOOLTIPS["auto_finalization_retry"]}),
|
||||
"hf_token": ("STRING", {"default": "", "multiline": False, "tooltip": GGUF_PROMPT_TOOLTIPS["hf_token"]}),
|
||||
},
|
||||
"hidden": {
|
||||
@@ -715,6 +717,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
|
||||
stream_to_terminal=False,
|
||||
initial_stage_label="INITIAL GENERATION",
|
||||
enable_thinking=True,
|
||||
auto_finalization_retry=False,
|
||||
):
|
||||
"""Returns (cleaned_prompt, raw_trace) tuple."""
|
||||
stream_display = TerminalStreamDisplay("QwenVL GGUF", suppress_planning=True, compact=True) if stream_to_terminal else None
|
||||
@@ -860,6 +863,8 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
|
||||
best_cleaned = cleaned.strip()
|
||||
if _prompt_output_is_usable(best_cleaned):
|
||||
return best_cleaned, "\n\n".join(raw_trace_parts)
|
||||
if not auto_finalization_retry:
|
||||
return best_cleaned or "", "\n\n".join(raw_trace_parts)
|
||||
|
||||
current_raw = raw
|
||||
for attempt_number in range(2, _PROMPT_ENHANCER_MAX_FINALIZATION_ATTEMPTS + 1):
|
||||
@@ -924,6 +929,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
|
||||
unique_id=None,
|
||||
extra_pnginfo=None,
|
||||
enable_thinking=True,
|
||||
auto_finalization_retry=False,
|
||||
hf_token="",
|
||||
):
|
||||
node_class = "ThinkingLLM_QwenVL_GGUF_PromptEnhancer"
|
||||
@@ -935,6 +941,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
|
||||
english_output=bool(english_output),
|
||||
device=device,
|
||||
enable_thinking=bool(enable_thinking),
|
||||
auto_finalization_retry=bool(auto_finalization_retry),
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
@@ -1021,6 +1028,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
|
||||
stream_to_terminal=stream_tokens_to_terminal,
|
||||
initial_stage_label="INITIAL GENERATION",
|
||||
enable_thinking=effective_thinking,
|
||||
auto_finalization_retry=auto_finalization_retry,
|
||||
)
|
||||
full_raw_trace = raw_trace
|
||||
if english_output:
|
||||
@@ -1039,6 +1047,7 @@ class ThinkingLLM_QwenVL_GGUF_PromptEnhancer:
|
||||
stream_to_terminal=stream_tokens_to_terminal,
|
||||
initial_stage_label="TRANSLATION STAGE",
|
||||
enable_thinking=effective_thinking,
|
||||
auto_finalization_retry=auto_finalization_retry,
|
||||
)
|
||||
full_raw_trace = f"{raw_trace}\n\n{trans_trace}"
|
||||
final = clean_model_output(translated, OutputCleanConfig(mode="prompt")) or translated.strip()
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"name": "ComfyUI-ThinkingLLM",
|
||||
"description": "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time.",
|
||||
"author": "goodguy1963",
|
||||
"version": "2.3.10",
|
||||
"version": "2.3.11",
|
||||
"url": "https://github.com/goodguy1963/ComfyUI-ThinkingLLM",
|
||||
"category": "image"
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@
|
||||
"description": "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time.",
|
||||
"category": "image",
|
||||
"tags": ["vision", "language", "multimodal", "thinkingllm", "qwen", "gguf", "flash_attention", "performance", "wan2.2", "video", "i2v", "t2v", "storyboard", "caching", "fork", "gpl-3.0"],
|
||||
"version": "2.3.10"
|
||||
"version": "2.3.11"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "ComfyUI-ThinkingLLM"
|
||||
description = "A multimodal ComfyUI AI node with Qwen3.5, Qwen3-VL, Qwen2.5-VL, Qwen3, and Gemma 4 integrations. Features live thinking in the terminal to see what the LLM is doing in real time."
|
||||
version = "2.3.10"
|
||||
version = "2.3.11"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["transformers>=5.2.0", "torch", "huggingface-hub", "hf_xet", "psutil", "numpy", "Pillow", "opencv-python", "bitsandbytes", "accelerate", "sentencepiece", "tiktoken", "faster-whisper", "llama-cpp-python"]
|
||||
|
||||
|
||||
@@ -1068,6 +1068,24 @@ class TestGGUFAdvancedWorkflowCompatibility(unittest.TestCase):
|
||||
self.assertIn("gemma-4-12b", model_meta["default"].lower())
|
||||
self.assertIn("Audio", input_types["required"]["custom_prompt"][1]["default"])
|
||||
|
||||
def test_gguf_nodes_disable_auto_finalization_retry_by_default(self):
|
||||
package = load_thinkingllm_loader_subset([
|
||||
"AILab_QwenVL_GGUF.py",
|
||||
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
||||
])
|
||||
|
||||
for node_name in (
|
||||
"ThinkingLLM_QwenVL_GGUF",
|
||||
"ThinkingLLM_QwenVL_GGUF_Advanced",
|
||||
"ThinkingLLM_Gemma4_Audio_GGUF",
|
||||
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
|
||||
):
|
||||
with self.subTest(node_name=node_name):
|
||||
required = package.NODE_CLASS_MAPPINGS[node_name].INPUT_TYPES()["required"]
|
||||
self.assertIn("auto_finalization_retry", required)
|
||||
self.assertEqual(required["auto_finalization_retry"][0], "BOOLEAN")
|
||||
self.assertEqual(required["auto_finalization_retry"][1]["default"], False)
|
||||
|
||||
def test_qwen3_asr_node_is_not_registered(self):
|
||||
package = load_thinkingllm_loader_subset(["AILab_QwenVL_GGUF.py", "AILab_WhisperASR.py"])
|
||||
|
||||
@@ -1535,6 +1553,109 @@ class TestLlamaCppInstaller(unittest.TestCase):
|
||||
|
||||
|
||||
class TestGGUFWindllRuntimeFallback(unittest.TestCase):
|
||||
def test_gguf_loader_reuses_catalog_model_from_extra_llm_path_without_download(self):
|
||||
calls = []
|
||||
|
||||
class DummyChatHandler:
|
||||
def __init__(self, **kwargs):
|
||||
calls.append(("chat", kwargs.get("clip_model_path")))
|
||||
|
||||
class DummyLlama:
|
||||
def __init__(self, **kwargs):
|
||||
calls.append(("llama", kwargs.get("model_path")))
|
||||
|
||||
chat_format_module = build_stub_module(
|
||||
"llama_cpp.llama_chat_format",
|
||||
Qwen35VLChatHandler=DummyChatHandler,
|
||||
Qwen3VLChatHandler=DummyChatHandler,
|
||||
)
|
||||
installer_module = build_stub_module(
|
||||
"AILab_LlamaCppInstaller",
|
||||
ensure_llama_cpp_backend=lambda *args, **kwargs: DummyLlama,
|
||||
format_llama_cpp_backend_info=lambda info=None: "stub backend",
|
||||
get_last_llama_cpp_backend_info=lambda: {
|
||||
"gpu_offload": True,
|
||||
"vision_handlers": ["Qwen35VLChatHandler"],
|
||||
},
|
||||
relax_windows_dll_directory_for_long_paths=contextlib.nullcontext,
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_root = Path(temp_dir)
|
||||
dev_models = temp_root / "dev" / "models"
|
||||
shared_llm = temp_root / "shared" / "models" / "LLM"
|
||||
shared_model_dir = shared_llm / "GGUF" / "HauhauCS" / "Qwen3.5-4B-Uncensored-HauhauCS-Aggressive"
|
||||
shared_model_dir.mkdir(parents=True)
|
||||
model_filename = "Qwen3.5-4B-Uncensored-HauhauCS-Aggressive-Q8_0.gguf"
|
||||
mmproj_filename = "mmproj-Qwen3.5-4B-Uncensored-HauhauCS-Aggressive-BF16.gguf"
|
||||
(shared_model_dir / model_filename).write_bytes(b"GGUF")
|
||||
(shared_model_dir / mmproj_filename).write_bytes(b"mmproj")
|
||||
|
||||
stubs = build_loader_test_stubs()
|
||||
stubs.update(
|
||||
{
|
||||
"folder_paths": build_stub_module(
|
||||
"folder_paths",
|
||||
models_dir=str(dev_models),
|
||||
folder_names_and_paths={"LLM": ([str(shared_llm)], set())},
|
||||
get_folder_paths=lambda name: [str(shared_llm)] if name == "LLM" else [],
|
||||
),
|
||||
"AILab_LlamaCppInstaller": installer_module,
|
||||
"llama_cpp": build_stub_module("llama_cpp", llama_chat_format=chat_format_module),
|
||||
"llama_cpp.llama_chat_format": chat_format_module,
|
||||
}
|
||||
)
|
||||
|
||||
with mock.patch.dict(sys.modules, stubs, clear=False):
|
||||
module = load_module_from_file(
|
||||
"AILab_QwenVL_GGUF.py",
|
||||
"thinkingllm_gguf_extra_llm_reuse_test",
|
||||
)
|
||||
|
||||
model_name = next(
|
||||
name
|
||||
for name in module.GGUF_VL_CATALOG["models"]
|
||||
if name.startswith(model_filename)
|
||||
)
|
||||
loader = module.QwenVLGGUFBase()
|
||||
|
||||
with mock.patch.dict(sys.modules, stubs, clear=False), mock.patch.object(
|
||||
module,
|
||||
"_download_single_file",
|
||||
side_effect=AssertionError("download should not be called for shared local GGUF"),
|
||||
), mock.patch.object(
|
||||
module,
|
||||
"_pick_device",
|
||||
return_value="cpu",
|
||||
), mock.patch.object(
|
||||
module,
|
||||
"read_gguf_architecture",
|
||||
return_value="qwen35",
|
||||
):
|
||||
loader._load_model(
|
||||
model_name=model_name,
|
||||
device="cpu",
|
||||
ctx=128,
|
||||
n_batch=16,
|
||||
n_ubatch=None,
|
||||
gpu_layers=0,
|
||||
image_max_tokens=64,
|
||||
top_k=0,
|
||||
pool_size=1024,
|
||||
n_threads=None,
|
||||
n_threads_batch=None,
|
||||
flash_attn=False,
|
||||
offload_kqv=False,
|
||||
ctx_checkpoints=None,
|
||||
enable_thinking=True,
|
||||
unique_id=None,
|
||||
)
|
||||
|
||||
self.assertEqual(calls[0][0], "chat")
|
||||
self.assertTrue(str(calls[0][1]).endswith(mmproj_filename))
|
||||
self.assertEqual(calls[1][0], "llama")
|
||||
self.assertTrue(str(calls[1][1]).endswith(model_filename))
|
||||
|
||||
def test_gguf_runtime_reuses_windows_dll_relaxation_for_handler_and_llama(self):
|
||||
enter_events = []
|
||||
|
||||
@@ -1754,6 +1875,50 @@ class TestGGUFWindllRuntimeFallback(unittest.TestCase):
|
||||
self.assertEqual(loader.llm.reset_count, 1)
|
||||
self.assertEqual(loader.llm.events, ["reset", "completion"])
|
||||
|
||||
def test_gguf_invoke_does_not_auto_finalize_by_default(self):
|
||||
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
||||
module = load_module_from_file(
|
||||
"AILab_QwenVL_GGUF.py",
|
||||
"thinkingllm_gguf_no_default_finalization_test",
|
||||
)
|
||||
|
||||
class DummyLlama:
|
||||
def __init__(self):
|
||||
self.events = []
|
||||
self.calls = 0
|
||||
|
||||
def reset(self):
|
||||
self.events.append(("reset", self.calls))
|
||||
|
||||
def create_chat_completion(self, **kwargs):
|
||||
self.calls += 1
|
||||
self.events.append(("completion", self.calls))
|
||||
return iter([
|
||||
{"choices": [{"delta": {"reasoning_content": "hidden reasoning only"}}]},
|
||||
])
|
||||
|
||||
loader = module.QwenVLGGUFBase()
|
||||
loader.llm = DummyLlama()
|
||||
|
||||
text, raw_trace = loader._invoke(
|
||||
system_prompt="system",
|
||||
user_prompt="user",
|
||||
images_b64=[],
|
||||
audio_b64=[],
|
||||
max_tokens=8,
|
||||
temperature=0.0,
|
||||
top_p=1.0,
|
||||
repetition_penalty=1.0,
|
||||
seed=1,
|
||||
stream_to_terminal=False,
|
||||
enable_thinking=True,
|
||||
)
|
||||
|
||||
self.assertEqual(text, "")
|
||||
self.assertIn("[STREAMING]", raw_trace)
|
||||
self.assertNotIn("FINALIZATION ATTEMPT", raw_trace)
|
||||
self.assertEqual(loader.llm.events, [("reset", 0), ("completion", 1)])
|
||||
|
||||
def test_gguf_invoke_resets_before_finalization_retry_completion(self):
|
||||
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
||||
module = load_module_from_file(
|
||||
@@ -1795,6 +1960,7 @@ class TestGGUFWindllRuntimeFallback(unittest.TestCase):
|
||||
seed=1,
|
||||
stream_to_terminal=False,
|
||||
enable_thinking=True,
|
||||
auto_finalization_retry=True,
|
||||
)
|
||||
|
||||
self.assertEqual(text, "final answer")
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# ComfyUI-QwenVL Update Log
|
||||
|
||||
## Version 2.3.11 (2026/06/19) — GGUF Single-Pass Execution Fix
|
||||
|
||||
### 2.3.11 Changes
|
||||
|
||||
- Disabled automatic GGUF finalization retries by default so one node execution performs one LLM generation pass unless explicitly configured otherwise.
|
||||
- Added `auto_finalization_retry` to `ThinkingLLM (GGUF)`, `ThinkingLLM Advanced (GGUF)`, `ThinkingLLM Gemma 4 Audio (GGUF)`, and `ThinkingLLM Prompt Enhancer (GGUF)` for users who still want the old empty/reasoning-only retry behavior.
|
||||
- Fixed catalogued GGUF model reuse across ComfyUI `extra_model_paths.yaml` `LLM` mappings, preventing unnecessary re-downloads when models already exist in a shared `models/LLM/GGUF` directory.
|
||||
|
||||
### 2.3.11 Validation
|
||||
|
||||
- Added regression coverage for default single-pass GGUF execution, explicit opt-in finalization retry, GGUF node UI defaults, and shared `LLM/GGUF` model reuse.
|
||||
- Targeted GGUF tests pass.
|
||||
|
||||
## Version 2.3.10 (2026/06/19) — ComfyUI 2.0 Advanced GGUF Layout Fix
|
||||
|
||||
### 2.3.10 Changes
|
||||
|
||||
Reference in New Issue
Block a user