diff --git a/AILab_QwenVL_GGUF.py b/AILab_QwenVL_GGUF.py index 23eb9bd..13e3318 100644 --- a/AILab_QwenVL_GGUF.py +++ b/AILab_QwenVL_GGUF.py @@ -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 = "" diff --git a/AILab_QwenVL_GGUF_PromptEnhancer.py b/AILab_QwenVL_GGUF_PromptEnhancer.py index f7da756..977d02f 100644 --- a/AILab_QwenVL_GGUF_PromptEnhancer.py +++ b/AILab_QwenVL_GGUF_PromptEnhancer.py @@ -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() diff --git a/__init__.py b/__init__.py index dad1ca8..ef186f3 100644 --- a/__init__.py +++ b/__init__.py @@ -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" } diff --git a/custom_nodes.json b/custom_nodes.json index 5f7bd9e..d118406 100644 --- a/custom_nodes.json +++ b/custom_nodes.json @@ -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" } diff --git a/pyproject.toml b/pyproject.toml index ee294de..75d8993 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/tests/test_new_features.py b/tests/test_new_features.py index f26f571..86e55fc 100644 --- a/tests/test_new_features.py +++ b/tests/test_new_features.py @@ -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") diff --git a/update.md b/update.md index 0af8e86..386c149 100644 --- a/update.md +++ b/update.md @@ -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