4680 lines
210 KiB
Python
4680 lines
210 KiB
Python
"""Lightweight regression tests for the local ThinkingLLM fork.
|
|
|
|
Run from the ComfyUI-ThinkingLLM folder:
|
|
python tests/test_new_features.py
|
|
|
|
These tests intentionally avoid importing the model modules so they can run in a
|
|
plain shell without ML dependencies.
|
|
"""
|
|
|
|
import ast
|
|
import base64
|
|
import contextlib
|
|
import hashlib
|
|
import importlib.util
|
|
import inspect
|
|
import io
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import types
|
|
import unittest
|
|
import wave
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
import torch
|
|
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
PKG = HERE.parent
|
|
WORKFLOW_DIR = PKG / "example_workflows"
|
|
|
|
STREAMING_FILES = {
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py": {
|
|
"stage": "INITIAL GENERATION",
|
|
"label": "QwenVL GGUF",
|
|
},
|
|
"AILab_QwenVL_PromptEnhancer.py": {
|
|
"stage": "HF TEXT GENERATION",
|
|
"label": "QwenVL HF",
|
|
},
|
|
"thinkingllm_core/hf_runtime.py": {
|
|
"stage": None,
|
|
"label": "QwenVL HF",
|
|
},
|
|
"AILab_QwenVL_GGUF.py": {
|
|
"stage": "STREAMING",
|
|
"label": "QwenVL GGUF",
|
|
},
|
|
}
|
|
|
|
|
|
def read_source(filename: str) -> str:
|
|
return (PKG / filename).read_text(encoding="utf-8")
|
|
|
|
|
|
def parse_source(filename: str) -> ast.AST:
|
|
return ast.parse(read_source(filename), filename=filename)
|
|
|
|
|
|
def build_stub_module(name: str, **attrs):
|
|
module = types.ModuleType(name)
|
|
for key, value in attrs.items():
|
|
setattr(module, key, value)
|
|
return module
|
|
|
|
|
|
class _StubQuantization:
|
|
FP16 = types.SimpleNamespace(value="fp16")
|
|
|
|
@classmethod
|
|
def get_values(cls):
|
|
return [cls.FP16.value]
|
|
|
|
|
|
def build_loader_test_stubs() -> dict[str, types.ModuleType]:
|
|
pil_image = build_stub_module("PIL.Image", Image=object)
|
|
pil_package = build_stub_module("PIL", Image=pil_image)
|
|
comfy_model_management = build_stub_module(
|
|
"comfy.model_management",
|
|
get_torch_device=lambda: types.SimpleNamespace(type="cuda", index=0),
|
|
throw_exception_if_processing_interrupted=lambda: None,
|
|
)
|
|
comfy_package = build_stub_module("comfy", model_management=comfy_model_management)
|
|
output_clean_config = type("OutputCleanConfig", (), {"__init__": lambda self, *args, **kwargs: None})
|
|
terminal_stream_display = type("TerminalStreamDisplay", (), {})
|
|
stream_degeneration_error = type("StreamDegenerationError", (RuntimeError,), {})
|
|
qwen_base = type("QwenVLBase", (), {"__init__": lambda self, *args, **kwargs: None})
|
|
return {
|
|
"numpy": build_stub_module("numpy", ndarray=object),
|
|
"torch": build_stub_module(
|
|
"torch",
|
|
Tensor=object,
|
|
dtype=object,
|
|
float16=object,
|
|
float32=object,
|
|
device=type("device", (), {}),
|
|
cuda=types.SimpleNamespace(device_count=lambda: 0, is_available=lambda: False),
|
|
backends=types.SimpleNamespace(mps=types.SimpleNamespace(is_available=lambda: False)),
|
|
no_grad=lambda func=None, *args, **kwargs: (lambda inner: inner) if func is None else func,
|
|
inference_mode=lambda func=None, *args, **kwargs: (lambda inner: inner) if func is None else func,
|
|
),
|
|
"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: "",
|
|
),
|
|
"PIL": pil_package,
|
|
"PIL.Image": pil_image,
|
|
"folder_paths": build_stub_module(
|
|
"folder_paths",
|
|
models_dir=str(PKG),
|
|
folder_names_and_paths={},
|
|
get_folder_paths=lambda *args, **kwargs: [],
|
|
get_filename_list=lambda *args, **kwargs: [],
|
|
get_full_path_or_raise=lambda category, filename: str(PKG / filename),
|
|
),
|
|
"AILab_StreamDisplay": build_stub_module(
|
|
"AILab_StreamDisplay",
|
|
StreamDegenerationError=stream_degeneration_error,
|
|
StreamDegenerationGuard=type("StreamDegenerationGuard", (), {"__init__": lambda self, *args, **kwargs: None, "push": lambda self, text: None}),
|
|
TerminalStreamDisplay=terminal_stream_display,
|
|
compose_streamed_model_output=lambda reasoning, content: (
|
|
f"<think>\n{reasoning.strip()}\n</think>\n\n{content.strip()}" if reasoning and content
|
|
else f"<think>\n{reasoning.strip()}\n</think>" if reasoning
|
|
else content.strip()
|
|
),
|
|
extract_stream_token=lambda chunk: {"reasoning": "", "content": (chunk.get("choices") or [{}])[0].get("delta", {}).get("content", "")},
|
|
strip_degenerate_repetition=lambda text, *args, **kwargs: text,
|
|
),
|
|
"AILab_LlamaCppInstaller": build_stub_module(
|
|
"AILab_LlamaCppInstaller",
|
|
ensure_llama_cpp_backend=lambda *args, **kwargs: object(),
|
|
format_llama_cpp_backend_info=lambda info=None: "stub backend",
|
|
get_last_llama_cpp_backend_info=lambda: {"gpu_offload": None, "vision_handlers": ["Qwen3VLChatHandler"]},
|
|
relax_windows_dll_directory_for_long_paths=contextlib.nullcontext,
|
|
),
|
|
"AILab_OutputCleaner": build_stub_module(
|
|
"AILab_OutputCleaner",
|
|
OUTPUT_CLEANER_VERSION=4,
|
|
OutputCleanConfig=output_clean_config,
|
|
clean_model_output=lambda text, *args, **kwargs: text,
|
|
prompt_output_guard=lambda text, *args, **kwargs: text,
|
|
),
|
|
"transformers": build_stub_module(
|
|
"transformers",
|
|
AutoModelForCausalLM=object,
|
|
AutoModelForVision2Seq=object,
|
|
AutoModelForImageTextToText=object,
|
|
AutoProcessor=object,
|
|
AutoTokenizer=object,
|
|
BitsAndBytesConfig=type("BitsAndBytesConfig", (), {}),
|
|
),
|
|
"comfy": comfy_package,
|
|
"comfy.model_management": comfy_model_management,
|
|
"AILab_QwenVL": build_stub_module(
|
|
"AILab_QwenVL",
|
|
ATTENTION_MODES=["auto"],
|
|
HF_ALL_MODELS={"stub-model": {}},
|
|
HF_TEXT_MODELS={},
|
|
HF_VL_MODELS={},
|
|
NODE_PROMPT_STATE={},
|
|
PROMPT_CACHE={},
|
|
MASK_FOCUS_INSTRUCTION="mask focus instruction",
|
|
_make_node_state_key=lambda *args, **kwargs: "node-key",
|
|
_build_workflow_fingerprint=lambda *args, **kwargs: "workflow-fingerprint",
|
|
_default_model_from_config=lambda models, fallback: next(
|
|
(
|
|
name
|
|
for name, info in models.items()
|
|
if isinstance(info, dict) and info.get("default")
|
|
),
|
|
next(iter(models.keys()), fallback),
|
|
),
|
|
apply_qwen_soft_thinking_directive=lambda *args, **kwargs: None,
|
|
apply_mask_highlight=lambda image, mask: (image, "mask-hash" if mask is not None else None),
|
|
build_node_input_signature=lambda *args, **kwargs: {},
|
|
download_hf_file_to_path=lambda *args, **kwargs: None,
|
|
ensure_model=lambda *args, **kwargs: None,
|
|
ensure_cuda_vram_headroom=lambda *args, **kwargs: None,
|
|
estimate_qwen_text_tokens=lambda *args, **kwargs: 0,
|
|
get_cache_key=lambda *args, **kwargs: "cache-key",
|
|
get_alternative_cache_key=lambda *args, **kwargs: "alt-cache-key",
|
|
get_image_hash=lambda *args, **kwargs: "image-hash",
|
|
get_video_hash=lambda *args, **kwargs: "video-hash",
|
|
log_llm_input=lambda *args, **kwargs: None,
|
|
model_catalog_label=lambda name, info: name,
|
|
model_catalog_options=lambda models, predicate=None: sorted(
|
|
name for name, info in models.items() if predicate is None or predicate(name, info or {})
|
|
),
|
|
normalize_commercial_status=lambda info: "local" if (info or {}).get("is_local") else (info or {}).get("commercial_status", "unclear"),
|
|
resolve_model_catalog_name=lambda models, selected: selected,
|
|
enforce_model_access=lambda *args, **kwargs: None,
|
|
save_prompt_cache=lambda *args, **kwargs: None,
|
|
get_node_saved_prompt=lambda *args, **kwargs: None,
|
|
get_node_saved_prompt_with_seed=lambda *args, **kwargs: None,
|
|
resolve_qwen_thinking_mode=lambda *args, **kwargs: False,
|
|
resolve_qwen_context_window=lambda *args, **kwargs: 0,
|
|
set_node_saved_prompt=lambda *args, **kwargs: None,
|
|
apply_video_duration_context=lambda prompt, *args, **kwargs: prompt,
|
|
ensure_video_prompt_duration=lambda prompt, *args, **kwargs: prompt,
|
|
resolve_video_duration=lambda *args, **kwargs: None,
|
|
validate_minimax_source_duration=lambda *args, **kwargs: [],
|
|
validate_minimax_reference_request=lambda *args, **kwargs: None,
|
|
load_node_prompt_state=lambda *args, **kwargs: None,
|
|
QwenVLBase=qwen_base,
|
|
Quantization=_StubQuantization,
|
|
TOOLTIPS={},
|
|
VIDEO_DURATION_INPUT=("FLOAT", {"default": 5.0, "min": 0.2, "max": 150.0, "step": 0.1}),
|
|
),
|
|
}
|
|
|
|
|
|
def load_thinkingllm_loader_subset(
|
|
module_filenames: list[str],
|
|
node_filenames: list[str] | None = None,
|
|
):
|
|
package_name = "thinkingllm_loader_smoke"
|
|
node_filenames = node_filenames or []
|
|
tracked_names = [
|
|
package_name,
|
|
*[Path(filename).stem for filename in module_filenames],
|
|
*[Path(filename).stem for filename in node_filenames],
|
|
]
|
|
previous_modules = {
|
|
name: sys.modules[name]
|
|
for name in tracked_names
|
|
if name in sys.modules
|
|
}
|
|
real_listdir = os.listdir
|
|
pkg_path = PKG.resolve()
|
|
nodes_path = (PKG / "nodes").resolve()
|
|
|
|
def fake_listdir(path):
|
|
resolved = Path(path).resolve()
|
|
if resolved == pkg_path:
|
|
return ["__init__.py", *module_filenames]
|
|
if resolved == nodes_path:
|
|
return node_filenames
|
|
return real_listdir(path)
|
|
|
|
try:
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
with mock.patch("os.listdir", side_effect=fake_listdir):
|
|
spec = importlib.util.spec_from_file_location(package_name, PKG / "__init__.py")
|
|
if spec is None or spec.loader is None:
|
|
raise AssertionError("failed to create loader spec for ThinkingLLM package")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[package_name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
finally:
|
|
for name in tracked_names:
|
|
if name in previous_modules:
|
|
sys.modules[name] = previous_modules[name]
|
|
else:
|
|
sys.modules.pop(name, None)
|
|
|
|
|
|
def load_module_from_file(filename: str, module_name: str):
|
|
previous_module = sys.modules.get(module_name)
|
|
try:
|
|
spec = importlib.util.spec_from_file_location(module_name, PKG / filename)
|
|
if spec is None or spec.loader is None:
|
|
raise AssertionError(f"failed to create loader spec for {filename}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[module_name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
finally:
|
|
if previous_module is None:
|
|
sys.modules.pop(module_name, None)
|
|
else:
|
|
sys.modules[module_name] = previous_module
|
|
|
|
|
|
def extract_dict_node(filename: str, name: str) -> ast.Dict:
|
|
tree = parse_source(filename)
|
|
for node in tree.body:
|
|
if not isinstance(node, ast.Assign):
|
|
continue
|
|
for target in node.targets:
|
|
if isinstance(target, ast.Name) and target.id == name:
|
|
if isinstance(node.value, ast.Dict):
|
|
return node.value
|
|
raise AssertionError(f"{name} in {filename} is not a dict literal")
|
|
raise AssertionError(f"{name} not found in {filename}")
|
|
|
|
|
|
def extract_mapping_target_names(filename: str, name: str) -> dict[str, str]:
|
|
mapping = {}
|
|
dict_node = extract_dict_node(filename, name)
|
|
for key_node, value_node in zip(dict_node.keys, dict_node.values):
|
|
key = ast.literal_eval(key_node)
|
|
if isinstance(value_node, ast.Name):
|
|
mapping[key] = value_node.id
|
|
else:
|
|
raise AssertionError(f"{name}[{key}] in {filename} is not a direct name reference")
|
|
return mapping
|
|
|
|
|
|
def extract_literal_dict(filename: str, name: str) -> dict[str, str]:
|
|
mapping = {}
|
|
dict_node = extract_dict_node(filename, name)
|
|
for key_node, value_node in zip(dict_node.keys, dict_node.values):
|
|
key = ast.literal_eval(key_node)
|
|
mapping[key] = ast.literal_eval(value_node)
|
|
return mapping
|
|
|
|
|
|
def iter_workflow_types(path: Path) -> list[str]:
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
return [
|
|
node.get("type")
|
|
for node in payload.get("nodes", [])
|
|
if isinstance(node, dict) and isinstance(node.get("type"), str)
|
|
]
|
|
|
|
|
|
def load_model_recommendations() -> dict:
|
|
return json.loads((PKG / "web" / "model_recommendations.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
def matches_recommendation_rule(model_name: str, rules: list[dict]) -> bool:
|
|
lowered = model_name.lower()
|
|
ordered = sorted(rules, key=lambda rule: rule.get("priority", 0), reverse=True)
|
|
for rule in ordered:
|
|
include = [token.lower() for token in rule.get("match_any", [])]
|
|
exclude = [token.lower() for token in rule.get("exclude_any", [])]
|
|
if any(token in lowered for token in include) and not any(token in lowered for token in exclude):
|
|
return True
|
|
return False
|
|
|
|
|
|
def iter_catalog_display_names() -> list[str]:
|
|
names: list[str] = []
|
|
hf_payload = json.loads((PKG / "hf_models.json").read_text(encoding="utf-8"))
|
|
for section in ("hf_vl_models", "hf_text_models"):
|
|
names.extend(list((hf_payload.get(section) or {}).keys()))
|
|
|
|
gguf_payload = json.loads((PKG / "gguf_models.json").read_text(encoding="utf-8"))
|
|
for section in ("Qwen_model", "qwenVL_model"):
|
|
entries = gguf_payload.get(section) or {}
|
|
for repo_key, repo in entries.items():
|
|
if not isinstance(repo, dict):
|
|
continue
|
|
for model_file in repo.get("model_files") or []:
|
|
names.append(str(Path(model_file).name))
|
|
return names
|
|
|
|
|
|
class TestMaskFocusedImageAnalysis(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
function_node = next(
|
|
node
|
|
for node in parse_source("thinkingllm_core/media.py").body
|
|
if isinstance(node, ast.FunctionDef) and node.name == "apply_mask_highlight"
|
|
)
|
|
namespace = {"hashlib": hashlib, "torch": torch}
|
|
exec(compile(ast.fix_missing_locations(ast.Module(body=[function_node], type_ignores=[])), "thinkingllm_core/media.py", "exec"), namespace)
|
|
cls.apply_mask_highlight = staticmethod(namespace["apply_mask_highlight"])
|
|
|
|
def test_mask_dims_rgb_and_preserves_alpha_without_mutating_input(self):
|
|
image = torch.ones((1, 2, 2, 4), dtype=torch.float32)
|
|
image[..., 3] = 0.5
|
|
original = image.clone()
|
|
mask = torch.tensor([[[1.0, 0.0], [0.5, 1.0]]])
|
|
|
|
highlighted, mask_hash = self.apply_mask_highlight(image, mask)
|
|
|
|
expected_brightness = torch.tensor([[1.0, 0.2], [0.6, 1.0]])
|
|
torch.testing.assert_close(highlighted[0, ..., :3], expected_brightness[..., None].expand(-1, -1, 3))
|
|
torch.testing.assert_close(highlighted[..., 3], original[..., 3])
|
|
torch.testing.assert_close(image, original)
|
|
self.assertEqual(len(mask_hash), 16)
|
|
|
|
def test_mask_resizes_and_hashes_complete_content(self):
|
|
image = torch.ones((1, 4, 4, 3), dtype=torch.float32)
|
|
first_mask = torch.zeros((1, 4, 4), dtype=torch.float32)
|
|
second_mask = first_mask.clone()
|
|
first_mask[0, 0, 0] = 1.0
|
|
second_mask[0, 0, 0] = 1.0
|
|
second_mask[0, -1, -1] = 1.0
|
|
small_mask = torch.tensor([[[1.0, 0.0], [0.0, 0.0]]])
|
|
|
|
resized, _ = self.apply_mask_highlight(image, small_mask)
|
|
_, first_hash = self.apply_mask_highlight(image, first_mask)
|
|
_, second_hash = self.apply_mask_highlight(image, second_mask)
|
|
|
|
self.assertEqual(resized.shape, image.shape)
|
|
self.assertGreater(float(resized[0, 0, 0, 0]), float(resized[0, -1, -1, 0]))
|
|
self.assertNotEqual(first_hash, second_hash)
|
|
|
|
def test_mask_requires_an_image_and_a_nonempty_selection(self):
|
|
with self.assertRaisesRegex(ValueError, "requires an image"):
|
|
self.apply_mask_highlight(None, torch.ones((1, 2, 2)))
|
|
with self.assertRaisesRegex(ValueError, "MASK is empty"):
|
|
self.apply_mask_highlight(torch.ones((1, 2, 2, 3)), torch.zeros((1, 2, 2)))
|
|
with self.assertRaisesRegex(ValueError, "mask mode"):
|
|
self.apply_mask_highlight(
|
|
torch.ones((1, 2, 2, 3)),
|
|
torch.ones((1, 2, 2)),
|
|
mode="unsupported",
|
|
)
|
|
|
|
def test_reconstruct_mode_conceals_selected_pixels_and_preserves_alpha(self):
|
|
image = torch.zeros((1, 16, 16, 4), dtype=torch.float32)
|
|
image[..., :3] = 0.25
|
|
image[:, 7:9, 7:9, :3] = 1.0
|
|
image[..., 3] = 0.75
|
|
mask = torch.zeros((1, 16, 16), dtype=torch.float32)
|
|
mask[:, 7:9, 7:9] = 1.0
|
|
|
|
concealed, reconstruct_hash = self.apply_mask_highlight(image, mask, mode="reconstruct")
|
|
_, focus_hash = self.apply_mask_highlight(image, mask, mode="focus")
|
|
|
|
torch.testing.assert_close(concealed[0, 8, 8, :3], torch.full((3,), 0.25))
|
|
torch.testing.assert_close(concealed[..., 3], image[..., 3])
|
|
self.assertNotEqual(reconstruct_hash, focus_hash)
|
|
|
|
def test_both_backends_apply_the_shared_focus_instruction(self):
|
|
hf_source = read_source("thinkingllm_core/hf_runtime.py")
|
|
gguf_source = read_source("AILab_QwenVL_GGUF.py")
|
|
|
|
self.assertIn("image, mask_hash = apply_mask_highlight(image, mask)", hf_source)
|
|
self.assertIn("image, mask_hash = apply_mask_highlight(image, mask, mode=mask_mode)", gguf_source)
|
|
self.assertIn('image_hash = f"{image_hash}:{mask_hash}"', hf_source)
|
|
self.assertIn('image_hash = f"{image_hash}:{mask_hash}"', gguf_source)
|
|
self.assertIn('prompt = f"{prompt}\\n\\n{MASK_FOCUS_INSTRUCTION}".strip()', hf_source)
|
|
self.assertIn("MASK_RECONSTRUCTION_INSTRUCTION", gguf_source)
|
|
self.assertIn(
|
|
"mask_preview, _ = apply_mask_highlight(image, mask, mode=mask_mode)",
|
|
gguf_source,
|
|
)
|
|
|
|
|
|
class TestTerminalDisplayHelper(unittest.TestCase):
|
|
def test_helper_file_exists(self):
|
|
self.assertTrue((PKG / "AILab_StreamDisplay.py").exists())
|
|
|
|
def test_helper_class_and_methods_exist(self):
|
|
tree = parse_source("AILab_StreamDisplay.py")
|
|
helper_class = None
|
|
for node in tree.body:
|
|
if isinstance(node, ast.ClassDef) and node.name == "TerminalStreamDisplay":
|
|
helper_class = node
|
|
break
|
|
self.assertIsNotNone(helper_class, "TerminalStreamDisplay class missing")
|
|
method_names = {
|
|
node.name for node in helper_class.body if isinstance(node, ast.FunctionDef)
|
|
}
|
|
for method in ["start_stage", "push", "flush", "end_stage", "push_compact", "end_compact"]:
|
|
self.assertIn(method, method_names)
|
|
|
|
|
|
class TestBufferedStreamingIntegration(unittest.TestCase):
|
|
def test_old_rolling_tail_patterns_removed(self):
|
|
old_patterns = [
|
|
r'print\(f"\\r\{tail\}"',
|
|
r"tail\s*=\s*full_streamed\[-200:\]",
|
|
r"tail\s*=\s*full_text\[-200:\]",
|
|
]
|
|
for filename in STREAMING_FILES:
|
|
source = read_source(filename)
|
|
for pattern in old_patterns:
|
|
with self.subTest(filename=filename, pattern=pattern):
|
|
self.assertIsNone(re.search(pattern, source))
|
|
|
|
def test_all_streaming_files_use_helper(self):
|
|
for filename, expectations in STREAMING_FILES.items():
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
self.assertIn("TerminalStreamDisplay", source)
|
|
self.assertIn(expectations["label"], source)
|
|
if expectations["stage"]:
|
|
self.assertIn(expectations["stage"], source)
|
|
self.assertIn(".push_compact(", source, f"{filename} should use compact streaming display")
|
|
self.assertIn(".end_compact(", source, f"{filename} should finish compact streaming cleanly")
|
|
|
|
def test_gguf_prompt_enhancer_has_no_off_toggle_text_stream_fallback(self):
|
|
source = read_source("AILab_QwenVL_GGUF_PromptEnhancer.py")
|
|
self.assertNotIn("_maybe_emit_prompt_compact_progress", source)
|
|
|
|
|
|
class TestModelRecommendations(unittest.TestCase):
|
|
def test_model_catalog_statuses_are_valid_and_assignments_are_conservative(self):
|
|
valid = {"cleared", "external_gated", "unclear", "noncommercial"}
|
|
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"))
|
|
hf_entries = [
|
|
entry
|
|
for section in ("hf_vl_models", "hf_text_models")
|
|
for entry in (hf_payload.get(section) or {}).values()
|
|
]
|
|
gguf_entries = [
|
|
entry
|
|
for section in ("Qwen_model", "qwenVL_model")
|
|
for entry in (gguf_payload.get(section) or {}).values()
|
|
]
|
|
|
|
self.assertTrue(all(entry.get("commercial_status") in valid for entry in hf_entries + gguf_entries))
|
|
self.assertEqual(2, sum(entry["commercial_status"] == "cleared" for entry in hf_entries))
|
|
self.assertEqual(2, sum(entry["commercial_status"] == "external_gated" for entry in hf_entries))
|
|
self.assertFalse(any(entry["commercial_status"] == "noncommercial" for entry in hf_entries + gguf_entries))
|
|
self.assertTrue(all(entry["commercial_status"] == "unclear" for entry in gguf_entries))
|
|
|
|
def test_commercial_catalog_contains_only_release_locked_models(self):
|
|
payload = json.loads((PKG / "hf_models.commercial.json").read_text(encoding="utf-8"))
|
|
entries = [
|
|
entry
|
|
for section in ("hf_vl_models", "hf_text_models")
|
|
for entry in payload.get(section, {}).values()
|
|
]
|
|
self.assertEqual({"MODEL-006", "MODEL-014"}, {entry["component_id"] for entry in entries})
|
|
self.assertTrue(all(re.fullmatch(r"[0-9a-f]{40}", entry["revision"]) for entry in entries))
|
|
self.assertTrue(all(entry["commercial_status"] == "cleared" for entry in entries))
|
|
|
|
def test_model_status_dropdowns_are_commercial_only_and_legacy_labels_resolve(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_model_status_ui_test")
|
|
catalog = {
|
|
"unclear": {},
|
|
"noncommercial": {"commercial_status": "noncommercial"},
|
|
"local": {"is_local": True},
|
|
"gated": {"commercial_status": "external_gated"},
|
|
"cleared": {"commercial_status": "cleared"},
|
|
"invalid": {"commercial_status": "not-a-status"},
|
|
}
|
|
|
|
self.assertEqual(list(catalog), module.model_catalog_options(catalog))
|
|
access_globals = module.model_catalog_options.__globals__
|
|
with mock.patch.dict(access_globals, {"COMMERCIAL_RELEASE": True}):
|
|
options = module.model_catalog_options(catalog)
|
|
self.assertEqual(["✅", "🔑", "📁", "⚠", "⚠", "🚫"], [option.split()[0] for option in options])
|
|
for raw_name, entry in catalog.items():
|
|
decorated = f"{module.MODEL_STATUS_PRESENTATION[module.normalize_commercial_status(entry)][1]}{raw_name}"
|
|
self.assertEqual(raw_name, module.resolve_model_catalog_name(catalog, raw_name))
|
|
self.assertEqual(raw_name, module.resolve_model_catalog_name(catalog, decorated))
|
|
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")
|
|
|
|
self.assertTrue(all(hasattr(module, name) for name in module.__all__))
|
|
self.assertIs(module.PROMPT_CACHE, module.get_cache_key.__globals__["PROMPT_CACHE"])
|
|
self.assertIs(module.NODE_PROMPT_STATE, module.get_node_saved_prompt.__globals__["NODE_PROMPT_STATE"])
|
|
self.assertIs(module.VIDEO_PRESET_METADATA, module.get_video_preset_metadata.__globals__["VIDEO_PRESET_METADATA"])
|
|
|
|
model_globals = module.load_model_configs.__globals__
|
|
exported_catalogs = (module.HF_VL_MODELS, module.HF_TEXT_MODELS, module.HF_ALL_MODELS)
|
|
module.load_model_configs()
|
|
self.assertEqual(
|
|
exported_catalogs,
|
|
(model_globals["HF_VL_MODELS"], model_globals["HF_TEXT_MODELS"], model_globals["HF_ALL_MODELS"]),
|
|
)
|
|
self.assertTrue(all(before is after for before, after in zip(
|
|
exported_catalogs,
|
|
(model_globals["HF_VL_MODELS"], model_globals["HF_TEXT_MODELS"], model_globals["HF_ALL_MODELS"]),
|
|
)))
|
|
|
|
def test_community_hf_and_gguf_nodes_expose_plain_model_names(self):
|
|
package = load_thinkingllm_loader_subset([
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
])
|
|
prefixes = ["✅ Commercial | ", "🔑 External / gated | ", "📁 Local / user-supplied | ", "⚠ Rights unclear | ", "🚫 Non-commercial | "]
|
|
node_names = (
|
|
"ThinkingLLM_QwenVL",
|
|
"ThinkingLLM_QwenVL_Advanced",
|
|
"ThinkingLLM_QwenVL_PromptEnhancer",
|
|
"ThinkingLLM_QwenVL_GGUF",
|
|
"ThinkingLLM_QwenVL_GGUF_Advanced",
|
|
"ThinkingLLM_Gemma4_Audio_GGUF",
|
|
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
|
|
)
|
|
|
|
for node_name in node_names:
|
|
with self.subTest(node_name=node_name):
|
|
options = package.NODE_CLASS_MAPPINGS[node_name].INPUT_TYPES()["required"]["model_name"][0]
|
|
self.assertTrue(all(not option.startswith(tuple(prefixes)) for option in options))
|
|
|
|
def test_commercial_mode_disables_mutable_runtime_behaviour(self):
|
|
init_source = read_source("__init__.py")
|
|
access_source = read_source("thinkingllm_core/model_access.py")
|
|
hf_models_source = read_source("thinkingllm_core/hf_models.py")
|
|
hf_runtime_source = read_source("thinkingllm_core/hf_runtime.py")
|
|
enhancer_source = read_source("AILab_QwenVL_PromptEnhancer.py")
|
|
|
|
self.assertIn('os.environ.get("THINKINGLLM_COMMERCIAL_RELEASE") == "1"', init_source)
|
|
self.assertIn('WEB_DIRECTORY = None if COMMERCIAL_RELEASE else "./web"', init_source)
|
|
self.assertNotIn('"AILab_QwenVL_GGUF"', re.search(r"COMMERCIAL_MODULES = \{.*?\}", init_source, re.DOTALL).group(0))
|
|
commercial_node_modules = re.search(
|
|
r"COMMERCIAL_NODE_MODULES\s*=\s*(\{.*?\})",
|
|
init_source,
|
|
re.DOTALL,
|
|
)
|
|
self.assertIsNotNone(commercial_node_modules)
|
|
self.assertEqual(
|
|
ast.literal_eval(commercial_node_modules.group(1)),
|
|
{"story_split_node", "system_prompt_preset", "vram_cleanup"},
|
|
)
|
|
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 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)
|
|
self.assertIn("commercial release mode forbids runtime package installation", enhancer_source)
|
|
|
|
def test_recommendation_rules_have_sources(self):
|
|
payload = load_model_recommendations()
|
|
rules = payload.get("rules") or []
|
|
self.assertTrue(rules, "model_recommendations.json should define at least one rule")
|
|
for rule in rules:
|
|
with self.subTest(rule=rule.get("id")):
|
|
self.assertTrue(rule.get("sources"), "each recommendation rule should record its research source")
|
|
|
|
def test_recommendation_notes_are_creative_work_focused(self):
|
|
payload = load_model_recommendations()
|
|
banned_terms = ("coding", "programming", "benchmark", "webdev", "math competition")
|
|
for rule in payload.get("rules") or []:
|
|
for note in rule.get("notes") or []:
|
|
lowered = note.lower()
|
|
with self.subTest(rule=rule.get("id"), note=note):
|
|
self.assertFalse(
|
|
any(term in lowered for term in banned_terms),
|
|
"recommendation notes should focus on creative prompt generation rather than coding/benchmarking",
|
|
)
|
|
|
|
def test_recommendations_cover_all_catalog_models(self):
|
|
payload = load_model_recommendations()
|
|
rules = payload.get("rules") or []
|
|
uncovered = [name for name in iter_catalog_display_names() if not matches_recommendation_rule(name, rules)]
|
|
self.assertEqual([], uncovered)
|
|
|
|
def test_qwen36_hauhau_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 {}
|
|
|
|
text_key = next((key for key in text_catalog if "Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive" in key), None)
|
|
vision_key = next((key for key in vision_catalog if "Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive" in key), None)
|
|
|
|
self.assertIsNotNone(text_key)
|
|
self.assertIsNotNone(vision_key)
|
|
self.assertIn("mmproj-Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-f16.gguf", vision_catalog[vision_key]["mmproj_file"])
|
|
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"))
|
|
|
|
self.assertTrue(any("Gemma-4-12B" in key for key in hf_payload.get("hf_vl_models", {})))
|
|
self.assertTrue(any("Gemma-4-12B" in key for key in hf_payload.get("hf_text_models", {})))
|
|
|
|
gguf_text = gguf_payload.get("Qwen_model") or {}
|
|
gguf_vision = gguf_payload.get("qwenVL_model") or {}
|
|
text_key = next((key for key in gguf_text if "Gemma-4-12B" in key), None)
|
|
vision_key = next((key for key in gguf_vision if "Gemma-4-12B" in key), None)
|
|
|
|
self.assertIsNotNone(text_key)
|
|
self.assertIsNotNone(vision_key)
|
|
self.assertIn("mmproj-F16.gguf", gguf_vision[vision_key]["mmproj_file"])
|
|
self.assertGreaterEqual(len(gguf_text[text_key].get("model_files") or []), 8)
|
|
self.assertGreaterEqual(len(gguf_vision[vision_key].get("model_files") or []), 8)
|
|
|
|
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 and "Aggressive" not in key
|
|
]
|
|
|
|
self.assertTrue(entries)
|
|
for entry in entries:
|
|
self.assertEqual(entry["repo_id"], "bartowski/google_gemma-4-E2B-it-GGUF")
|
|
self.assertEqual(entry["model_files"], [
|
|
"google_gemma-4-E2B-it-Q4_K_S.gguf",
|
|
"google_gemma-4-E2B-it-Q8_0.gguf",
|
|
"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 {}
|
|
asr_entries = {key: value for key, value in vision_catalog.items() if "Qwen3-ASR" in key}
|
|
|
|
self.assertEqual({}, asr_entries)
|
|
|
|
def test_legacy_gguf_model_file_entries_expand_to_runnable_models(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_legacy_gguf_model_files_expand_test",
|
|
)
|
|
|
|
models = module.GGUF_VL_CATALOG.get("models") or {}
|
|
self.assertIn("gemma-4-12b-it-Q4_K_M.gguf [~7.5GB]", models)
|
|
resolved = module._resolve_model_entry("gemma-4-12b-it-Q4_K_M.gguf [~7.5GB]")
|
|
|
|
self.assertEqual(resolved.model_filename, "gemma-4-12b-it-Q4_K_M.gguf")
|
|
self.assertEqual(resolved.mmproj_filename, "mmproj-F16.gguf")
|
|
|
|
def test_minimax_h3_presets_are_split_by_input_mode(self):
|
|
data = json.loads((PKG / "AILab_System_Prompts.json").read_text(encoding="utf-8"))
|
|
text_label = "🎬 MiniMax H3 Text-to-Video"
|
|
first_label = "🖼️ MiniMax H3 First-Frame-to-Video (I2VA)"
|
|
first_last_label = "🎞️ MiniMax H3 First/Last-to-Video (FL2VA)"
|
|
last_label = "🏁 MiniMax H3 Last-Frame-to-Video (L2VA)"
|
|
reference_label = "🖼️ MiniMax H3 Reference-to-Video"
|
|
|
|
self.assertNotIn("🎬 MiniMax H3 Multimodal Video", data["_preset_prompts"])
|
|
for label in (text_label, first_label, first_last_label, last_label, reference_label):
|
|
self.assertIn(label, data["_preset_prompts"])
|
|
self.assertIn(label, data["qwenvl"])
|
|
self.assertIn("integrated_multimodal_description", data["qwenvl"][text_label])
|
|
self.assertNotIn("subject_definitions", data["qwenvl"][text_label])
|
|
self.assertIn("literal square brackets", data["qwenvl"][text_label])
|
|
self.assertIn("may be visual-only", data["qwenvl"][text_label])
|
|
self.assertIn("<d>[English]", data["qwen_text"]["styles"][text_label]["system_prompt"])
|
|
self.assertIn("subject_definitions", data["qwenvl"][reference_label])
|
|
self.assertIn("retention_analysis", data["qwenvl"][reference_label])
|
|
self.assertIn(text_label, data["qwen_text"]["styles"])
|
|
self.assertNotIn(reference_label, data["qwen_text"]["styles"])
|
|
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
package = load_thinkingllm_loader_subset(
|
|
[
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
]
|
|
)
|
|
|
|
for node_name in ("ThinkingLLM_QwenVL", "ThinkingLLM_QwenVL_Advanced", "ThinkingLLM_QwenVL_GGUF", "ThinkingLLM_QwenVL_GGUF_Advanced"):
|
|
values = package.NODE_CLASS_MAPPINGS[node_name].INPUT_TYPES()["required"]["preset_prompt"][0]
|
|
for label in (text_label, first_label, first_last_label, last_label, reference_label):
|
|
self.assertIn(label, values)
|
|
for node_name, widget_name in (("ThinkingLLM_QwenVL_PromptEnhancer", "enhancement_style"), ("ThinkingLLM_QwenVL_GGUF_PromptEnhancer", "preset_system_prompt")):
|
|
values = package.NODE_CLASS_MAPPINGS[node_name].INPUT_TYPES()["required"][widget_name][0]
|
|
self.assertIn(text_label, values)
|
|
for label in (first_label, first_last_label, last_label, reference_label):
|
|
self.assertNotIn(label, values)
|
|
|
|
def test_minimax_reference_requires_a_custom_target_prompt(self):
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.pop("AILab_QwenVL", None)
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_minimax_reference_validation_test")
|
|
|
|
with self.assertRaisesRegex(ValueError, "requires a custom prompt"):
|
|
module.validate_minimax_reference_request("🖼️ MiniMax H3 Reference-to-Video", " ")
|
|
|
|
module.validate_minimax_reference_request("🖼️ MiniMax H3 Reference-to-Video", "Keep Picture 1 as the character identity.")
|
|
module.validate_minimax_reference_request("🎬 MiniMax H3 Text-to-Video", "")
|
|
for filename in ("thinkingllm_core/hf_runtime.py", "AILab_QwenVL_GGUF.py"):
|
|
self.assertIn(
|
|
"validate_minimax_reference_request(preset_prompt, custom_prompt)",
|
|
read_source(filename),
|
|
)
|
|
|
|
def test_llm_input_logging_is_complete_and_shared(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_input_logging_test")
|
|
|
|
long_tail = "END-OF-UNTRUNCATED-PROMPT"
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
module.log_llm_input(
|
|
"Backend",
|
|
"FINALIZATION ATTEMPT 2/3",
|
|
"Preset Name",
|
|
f"user text {long_tail}",
|
|
system_text="system text",
|
|
formatted_text="<chat>formatted text</chat>",
|
|
media={"image": 2, "audio": 1},
|
|
)
|
|
logged = output.getvalue()
|
|
self.assertIn("Preset: Preset Name", logged)
|
|
self.assertIn("Stage: FINALIZATION ATTEMPT 2/3", logged)
|
|
self.assertIn("system text", logged)
|
|
self.assertIn(long_tail, logged)
|
|
self.assertIn("<chat>formatted text</chat>", logged)
|
|
self.assertIn("image=2, audio=1", logged)
|
|
self.assertNotIn("data:image", logged)
|
|
self.assertNotIn("base64", logged.lower())
|
|
|
|
for filename in ("thinkingllm_core/hf_runtime.py", "AILab_QwenVL_GGUF.py", "AILab_QwenVL_PromptEnhancer.py", "AILab_QwenVL_GGUF_PromptEnhancer.py"):
|
|
with self.subTest(filename=filename):
|
|
source = read_source(filename)
|
|
self.assertIn("log_llm_input(", source)
|
|
self.assertIn("no LLM call was made", source)
|
|
|
|
def test_ltx_presets_are_available_for_vision_and_prompt_enhancers(self):
|
|
data = json.loads(Path("AILab_System_Prompts.json").read_text(encoding="utf-8"))
|
|
|
|
self.assertIn("🎦 LTX 2.3 NSFW I2V Scene", data["_preset_prompts"])
|
|
self.assertIn("🎦 LTX 2.3 NSFW I2V Scene", data["qwenvl"])
|
|
self.assertIn("📖 LTX 2.3 NSFW T2V Scene", data["qwen_text"]["styles"])
|
|
|
|
vision_prompt = data["qwenvl"]["🎦 LTX 2.3 NSFW I2V Scene"]
|
|
text_prompt = data["qwen_text"]["styles"]["📖 LTX 2.3 NSFW T2V Scene"]["system_prompt"]
|
|
self.assertIn("LTX 2.3", vision_prompt)
|
|
self.assertIn("image first", vision_prompt)
|
|
self.assertIn("LTX 2.3", text_prompt)
|
|
self.assertIn("Do not change the user input intent", text_prompt)
|
|
self.assertNotIn("Example:", vision_prompt)
|
|
self.assertNotIn("Examples:", text_prompt)
|
|
|
|
def test_video_preset_duration_and_recommendation_metadata_are_complete(self):
|
|
data = json.loads((PKG / "AILab_System_Prompts.json").read_text(encoding="utf-8"))
|
|
metadata = data["_video_presets"]
|
|
duration_labels = {
|
|
"📖 LTX 2.3 NSFW T2V Scene",
|
|
"🎦 LTX 2.3 NSFW I2V Scene",
|
|
"🎞️ LTX 2.3 First/Last-to-Video",
|
|
"🖼️ LTX 2.3 Reference-to-Video",
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
"🖼️ MiniMax H3 First-Frame-to-Video (I2VA)",
|
|
"🎞️ MiniMax H3 First/Last-to-Video (FL2VA)",
|
|
"🏁 MiniMax H3 Last-Frame-to-Video (L2VA)",
|
|
"🖼️ MiniMax H3 Reference-to-Video",
|
|
}
|
|
wan_labels = {
|
|
"🍿 Wan 2.2 NSFW I2V Timeline (3s)",
|
|
"🍿 Wan 2.2 NSFW I2V Timeline (5s)",
|
|
"🎥 Wan 2.2 NSFW I2V Scene (3s)",
|
|
"🎥 Wan 2.2 NSFW I2V Scene (5s)",
|
|
"🎬 Wan 2.2 NSFW I2V Timeline (20s)",
|
|
"📖 Wan 2.2 NSFW I2V Scene (20s)",
|
|
"🍿 Wan 2.2 NSFW T2V Timeline (3s)",
|
|
"🍿 Wan 2.2 NSFW T2V Timeline (5s)",
|
|
"🎥 Wan 2.2 NSFW T2V Scene (3s)",
|
|
"🎥 Wan 2.2 NSFW T2V Scene (5s)",
|
|
"🎬 Wan 2.2 NSFW T2V Timeline (20s)",
|
|
"📖 Wan 2.2 NSFW T2V Scene (20s)",
|
|
}
|
|
|
|
self.assertEqual(set(metadata), duration_labels | wan_labels)
|
|
self.assertNotIn("🖼️ MiniMax H3 Reference-to-Video", data["qwen_text"]["styles"])
|
|
for label, entry in metadata.items():
|
|
with self.subTest(label=label):
|
|
self.assertIn(entry["provider"], {"ltx_2_3", "minimax_h3", "wan_2_2"})
|
|
self.assertTrue(entry["video_mode"])
|
|
self.assertIs(entry["duration_required"], label in duration_labels)
|
|
self.assertIs(entry["recommended_settings"]["enable_thinking"], False)
|
|
self.assertIn(entry["recommended_settings"]["max_tokens"], {512, 768, 1024, 1536, 2048, 3072})
|
|
self.assertTrue(entry["recommendation_note"])
|
|
|
|
styles = data["qwen_text"]["styles"]
|
|
self.assertIn("🍿 Wan 2.2 NSFW T2V Timeline (3s)", styles)
|
|
self.assertNotIn("🍿 Wan 2.2 NSFW T2V Timeline (3)", styles)
|
|
wan_t2v_20 = styles["📖 Wan 2.2 NSFW T2V Scene (20s)"]["system_prompt"]
|
|
self.assertIn("EXACTLY FOUR continuous 5-second", wan_t2v_20)
|
|
self.assertNotIn("5s total) to establish initial scene", wan_t2v_20)
|
|
self.assertNotIn("PERFORMANCE OPTIMIZATION", json.dumps(data, ensure_ascii=False))
|
|
self.assertNotIn("RECOMMENDED: Use max_tokens", json.dumps(data, ensure_ascii=False))
|
|
|
|
def test_video_duration_normalization_and_native_output_enforcement(self):
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.pop("AILab_QwenVL", None)
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_video_duration_test")
|
|
|
|
for requested, expected_frames, expected_seconds, expected_text in (
|
|
(5.0, 124, 124 / 24, "5.17"),
|
|
(8.0, 192, 8.0, "8.00"),
|
|
(12.0, 294, 12.25, "12.25"),
|
|
):
|
|
with self.subTest(requested=requested):
|
|
resolved = module.resolve_video_duration("🎬 MiniMax H3 Text-to-Video", requested)
|
|
self.assertEqual(resolved["frames"], expected_frames)
|
|
self.assertAlmostEqual(resolved["effective_seconds"], expected_seconds)
|
|
self.assertEqual(resolved["duration_text"], expected_text)
|
|
|
|
ltx = module.resolve_video_duration("📖 LTX 2.3 NSFW T2V Scene", 10.0)
|
|
self.assertEqual(ltx["effective_seconds"], 10.0)
|
|
self.assertEqual(ltx["duration_text"], "10")
|
|
self.assertIsNone(module.resolve_video_duration("🖼️ Detailed Description", 12.0))
|
|
|
|
ltx_output = module.ensure_video_prompt_duration(
|
|
"A dancer turns.\nThe camera follows.",
|
|
"📖 LTX 2.3 NSFW T2V Scene",
|
|
10.0,
|
|
)
|
|
self.assertEqual(ltx_output.count("10-second continuous shot"), 1)
|
|
self.assertNotIn("\n", ltx_output)
|
|
|
|
minimax_output = module.ensure_video_prompt_duration(
|
|
"integrated_multimodal_description: Target duration: 5.17 seconds. [Shot 1] A dancer turns.\n"
|
|
"overall_soundscape: Footsteps.\n"
|
|
"non_diegetic_music: N/A",
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
5.0,
|
|
)
|
|
self.assertNotIn("Target duration", minimax_output)
|
|
self.assertTrue(minimax_output.startswith("integrated_multimodal_description: [Shot 1]"))
|
|
self.assertLess(minimax_output.index("integrated_multimodal_description"), minimax_output.index("overall_soundscape"))
|
|
|
|
reference_output = module.ensure_video_prompt_duration(
|
|
"subject_definitions: <Subject 1> is the actor in <Picture 1>.\n"
|
|
"summary: [reference generation] Target duration: 12.25 seconds. The target video uses <Subject 1>.\n"
|
|
"retention_analysis: <Subject 1> (appears in [Shot 1]): fully_preserved - identity is retained.\n"
|
|
"detailed_description: Cinematic live action.\n[Shot 1] <Subject 1> performs the requested action.\n"
|
|
"overall_soundscape: Room tone.\n"
|
|
"non_diegetic_music: N/A",
|
|
"🖼️ MiniMax H3 Reference-to-Video",
|
|
12.0,
|
|
)
|
|
self.assertNotIn("Target duration", reference_output)
|
|
self.assertIn("summary: [reference generation] The target video uses <Subject 1>.", reference_output)
|
|
self.assertEqual(
|
|
[match.group(1) for match in re.finditer(r"(?m)^(subject_definitions|summary|retention_analysis|detailed_description|overall_soundscape|non_diegetic_music):", reference_output)],
|
|
["subject_definitions", "summary", "retention_analysis", "detailed_description", "overall_soundscape", "non_diegetic_music"],
|
|
)
|
|
|
|
ordinary = "A plain image description."
|
|
self.assertEqual(module.ensure_video_prompt_duration(ordinary, "🖼️ Detailed Description", 30.0), ordinary)
|
|
|
|
def test_video_duration_context_and_cache_use_effective_duration(self):
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.pop("AILab_QwenVL", None)
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_video_duration_cache_test")
|
|
|
|
context = module.apply_video_duration_context(
|
|
"Make it 3 seconds long.",
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
5.0,
|
|
)
|
|
self.assertIn("TARGET_DURATION_SECONDS: 5.17", context)
|
|
self.assertIn("overrides any conflicting duration", context)
|
|
resolved_five = module.resolve_video_duration("🎬 MiniMax H3 Text-to-Video", 5.0)
|
|
self.assertEqual(resolved_five["contract_version"], module.VIDEO_PROMPT_CONTRACT_VERSION)
|
|
five_seconds = resolved_five["effective_seconds"]
|
|
five_point_one_seconds = module.resolve_video_duration("🎬 MiniMax H3 Text-to-Video", 5.1)["effective_seconds"]
|
|
eight_seconds = module.resolve_video_duration("🎬 MiniMax H3 Text-to-Video", 8.0)["effective_seconds"]
|
|
self.assertEqual(five_seconds, five_point_one_seconds)
|
|
self.assertEqual(
|
|
module.get_cache_key("m", "p", "x", effective_duration_seconds=five_seconds),
|
|
module.get_cache_key("m", "p", "x", effective_duration_seconds=five_point_one_seconds),
|
|
)
|
|
self.assertNotEqual(
|
|
module.get_cache_key("m", "p", "x", effective_duration_seconds=five_seconds),
|
|
module.get_cache_key("m", "p", "x", effective_duration_seconds=eight_seconds),
|
|
)
|
|
self.assertNotEqual(
|
|
module.get_cache_key(
|
|
"m",
|
|
"p",
|
|
"x",
|
|
effective_duration_seconds=five_seconds,
|
|
video_prompt_contract_version=module.VIDEO_PROMPT_CONTRACT_VERSION - 1,
|
|
),
|
|
module.get_cache_key(
|
|
"m",
|
|
"p",
|
|
"x",
|
|
effective_duration_seconds=five_seconds,
|
|
video_prompt_contract_version=module.VIDEO_PROMPT_CONTRACT_VERSION,
|
|
),
|
|
)
|
|
self.assertEqual(
|
|
module.build_node_input_signature(effective_duration_seconds=five_seconds),
|
|
module.build_node_input_signature(effective_duration_seconds=five_point_one_seconds),
|
|
)
|
|
self.assertNotEqual(
|
|
module.build_node_input_signature(effective_duration_seconds=five_seconds),
|
|
module.build_node_input_signature(effective_duration_seconds=eight_seconds),
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "between 0.2 and 150.0"):
|
|
module.resolve_video_duration("🎬 MiniMax H3 Text-to-Video", 0.1)
|
|
self.assertIsNone(module.resolve_video_duration("🖼️ Detailed Description", 0.1))
|
|
|
|
def test_minimax_h3_output_is_normalized_without_becoming_a_schema_gate(self):
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.pop("AILab_QwenVL", None)
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_minimax_contract_test")
|
|
|
|
base_body = (
|
|
"integrated_multimodal_description: Target duration: 8.00 seconds. "
|
|
"[Shot 1] Live-action, a baker opens the shutters. "
|
|
"[Shot 2] At 00:3.5 the camera cuts to the bread on the counter.\n"
|
|
"overall_soundscape: Shutters scrape and trays clink.\n"
|
|
"non_diegetic_music: An invented electronic drone."
|
|
)
|
|
t2va = module.ensure_video_prompt_duration(
|
|
base_body,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
source_prompt_text="A baker opens the shutters and the camera cuts to bread.",
|
|
)
|
|
self.assertTrue(t2va.startswith("integrated_multimodal_description: [Shot 1]"))
|
|
self.assertIn("[Shot 2] At 00:03.500,", t2va)
|
|
self.assertNotIn("Target duration", t2va)
|
|
self.assertTrue(t2va.endswith("non_diegetic_music: An invented electronic drone."))
|
|
|
|
for preset in (
|
|
"🖼️ MiniMax H3 First-Frame-to-Video (I2VA)",
|
|
"🎞️ MiniMax H3 First/Last-to-Video (FL2VA)",
|
|
"🏁 MiniMax H3 Last-Frame-to-Video (L2VA)",
|
|
):
|
|
with self.subTest(preset=preset):
|
|
output = module.ensure_video_prompt_duration(base_body, preset, 8.0)
|
|
self.assertTrue(output.startswith("integrated_multimodal_description:"))
|
|
self.assertEqual(output.count("integrated_multimodal_description:"), 1)
|
|
|
|
reference = module.ensure_video_prompt_duration(
|
|
"subject_definitions: <Subject 1> is the baker from <Picture 1>.\n"
|
|
"summary: [reference generation] The target video shows <Subject 1> opening the bakery.\n"
|
|
"retention_analysis: <Subject 1> (appears in [Shot 1]): fully_preserved - identity and clothing are retained.\n"
|
|
"detailed_description: Live-action with cool dawn light.\n"
|
|
"[Shot 1] <Subject 1> opens the shutters.\n"
|
|
"overall_soundscape: Shutters scrape over quiet street ambience.\n"
|
|
"non_diegetic_music: A fabricated synth cue.",
|
|
"🖼️ MiniMax H3 Reference-to-Video",
|
|
12.0,
|
|
source_prompt_text="Use Picture 1 for the baker opening the bakery, without music.",
|
|
)
|
|
self.assertEqual(
|
|
re.findall(
|
|
r"(?m)^(subject_definitions|summary|retention_analysis|detailed_description|overall_soundscape|non_diegetic_music):",
|
|
reference,
|
|
),
|
|
[
|
|
"subject_definitions",
|
|
"summary",
|
|
"retention_analysis",
|
|
"detailed_description",
|
|
"overall_soundscape",
|
|
"non_diegetic_music",
|
|
],
|
|
)
|
|
self.assertTrue(reference.endswith("non_diegetic_music: A fabricated synth cue."))
|
|
|
|
malformed_dialogue = (
|
|
"integrated_multimodal_description: [Shot 1] Peter (S1) says, "
|
|
"<d>English</d> This text is outside the dialogue tag.\n"
|
|
"overall_soundscape: Room tone.\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
self.assertIn(
|
|
"<d>English</d> This text is outside the dialogue tag.",
|
|
module.ensure_video_prompt_duration(
|
|
malformed_dialogue,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
),
|
|
)
|
|
|
|
impossible_dialogue = (
|
|
"integrated_multimodal_description: [Shot 1] Peter (S1) says, "
|
|
"<d>[English] " + "word " * 40 + "</d>\n"
|
|
"overall_soundscape: Room tone.\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
self.assertIn(
|
|
"word word word",
|
|
module.ensure_video_prompt_duration(
|
|
impossible_dialogue,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
5.0,
|
|
),
|
|
)
|
|
|
|
missing_speaker_id = (
|
|
"integrated_multimodal_description: [Shot 1] Peter says, "
|
|
"<d>[English] This is still forwarded without an S1 marker.</d>\n"
|
|
"overall_soundscape: Room tone.\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
self.assertIn(
|
|
"forwarded without an S1 marker",
|
|
module.ensure_video_prompt_duration(
|
|
missing_speaker_id,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
),
|
|
)
|
|
|
|
self.assertTrue(
|
|
module.ensure_video_prompt_duration(
|
|
"duration: 8 seconds\n" + base_body,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
).startswith("duration: 8 seconds")
|
|
)
|
|
|
|
def test_minimax_dialogue_preflight_fidelity_and_format_recovery(self):
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.pop("AILab_QwenVL", None)
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_minimax_dialogue_preflight_test")
|
|
|
|
source = (
|
|
"Peter looks at the screen.\n\n"
|
|
"He says:\n\n"
|
|
"“Hello there.”\n\n"
|
|
"The screen reads “STATUS”.\n\n"
|
|
"“This is still the original second line!”"
|
|
)
|
|
self.assertEqual(
|
|
module.extract_minimax_source_dialogue(source),
|
|
["Hello there.", "This is still the original second line!"],
|
|
)
|
|
|
|
too_long_source = "Peter says:\n\n“" + "word " * 100 + "”"
|
|
self.assertEqual(
|
|
module.validate_minimax_source_duration(
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
7.0,
|
|
too_long_source,
|
|
),
|
|
[("word " * 100).strip()],
|
|
)
|
|
|
|
inline_candidate = (
|
|
"integrated_multimodal_description: [Shot 1] at 00:00.000, Peter (S1) says, "
|
|
"<d>[English] Hello there.</d> Peter (S1) continues, "
|
|
"<d>[English] This is still the original second line!</d> "
|
|
"[Shot 2] at 00:03.5, the camera cuts to the screen. "
|
|
"overall_soundscape: Room tone. non_diegetic_music: N/A"
|
|
)
|
|
normalized = module.ensure_video_prompt_duration(
|
|
inline_candidate,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
source_prompt_text=source,
|
|
)
|
|
self.assertTrue(normalized.startswith("integrated_multimodal_description: [Shot 1] Peter"))
|
|
self.assertNotIn("[Shot 1] At", normalized)
|
|
self.assertIn("[Shot 2] At 00:03.500,", normalized)
|
|
self.assertIn("\n\noverall_soundscape: Room tone.\n\nnon_diegetic_music: N/A", normalized)
|
|
|
|
unbracketed_language = (
|
|
"integrated_multimodal_description: [Shot 1] Peter (S1) says, "
|
|
"<d>English Hello there.</d>\n"
|
|
"overall_soundscape: Room tone.\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
normalized_language = module.ensure_video_prompt_duration(
|
|
unbracketed_language,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
source_prompt_text=source,
|
|
)
|
|
self.assertIn("<d>[English] Hello there.</d>", normalized_language)
|
|
self.assertNotIn("<d>English ", normalized_language)
|
|
|
|
selected_subset = (
|
|
"integrated_multimodal_description: [Shot 1] Peter (S1) says, "
|
|
"<d>[English] This is still the original second line!</d>\n"
|
|
"overall_soundscape: Room tone.\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
self.assertIn(
|
|
"This is still the original second line!",
|
|
module.ensure_video_prompt_duration(
|
|
selected_subset,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
source_prompt_text=source,
|
|
),
|
|
)
|
|
|
|
missing_dialogue = (
|
|
"integrated_multimodal_description: [Shot 1] Peter silently looks at the screen.\n"
|
|
"overall_soundscape: Room tone.\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
visual_only = module.ensure_video_prompt_duration(
|
|
missing_dialogue,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
source_prompt_text=source,
|
|
)
|
|
self.assertIn("Peter silently looks at the screen", visual_only)
|
|
self.assertNotIn("<d>", visual_only)
|
|
|
|
rewritten_dialogue = selected_subset.replace(
|
|
"This is still the original second line!",
|
|
"This is a rewritten line!",
|
|
)
|
|
self.assertIn(
|
|
"This is a rewritten line!",
|
|
module.ensure_video_prompt_duration(
|
|
rewritten_dialogue,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
source_prompt_text=source,
|
|
),
|
|
)
|
|
|
|
over_duration = (
|
|
"integrated_multimodal_description: [Shot 1] Opening. "
|
|
"[Shot 2] At 00:10.000, cut to the ending.\n"
|
|
"overall_soundscape: Room tone.\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
self.assertIn(
|
|
"[Shot 2] At 00:10.000,",
|
|
module.ensure_video_prompt_duration(
|
|
over_duration,
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
8.0,
|
|
),
|
|
)
|
|
|
|
def test_minimax_gguf_forwards_best_effort_output_without_repair_pass(self):
|
|
source = read_source("AILab_QwenVL_GGUF_PromptEnhancer.py")
|
|
|
|
self.assertNotIn("is_recoverable_minimax_validation_error", source)
|
|
self.assertNotIn("ORIGINAL USER REQUEST:", source)
|
|
self.assertNotIn("CANDIDATE TO REPAIR:", source)
|
|
self.assertNotIn("MINIMAX STRUCTURE REPAIR", source)
|
|
self.assertNotIn("_NON_THINKING_REASONING_CHAR_LIMIT", source)
|
|
self.assertIn("final = ensure_video_prompt_duration(", source)
|
|
self.assertIn("arch.startswith(\"qwen35\")", source)
|
|
self.assertIn('completion_kwargs["chat_template_kwargs"]', source)
|
|
self.assertIn('"thinking_mode": "enabled" if current_enable_thinking else "disabled"', source)
|
|
for filename in (
|
|
"thinkingllm_core/hf_runtime.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
):
|
|
with self.subTest(filename=filename):
|
|
self.assertIn("validate_minimax_source_duration(", read_source(filename))
|
|
|
|
def test_output_cleaner_preserves_structured_video_prompt_after_thinking(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = (
|
|
"<think>\nFirst, I need to plan the output.\nThen, I will write it.\n</think>\n\n"
|
|
"integrated_multimodal_description: Target duration: 7.29 seconds. "
|
|
"[Shot 1] Peter clicks to the next slide while the first robot appears, then the camera pans left.\n\n"
|
|
"overall_soundscape: Conference ambience and keyboard clicks.\n\n"
|
|
"non_diegetic_music: Minimal electronic drone."
|
|
)
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="prompt"))
|
|
|
|
self.assertNotIn("<think>", cleaned)
|
|
self.assertIn("integrated_multimodal_description:", cleaned)
|
|
self.assertIn("[Shot 1] Peter clicks to the next slide", cleaned)
|
|
self.assertIn("the first robot appears, then the camera pans left", cleaned)
|
|
self.assertLess(cleaned.index("integrated_multimodal_description"), cleaned.index("overall_soundscape"))
|
|
self.assertLess(cleaned.index("overall_soundscape"), cleaned.index("non_diegetic_music"))
|
|
|
|
def test_output_cleaner_still_drops_actual_leading_planning_paragraph(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = "First: I need to plan this response.\n\nA cinematic close-up of a dancer in rain."
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="prompt"))
|
|
self.assertEqual(cleaned, "A cinematic close-up of a dancer in rain.")
|
|
|
|
def test_output_cleaner_returns_only_final_channel_content(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = (
|
|
"<|start|>assistant<|channel|>analysis<|message|>I should inspect the image first."
|
|
"<|end|><|start|>assistant<|channel|>final<|message|>The image shows a red fox."
|
|
"<|end|>"
|
|
)
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text"))
|
|
|
|
self.assertEqual(cleaned, "The image shows a red fox.")
|
|
|
|
def test_output_cleaner_removes_mirrored_channel_reasoning_before_plain_final(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = "<|channel>I should inspect the image first.<channel|>The image shows a red fox."
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text"))
|
|
|
|
self.assertEqual(cleaned, "The image shows a red fox.")
|
|
|
|
def test_output_cleaner_drops_channel_reasoning_without_final(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = "<|start|>assistant<|channel|>analysis<|message|>I should inspect the image first.<|end|>"
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text"))
|
|
|
|
self.assertEqual(cleaned, "")
|
|
|
|
def test_output_cleaner_handles_final_only_and_channel_token_variants(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
cases = (
|
|
"<|channel|>final<|message|>Final answer.<|end|>",
|
|
"<|CHANNEL>FiNaL<|MESSAGE>Final answer.<|END>",
|
|
"<channel|>FINAL<message|>Final answer.<end|>",
|
|
)
|
|
for raw in cases:
|
|
with self.subTest(raw=raw):
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text"))
|
|
self.assertEqual(cleaned, "Final answer.")
|
|
|
|
def test_output_cleaner_keeps_plain_final_after_completed_reasoning(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = "<|channel|>reasoning<|message|>Private draft.<|end|>Public answer."
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text"))
|
|
|
|
self.assertEqual(cleaned, "Public answer.")
|
|
|
|
def test_output_cleaner_uses_last_final_channel(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = (
|
|
"<|channel|>final<|message|>Discarded answer.<|end|>"
|
|
"<|channel|>analysis<|message|>I need to correct it.<|end|>"
|
|
"<|channel|>final<|message|>Correct answer.<|end|>"
|
|
)
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text"))
|
|
|
|
self.assertEqual(cleaned, "Correct answer.")
|
|
|
|
def test_output_cleaner_drops_unclosed_channel_reasoning(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = "<|channel|>thinking<|message|>An unfinished private draft"
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text"))
|
|
|
|
self.assertEqual(cleaned, "")
|
|
|
|
def test_output_cleaner_preserves_ambiguous_or_literal_channel_text(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
cases = (
|
|
"<|channel>Final answer only.<channel|>",
|
|
"Explain this token: <|channel|>final<|message|> without treating it as protocol.",
|
|
)
|
|
for raw in cases:
|
|
with self.subTest(raw=raw):
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text"))
|
|
self.assertEqual(cleaned, raw)
|
|
|
|
def test_output_cleaner_preserves_channel_protocol_when_think_stripping_is_disabled(self):
|
|
from AILab_OutputCleaner import OutputCleanConfig, clean_model_output
|
|
|
|
raw = "<|channel|>analysis<|message|>Private draft.<|end|>"
|
|
cleaned = clean_model_output(raw, OutputCleanConfig(mode="text", strip_think=False))
|
|
|
|
self.assertEqual(cleaned, raw)
|
|
|
|
def test_gguf_channel_cleaning_keeps_raw_trace_assignment(self):
|
|
source = read_source("AILab_QwenVL_GGUF.py")
|
|
|
|
self.assertIn("cleaned = clean_model_output(raw_full, OutputCleanConfig(mode=\"text\"))", source)
|
|
self.assertIn("raw_for_trace = raw_full", source)
|
|
self.assertIn("cleaned = clean_model_output(raw_content, OutputCleanConfig(mode=\"text\"))", source)
|
|
self.assertIn("raw_for_trace = raw_content", source)
|
|
|
|
def test_llm_input_logging_is_complete_and_shared(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_input_logging_test")
|
|
|
|
long_tail = "END-OF-UNTRUNCATED-PROMPT"
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
module.log_llm_input(
|
|
"Backend",
|
|
"FINALIZATION ATTEMPT 2/3",
|
|
"Preset Name",
|
|
f"user text {long_tail}",
|
|
system_text="system text",
|
|
formatted_text="<chat>formatted text</chat>",
|
|
media={"image": 2, "audio": 1},
|
|
)
|
|
logged = output.getvalue()
|
|
self.assertIn("Preset: Preset Name", logged)
|
|
self.assertIn("Stage: FINALIZATION ATTEMPT 2/3", logged)
|
|
self.assertIn("system text", logged)
|
|
self.assertIn(long_tail, logged)
|
|
self.assertIn("<chat>formatted text</chat>", logged)
|
|
self.assertIn("image=2, audio=1", logged)
|
|
self.assertNotIn("data:image", logged)
|
|
self.assertNotIn("base64", logged.lower())
|
|
|
|
for filename in ("thinkingllm_core/hf_runtime.py", "AILab_QwenVL_GGUF.py", "AILab_QwenVL_PromptEnhancer.py", "AILab_QwenVL_GGUF_PromptEnhancer.py"):
|
|
with self.subTest(filename=filename):
|
|
source = read_source(filename)
|
|
self.assertIn("log_llm_input(", source)
|
|
self.assertIn("no LLM call was made", source)
|
|
|
|
def test_custom_prompt_image_no_preset_alias_is_available_for_saved_workflows(self):
|
|
label = "💬 Custom prompt + image (no preset)"
|
|
data = json.loads(Path("AILab_System_Prompts.json").read_text(encoding="utf-8"))
|
|
|
|
self.assertIn(label, data["_preset_prompts"])
|
|
self.assertEqual(data["qwenvl"][label], "")
|
|
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
package = load_thinkingllm_loader_subset(["AILab_QwenVL_GGUF.py"])
|
|
|
|
values = package.NODE_CLASS_MAPPINGS["ThinkingLLM_QwenVL_GGUF"].INPUT_TYPES()["required"]["preset_prompt"][0]
|
|
self.assertIn("🚫 No preset (image-only)", values)
|
|
self.assertIn(label, values)
|
|
|
|
def test_ltx_presets_are_exposed_by_relevant_nodes(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
package = load_thinkingllm_loader_subset(
|
|
[
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
]
|
|
)
|
|
|
|
expectations = {
|
|
"ThinkingLLM_QwenVL": "preset_prompt",
|
|
"ThinkingLLM_QwenVL_Advanced": "preset_prompt",
|
|
"ThinkingLLM_QwenVL_GGUF": "preset_prompt",
|
|
"ThinkingLLM_QwenVL_GGUF_Advanced": "preset_prompt",
|
|
"ThinkingLLM_QwenVL_PromptEnhancer": "enhancement_style",
|
|
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer": "preset_system_prompt",
|
|
}
|
|
for node_name, widget_name in expectations.items():
|
|
with self.subTest(node_name=node_name):
|
|
values = package.NODE_CLASS_MAPPINGS[node_name].INPUT_TYPES()["required"][widget_name][0]
|
|
self.assertTrue(any("LTX 2.3" in value for value in values))
|
|
|
|
def test_preset_tooltip_payload_matches_system_prompts(self):
|
|
system_prompts = json.loads(Path("AILab_System_Prompts.json").read_text(encoding="utf-8"))
|
|
tooltip_payload = json.loads(Path("web/preset_tooltips.json").read_text(encoding="utf-8"))
|
|
|
|
expected = {
|
|
"preset_prompt": system_prompts["qwenvl"],
|
|
"enhancement_style": {
|
|
name: entry["system_prompt"]
|
|
for name, entry in system_prompts["qwen_text"]["styles"].items()
|
|
},
|
|
"preset_system_prompt": {
|
|
name: entry["system_prompt"]
|
|
for name, entry in system_prompts["qwen_text"]["styles"].items()
|
|
},
|
|
"video_presets": system_prompts["_video_presets"],
|
|
}
|
|
self.assertEqual(tooltip_payload, expected)
|
|
|
|
def test_standalone_system_prompt_preset_node(self):
|
|
data = json.loads((PKG / "AILab_System_Prompts.json").read_text(encoding="utf-8"))
|
|
module = load_module_from_file(
|
|
"nodes/system_prompt_preset.py",
|
|
"thinkingllm_system_prompt_preset_test",
|
|
)
|
|
node_name = "ThinkingLLM_SystemPromptPreset"
|
|
node_cls = module.NODE_CLASS_MAPPINGS[node_name]
|
|
combo = node_cls.INPUT_TYPES()["required"]["preset_prompt"][0]
|
|
|
|
expected_presets = [
|
|
name for name in data["_preset_prompts"] if name in data["qwenvl"]
|
|
]
|
|
self.assertEqual(combo, expected_presets)
|
|
self.assertEqual(node_cls.RETURN_TYPES, ("STRING",))
|
|
self.assertEqual(node_cls.RETURN_NAMES, ("system_prompt",))
|
|
self.assertEqual(
|
|
node_cls().get_system_prompt("🖼️ Detailed Description"),
|
|
(data["qwenvl"]["🖼️ Detailed Description"],),
|
|
)
|
|
self.assertEqual(
|
|
node_cls().get_system_prompt(module.NO_PRESET_PROMPT),
|
|
("",),
|
|
)
|
|
|
|
legacy_preset = "🍿 Wan 2.2 NSFW T2V Timeline (3s)"
|
|
self.assertNotIn(legacy_preset, data["qwenvl"])
|
|
self.assertNotIn(legacy_preset, combo)
|
|
self.assertEqual(
|
|
node_cls().get_system_prompt(legacy_preset),
|
|
("",),
|
|
)
|
|
|
|
def test_standalone_system_prompt_preset_node_is_registered_by_all_loaders(self):
|
|
node_name = "ThinkingLLM_SystemPromptPreset"
|
|
node_files = ["system_prompt_preset.py"]
|
|
|
|
community_package = load_thinkingllm_loader_subset([], node_files)
|
|
self.assertIn(node_name, community_package.NODE_CLASS_MAPPINGS)
|
|
|
|
with mock.patch.dict(os.environ, {"THINKINGLLM_COMMERCIAL_RELEASE": "1"}):
|
|
commercial_package = load_thinkingllm_loader_subset([], node_files)
|
|
self.assertIn(node_name, commercial_package.NODE_CLASS_MAPPINGS)
|
|
|
|
def test_appearance_js_hooks_preset_dropdown_tooltips(self):
|
|
source = read_source("web/js/appearance.js")
|
|
self.assertIn("preset_tooltips.json", source)
|
|
self.assertIn("hookPresetTooltipPreviews(node)", source)
|
|
self.assertIn("PRESET_TOOLTIP_NODE_NAMES.has(node.comfyClass)", source)
|
|
inference_nodes = re.search(
|
|
r"const THINKINGLLM_NODE_NAMES = new Set\(\[(.*?)\]\);",
|
|
source,
|
|
re.DOTALL,
|
|
).group(1)
|
|
tooltip_nodes = re.search(
|
|
r"const PRESET_TOOLTIP_NODE_NAMES = new Set\(\[(.*?)\]\);",
|
|
source,
|
|
re.DOTALL,
|
|
).group(1)
|
|
self.assertNotIn("ThinkingLLM_SystemPromptPreset", inference_nodes)
|
|
self.assertIn("ThinkingLLM_SystemPromptPreset", tooltip_nodes)
|
|
for widget_name in ["preset_prompt", "enhancement_style", "preset_system_prompt"]:
|
|
with self.subTest(widget_name=widget_name):
|
|
self.assertIn(f'"{widget_name}"', source)
|
|
self.assertIn("updateDurationVisibility(node, promptMap)", source)
|
|
self.assertIn('findWidget(node, "duration_seconds")', source)
|
|
self.assertIn('fetch(PRESET_TOOLTIPS_URL, { cache: "no-store" })', source)
|
|
self.assertIn("findVideoPresetMetadata(payload, selectedPreset)", source)
|
|
self.assertIn("metadataAvailable: false", source)
|
|
self.assertIn('replaceAll("\\uFE0F", "")', source)
|
|
self.assertIn("Boolean(metadata?.duration_required)", source)
|
|
self.assertIn("buildVideoPresetRecommendationText(node, payload)", source)
|
|
self.assertIn("metadata?.recommended_settings", source)
|
|
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"])
|
|
required = package.NODE_CLASS_MAPPINGS["ThinkingLLM_QwenVL"].INPUT_TYPES()["required"]
|
|
_, meta = required["model_name"]
|
|
self.assertEqual(meta["default"], "Qwen3-VL-4B-Instruct-Abliterated [DL: 7.5GB, VRAM: 6.0GB]")
|
|
|
|
def test_appearance_js_loads_recommendation_widget(self):
|
|
source = read_source("web/js/appearance.js")
|
|
self.assertIn("model_recommendations.json", source)
|
|
self.assertIn("ComfyWidgets", source)
|
|
self.assertIn("recommended_settings", source)
|
|
self.assertIn("findWidgets(node, RECOMMENDATION_WIDGET_NAME)", source)
|
|
self.assertIn("removeDuplicateRecommendationWidgets(node, widget)", source)
|
|
self.assertIn("node.widgets.splice(index, 1)", source)
|
|
self.assertIn("support_notes?.audio", source)
|
|
self.assertIn("AUDIO_UNSUPPORTED_MESSAGE", source)
|
|
self.assertIn("else if (hasAudioInput(node))", source)
|
|
self.assertIn("ThinkingLLM_Gemma4_Audio_GGUF", source)
|
|
self.assertIn("ThinkingLLM_Whisper_ASR", source)
|
|
self.assertIn("Whisper ASR transcription", source)
|
|
self.assertNotIn("Prompt enhancer compact terminal progress is active", source)
|
|
self.assertIn("queueRecommendationRefresh", source)
|
|
|
|
def test_appearance_js_styles_all_visible_plugin_nodes(self):
|
|
source = read_source("web/js/appearance.js")
|
|
self.assertIn("if (!node.color && theme.nodeColor)", source)
|
|
self.assertIn("if (!node.bgcolor && theme.nodeBgColor)", source)
|
|
expected_node_names = [
|
|
"AILab_QwenVL",
|
|
"AILab_QwenVL_Advanced",
|
|
"AILab_QwenVL_PromptEnhancer",
|
|
"AILab_QwenVL_GGUF",
|
|
"AILab_QwenVL_GGUF_Advanced",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer",
|
|
"ThinkingLLM_QwenVL",
|
|
"ThinkingLLM_QwenVL_Advanced",
|
|
"ThinkingLLM_QwenVL_PromptEnhancer",
|
|
"ThinkingLLM_QwenVL_GGUF",
|
|
"ThinkingLLM_QwenVL_GGUF_Advanced",
|
|
"ThinkingLLM_Gemma4_Audio_GGUF",
|
|
"ThinkingLLM_Whisper_ASR",
|
|
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
|
|
"ThinkingLLM_SystemPromptPreset",
|
|
"VRAMCleanup",
|
|
"StorySplitNode",
|
|
]
|
|
for node_name in expected_node_names:
|
|
with self.subTest(node_name=node_name):
|
|
self.assertIn(f'"{node_name}"', source)
|
|
|
|
def test_gguf_prompt_enhancer_caps_context_and_safe_constructs_llama(self):
|
|
source = read_source("AILab_QwenVL_GGUF_PromptEnhancer.py")
|
|
self.assertIn("_PROMPT_ENHANCER_MAX_CONTEXT_LENGTH = 32768", source)
|
|
self.assertIn("_resolve_prompt_enhancer_context_length", source)
|
|
self.assertIn("construct_llama_safely(Llama, kwargs", source)
|
|
|
|
def test_gguf_nodes_guard_failed_llama_construction(self):
|
|
source = read_source("AILab_QwenVL_GGUF.py")
|
|
self.assertIn("def construct_llama_safely", source)
|
|
self.assertIn("_thinkingllm_llama_init_complete", source)
|
|
self.assertIn("construct_llama_safely(Llama, llm_kwargs_filtered", source)
|
|
|
|
def test_gguf_streaming_files_use_shared_extractor(self):
|
|
for filename in ("AILab_QwenVL_GGUF.py", "AILab_QwenVL_GGUF_PromptEnhancer.py"):
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
self.assertIn("extract_stream_token", source, f"{filename} should use shared chunk extraction")
|
|
self.assertIn("compose_streamed_model_output", source)
|
|
self.assertIn("display_token = reasoning_token + content_token", source)
|
|
self.assertNotIn("display_token = reasoning_token or content_token", source)
|
|
self.assertNotIn("stream_display.push(display_token)", source)
|
|
|
|
def test_stream_token_extractor_handles_reasoning_content(self):
|
|
from AILab_StreamDisplay import extract_stream_token
|
|
# Reasoning chunk
|
|
chunk = {"choices": [{"delta": {"reasoning_content": "Let me think...", "content": ""}}]}
|
|
self.assertEqual(extract_stream_token(chunk), {"reasoning": "Let me think...", "content": ""})
|
|
# Content-only chunk
|
|
chunk2 = {"choices": [{"delta": {"reasoning_content": "", "content": "The answer is 42."}}]}
|
|
self.assertEqual(extract_stream_token(chunk2), {"reasoning": "", "content": "The answer is 42."})
|
|
# Mixed chunk
|
|
chunk3 = {"choices": [{"delta": {"reasoning_content": "Hmm...", "content": "OK"}}]}
|
|
self.assertEqual(extract_stream_token(chunk3), {"reasoning": "Hmm...", "content": "OK"})
|
|
# Empty chunk
|
|
self.assertEqual(extract_stream_token({}), {"reasoning": "", "content": ""})
|
|
|
|
# llama.cpp/OpenAI-compatible variants seen across releases
|
|
top_level_reasoning = {"reasoning_content": "thinking..."}
|
|
self.assertEqual(extract_stream_token(top_level_reasoning), {"reasoning": "thinking...", "content": ""})
|
|
|
|
message_chunk = {"choices": [{"message": {"content": "final answer"}}]}
|
|
self.assertEqual(extract_stream_token(message_chunk), {"reasoning": "", "content": "final answer"})
|
|
|
|
object_chunk = types.SimpleNamespace(
|
|
choices=[types.SimpleNamespace(delta=types.SimpleNamespace(reasoning_content="why", content=" then"))]
|
|
)
|
|
self.assertEqual(extract_stream_token(object_chunk), {"reasoning": "why", "content": " then"})
|
|
|
|
def test_stream_channel_composer_keeps_reasoning_out_of_clean_response(self):
|
|
cleaner = load_module_from_file(
|
|
"AILab_OutputCleaner.py",
|
|
"thinkingllm_output_cleaner_channel_test",
|
|
)
|
|
stream_helpers = load_module_from_file(
|
|
"AILab_StreamDisplay.py",
|
|
"thinkingllm_stream_channel_test",
|
|
)
|
|
|
|
reasoning = (
|
|
"integrated_multimodal_description: obsolete draft\n\n"
|
|
"Wait, I need to rethink the structure."
|
|
)
|
|
final_prompt = (
|
|
"integrated_multimodal_description: [Shot 1] The actual shot.\n\n"
|
|
"overall_soundscape: N/A\n\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
raw_trace = stream_helpers.compose_streamed_model_output(reasoning, final_prompt)
|
|
cleaned = cleaner.clean_model_output(
|
|
raw_trace,
|
|
cleaner.OutputCleanConfig(mode="prompt"),
|
|
)
|
|
|
|
self.assertIn(reasoning, raw_trace)
|
|
self.assertEqual(cleaned, final_prompt)
|
|
self.assertNotIn("obsolete draft", cleaned)
|
|
|
|
def test_output_cleaner_drops_unclosed_thinking_instead_of_salvaging_it(self):
|
|
cleaner = load_module_from_file(
|
|
"AILab_OutputCleaner.py",
|
|
"thinkingllm_unclosed_thinking_test",
|
|
)
|
|
truncated = (
|
|
"<think>\nThinking Process:\n\n"
|
|
"integrated_multimodal_description: obsolete example\n\n"
|
|
"1. Analyze the request: I need to"
|
|
)
|
|
cleaned = cleaner.clean_model_output(
|
|
truncated,
|
|
cleaner.OutputCleanConfig(mode="prompt"),
|
|
)
|
|
self.assertEqual(cleaned, "")
|
|
|
|
def test_compact_stream_display_does_not_flush_unstyled_tail(self):
|
|
from AILab_StreamDisplay import TerminalStreamDisplay
|
|
import io
|
|
|
|
display = TerminalStreamDisplay("UnitTest", flush_interval=999, min_chunk_chars=999, compact=True, line_width=24)
|
|
stream = io.StringIO()
|
|
with contextlib.redirect_stdout(stream):
|
|
display.start_stage("STREAMING")
|
|
display.push_compact("partial compact token output with visible words")
|
|
display.end_stage()
|
|
output = stream.getvalue()
|
|
self.assertNotIn("[UnitTest]", output)
|
|
self.assertNotIn("\r", output)
|
|
self.assertNotIn("…", output)
|
|
self.assertIn("partial compact token", output)
|
|
self.assertIn("output with visible", output)
|
|
self.assertIn("words", output)
|
|
|
|
def test_terminal_display_uses_ascii_safe_typographic_punctuation(self):
|
|
from AILab_StreamDisplay import TerminalStreamDisplay
|
|
import io
|
|
|
|
original = "\u201cThat\u2019s useful\u2026 really\u201d \u2014 Peter"
|
|
display = TerminalStreamDisplay("UnitTest", compact=True, line_width=80)
|
|
stream = io.StringIO()
|
|
with contextlib.redirect_stdout(stream):
|
|
display.start_stage("STREAMING")
|
|
display.push_compact(original)
|
|
display.end_stage()
|
|
|
|
self.assertEqual(stream.getvalue(), '"That\'s useful... really" -- Peter\n')
|
|
self.assertEqual(original, "\u201cThat\u2019s useful\u2026 really\u201d \u2014 Peter")
|
|
|
|
def test_stream_degeneracy_guard_catches_repeated_word_loop(self):
|
|
from AILab_StreamDisplay import StreamDegenerationError, StreamDegenerationGuard, strip_degenerate_repetition
|
|
|
|
guard = StreamDegenerationGuard(repeated_word_limit=5)
|
|
with self.assertRaises(StreamDegenerationError):
|
|
guard.push("mightiest, mightiest, mightiest, mightiest, mightiest, ")
|
|
self.assertEqual(
|
|
strip_degenerate_repetition(
|
|
"A strong start, mightiest, mightiest, mightiest, mightiest, mightiest",
|
|
repeated_word_limit=5,
|
|
),
|
|
"A strong start",
|
|
)
|
|
|
|
def test_visible_streaming_has_no_reasoning_heartbeat_pollution(self):
|
|
for filename in STREAMING_FILES:
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
if "GGUF" in filename or filename in {"thinkingllm_core/hf_runtime.py", "AILab_QwenVL_PromptEnhancer.py"}:
|
|
stream_pushes = [line for line in source.splitlines() if "push_compact" in line]
|
|
self.assertTrue(stream_pushes)
|
|
self.assertNotIn("last_status_at = _maybe_emit_answer_stream_heartbeat", source)
|
|
self.assertNotIn("last_status_at = _maybe_emit_hf_stream_heartbeat", source)
|
|
self.assertNotIn("last_status_at = _maybe_emit_hf_prompt_stream_heartbeat", source)
|
|
self.assertNotIn("last_status_at = _maybe_emit_prompt_stream_heartbeat", source)
|
|
self.assertNotIn("terminal stream shows readable progress", source)
|
|
|
|
|
|
class TestPromptEnhancerMetadata(unittest.TestCase):
|
|
def test_hf_prompt_enhancer_metadata_matches_runtime(self):
|
|
source = read_source("AILab_QwenVL_PromptEnhancer.py")
|
|
self.assertIn('RETURN_TYPES = ("STRING", "STRING")', source)
|
|
self.assertIn('RETURN_NAMES = ("ENHANCED_OUTPUT", "RAW_TRACE")', source)
|
|
self.assertIn('"stream_tokens_to_terminal": ("BOOLEAN"', source)
|
|
self.assertIn('"unique_id": "UNIQUE_ID"', source)
|
|
self.assertIn('"extra_pnginfo": "EXTRA_PNGINFO"', source)
|
|
|
|
def test_raw_trace_still_exposed_across_nodes(self):
|
|
expectations = {
|
|
"AILab_QwenVL_PromptEnhancer.py": 'RETURN_NAMES = ("ENHANCED_OUTPUT", "RAW_TRACE")',
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py": 'RETURN_NAMES = ("ENHANCED_OUTPUT", "RAW_TRACE")',
|
|
"AILab_QwenVL.py": 'RETURN_NAMES = ("RESPONSE", "RAW_TRACE")',
|
|
"AILab_QwenVL_GGUF.py": 'RETURN_NAMES = ("RESPONSE", "RAW_TRACE")',
|
|
}
|
|
for filename, marker in expectations.items():
|
|
with self.subTest(filename=filename):
|
|
self.assertIn(marker, read_source(filename))
|
|
|
|
|
|
class TestSharedComfyUITextEncoders(unittest.TestCase):
|
|
def test_discovers_and_runs_supported_native_text_encoders(self):
|
|
filenames = [
|
|
"qwen3vl_4b_convrot.safetensors",
|
|
"qwen3vl_8b_fp8_scaled.safetensors",
|
|
"gemma-3-12b-it-heretic-v2_int8.safetensors",
|
|
"qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors",
|
|
"qwen3vl_32b_minimax_h3_nvfp4_awq.safetensor",
|
|
"clip_l.safetensors",
|
|
]
|
|
calls = {}
|
|
calls["decoded_text"] = "native answer"
|
|
|
|
class FakeClip:
|
|
def tokenize(self, text, **kwargs):
|
|
calls["tokenize"] = (text, kwargs)
|
|
return "tokens"
|
|
|
|
def generate(self, tokens, **kwargs):
|
|
calls["generate"] = (tokens, kwargs)
|
|
return [1, 2, 3]
|
|
|
|
def decode(self, tokens):
|
|
return calls["decoded_text"]
|
|
|
|
def load_clip(**kwargs):
|
|
calls["load"] = kwargs
|
|
return FakeClip()
|
|
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.pop("AILab_QwenVL", None)
|
|
stub_modules["folder_paths"].get_filename_list = lambda category: filenames if category == "text_encoders" else []
|
|
stub_modules["folder_paths"].get_full_path_or_raise = lambda category, filename: str(PKG / filename)
|
|
comfy_sd = build_stub_module("comfy.sd", load_clip=load_clip)
|
|
stub_modules["comfy"].sd = comfy_sd
|
|
stub_modules["comfy.sd"] = comfy_sd
|
|
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file("AILab_QwenVL.py", "thinkingllm_native_text_encoder_test")
|
|
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)
|
|
|
|
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)
|
|
|
|
|
|
class TestHuggingFaceTokenSupport(unittest.TestCase):
|
|
TOKEN_NODE_FILES = [
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
]
|
|
|
|
def _load_qwenvl_with_stubs(self):
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.pop("AILab_QwenVL", None)
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
return load_module_from_file("AILab_QwenVL.py", "thinkingllm_qwenvl_hf_token_test")
|
|
|
|
def test_hf_token_widget_exists_in_download_nodes(self):
|
|
for filename in self.TOKEN_NODE_FILES:
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
self.assertIn('"hf_token": ("STRING"', source)
|
|
self.assertRegex(source, r'"hf_token": \("STRING", \{[^\n]+"tooltip"')
|
|
|
|
def test_community_access_policy_only_requires_auth_for_gated_models(self):
|
|
module = self._load_qwenvl_with_stubs()
|
|
gated = {"commercial_status": "external_gated"}
|
|
|
|
module.enforce_model_access(gated, "private/repo", local_exists=True)
|
|
with mock.patch.dict(module.os.environ, {}, clear=True):
|
|
with self.assertRaisesRegex(PermissionError, "no Hugging Face access token"):
|
|
module.enforce_model_access(gated, "private/repo", local_exists=False)
|
|
module.enforce_model_access(gated, "private/repo", local_exists=False, hf_token="hf_test")
|
|
|
|
for status in ("unclear", "noncommercial"):
|
|
with self.subTest(status=status):
|
|
module.enforce_model_access({"commercial_status": status}, "example/model", local_exists=False)
|
|
module.enforce_model_access({"commercial_status": status}, "example/model", local_exists=True)
|
|
|
|
def test_commercial_mode_rejects_every_status_except_cleared(self):
|
|
module = self._load_qwenvl_with_stubs()
|
|
access_globals = module.enforce_model_access.__globals__
|
|
with mock.patch.dict(access_globals, {"COMMERCIAL_RELEASE": True}):
|
|
module.enforce_model_access({"commercial_status": "cleared"}, "official/model", local_exists=True)
|
|
for info in (
|
|
{"commercial_status": "external_gated"},
|
|
{"commercial_status": "unclear"},
|
|
{"commercial_status": "noncommercial"},
|
|
{"is_local": True},
|
|
):
|
|
with self.subTest(info=info):
|
|
with self.assertRaisesRegex(PermissionError, "Commercial mode rejects"):
|
|
module.enforce_model_access(info, "blocked/model", local_exists=True)
|
|
|
|
def test_gated_hf_snapshot_uses_local_copy_without_token_and_requires_token_before_download(self):
|
|
module = self._load_qwenvl_with_stubs()
|
|
models_globals = module.ensure_model.__globals__
|
|
model_folder_paths = models_globals["folder_paths"]
|
|
entry = {"repo_id": "private/repo", "commercial_status": "external_gated"}
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
models_dir = Path(temp_dir)
|
|
target = models_dir / "LLM" / "Qwen-VL" / "repo"
|
|
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": {"gated": entry}}), mock.patch.object(
|
|
model_folder_paths, "models_dir", str(models_dir)
|
|
), mock.patch.object(model_folder_paths, "folder_names_and_paths", {}), mock.patch.dict(
|
|
models_globals, {"_download_model_snapshot": mock.Mock(side_effect=AssertionError("local gated model must not download"))}
|
|
):
|
|
self.assertEqual(str(target), module.ensure_model("gated"))
|
|
|
|
shutil.rmtree(target)
|
|
with mock.patch.dict(models_globals, {"HF_ALL_MODELS": {"gated": entry}}), mock.patch.object(
|
|
model_folder_paths, "models_dir", str(models_dir)
|
|
), mock.patch.object(model_folder_paths, "folder_names_and_paths", {}), mock.patch.dict(
|
|
models_globals, {"_download_model_snapshot": mock.Mock(side_effect=AssertionError("missing token must stop before download"))}
|
|
), mock.patch.dict(module.os.environ, {}, clear=True):
|
|
with self.assertRaisesRegex(PermissionError, "no Hugging Face access token"):
|
|
module.ensure_model("gated", hf_token="")
|
|
|
|
def test_gated_hf_snapshot_forwards_token_only_to_download(self):
|
|
module = self._load_qwenvl_with_stubs()
|
|
models_globals = module.ensure_model.__globals__
|
|
model_folder_paths = models_globals["folder_paths"]
|
|
captured = []
|
|
|
|
def fake_download(repo_id, target, **kwargs):
|
|
captured.append((repo_id, kwargs.get("hf_token")))
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
(target / "config.json").write_text("{}", encoding="utf-8")
|
|
(target / "model.safetensors").write_bytes(b"weights")
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir, mock.patch.dict(
|
|
models_globals,
|
|
{
|
|
"HF_ALL_MODELS": {"gated": {"repo_id": "private/repo", "commercial_status": "external_gated"}},
|
|
"_download_model_snapshot": mock.Mock(side_effect=fake_download),
|
|
},
|
|
), mock.patch.object(model_folder_paths, "models_dir", temp_dir), mock.patch.object(
|
|
model_folder_paths, "folder_names_and_paths", {}
|
|
):
|
|
module.ensure_model("gated", hf_token="hf_test_secret")
|
|
|
|
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()
|
|
stubs["AILab_QwenVL"] = hf_module
|
|
with mock.patch.dict(sys.modules, stubs, clear=False):
|
|
gguf_module = load_module_from_file("AILab_QwenVL_GGUF.py", "thinkingllm_gated_gguf_policy_test")
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir, mock.patch.object(
|
|
gguf_module,
|
|
"GGUF_VL_CATALOG",
|
|
{
|
|
"base_dir": temp_dir,
|
|
"models": {
|
|
"gated.gguf": {
|
|
"filename": "gated.gguf",
|
|
"repo_id": "private/repo",
|
|
"commercial_status": "external_gated",
|
|
}
|
|
},
|
|
},
|
|
), mock.patch.object(gguf_module.QwenVLGGUFBase, "_load_backend", return_value=object()), mock.patch.object(
|
|
gguf_module, "_download_single_file", side_effect=AssertionError("missing token must stop before GGUF download")
|
|
), mock.patch.dict(hf_module.os.environ, {}, clear=True):
|
|
with self.assertRaisesRegex(PermissionError, "no Hugging Face access token"):
|
|
gguf_module.QwenVLGGUFBase()._load_model(
|
|
model_name="gated.gguf",
|
|
device="cpu",
|
|
ctx=None,
|
|
n_batch=None,
|
|
n_ubatch=None,
|
|
gpu_layers=None,
|
|
image_max_tokens=None,
|
|
top_k=None,
|
|
pool_size=None,
|
|
n_threads=None,
|
|
n_threads_batch=None,
|
|
flash_attn=False,
|
|
offload_kqv=False,
|
|
ctx_checkpoints=None,
|
|
hf_token="",
|
|
)
|
|
|
|
def test_hf_token_is_not_part_of_prompt_or_cache_signatures(self):
|
|
for filename in self.TOKEN_NODE_FILES:
|
|
tree = parse_source(filename)
|
|
with self.subTest(filename=filename):
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.Call):
|
|
continue
|
|
if isinstance(node.func, ast.Name):
|
|
function_name = node.func.id
|
|
elif isinstance(node.func, ast.Attribute):
|
|
function_name = node.func.attr
|
|
else:
|
|
continue
|
|
if function_name not in {"build_node_input_signature", "get_cache_key"}:
|
|
continue
|
|
keyword_names = {keyword.arg for keyword in node.keywords if keyword.arg}
|
|
self.assertNotIn("hf_token", keyword_names)
|
|
for arg in node.args:
|
|
self.assertNotEqual(getattr(arg, "id", None), "hf_token")
|
|
|
|
def test_hf_download_prefers_exact_file_and_passes_token(self):
|
|
module = self._load_qwenvl_with_stubs()
|
|
access_globals = module.download_hf_file_to_path.__globals__
|
|
captured_kwargs = []
|
|
|
|
def fake_hf_hub_download(**kwargs):
|
|
captured_kwargs.append(dict(kwargs))
|
|
local_dir = Path(kwargs["local_dir"])
|
|
local_dir.mkdir(parents=True, exist_ok=True)
|
|
downloaded_path = local_dir / kwargs["filename"]
|
|
downloaded_path.parent.mkdir(parents=True, exist_ok=True)
|
|
downloaded_path.write_bytes(b"GGUF")
|
|
return str(downloaded_path)
|
|
|
|
def fail_snapshot_download(**kwargs):
|
|
raise AssertionError("snapshot_download should not run when exact file download succeeds")
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
target_path = Path(temp_dir) / "model.gguf"
|
|
with mock.patch.dict(
|
|
access_globals,
|
|
{
|
|
"hf_hub_download": mock.Mock(side_effect=fake_hf_hub_download),
|
|
"snapshot_download": mock.Mock(side_effect=fail_snapshot_download),
|
|
},
|
|
):
|
|
module.download_hf_file_to_path(
|
|
["private/repo"],
|
|
"model.gguf",
|
|
target_path,
|
|
hf_token="hf_secret_test_token",
|
|
)
|
|
|
|
self.assertEqual(captured_kwargs[0]["token"], "hf_secret_test_token")
|
|
self.assertEqual(captured_kwargs[0]["filename"], "model.gguf")
|
|
|
|
def test_hf_download_errors_redact_token_and_add_access_hint(self):
|
|
module = self._load_qwenvl_with_stubs()
|
|
secret = "hf_secret_test_token"
|
|
message = module._hf_download_error_message(
|
|
RuntimeError(f"401 Unauthorized: {secret}"),
|
|
repo_id="private/repo",
|
|
hf_token=secret,
|
|
)
|
|
|
|
self.assertNotIn(secret, message)
|
|
self.assertIn("<redacted HF token>", message)
|
|
self.assertIn("access was rejected", message)
|
|
|
|
def test_gated_hf_download_without_token_names_both_token_options(self):
|
|
module = self._load_qwenvl_with_stubs()
|
|
with mock.patch.dict(module.os.environ, {}, clear=True):
|
|
message = module._hf_download_error_message(
|
|
RuntimeError("401 Unauthorized"),
|
|
repo_id="private/repo",
|
|
hf_token="",
|
|
)
|
|
|
|
self.assertIn("no Hugging Face access token was supplied", message)
|
|
self.assertIn("hf_token field", message)
|
|
self.assertIn("HF_TOKEN environment variable", message)
|
|
|
|
def test_hf_token_can_fall_back_to_environment(self):
|
|
module = self._load_qwenvl_with_stubs()
|
|
with mock.patch.dict(module.os.environ, {"HF_TOKEN": "hf_env_secret"}, clear=True):
|
|
self.assertEqual(module._clean_hf_token(""), "hf_env_secret")
|
|
message = module._hf_download_error_message(
|
|
RuntimeError("401 Unauthorized: hf_env_secret"),
|
|
repo_id="private/repo",
|
|
hf_token="",
|
|
)
|
|
|
|
self.assertNotIn("hf_env_secret", message)
|
|
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__
|
|
secret = "hf_secret_test_token"
|
|
|
|
def fake_snapshot_download(**kwargs):
|
|
raise RuntimeError(f"403 Forbidden: {secret}")
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
target_path = Path(temp_dir) / "missing.gguf"
|
|
with mock.patch.dict(
|
|
access_globals,
|
|
{
|
|
"hf_hub_download": mock.Mock(side_effect=fake_snapshot_download),
|
|
"snapshot_download": mock.Mock(side_effect=fake_snapshot_download),
|
|
},
|
|
):
|
|
with self.assertRaises(FileNotFoundError) as raised:
|
|
module.download_hf_file_to_path(
|
|
["private/repo"],
|
|
"missing.gguf",
|
|
target_path,
|
|
hf_token=secret,
|
|
)
|
|
|
|
message = str(raised.exception)
|
|
self.assertNotIn(secret, message)
|
|
self.assertIn("<redacted HF token>", message)
|
|
self.assertIn("access was rejected", message)
|
|
|
|
def test_frontend_hides_only_legacy_gguf_advanced_shims(self):
|
|
source = read_source("web/js/appearance.js")
|
|
for widget_name in [
|
|
"legacy_seed_mode",
|
|
"legacy_unload_after_run",
|
|
]:
|
|
with self.subTest(widget_name=widget_name):
|
|
self.assertIn(f'"{widget_name}"', source)
|
|
internal_widgets_block = source[
|
|
source.index("const GGUF_ADVANCED_INTERNAL_WIDGETS = new Set(["):
|
|
source.index("]);", source.index("const GGUF_ADVANCED_INTERNAL_WIDGETS = new Set([")):
|
|
]
|
|
for widget_name in [
|
|
"n_ubatch",
|
|
"n_threads",
|
|
"n_threads_batch",
|
|
"flash_attn",
|
|
"offload_kqv",
|
|
"ctx_checkpoints",
|
|
]:
|
|
with self.subTest(widget_name=widget_name):
|
|
self.assertNotIn(f'"{widget_name}"', internal_widgets_block)
|
|
self.assertNotIn('GGUF_ADVANCED_INTERNAL_WIDGETS = new Set([\n "hf_token"', source)
|
|
self.assertIn("hideStableWidget", source)
|
|
self.assertIn("clearHfTokenAfterExecution", source)
|
|
|
|
|
|
class TestInterruptSupport(unittest.TestCase):
|
|
INTERRUPT_FILES = [
|
|
"thinkingllm_core/hf_runtime.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
]
|
|
|
|
def test_interrupt_import_present(self):
|
|
for filename in self.INTERRUPT_FILES:
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
self.assertIn("from comfy.model_management import throw_exception_if_processing_interrupted", source)
|
|
|
|
def test_interrupt_check_in_loops(self):
|
|
for filename in self.INTERRUPT_FILES:
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
self.assertIn("throw_exception_if_processing_interrupted()", source)
|
|
|
|
def test_interrupt_after_blocking_calls(self):
|
|
"""Verify interrupt checks exist after non-streaming generate/create_chat_completion calls."""
|
|
for filename in self.INTERRUPT_FILES:
|
|
source = read_source(filename)
|
|
with self.subTest(filename=filename):
|
|
# Every file should have at least one interrupt check outside streaming loops
|
|
# (after blocking calls + in stream loops = at least 2 checks per file,
|
|
# but simple nodes may have only 1 non-streaming path)
|
|
count = source.count("throw_exception_if_processing_interrupted()")
|
|
self.assertGreaterEqual(
|
|
count, 1,
|
|
f"{filename} has only {count} interrupt check(s); expected >= 1"
|
|
)
|
|
|
|
|
|
class TestNoResidualGlobals(unittest.TestCase):
|
|
def test_no_global_last_saved_prompt_statement(self):
|
|
for filename in [
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
]:
|
|
tree = parse_source(filename)
|
|
visitor = _GlobalCheck()
|
|
visitor.visit(tree)
|
|
with self.subTest(filename=filename):
|
|
self.assertFalse(visitor.found)
|
|
|
|
|
|
class TestLoaderModuleRegistration(unittest.TestCase):
|
|
def test_commercial_loader_registers_only_reviewed_hf_modules(self):
|
|
with mock.patch.dict(os.environ, {"THINKINGLLM_COMMERCIAL_RELEASE": "1"}):
|
|
package = load_thinkingllm_loader_subset(
|
|
[
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_WhisperASR.py",
|
|
]
|
|
)
|
|
|
|
self.assertIn("ThinkingLLM_QwenVL", package.NODE_CLASS_MAPPINGS)
|
|
self.assertIn("ThinkingLLM_QwenVL_PromptEnhancer", package.NODE_CLASS_MAPPINGS)
|
|
self.assertNotIn("ThinkingLLM_QwenVL_GGUF", package.NODE_CLASS_MAPPINGS)
|
|
self.assertNotIn("ThinkingLLM_Whisper_ASR", package.NODE_CLASS_MAPPINGS)
|
|
self.assertIsNone(package.WEB_DIRECTORY)
|
|
|
|
def test_loader_registers_qwenvl_modules_with_stubbed_dependencies(self):
|
|
package = load_thinkingllm_loader_subset(
|
|
[
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_WhisperASR.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
]
|
|
)
|
|
|
|
for node_name in [
|
|
"ThinkingLLM_QwenVL_GGUF",
|
|
"ThinkingLLM_QwenVL_GGUF_Advanced",
|
|
"ThinkingLLM_Gemma4_Audio_GGUF",
|
|
"ThinkingLLM_Whisper_ASR",
|
|
"ThinkingLLM_QwenVL_PromptEnhancer",
|
|
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
|
|
]:
|
|
with self.subTest(node_name=node_name):
|
|
self.assertIn(node_name, package.NODE_CLASS_MAPPINGS)
|
|
self.assertNotIn("ThinkingLLM_Qwen3_ASR_GGUF", package.NODE_CLASS_MAPPINGS)
|
|
|
|
|
|
class TestLegacyNodeNameCompatibility(unittest.TestCase):
|
|
CURRENT_TO_LEGACY = {
|
|
"ThinkingLLM_QwenVL": "AILab_QwenVL",
|
|
"ThinkingLLM_QwenVL_Advanced": "AILab_QwenVL_Advanced",
|
|
"ThinkingLLM_QwenVL_GGUF": "AILab_QwenVL_GGUF",
|
|
"ThinkingLLM_QwenVL_GGUF_Advanced": "AILab_QwenVL_GGUF_Advanced",
|
|
"ThinkingLLM_QwenVL_PromptEnhancer": "AILab_QwenVL_PromptEnhancer",
|
|
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer": "AILab_QwenVL_GGUF_PromptEnhancer",
|
|
}
|
|
|
|
MAPPING_FILES = {
|
|
"AILab_QwenVL.py": [
|
|
"ThinkingLLM_QwenVL",
|
|
"ThinkingLLM_QwenVL_Advanced",
|
|
],
|
|
"AILab_QwenVL_GGUF.py": [
|
|
"ThinkingLLM_QwenVL_GGUF",
|
|
"ThinkingLLM_QwenVL_GGUF_Advanced",
|
|
],
|
|
"AILab_QwenVL_PromptEnhancer.py": [
|
|
"ThinkingLLM_QwenVL_PromptEnhancer",
|
|
],
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py": [
|
|
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
|
|
],
|
|
}
|
|
|
|
def test_current_names_remain_canonical(self):
|
|
for filename, canonical_names in self.MAPPING_FILES.items():
|
|
mappings = extract_mapping_target_names(filename, "NODE_CLASS_MAPPINGS")
|
|
for canonical_name in canonical_names:
|
|
with self.subTest(filename=filename, canonical_name=canonical_name):
|
|
self.assertIn(canonical_name, mappings)
|
|
|
|
def test_legacy_aliases_map_to_same_runtime_class_name(self):
|
|
for filename, canonical_names in self.MAPPING_FILES.items():
|
|
mappings = extract_mapping_target_names(filename, "NODE_CLASS_MAPPINGS")
|
|
displays = extract_literal_dict(filename, "NODE_DISPLAY_NAME_MAPPINGS")
|
|
for canonical_name in canonical_names:
|
|
legacy_name = self.CURRENT_TO_LEGACY[canonical_name]
|
|
with self.subTest(filename=filename, legacy_name=legacy_name):
|
|
self.assertEqual(mappings[canonical_name], mappings[legacy_name])
|
|
self.assertEqual(displays[canonical_name], displays[legacy_name])
|
|
|
|
def test_package_export_search_surface_has_no_duplicate_thinkingllm_rows(self):
|
|
package = load_thinkingllm_loader_subset([
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
"AILab_WhisperASR.py",
|
|
])
|
|
|
|
exported_names = set(package.NODE_CLASS_MAPPINGS)
|
|
exported_displays = package.NODE_DISPLAY_NAME_MAPPINGS
|
|
self.assertFalse(
|
|
[name for name in exported_names if name.startswith("AILab_QwenVL")],
|
|
"legacy AILab aliases should not be exported as visible search rows",
|
|
)
|
|
seen = {}
|
|
duplicates = {}
|
|
for node_name, display_name in exported_displays.items():
|
|
if not str(display_name).startswith("ThinkingLLM"):
|
|
continue
|
|
if display_name in seen:
|
|
duplicates.setdefault(display_name, [seen[display_name]]).append(node_name)
|
|
else:
|
|
seen[display_name] = node_name
|
|
self.assertEqual(duplicates, {})
|
|
|
|
def test_package_exposes_legacy_replacements_for_old_workflows(self):
|
|
package = load_thinkingllm_loader_subset([
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
])
|
|
|
|
replacements = getattr(package, "LEGACY_NODE_REPLACEMENTS", {})
|
|
expected = {legacy: current for current, legacy in self.CURRENT_TO_LEGACY.items()}
|
|
self.assertEqual(replacements, expected)
|
|
|
|
def test_package_registers_legacy_replacements_when_comfy_manager_is_available(self):
|
|
class StubNodeReplace:
|
|
def __init__(self, new_node_id, old_node_id):
|
|
self.new_node_id = new_node_id
|
|
self.old_node_id = old_node_id
|
|
|
|
class StubManager:
|
|
def __init__(self):
|
|
self.registered = []
|
|
|
|
def register(self, node_replace):
|
|
self.registered.append((node_replace.old_node_id, node_replace.new_node_id))
|
|
|
|
manager = StubManager()
|
|
prompt_server = type("PromptServer", (), {"instance": types.SimpleNamespace(node_replace_manager=manager)})
|
|
stubs = build_loader_test_stubs()
|
|
stubs.update({
|
|
"server": build_stub_module("server", PromptServer=prompt_server),
|
|
"comfy_api": build_stub_module("comfy_api"),
|
|
"comfy_api.latest": build_stub_module("comfy_api.latest"),
|
|
"comfy_api.latest._io": build_stub_module("comfy_api.latest._io", NodeReplace=StubNodeReplace),
|
|
})
|
|
|
|
with mock.patch.dict(sys.modules, stubs, clear=False):
|
|
package = load_thinkingllm_loader_subset([
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
])
|
|
|
|
self.assertEqual(dict(manager.registered), package.LEGACY_NODE_REPLACEMENTS)
|
|
|
|
def test_example_workflow_legacy_types_are_covered(self):
|
|
resolved_types = set()
|
|
for filename in self.MAPPING_FILES:
|
|
mappings = extract_mapping_target_names(filename, "NODE_CLASS_MAPPINGS")
|
|
resolved_types.update(mappings.keys())
|
|
|
|
workflow_files = sorted(WORKFLOW_DIR.glob("*.json"))
|
|
self.assertTrue(workflow_files, "expected shipped example workflows")
|
|
for workflow_file in workflow_files:
|
|
for node_type in iter_workflow_types(workflow_file):
|
|
if node_type.startswith("AILab_QwenVL"):
|
|
with self.subTest(workflow=workflow_file.name, node_type=node_type):
|
|
self.assertIn(node_type, resolved_types)
|
|
|
|
|
|
class TestGGUFAdvancedWorkflowCompatibility(unittest.TestCase):
|
|
LEGACY_REQUIRED_PREFIX = [
|
|
"model_name",
|
|
"device",
|
|
"preset_prompt",
|
|
"custom_prompt",
|
|
"max_tokens",
|
|
"temperature",
|
|
"top_p",
|
|
"repetition_penalty",
|
|
"frame_count",
|
|
"ctx",
|
|
"n_batch",
|
|
"gpu_layers",
|
|
"image_max_tokens",
|
|
"top_k",
|
|
"pool_size",
|
|
"keep_model_loaded",
|
|
"seed",
|
|
]
|
|
|
|
def _load_gguf_advanced_class(self):
|
|
package = load_thinkingllm_loader_subset(["AILab_QwenVL_GGUF.py"])
|
|
return package.NODE_CLASS_MAPPINGS["ThinkingLLM_QwenVL_GGUF_Advanced"]
|
|
|
|
def test_gguf_advanced_required_widgets_preserve_legacy_order(self):
|
|
node_cls = self._load_gguf_advanced_class()
|
|
required_keys = list(node_cls.INPUT_TYPES()["required"].keys())
|
|
|
|
self.assertEqual(
|
|
required_keys[: len(self.LEGACY_REQUIRED_PREFIX)],
|
|
self.LEGACY_REQUIRED_PREFIX,
|
|
)
|
|
self.assertEqual(
|
|
required_keys[len(self.LEGACY_REQUIRED_PREFIX): len(self.LEGACY_REQUIRED_PREFIX) + 2],
|
|
["legacy_seed_mode", "legacy_unload_after_run"],
|
|
)
|
|
self.assertGreater(required_keys.index("n_ubatch"), required_keys.index("legacy_unload_after_run"))
|
|
|
|
def test_gguf_advanced_process_signature_matches_required_widget_order(self):
|
|
node_cls = self._load_gguf_advanced_class()
|
|
signature = inspect.signature(node_cls.process)
|
|
parameter_names = [
|
|
name
|
|
for name, parameter in signature.parameters.items()
|
|
if name != "self" and parameter.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
|
|
]
|
|
|
|
expected_prefix = self.LEGACY_REQUIRED_PREFIX + [
|
|
"legacy_seed_mode",
|
|
"legacy_unload_after_run",
|
|
"n_ubatch",
|
|
"n_threads",
|
|
"n_threads_batch",
|
|
"flash_attn",
|
|
"offload_kqv",
|
|
"ctx_checkpoints",
|
|
]
|
|
self.assertEqual(parameter_names[: len(expected_prefix)], expected_prefix)
|
|
|
|
def test_gguf_advanced_accepts_legacy_serialized_values(self):
|
|
node_cls = self._load_gguf_advanced_class()
|
|
required = node_cls.INPUT_TYPES()["required"]
|
|
|
|
legacy_seed_mode_options, legacy_seed_mode_meta = required["legacy_seed_mode"]
|
|
n_ubatch_type, n_ubatch_meta = required["n_ubatch"]
|
|
|
|
self.assertIn(False, legacy_seed_mode_options)
|
|
self.assertEqual(legacy_seed_mode_meta["default"], "fixed")
|
|
self.assertEqual(n_ubatch_type, "INT")
|
|
self.assertEqual(n_ubatch_meta["min"], 0)
|
|
|
|
def test_gguf_runtime_normalizes_legacy_zero_ubatch(self):
|
|
source = read_source("AILab_QwenVL_GGUF.py")
|
|
self.assertIn("n_ubatch_val = int(n_ubatch) if n_ubatch is not None else 0", source)
|
|
self.assertIn("if n_ubatch_val <= 0:", source)
|
|
self.assertIn("n_ubatch_val = min(n_batch_val, 512)", source)
|
|
|
|
def test_gguf_vision_nodes_accept_optional_audio_without_legacy_order_change(self):
|
|
node_cls = self._load_gguf_advanced_class()
|
|
input_types = node_cls.INPUT_TYPES()
|
|
|
|
self.assertEqual(input_types["optional"]["audio"], ("AUDIO",))
|
|
|
|
simple_cls = load_thinkingllm_loader_subset(["AILab_QwenVL_GGUF.py"]).NODE_CLASS_MAPPINGS["ThinkingLLM_QwenVL_GGUF"]
|
|
self.assertEqual(simple_cls.INPUT_TYPES()["optional"]["audio"], ("AUDIO",))
|
|
|
|
def test_thinkingllm_node_input_capabilities_are_intentional(self):
|
|
package = load_thinkingllm_loader_subset([
|
|
"AILab_QwenVL.py",
|
|
"AILab_QwenVL_GGUF.py",
|
|
"AILab_QwenVL_PromptEnhancer.py",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
"AILab_WhisperASR.py",
|
|
])
|
|
expected_optional_inputs = {
|
|
"ThinkingLLM_QwenVL": {"image", "video", "mask", "duration_seconds"},
|
|
"ThinkingLLM_QwenVL_Advanced": {"image", "video", "mask", "duration_seconds"},
|
|
"ThinkingLLM_QwenVL_PromptEnhancer": {"duration_seconds"},
|
|
"ThinkingLLM_QwenVL_GGUF": {"image", "video", "mask", "mask_mode", "audio", "audio_file_path", "duration_seconds"},
|
|
"ThinkingLLM_QwenVL_GGUF_Advanced": {"image", "video", "mask", "mask_mode", "audio", "audio_file_path", "duration_seconds"},
|
|
"ThinkingLLM_Gemma4_Audio_GGUF": {"audio", "audio_file_path"},
|
|
"ThinkingLLM_Whisper_ASR": {"audio", "audio_file_path"},
|
|
"ThinkingLLM_QwenVL_GGUF_PromptEnhancer": {"duration_seconds"},
|
|
}
|
|
|
|
for node_name, expected in expected_optional_inputs.items():
|
|
with self.subTest(node_name=node_name):
|
|
input_types = package.NODE_CLASS_MAPPINGS[node_name].INPUT_TYPES()
|
|
self.assertEqual(set(input_types.get("optional", {})), expected)
|
|
if "duration_seconds" in expected:
|
|
duration_type, duration_meta = input_types["optional"]["duration_seconds"]
|
|
self.assertEqual(duration_type, "FLOAT")
|
|
self.assertEqual(duration_meta["default"], 5.0)
|
|
self.assertEqual(duration_meta["min"], 0.2)
|
|
self.assertEqual(duration_meta["max"], 150.0)
|
|
self.assertEqual(duration_meta["step"], 0.1)
|
|
|
|
def test_gguf_vision_selector_includes_google_gemma4_e4b_image_model(self):
|
|
package = load_thinkingllm_loader_subset(["AILab_QwenVL_GGUF.py"])
|
|
expected_model_file = "google_gemma-4-E4B-it-Q4_K_M.gguf"
|
|
|
|
for node_name in ("ThinkingLLM_QwenVL_GGUF", "ThinkingLLM_QwenVL_GGUF_Advanced"):
|
|
with self.subTest(node_name=node_name):
|
|
model_keys = package.NODE_CLASS_MAPPINGS[node_name].INPUT_TYPES()["required"]["model_name"][0]
|
|
self.assertTrue(
|
|
any(expected_model_file in key for key in model_keys),
|
|
f"{expected_model_file} should be available in the image-capable GGUF selector",
|
|
)
|
|
|
|
def test_catalog_model_is_marked_installed_and_old_display_name_still_resolves(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file("AILab_QwenVL_GGUF.py", "thinkingllm_installed_catalog_test")
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
models_dir = Path(temp_dir)
|
|
gguf_dir = models_dir / "LLM" / "GGUF"
|
|
gguf_dir.mkdir(parents=True)
|
|
(gguf_dir / "model.gguf").write_bytes(b"GGUF")
|
|
config_path = models_dir / "gguf_models.json"
|
|
config_path.write_text(json.dumps({
|
|
"base_dir": "LLM/GGUF",
|
|
"repos": {
|
|
"test": {
|
|
"repo_id": "example/test",
|
|
"mmproj_file": "mmproj.gguf",
|
|
"model_files": ["model.gguf"],
|
|
}
|
|
},
|
|
}), encoding="utf-8")
|
|
|
|
with mock.patch.object(module, "GGUF_CONFIG_PATH", config_path):
|
|
with mock.patch.object(module.folder_paths, "models_dir", str(models_dir)):
|
|
catalog = module._load_gguf_vl_catalog()
|
|
|
|
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()
|
|
max_tokens_type, max_tokens_meta = node_cls.INPUT_TYPES()["required"]["max_tokens"]
|
|
|
|
self.assertEqual(max_tokens_type, "INT")
|
|
self.assertGreaterEqual(max_tokens_meta["max"], 32768)
|
|
|
|
def test_simple_gguf_node_exposes_raw_trace(self):
|
|
node_cls = load_thinkingllm_loader_subset(["AILab_QwenVL_GGUF.py"]).NODE_CLASS_MAPPINGS["ThinkingLLM_QwenVL_GGUF"]
|
|
|
|
self.assertEqual(node_cls.RETURN_TYPES, ("STRING", "STRING"))
|
|
self.assertEqual(node_cls.RETURN_NAMES, ("RESPONSE", "RAW_TRACE"))
|
|
|
|
def test_advanced_vision_nodes_expose_mask_preview(self):
|
|
package = load_thinkingllm_loader_subset(["AILab_QwenVL.py", "AILab_QwenVL_GGUF.py"])
|
|
|
|
for node_name in ("ThinkingLLM_QwenVL_Advanced", "ThinkingLLM_QwenVL_GGUF_Advanced"):
|
|
with self.subTest(node_name=node_name):
|
|
node_cls = package.NODE_CLASS_MAPPINGS[node_name]
|
|
self.assertEqual(node_cls.RETURN_TYPES, ("STRING", "STRING", "IMAGE"))
|
|
self.assertEqual(node_cls.RETURN_NAMES, ("RESPONSE", "RAW_TRACE", "MASK_PREVIEW"))
|
|
|
|
def test_gemma4_audio_node_is_audio_only_and_model_filtered(self):
|
|
node_cls = load_thinkingllm_loader_subset(["AILab_QwenVL_GGUF.py"]).NODE_CLASS_MAPPINGS["ThinkingLLM_Gemma4_Audio_GGUF"]
|
|
input_types = node_cls.INPUT_TYPES()
|
|
|
|
optional = input_types["optional"]
|
|
self.assertEqual(optional["audio"], ("AUDIO",))
|
|
self.assertIn("audio_file_path", optional)
|
|
self.assertNotIn("image", optional)
|
|
self.assertNotIn("video", optional)
|
|
|
|
model_keys, model_meta = input_types["required"]["model_name"]
|
|
self.assertTrue(model_keys, "audio node should expose at least one Gemma 4 audio model")
|
|
self.assertTrue(all("gemma-4" in key.lower() for key in model_keys))
|
|
self.assertTrue(all("26b" not in key.lower() and "31b" not in key.lower() for key in model_keys))
|
|
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"])
|
|
|
|
self.assertNotIn("ThinkingLLM_Qwen3_ASR_GGUF", package.NODE_CLASS_MAPPINGS)
|
|
|
|
def test_whisper_asr_node_is_audio_only(self):
|
|
node_cls = load_thinkingllm_loader_subset(["AILab_WhisperASR.py"]).NODE_CLASS_MAPPINGS["ThinkingLLM_Whisper_ASR"]
|
|
input_types = node_cls.INPUT_TYPES()
|
|
|
|
self.assertEqual(input_types["optional"]["audio"], ("AUDIO",))
|
|
self.assertIn("audio_file_path", input_types["optional"])
|
|
model_keys, model_meta = input_types["required"]["model_size"]
|
|
self.assertIn("base", model_keys)
|
|
self.assertIn("large-v3", model_keys)
|
|
self.assertEqual(model_meta["default"], "small")
|
|
self.assertEqual(input_types["required"]["device"][1]["default"], "cpu")
|
|
self.assertEqual(input_types["required"]["compute_type"][1]["default"], "int8")
|
|
self.assertNotIn("image", input_types["optional"])
|
|
self.assertNotIn("video", input_types["optional"])
|
|
|
|
def test_whisper_asr_returns_install_message_when_backend_missing(self):
|
|
node_cls = load_thinkingllm_loader_subset(["AILab_WhisperASR.py"]).NODE_CLASS_MAPPINGS["ThinkingLLM_Whisper_ASR"]
|
|
with mock.patch.dict(sys.modules, {"faster_whisper": None}, clear=False):
|
|
transcript, segments_json, raw_trace = node_cls().process(
|
|
model_size="base",
|
|
language="auto",
|
|
task="transcribe",
|
|
device="auto",
|
|
compute_type="auto",
|
|
beam_size=5,
|
|
vad_filter=True,
|
|
audio_file_path="dummy.wav",
|
|
)
|
|
|
|
self.assertIn("faster-whisper is not installed", transcript)
|
|
self.assertEqual(segments_json, "[]")
|
|
self.assertIn("pip install faster-whisper", raw_trace)
|
|
|
|
def test_whisper_asr_transcribes_with_mock_backend(self):
|
|
module = load_module_from_file(
|
|
"AILab_WhisperASR.py",
|
|
"thinkingllm_whisper_mock_backend_test",
|
|
)
|
|
|
|
class FakeSegment:
|
|
def __init__(self, start, end, text):
|
|
self.start = start
|
|
self.end = end
|
|
self.text = text
|
|
|
|
fake_info = types.SimpleNamespace(language="de", language_probability=0.99, duration=1.0)
|
|
captured = {}
|
|
|
|
class FakeWhisperModel:
|
|
def __init__(self, model_size, device, compute_type, local_files_only=False):
|
|
captured["init"] = (model_size, device, compute_type)
|
|
|
|
def transcribe(self, path, language, task, beam_size, vad_filter):
|
|
captured["transcribe"] = (Path(path).suffix, language, task, beam_size, vad_filter)
|
|
return [FakeSegment(0.0, 1.0, " Hallo Welt. ")], fake_info
|
|
|
|
fake_backend = build_stub_module("faster_whisper", WhisperModel=FakeWhisperModel)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
source = Path(temp_dir) / "input.wav"
|
|
with wave.open(str(source), "wb") as wav:
|
|
wav.setnchannels(1)
|
|
wav.setsampwidth(2)
|
|
wav.setframerate(16000)
|
|
wav.writeframes(b"\0\0" * 1600)
|
|
|
|
with mock.patch.dict(sys.modules, {"faster_whisper": fake_backend}, clear=False):
|
|
transcript, segments_json, raw_trace = module.ThinkingLLM_Whisper_ASR().process(
|
|
model_size="base",
|
|
language="de",
|
|
task="transcribe",
|
|
device="cpu",
|
|
compute_type="int8",
|
|
beam_size=3,
|
|
vad_filter=True,
|
|
audio_file_path=str(source),
|
|
)
|
|
|
|
self.assertEqual(transcript, "Hallo Welt.")
|
|
self.assertEqual(json.loads(segments_json)[0]["text"], "Hallo Welt.")
|
|
self.assertEqual(captured["init"], ("base", "cpu", "int8"))
|
|
self.assertEqual(captured["transcribe"], (".wav", "de", "transcribe", 3, True))
|
|
self.assertIn("language=de", raw_trace)
|
|
|
|
def test_whisper_asr_prefers_cached_model_without_hf_ping(self):
|
|
module = load_module_from_file(
|
|
"AILab_WhisperASR.py",
|
|
"thinkingllm_whisper_cached_model_test",
|
|
)
|
|
|
|
class FakeSegment:
|
|
start = 0.0
|
|
end = 1.0
|
|
text = " cached transcript "
|
|
|
|
fake_info = types.SimpleNamespace(language="en", language_probability=0.99, duration=1.0)
|
|
captured = {}
|
|
|
|
class FakeWhisperModel:
|
|
def __init__(self, model_size, device, compute_type, local_files_only=False):
|
|
captured["init"] = (model_size, device, compute_type, local_files_only)
|
|
|
|
def transcribe(self, path, language, task, beam_size, vad_filter):
|
|
return [FakeSegment()], fake_info
|
|
|
|
fake_backend = build_stub_module("faster_whisper", WhisperModel=FakeWhisperModel)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
source = Path(temp_dir) / "input.wav"
|
|
with wave.open(str(source), "wb") as wav:
|
|
wav.setnchannels(1)
|
|
wav.setsampwidth(2)
|
|
wav.setframerate(16000)
|
|
wav.writeframes(b"\0\0" * 1600)
|
|
|
|
with mock.patch.dict(sys.modules, {"faster_whisper": fake_backend}, clear=False):
|
|
transcript, _, raw_trace = module.ThinkingLLM_Whisper_ASR().process(
|
|
model_size="small",
|
|
language="auto",
|
|
task="transcribe",
|
|
device="cpu",
|
|
compute_type="int8",
|
|
beam_size=5,
|
|
vad_filter=True,
|
|
audio_file_path=str(source),
|
|
)
|
|
|
|
self.assertEqual(transcript, "cached transcript")
|
|
self.assertEqual(captured["init"], ("small", "cpu", "int8", True))
|
|
self.assertIn("local_files_only=True", raw_trace)
|
|
|
|
def test_whisper_asr_downloads_once_when_cache_is_missing(self):
|
|
module = load_module_from_file(
|
|
"AILab_WhisperASR.py",
|
|
"thinkingllm_whisper_cache_miss_test",
|
|
)
|
|
|
|
class FakeSegment:
|
|
start = 0.0
|
|
end = 1.0
|
|
text = " downloaded transcript "
|
|
|
|
fake_info = types.SimpleNamespace(language="en", language_probability=0.99, duration=1.0)
|
|
attempts = []
|
|
|
|
class FakeWhisperModel:
|
|
def __init__(self, model_size, device, compute_type, local_files_only=False):
|
|
attempts.append(local_files_only)
|
|
if local_files_only:
|
|
raise RuntimeError("local_files_only=True and no cached snapshot was found")
|
|
|
|
def transcribe(self, path, language, task, beam_size, vad_filter):
|
|
return [FakeSegment()], fake_info
|
|
|
|
fake_backend = build_stub_module("faster_whisper", WhisperModel=FakeWhisperModel)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
source = Path(temp_dir) / "input.wav"
|
|
with wave.open(str(source), "wb") as wav:
|
|
wav.setnchannels(1)
|
|
wav.setsampwidth(2)
|
|
wav.setframerate(16000)
|
|
wav.writeframes(b"\0\0" * 1600)
|
|
|
|
with mock.patch.dict(sys.modules, {"faster_whisper": fake_backend}, clear=False):
|
|
transcript, _, raw_trace = module.ThinkingLLM_Whisper_ASR().process(
|
|
model_size="small",
|
|
language="auto",
|
|
task="transcribe",
|
|
device="cpu",
|
|
compute_type="int8",
|
|
beam_size=5,
|
|
vad_filter=True,
|
|
audio_file_path=str(source),
|
|
)
|
|
|
|
self.assertEqual(transcript, "downloaded transcript")
|
|
self.assertEqual(attempts, [True, False])
|
|
self.assertIn("local_files_only=False", raw_trace)
|
|
self.assertIn("Local Whisper cache miss", raw_trace)
|
|
|
|
def test_audio_to_wav_base64_resamples_to_16khz_mono(self):
|
|
import numpy as real_numpy
|
|
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_audio_resample_test",
|
|
)
|
|
module.np = real_numpy
|
|
|
|
waveform = real_numpy.stack([
|
|
real_numpy.linspace(-0.5, 0.5, 800, dtype=real_numpy.float32),
|
|
real_numpy.linspace(0.5, -0.5, 800, dtype=real_numpy.float32),
|
|
])
|
|
|
|
encoded = module._audio_to_wav_base64({"waveform": waveform, "sample_rate": 8000})
|
|
|
|
self.assertEqual(len(encoded), 1)
|
|
with wave.open(io.BytesIO(base64.b64decode(encoded[0])), "rb") as wav:
|
|
self.assertEqual(wav.getframerate(), 16000)
|
|
self.assertEqual(wav.getnchannels(), 1)
|
|
self.assertEqual(wav.getsampwidth(), 2)
|
|
self.assertEqual(wav.getnframes(), 1600)
|
|
|
|
def test_audio_to_wav_base64_rejects_invalid_audio_metadata(self):
|
|
import numpy as real_numpy
|
|
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_audio_validation_test",
|
|
)
|
|
module.np = real_numpy
|
|
|
|
self.assertEqual(module._audio_to_wav_base64({"waveform": real_numpy.zeros((1, 10)), "sample_rate": 0}), [])
|
|
self.assertEqual(module._audio_to_wav_base64({"sample_rate": 16000}), [])
|
|
|
|
def test_audio_file_path_to_wav_base64_decodes_m4a_with_ffmpeg(self):
|
|
if shutil.which("ffmpeg") is None:
|
|
self.skipTest("ffmpeg is required for audio file path decoding")
|
|
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_audio_file_path_test",
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="thinkingllm audio ") as temp_dir:
|
|
m4a_path = Path(temp_dir) / "sample voice memo.m4a"
|
|
subprocess.run(
|
|
[
|
|
"ffmpeg",
|
|
"-hide_banner",
|
|
"-v",
|
|
"error",
|
|
"-f",
|
|
"lavfi",
|
|
"-i",
|
|
"sine=frequency=440:duration=0.25",
|
|
"-c:a",
|
|
"aac",
|
|
str(m4a_path),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
encoded = module._audio_file_path_to_wav_base64(str(m4a_path))
|
|
|
|
self.assertEqual(len(encoded), 1)
|
|
with wave.open(io.BytesIO(base64.b64decode(encoded[0])), "rb") as wav:
|
|
self.assertEqual(wav.getframerate(), 16000)
|
|
self.assertEqual(wav.getnchannels(), 1)
|
|
self.assertEqual(wav.getsampwidth(), 2)
|
|
self.assertGreater(wav.getnframes(), 3000)
|
|
self.assertLess(wav.getnframes(), 5000)
|
|
|
|
def test_audio_nodes_skip_saved_prompt_cache(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_audio_nodes_skip_saved_prompt_cache_test",
|
|
)
|
|
|
|
class DummyLoader(module.QwenVLGGUFBase):
|
|
def _load_model(self, *args, **kwargs):
|
|
self.chat_handler = object()
|
|
self.last_backend_trace = ""
|
|
|
|
def _encode_media(self, image, video, frame_count):
|
|
return []
|
|
|
|
def _invoke(self, *args, **kwargs):
|
|
return "real transcript", "raw transcript"
|
|
|
|
with mock.patch.object(
|
|
module,
|
|
"get_node_saved_prompt_with_seed",
|
|
side_effect=AssertionError("audio nodes must not reuse saved prompts"),
|
|
), mock.patch.object(
|
|
module,
|
|
"set_node_saved_prompt",
|
|
side_effect=AssertionError("audio nodes must not store saved prompts"),
|
|
), mock.patch.object(
|
|
module,
|
|
"_audio_inputs_to_wav_base64",
|
|
return_value=(["BASE64WAV"], []),
|
|
), mock.patch.object(
|
|
module,
|
|
"_resolve_model_entry",
|
|
return_value=types.SimpleNamespace(context_length=4096),
|
|
):
|
|
text, raw_trace = DummyLoader().run(
|
|
model_name="Gemma-4-12B-it-Q4_K_M.gguf [~7.5GB]",
|
|
preset_prompt="🚫 No preset (image-only)",
|
|
custom_prompt="Analyze the audio.",
|
|
image=None,
|
|
video=None,
|
|
frame_count=1,
|
|
max_tokens=64,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
repetition_penalty=1.0,
|
|
seed=1,
|
|
keep_model_loaded=False,
|
|
device="auto",
|
|
node_class="ThinkingLLM_Gemma4_Audio_GGUF",
|
|
)
|
|
|
|
self.assertEqual(text, "real transcript")
|
|
self.assertEqual(raw_trace, "raw transcript")
|
|
|
|
|
|
class TestLlamaCppInstaller(unittest.TestCase):
|
|
def _resolve_linux_install_spec(self, installer, *, py_minor, cuda_version, machine="x86_64"):
|
|
with mock.patch.dict(installer.os.environ, {}, clear=True):
|
|
with mock.patch.object(
|
|
installer.sys,
|
|
"version_info",
|
|
types.SimpleNamespace(major=3, minor=py_minor),
|
|
):
|
|
with mock.patch.object(installer.platform, "machine", return_value=machine):
|
|
cuda_versions = {
|
|
"toolkit": "",
|
|
"torch": cuda_version,
|
|
"nvidia_smi": "",
|
|
"selected": cuda_version,
|
|
"selected_source": "torch" if cuda_version else "unknown",
|
|
}
|
|
with mock.patch.object(installer, "_detect_cuda_versions", return_value=cuda_versions):
|
|
return installer._resolve_linux_install_spec()
|
|
|
|
def test_linux_install_spec_prefers_known_cuda_128_wheels_for_verified_python_tags(self):
|
|
installer = load_module_from_file(
|
|
"AILab_LlamaCppInstaller.py",
|
|
"thinkingllm_llama_installer_linux_spec_test",
|
|
)
|
|
|
|
for py_tag in ("cp310", "cp311", "cp312", "cp313", "cp314"):
|
|
with self.subTest(py_tag=py_tag):
|
|
install_spec, install_reason = self._resolve_linux_install_spec(
|
|
installer,
|
|
py_minor=int(py_tag[-2:]),
|
|
cuda_version="12.8",
|
|
)
|
|
expected_spec = (
|
|
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
|
|
"v0.3.35-cu128-Basic-linux-20260406/"
|
|
f"llama_cpp_python-0.3.35+cu128.basic-{py_tag}-{py_tag}-linux_x86_64.whl"
|
|
)
|
|
|
|
self.assertEqual(install_spec, expected_spec)
|
|
self.assertEqual(install_reason, "known Linux wheel (torch CUDA 12.8)")
|
|
self.assertNotEqual(install_spec, installer.DEFAULT_JAMEPENG_GIT_SPEC)
|
|
|
|
def test_linux_install_spec_keeps_existing_known_cuda_124_wheel(self):
|
|
installer = load_module_from_file(
|
|
"AILab_LlamaCppInstaller.py",
|
|
"thinkingllm_llama_installer_linux_spec_124_test",
|
|
)
|
|
|
|
install_spec, install_reason = self._resolve_linux_install_spec(
|
|
installer,
|
|
py_minor=12,
|
|
cuda_version="12.4",
|
|
)
|
|
|
|
self.assertEqual(
|
|
install_spec,
|
|
"https://github.com/JamePeng/llama-cpp-python/releases/download/"
|
|
"v0.3.34-cu124-Basic-linux-20260331/"
|
|
"llama_cpp_python-0.3.34+cu124.basic-cp312-cp312-linux_x86_64.whl",
|
|
)
|
|
self.assertEqual(install_reason, "known Linux wheel (torch CUDA 12.4)")
|
|
self.assertNotEqual(install_spec, installer.DEFAULT_JAMEPENG_GIT_SPEC)
|
|
|
|
def test_linux_install_spec_blocks_unverified_combinations_by_default(self):
|
|
installer = load_module_from_file(
|
|
"AILab_LlamaCppInstaller.py",
|
|
"thinkingllm_llama_installer_linux_spec_fallback_test",
|
|
)
|
|
|
|
install_spec, install_reason = self._resolve_linux_install_spec(
|
|
installer,
|
|
py_minor=11,
|
|
cuda_version="12.4",
|
|
)
|
|
|
|
self.assertEqual(install_spec, "")
|
|
self.assertEqual(install_reason, "no verified Linux wheel match")
|
|
|
|
def test_linux_install_spec_source_build_requires_explicit_opt_in(self):
|
|
installer = load_module_from_file(
|
|
"AILab_LlamaCppInstaller.py",
|
|
"thinkingllm_llama_installer_linux_spec_source_opt_in_test",
|
|
)
|
|
|
|
with mock.patch.dict(installer.os.environ, {"THINKINGLLM_LLAMA_CPP_ALLOW_SOURCE_BUILD": "1"}, clear=True):
|
|
with mock.patch.object(installer.sys, "version_info", types.SimpleNamespace(major=3, minor=11)):
|
|
with mock.patch.object(installer.platform, "machine", return_value="x86_64"):
|
|
with mock.patch.object(
|
|
installer,
|
|
"_detect_cuda_versions",
|
|
return_value={"toolkit": "", "torch": "12.4", "nvidia_smi": "", "selected": "12.4", "selected_source": "torch"},
|
|
):
|
|
install_spec, install_reason = installer._resolve_linux_install_spec()
|
|
|
|
self.assertEqual(install_spec, installer.DEFAULT_JAMEPENG_GIT_SPEC)
|
|
self.assertEqual(install_reason, "source-build fallback (explicit opt-in)")
|
|
|
|
def test_source_build_env_enables_cuda_when_toolkit_exists(self):
|
|
installer = load_module_from_file(
|
|
"AILab_LlamaCppInstaller.py",
|
|
"thinkingllm_llama_installer_source_env_test",
|
|
)
|
|
|
|
with mock.patch.dict(installer.os.environ, {"CMAKE_ARGS": "-DOTHER=1"}, clear=True):
|
|
with mock.patch.object(installer, "_detect_cuda_home_version", return_value="12.8"):
|
|
install_env = installer._build_source_install_env()
|
|
|
|
self.assertIn("-DOTHER=1", install_env["CMAKE_ARGS"])
|
|
self.assertIn("-DGGML_CUDA=on", install_env["CMAKE_ARGS"])
|
|
self.assertEqual(install_env["FORCE_CMAKE"], "1")
|
|
|
|
def test_windows_runtime_dll_dirs_include_torch_lib(self):
|
|
installer = load_module_from_file(
|
|
"AILab_LlamaCppInstaller.py",
|
|
"thinkingllm_llama_installer_dirs_test",
|
|
)
|
|
torch_module = types.SimpleNamespace(__file__=r"C:\stub\torch\__init__.py")
|
|
|
|
with mock.patch.dict(sys.modules, {"torch": torch_module}, clear=False):
|
|
with mock.patch.object(installer.os.path, "isdir", return_value=True):
|
|
self.assertEqual(
|
|
installer._get_windows_runtime_dll_dirs(),
|
|
[r"C:\stub\torch\lib"],
|
|
)
|
|
|
|
def test_windows_long_path_dll_fallback_uses_path(self):
|
|
installer = load_module_from_file(
|
|
"AILab_LlamaCppInstaller.py",
|
|
"thinkingllm_llama_installer_test",
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
original_path = os.environ.get("PATH", "")
|
|
|
|
def raise_long_path_error(path):
|
|
error = FileNotFoundError(f"[WinError 206] path too long: {path}")
|
|
error.winerror = 206
|
|
raise error
|
|
|
|
with mock.patch.object(installer.platform, "system", return_value="Windows"):
|
|
with mock.patch.object(installer.os, "add_dll_directory", side_effect=raise_long_path_error):
|
|
with mock.patch.dict(installer.os.environ, {"PATH": original_path}, clear=False):
|
|
with installer._relax_windows_dll_directory_for_long_paths():
|
|
handle = installer.os.add_dll_directory(temp_dir)
|
|
self.assertTrue(installer.os.environ["PATH"].startswith(temp_dir))
|
|
|
|
self.assertFalse(installer.os.environ["PATH"].startswith(temp_dir + ";"))
|
|
self.assertTrue(hasattr(handle, "close"))
|
|
|
|
|
|
class TestGGUFWindllRuntimeFallback(unittest.TestCase):
|
|
def test_gguf_prompt_enhancer_reuses_catalog_model_from_extra_llm_path_without_download(self):
|
|
calls = []
|
|
|
|
class DummyLlama:
|
|
def __init__(self, **kwargs):
|
|
calls.append(kwargs.get("model_path"))
|
|
|
|
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": []},
|
|
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" / "Qwen" / "Qwen3-4B-GGUF"
|
|
shared_model_dir.mkdir(parents=True)
|
|
model_filename = "Qwen3-4B-Q4_K_M.gguf"
|
|
(shared_model_dir / model_filename).write_bytes(b"GGUF")
|
|
|
|
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,
|
|
}
|
|
)
|
|
|
|
with mock.patch.dict(sys.modules, stubs, clear=False):
|
|
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.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(
|
|
module,
|
|
"download_hf_file_to_path",
|
|
side_effect=AssertionError("download should not be called for shared local GGUF"),
|
|
), mock.patch.object(
|
|
module,
|
|
"read_gguf_architecture",
|
|
return_value="qwen3",
|
|
):
|
|
loader._load_model(
|
|
model_name=model_name,
|
|
device="cpu",
|
|
enable_thinking=True,
|
|
unique_id=None,
|
|
context_length_override=128,
|
|
quiet=True,
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertTrue(str(calls[0]).endswith(model_filename))
|
|
|
|
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_loader_reuses_google_gemma_alias_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",
|
|
Gemma4ChatHandler=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": ["Gemma4ChatHandler"],
|
|
},
|
|
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" / "unsloth" / "gemma-4-E4B-it-GGUF"
|
|
shared_model_dir.mkdir(parents=True)
|
|
local_model_filename = "gemma-4-E4B-it-Q8_0.gguf"
|
|
local_mmproj_filename = "mmproj-F16.gguf"
|
|
(shared_model_dir / local_model_filename).write_bytes(b"GGUF")
|
|
(shared_model_dir / local_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_google_gemma_alias_reuse_test",
|
|
)
|
|
|
|
model_name = "google_gemma-4-E4B-it-Q8_0.gguf [~8GB]"
|
|
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 alias"),
|
|
), mock.patch.object(
|
|
module,
|
|
"_pick_device",
|
|
return_value="cpu",
|
|
), mock.patch.object(
|
|
module,
|
|
"read_gguf_architecture",
|
|
return_value="gemma4",
|
|
):
|
|
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((local_mmproj_filename, "mmproj-google_gemma-4-E4B-it-f16.gguf")))
|
|
self.assertEqual(calls[1][0], "llama")
|
|
self.assertTrue(str(calls[1][1]).endswith((local_model_filename, "google_gemma-4-E4B-it-Q8_0.gguf")))
|
|
|
|
def test_gguf_prompt_enhancer_reuses_google_gemma_alias_from_extra_llm_path_without_download(self):
|
|
calls = []
|
|
|
|
class DummyLlama:
|
|
def __init__(self, **kwargs):
|
|
calls.append(kwargs.get("model_path"))
|
|
|
|
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": []},
|
|
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" / "unsloth" / "gemma-4-E4B-it-GGUF"
|
|
shared_model_dir.mkdir(parents=True)
|
|
local_model_filename = "gemma-4-E4B-it-Q8_0.gguf"
|
|
(shared_model_dir / local_model_filename).write_bytes(b"GGUF")
|
|
|
|
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,
|
|
}
|
|
)
|
|
|
|
with mock.patch.dict(sys.modules, stubs, clear=False):
|
|
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()
|
|
|
|
with mock.patch.dict(sys.modules, stubs, clear=False), mock.patch.object(
|
|
module,
|
|
"download_hf_file_to_path",
|
|
side_effect=AssertionError("download should not be called for shared local GGUF alias"),
|
|
), mock.patch.object(
|
|
module,
|
|
"read_gguf_architecture",
|
|
return_value="gemma4",
|
|
):
|
|
loader._load_model(
|
|
model_name=model_name,
|
|
device="cpu",
|
|
enable_thinking=True,
|
|
unique_id=None,
|
|
context_length_override=128,
|
|
quiet=True,
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertTrue(str(calls[0]).endswith(local_model_filename))
|
|
|
|
def test_gguf_runtime_reuses_windows_dll_relaxation_for_handler_and_llama(self):
|
|
enter_events = []
|
|
|
|
@contextlib.contextmanager
|
|
def tracking_relaxation():
|
|
enter_events.append("enter")
|
|
try:
|
|
yield
|
|
finally:
|
|
enter_events.append("exit")
|
|
|
|
class Qwen3VLChatHandler:
|
|
def __init__(self, **kwargs):
|
|
enter_events.append(("chat", sorted(kwargs)))
|
|
|
|
class DummyLlama:
|
|
def __init__(self, **kwargs):
|
|
enter_events.append(("llama", sorted(kwargs)))
|
|
|
|
chat_format_module = build_stub_module(
|
|
"llama_cpp.llama_chat_format",
|
|
Qwen3VLChatHandler=Qwen3VLChatHandler,
|
|
)
|
|
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": ["Qwen3VLChatHandler"]},
|
|
relax_windows_dll_directory_for_long_paths=tracking_relaxation,
|
|
)
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.update(
|
|
{
|
|
"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 tempfile.TemporaryDirectory() as temp_dir:
|
|
model_path = Path(temp_dir) / "model.gguf"
|
|
mmproj_path = Path(temp_dir) / "mmproj.gguf"
|
|
model_path.write_bytes(b"GGUF")
|
|
mmproj_path.write_bytes(b"mmproj")
|
|
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gguf_runtime_dll_test",
|
|
)
|
|
loader = module.QwenVLGGUFBase()
|
|
resolved = module.GGUFVLResolved(
|
|
display_name="stub",
|
|
repo_id=None,
|
|
alt_repo_ids=[],
|
|
author=None,
|
|
repo_dirname="stub",
|
|
model_filename=str(model_path),
|
|
mmproj_filename=str(mmproj_path),
|
|
context_length=4096,
|
|
image_max_tokens=1024,
|
|
n_batch=64,
|
|
gpu_layers=0,
|
|
top_k=20,
|
|
pool_size=1024,
|
|
)
|
|
|
|
with mock.patch.object(module, "_resolve_model_entry", return_value=resolved):
|
|
with mock.patch.object(module, "_pick_device", return_value="cpu"):
|
|
with mock.patch.object(module, "read_gguf_architecture", return_value="qwen3"):
|
|
loader._load_model(
|
|
model_name="stub",
|
|
device="cpu",
|
|
ctx=None,
|
|
n_batch=None,
|
|
n_ubatch=None,
|
|
gpu_layers=None,
|
|
image_max_tokens=None,
|
|
top_k=None,
|
|
pool_size=None,
|
|
n_threads=None,
|
|
n_threads_batch=None,
|
|
flash_attn=False,
|
|
offload_kqv=False,
|
|
ctx_checkpoints=None,
|
|
enable_thinking=True,
|
|
unique_id=None,
|
|
)
|
|
|
|
self.assertEqual(enter_events[0:3], [
|
|
"enter",
|
|
("chat", ["clip_model_path", "force_reasoning", "image_max_tokens", "image_min_tokens", "verbose"]),
|
|
"exit",
|
|
])
|
|
self.assertEqual(enter_events[3], "enter")
|
|
self.assertEqual(enter_events[5], "exit")
|
|
self.assertEqual(enter_events[4][0], "llama")
|
|
self.assertTrue(
|
|
{
|
|
"chat_handler",
|
|
"model_path",
|
|
"n_ctx",
|
|
"image_max_tokens",
|
|
"image_min_tokens",
|
|
}.issubset(set(enter_events[4][1]))
|
|
)
|
|
|
|
def test_gemma4_chat_handler_does_not_receive_qwen_force_reasoning_kwarg(self):
|
|
captured_chat_kwargs = []
|
|
|
|
Gemma4ChatHandler = type(
|
|
"Gemma4ChatHandler",
|
|
(),
|
|
{"__init__": lambda self, **kwargs: captured_chat_kwargs.append(dict(kwargs))},
|
|
)
|
|
|
|
class DummyLlama:
|
|
def __init__(self, **kwargs):
|
|
pass
|
|
|
|
chat_format_module = build_stub_module(
|
|
"llama_cpp.llama_chat_format",
|
|
Gemma4ChatHandler=Gemma4ChatHandler,
|
|
)
|
|
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": ["Gemma4ChatHandler"]},
|
|
relax_windows_dll_directory_for_long_paths=contextlib.nullcontext,
|
|
)
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.update(
|
|
{
|
|
"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 tempfile.TemporaryDirectory() as temp_dir:
|
|
model_path = Path(temp_dir) / "gemma.gguf"
|
|
mmproj_path = Path(temp_dir) / "mmproj.gguf"
|
|
model_path.write_bytes(b"GGUF")
|
|
mmproj_path.write_bytes(b"mmproj")
|
|
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gemma4_handler_kwargs_test",
|
|
)
|
|
loader = module.QwenVLGGUFBase()
|
|
resolved = module.GGUFVLResolved(
|
|
display_name="gemma",
|
|
repo_id=None,
|
|
alt_repo_ids=[],
|
|
author=None,
|
|
repo_dirname="gemma",
|
|
model_filename=str(model_path),
|
|
mmproj_filename=str(mmproj_path),
|
|
context_length=4096,
|
|
image_max_tokens=1024,
|
|
n_batch=64,
|
|
gpu_layers=0,
|
|
top_k=20,
|
|
pool_size=1024,
|
|
)
|
|
|
|
with mock.patch.object(module, "_resolve_model_entry", return_value=resolved):
|
|
with mock.patch.object(module, "_pick_device", return_value="cpu"):
|
|
with mock.patch.object(module, "read_gguf_architecture", return_value="gemma4"):
|
|
loader._load_model(
|
|
model_name="google_gemma-4-E4B-it-Q8_0.gguf",
|
|
device="cpu",
|
|
ctx=None,
|
|
n_batch=None,
|
|
n_ubatch=None,
|
|
gpu_layers=None,
|
|
image_max_tokens=None,
|
|
top_k=None,
|
|
pool_size=None,
|
|
n_threads=None,
|
|
n_threads_batch=None,
|
|
flash_attn=False,
|
|
offload_kqv=False,
|
|
ctx_checkpoints=None,
|
|
enable_thinking=True,
|
|
unique_id=None,
|
|
)
|
|
|
|
self.assertEqual(len(captured_chat_kwargs), 1)
|
|
self.assertNotIn("force_reasoning", captured_chat_kwargs[0])
|
|
self.assertIn("clip_model_path", captured_chat_kwargs[0])
|
|
|
|
def test_gguf_loader_passes_current_cuda_device_as_llama_main_gpu(self):
|
|
captured_llama_kwargs = []
|
|
|
|
class DummyLlama:
|
|
def __init__(self, **kwargs):
|
|
captured_llama_kwargs.append(dict(kwargs))
|
|
|
|
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": []},
|
|
relax_windows_dll_directory_for_long_paths=contextlib.nullcontext,
|
|
)
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules["AILab_LlamaCppInstaller"] = installer_module
|
|
stub_modules["comfy.model_management"].get_torch_device = lambda: types.SimpleNamespace(type="cuda", index=0)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
model_path = Path(temp_dir) / "model.gguf"
|
|
model_path.write_bytes(b"GGUF")
|
|
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gguf_main_gpu_test",
|
|
)
|
|
loader = module.QwenVLGGUFBase()
|
|
resolved = module.GGUFVLResolved(
|
|
display_name="model",
|
|
repo_id=None,
|
|
alt_repo_ids=[],
|
|
author=None,
|
|
repo_dirname="model",
|
|
model_filename=str(model_path),
|
|
mmproj_filename=None,
|
|
context_length=4096,
|
|
image_max_tokens=1024,
|
|
n_batch=64,
|
|
gpu_layers=-1,
|
|
top_k=20,
|
|
pool_size=1024,
|
|
)
|
|
|
|
with mock.patch.object(module, "_resolve_model_entry", return_value=resolved):
|
|
with mock.patch.object(module, "_pick_device", return_value="cuda"):
|
|
with mock.patch.object(module, "read_gguf_architecture", return_value="qwen"):
|
|
with mock.patch.object(module.torch.cuda, "is_available", return_value=True):
|
|
with mock.patch.object(module.torch.cuda, "current_device", return_value=1, create=True):
|
|
with mock.patch.object(module.torch.cuda, "synchronize", return_value=None, create=True):
|
|
with mock.patch.object(module.torch.cuda, "empty_cache", return_value=None, create=True):
|
|
with mock.patch.object(module.torch.cuda, "ipc_collect", return_value=None, create=True):
|
|
loader._load_model(
|
|
model_name="model.gguf",
|
|
device="cuda",
|
|
ctx=None,
|
|
n_batch=None,
|
|
n_ubatch=None,
|
|
gpu_layers=-1,
|
|
image_max_tokens=None,
|
|
top_k=None,
|
|
pool_size=None,
|
|
n_threads=None,
|
|
n_threads_batch=None,
|
|
flash_attn=False,
|
|
offload_kqv=False,
|
|
ctx_checkpoints=None,
|
|
enable_thinking=True,
|
|
unique_id=None,
|
|
)
|
|
|
|
self.assertEqual(captured_llama_kwargs[0]["main_gpu"], 0)
|
|
self.assertIn("main_gpu=0", loader.last_backend_trace)
|
|
|
|
def test_gemma4_multimodal_load_uses_compatible_image_ubatch_settings(self):
|
|
captured_chat_kwargs = []
|
|
captured_llama_kwargs = []
|
|
|
|
Gemma4ChatHandler = type(
|
|
"Gemma4ChatHandler",
|
|
(),
|
|
{"__init__": lambda self, **kwargs: captured_chat_kwargs.append(dict(kwargs))},
|
|
)
|
|
|
|
class DummyLlama:
|
|
def __init__(self, **kwargs):
|
|
captured_llama_kwargs.append(dict(kwargs))
|
|
|
|
chat_format_module = build_stub_module(
|
|
"llama_cpp.llama_chat_format",
|
|
Gemma4ChatHandler=Gemma4ChatHandler,
|
|
)
|
|
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": False, "vision_handlers": ["Gemma4ChatHandler"]},
|
|
relax_windows_dll_directory_for_long_paths=contextlib.nullcontext,
|
|
)
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.update(
|
|
{
|
|
"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 tempfile.TemporaryDirectory() as temp_dir:
|
|
model_path = Path(temp_dir) / "google_gemma-4-E4B-it-Q8_0.gguf"
|
|
mmproj_path = Path(temp_dir) / "mmproj-F16.gguf"
|
|
model_path.write_bytes(b"GGUF")
|
|
mmproj_path.write_bytes(b"mmproj")
|
|
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gemma4_cpu_load_downgrade_test",
|
|
)
|
|
loader = module.QwenVLGGUFBase()
|
|
resolved = module.GGUFVLResolved(
|
|
display_name="gemma",
|
|
repo_id=None,
|
|
alt_repo_ids=[],
|
|
author=None,
|
|
repo_dirname="gemma",
|
|
model_filename=str(model_path),
|
|
mmproj_filename=str(mmproj_path),
|
|
context_length=131072,
|
|
image_max_tokens=4096,
|
|
n_batch=512,
|
|
gpu_layers=-1,
|
|
top_k=64,
|
|
pool_size=4194304,
|
|
)
|
|
|
|
with mock.patch.object(module, "_resolve_model_entry", return_value=resolved):
|
|
with mock.patch.object(module, "_pick_device", return_value="cuda"):
|
|
with mock.patch.object(module, "read_gguf_architecture", return_value="gemma4"):
|
|
loader._load_model(
|
|
model_name="google_gemma-4-E4B-it-Q8_0.gguf",
|
|
device="cuda",
|
|
ctx=None,
|
|
n_batch=None,
|
|
n_ubatch=None,
|
|
gpu_layers=-1,
|
|
image_max_tokens=None,
|
|
top_k=None,
|
|
pool_size=None,
|
|
n_threads=None,
|
|
n_threads_batch=None,
|
|
flash_attn=False,
|
|
offload_kqv=False,
|
|
ctx_checkpoints=None,
|
|
enable_thinking=True,
|
|
unique_id=None,
|
|
)
|
|
|
|
self.assertEqual(captured_chat_kwargs[0]["image_max_tokens"], 1120)
|
|
self.assertEqual(captured_llama_kwargs[0]["n_ctx"], 32768)
|
|
self.assertEqual(captured_llama_kwargs[0]["n_batch"], 1120)
|
|
self.assertEqual(captured_llama_kwargs[0]["n_ubatch"], 1120)
|
|
self.assertEqual(captured_llama_kwargs[0]["image_max_tokens"], 1120)
|
|
self.assertEqual(loader.current_context_length, 32768)
|
|
self.assertEqual(loader.current_batch_size, 1120)
|
|
self.assertEqual(loader.current_image_token_budget, 1120)
|
|
self.assertIn("applied Gemma 4 multimodal compatibility settings", loader.last_backend_trace)
|
|
|
|
def test_gemma4_gpu_multimodal_load_clamps_crash_log_settings(self):
|
|
captured_chat_kwargs = []
|
|
captured_llama_kwargs = []
|
|
|
|
Gemma4ChatHandler = type(
|
|
"Gemma4ChatHandler",
|
|
(),
|
|
{"__init__": lambda self, **kwargs: captured_chat_kwargs.append(dict(kwargs))},
|
|
)
|
|
|
|
class DummyLlama:
|
|
def __init__(self, **kwargs):
|
|
captured_llama_kwargs.append(dict(kwargs))
|
|
|
|
chat_format_module = build_stub_module(
|
|
"llama_cpp.llama_chat_format",
|
|
Gemma4ChatHandler=Gemma4ChatHandler,
|
|
)
|
|
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": ["Gemma4ChatHandler"]},
|
|
relax_windows_dll_directory_for_long_paths=contextlib.nullcontext,
|
|
)
|
|
stub_modules = build_loader_test_stubs()
|
|
stub_modules.update(
|
|
{
|
|
"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 tempfile.TemporaryDirectory() as temp_dir:
|
|
model_path = Path(temp_dir) / "google_gemma-4-E4B-it-Q8_0.gguf"
|
|
mmproj_path = Path(temp_dir) / "mmproj-F16.gguf"
|
|
model_path.write_bytes(b"GGUF")
|
|
mmproj_path.write_bytes(b"mmproj")
|
|
|
|
with mock.patch.dict(sys.modules, stub_modules, clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gemma4_gpu_load_downgrade_test",
|
|
)
|
|
loader = module.QwenVLGGUFBase()
|
|
resolved = module.GGUFVLResolved(
|
|
display_name="gemma",
|
|
repo_id=None,
|
|
alt_repo_ids=[],
|
|
author=None,
|
|
repo_dirname="gemma",
|
|
model_filename=str(model_path),
|
|
mmproj_filename=str(mmproj_path),
|
|
context_length=131072,
|
|
image_max_tokens=4096,
|
|
n_batch=512,
|
|
gpu_layers=-1,
|
|
top_k=64,
|
|
pool_size=4194304,
|
|
)
|
|
|
|
with mock.patch.object(module, "_resolve_model_entry", return_value=resolved):
|
|
with mock.patch.object(module, "_pick_device", return_value="cuda"):
|
|
with mock.patch.object(module, "read_gguf_architecture", return_value="gemma4"):
|
|
loader._load_model(
|
|
model_name="google_gemma-4-E4B-it-Q8_0.gguf",
|
|
device="cuda",
|
|
ctx=None,
|
|
n_batch=None,
|
|
n_ubatch=None,
|
|
gpu_layers=-1,
|
|
image_max_tokens=None,
|
|
top_k=None,
|
|
pool_size=None,
|
|
n_threads=None,
|
|
n_threads_batch=None,
|
|
flash_attn=True,
|
|
offload_kqv=True,
|
|
ctx_checkpoints=0,
|
|
enable_thinking=True,
|
|
unique_id=None,
|
|
)
|
|
|
|
self.assertEqual(captured_chat_kwargs[0]["image_max_tokens"], 1120)
|
|
self.assertEqual(captured_llama_kwargs[0]["n_ctx"], 32768)
|
|
self.assertEqual(captured_llama_kwargs[0]["n_batch"], 1120)
|
|
self.assertEqual(captured_llama_kwargs[0]["n_ubatch"], 1120)
|
|
self.assertEqual(captured_llama_kwargs[0]["image_max_tokens"], 1120)
|
|
|
|
def test_gemma4_cpu_only_multimodal_guard_blocks_native_abort_risk(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gemma4_cpu_abort_guard_test",
|
|
)
|
|
|
|
loader = module.QwenVLGGUFBase()
|
|
loader.chat_handler = object()
|
|
loader.gguf_arch = "gemma4"
|
|
loader.current_backend_gpu_offload = False
|
|
loader.current_context_length = 131072
|
|
loader.current_batch_size = 512
|
|
loader.current_image_token_budget = 4096
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "Gemma 4 GGUF multimodal generation was blocked"):
|
|
loader._invoke(
|
|
system_prompt="You are helpful.",
|
|
user_prompt="Describe this image.",
|
|
images_b64=["BASE64PNG"],
|
|
audio_b64=[],
|
|
max_tokens=256,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
repetition_penalty=1.0,
|
|
seed=1,
|
|
model_name="google_gemma-4-E4B-it-Q8_0.gguf",
|
|
stream_to_terminal=False,
|
|
enable_thinking=False,
|
|
auto_finalization_retry=False,
|
|
top_k=64,
|
|
)
|
|
|
|
def test_gemma4_audio_guard_blocks_unsupported_native_audio_path(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gemma4_audio_abort_guard_test",
|
|
)
|
|
|
|
loader = module.QwenVLGGUFBase()
|
|
loader.chat_handler = object()
|
|
loader.gguf_arch = "gemma4"
|
|
loader.current_backend_gpu_offload = True
|
|
loader.current_context_length = 32768
|
|
loader.current_batch_size = 1120
|
|
loader.current_ubatch_size = 1120
|
|
loader.current_image_token_budget = 1120
|
|
loader.current_backend_info = {"version": "0.3.35"}
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "requires llama-cpp-python >=0.3.36"):
|
|
loader._invoke(
|
|
system_prompt="You are helpful.",
|
|
user_prompt="Transcribe this audio.",
|
|
images_b64=[],
|
|
audio_b64=["BASE64WAV"],
|
|
max_tokens=256,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
repetition_penalty=1.0,
|
|
seed=1,
|
|
model_name="google_gemma-4-E4B-it-Q8_0.gguf",
|
|
stream_to_terminal=False,
|
|
enable_thinking=False,
|
|
auto_finalization_retry=False,
|
|
top_k=64,
|
|
)
|
|
|
|
def test_gemma4_audio_runs_when_backend_and_mmproj_report_support(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gemma4_audio_supported_backend_test",
|
|
)
|
|
|
|
captured_messages = []
|
|
|
|
class DummyChatHandler:
|
|
def __init__(self):
|
|
self.is_support_audio = None
|
|
self.initialized_with = None
|
|
|
|
def _init_mtmd_context(self, llama):
|
|
self.initialized_with = llama
|
|
self.is_support_audio = True
|
|
|
|
class DummyLlama:
|
|
def reset(self):
|
|
return None
|
|
|
|
def create_chat_completion(self, **kwargs):
|
|
captured_messages.append(kwargs["messages"])
|
|
return iter([{"choices": [{"delta": {"content": "audio understood"}}]}])
|
|
|
|
loader = module.QwenVLGGUFBase()
|
|
loader.llm = DummyLlama()
|
|
loader.chat_handler = DummyChatHandler()
|
|
loader.gguf_arch = "gemma4"
|
|
loader.current_backend_info = {"version": "0.3.36+cu130"}
|
|
loader.current_context_length = 32768
|
|
loader.current_batch_size = 1120
|
|
loader.current_ubatch_size = 1120
|
|
loader.current_image_token_budget = 1120
|
|
|
|
text, _raw_trace = loader._invoke(
|
|
system_prompt="You are helpful.",
|
|
user_prompt="Transcribe this audio.",
|
|
images_b64=[],
|
|
audio_b64=["BASE64WAV"],
|
|
max_tokens=256,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
repetition_penalty=1.0,
|
|
seed=1,
|
|
model_name="google_gemma-4-E4B-it-Q8_0.gguf",
|
|
stream_to_terminal=False,
|
|
enable_thinking=False,
|
|
auto_finalization_retry=False,
|
|
top_k=64,
|
|
)
|
|
|
|
self.assertEqual(text, "audio understood")
|
|
self.assertIs(loader.chat_handler.initialized_with, loader.llm)
|
|
audio_item = captured_messages[0][1]["content"][1]
|
|
self.assertEqual(audio_item["type"], "input_audio")
|
|
self.assertEqual(audio_item["input_audio"], {"data": "BASE64WAV", "format": "wav"})
|
|
|
|
def test_gemma4_audio_guard_blocks_mmproj_without_audio_support(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gemma4_audio_unsupported_mmproj_test",
|
|
)
|
|
|
|
class DummyChatHandler:
|
|
is_support_audio = None
|
|
|
|
def _init_mtmd_context(self, _llama):
|
|
self.is_support_audio = False
|
|
|
|
loader = module.QwenVLGGUFBase()
|
|
loader.llm = object()
|
|
loader.chat_handler = DummyChatHandler()
|
|
loader.gguf_arch = "gemma4"
|
|
loader.current_backend_info = {"version": "0.3.36"}
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "does not report audio support"):
|
|
loader._raise_if_unsafe_native_multimodal_path([], ["BASE64WAV"])
|
|
|
|
def test_gguf_thinking_toggle_updates_without_reload_signature(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gguf_thinking_toggle_test",
|
|
)
|
|
source = read_source("AILab_QwenVL_GGUF.py")
|
|
self.assertNotIn("ctx_checkpoints_val,\n bool(enable_thinking),", source)
|
|
|
|
captured_kwargs = []
|
|
|
|
class DummyLlama:
|
|
def __init__(self):
|
|
self.chat_template_kwargs = {"enable_thinking": True}
|
|
|
|
def create_chat_completion(self, **kwargs):
|
|
captured_kwargs.append(kwargs)
|
|
return {"choices": [{"message": {"content": "ok"}}]}
|
|
|
|
loader = module.QwenVLGGUFBase()
|
|
loader.llm = DummyLlama()
|
|
loader.uses_qwen_template_thinking = True
|
|
loader._create_chat_completion(
|
|
enable_thinking=False,
|
|
messages=[],
|
|
max_tokens=1,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
stream=False,
|
|
)
|
|
|
|
self.assertEqual(loader.llm.chat_template_kwargs["enable_thinking"], False)
|
|
self.assertEqual(captured_kwargs[0]["chat_template_kwargs"], {"enable_thinking": False})
|
|
|
|
def test_create_chat_completion_retries_without_unexpected_kwarg(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gguf_unexpected_kwarg_retry_test",
|
|
)
|
|
|
|
captured_kwargs = []
|
|
|
|
class DummyLlama:
|
|
def create_chat_completion(self, **kwargs):
|
|
captured_kwargs.append(dict(kwargs))
|
|
if "repeat_last_n" in kwargs:
|
|
raise TypeError("Llama.create_chat_completion() got an unexpected keyword argument 'repeat_last_n'")
|
|
return {"choices": [{"message": {"content": "ok"}}]}
|
|
|
|
loader = module.QwenVLGGUFBase()
|
|
loader.llm = DummyLlama()
|
|
loader.uses_qwen_template_thinking = False
|
|
|
|
result = loader._create_chat_completion(
|
|
enable_thinking=False,
|
|
messages=[],
|
|
max_tokens=1,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
stream=True,
|
|
repeat_last_n=48,
|
|
)
|
|
|
|
self.assertEqual(result["choices"][0]["message"]["content"], "ok")
|
|
self.assertIn("repeat_last_n", captured_kwargs[0])
|
|
self.assertNotIn("repeat_last_n", captured_kwargs[1])
|
|
|
|
def test_gguf_invoke_resets_before_each_completion_call(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gguf_reset_before_completion_test",
|
|
)
|
|
|
|
class DummyLlama:
|
|
def __init__(self):
|
|
self.events = []
|
|
self.reset_count = 0
|
|
|
|
def reset(self):
|
|
self.events.append("reset")
|
|
self.reset_count += 1
|
|
|
|
def create_chat_completion(self, **kwargs):
|
|
self.events.append("completion")
|
|
return iter([
|
|
{"choices": [{"delta": {"content": "ok"}}]},
|
|
])
|
|
|
|
loader = module.QwenVLGGUFBase()
|
|
loader.llm = DummyLlama()
|
|
|
|
text, raw_trace = loader._invoke(
|
|
system_prompt="system",
|
|
user_prompt="user",
|
|
images_b64=[],
|
|
audio_b64=[],
|
|
max_tokens=1,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
repetition_penalty=1.0,
|
|
seed=1,
|
|
stream_to_terminal=False,
|
|
enable_thinking=False,
|
|
)
|
|
|
|
self.assertEqual(text, "ok")
|
|
self.assertIn("ok", raw_trace)
|
|
self.assertEqual(loader.llm.reset_count, 1)
|
|
self.assertEqual(loader.llm.events, ["reset", "completion"])
|
|
|
|
def test_gguf_invoke_returns_content_without_reasoning_drafts(self):
|
|
with mock.patch.dict(sys.modules, build_loader_test_stubs(), clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gguf_separate_reasoning_content_test",
|
|
)
|
|
|
|
cleaner = load_module_from_file(
|
|
"AILab_OutputCleaner.py",
|
|
"thinkingllm_output_cleaner_invoke_test",
|
|
)
|
|
stream_helpers = load_module_from_file(
|
|
"AILab_StreamDisplay.py",
|
|
"thinkingllm_stream_helpers_invoke_test",
|
|
)
|
|
module.OutputCleanConfig = cleaner.OutputCleanConfig
|
|
module.clean_model_output = cleaner.clean_model_output
|
|
module.extract_stream_token = stream_helpers.extract_stream_token
|
|
module.compose_streamed_model_output = stream_helpers.compose_streamed_model_output
|
|
|
|
final_prompt = (
|
|
"integrated_multimodal_description: [Shot 1] The actual shot.\n\n"
|
|
"overall_soundscape: N/A\n\n"
|
|
"non_diegetic_music: N/A"
|
|
)
|
|
|
|
class DummyLlama:
|
|
def reset(self):
|
|
pass
|
|
|
|
def create_chat_completion(self, **kwargs):
|
|
return iter([
|
|
{"choices": [{"delta": {"reasoning_content": (
|
|
"integrated_multimodal_description: obsolete internal draft. "
|
|
"Wait, I need to reconsider it."
|
|
)}}]},
|
|
{"choices": [{"delta": {"content": final_prompt}}]},
|
|
])
|
|
|
|
loader = module.QwenVLGGUFBase()
|
|
loader.llm = DummyLlama()
|
|
|
|
text, raw_trace = loader._invoke(
|
|
system_prompt="system",
|
|
user_prompt="user",
|
|
images_b64=[],
|
|
audio_b64=[],
|
|
max_tokens=256,
|
|
temperature=0.0,
|
|
top_p=1.0,
|
|
repetition_penalty=1.0,
|
|
seed=1,
|
|
stream_to_terminal=False,
|
|
enable_thinking=True,
|
|
)
|
|
|
|
self.assertEqual(text, final_prompt)
|
|
self.assertNotIn("obsolete internal draft", text)
|
|
self.assertIn("obsolete internal draft", raw_trace)
|
|
self.assertIn(final_prompt, raw_trace)
|
|
|
|
def test_gguf_video_prompt_enhancer_retries_unfinished_thinking_with_schema_prefill(self):
|
|
stubs = build_loader_test_stubs()
|
|
stubs["AILab_QwenVL_GGUF"] = build_stub_module(
|
|
"AILab_QwenVL_GGUF",
|
|
construct_llama_safely=lambda *args, **kwargs: None,
|
|
read_gguf_architecture=lambda *args, **kwargs: "qwen35",
|
|
register_active_gguf_loader=lambda *args, **kwargs: None,
|
|
release_other_gguf_loaders=lambda *args, **kwargs: None,
|
|
_filter_kwargs_for_callable=lambda func, kwargs: kwargs,
|
|
_find_existing_local_file_in_dirs=lambda *args, **kwargs: None,
|
|
_gguf_search_dirs=lambda *args, **kwargs: [],
|
|
)
|
|
with mock.patch.dict(sys.modules, stubs, clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
"thinkingllm_gguf_video_direct_retry_test",
|
|
)
|
|
|
|
cleaner = load_module_from_file(
|
|
"AILab_OutputCleaner.py",
|
|
"thinkingllm_output_cleaner_video_retry_test",
|
|
)
|
|
stream_helpers = load_module_from_file(
|
|
"AILab_StreamDisplay.py",
|
|
"thinkingllm_stream_helpers_video_retry_test",
|
|
)
|
|
module.OutputCleanConfig = cleaner.OutputCleanConfig
|
|
module.clean_model_output = cleaner.clean_model_output
|
|
module.extract_stream_token = stream_helpers.extract_stream_token
|
|
module.compose_streamed_model_output = stream_helpers.compose_streamed_model_output
|
|
module.resolve_video_duration = lambda preset, duration: {
|
|
"provider": "minimax_h3",
|
|
"video_mode": "t2va",
|
|
} if "MiniMax" in preset else None
|
|
module.apply_qwen_soft_thinking_directive = lambda text, *args, **kwargs: text
|
|
|
|
class DummyLlama:
|
|
def __init__(self):
|
|
self.calls = []
|
|
|
|
def reset(self):
|
|
pass
|
|
|
|
def create_chat_completion(self, **kwargs):
|
|
self.calls.append(kwargs)
|
|
if len(self.calls) == 1:
|
|
return iter([{
|
|
"choices": [{"delta": {"content": (
|
|
"<think>Thinking Process:\n\n"
|
|
"integrated_multimodal_description: obsolete draft\n\n"
|
|
"I need to continue"
|
|
)}}]
|
|
}])
|
|
return iter([{
|
|
"choices": [{"delta": {"content": (
|
|
"<think>\n</think>\n\n"
|
|
"integrated_multimodal_description: [Shot 1] The actual shot.\n\n"
|
|
"overall_soundscape: N/A\n\n"
|
|
"non_diegetic_music: N/A"
|
|
)}}]
|
|
}])
|
|
|
|
loader = module.ThinkingLLM_QwenVL_GGUF_PromptEnhancer()
|
|
loader.llm = DummyLlama()
|
|
loader.supports_qwen_soft_think = True
|
|
|
|
text, raw_trace = loader._invoke_llama(
|
|
system_prompt="system",
|
|
user_prompt="original request",
|
|
max_tokens=1024,
|
|
temperature=0.5,
|
|
top_p=0.9,
|
|
repetition_penalty=1.1,
|
|
seed=1,
|
|
stream_to_terminal=False,
|
|
enable_thinking=False,
|
|
auto_finalization_retry=False,
|
|
preset_name="🎬 MiniMax H3 Text-to-Video",
|
|
)
|
|
|
|
self.assertEqual(len(loader.llm.calls), 2)
|
|
self.assertTrue(text.startswith("integrated_multimodal_description: [Shot 1]"))
|
|
self.assertEqual(text.count("integrated_multimodal_description:"), 1)
|
|
self.assertNotIn("obsolete draft", text)
|
|
self.assertIn("FINALIZATION ATTEMPT 2/3", raw_trace)
|
|
self.assertEqual(loader.llm.calls[1]["messages"][-1], {
|
|
"role": "assistant",
|
|
"content": "integrated_multimodal_description:",
|
|
})
|
|
|
|
def test_gguf_video_prompt_acceptance_is_not_a_schema_gate(self):
|
|
stubs = build_loader_test_stubs()
|
|
stubs["AILab_QwenVL_GGUF"] = build_stub_module(
|
|
"AILab_QwenVL_GGUF",
|
|
construct_llama_safely=lambda *args, **kwargs: None,
|
|
read_gguf_architecture=lambda *args, **kwargs: "qwen35",
|
|
register_active_gguf_loader=lambda *args, **kwargs: None,
|
|
release_other_gguf_loaders=lambda *args, **kwargs: None,
|
|
_filter_kwargs_for_callable=lambda func, kwargs: kwargs,
|
|
_find_existing_local_file_in_dirs=lambda *args, **kwargs: None,
|
|
_gguf_search_dirs=lambda *args, **kwargs: [],
|
|
)
|
|
with mock.patch.dict(sys.modules, stubs, clear=False):
|
|
module = load_module_from_file(
|
|
"AILab_QwenVL_GGUF_PromptEnhancer.py",
|
|
"thinkingllm_gguf_video_no_schema_gate_test",
|
|
)
|
|
|
|
module.resolve_video_duration = lambda preset, duration: {
|
|
"provider": "minimax_h3",
|
|
"video_mode": "t2va",
|
|
}
|
|
self.assertTrue(module._preset_output_is_usable(
|
|
"integrated_multimodal_description: A usable prompt with a minor missing optional section.",
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
))
|
|
self.assertFalse(module._preset_output_is_usable(
|
|
"",
|
|
"🎬 MiniMax H3 Text-to-Video",
|
|
))
|
|
|
|
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(
|
|
"AILab_QwenVL_GGUF.py",
|
|
"thinkingllm_gguf_reset_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))
|
|
if self.calls == 1:
|
|
return iter([
|
|
{"choices": [{"delta": {"reasoning_content": "hidden reasoning only"}}]},
|
|
])
|
|
return iter([
|
|
{"choices": [{"delta": {"content": "final answer"}}]},
|
|
])
|
|
|
|
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,
|
|
auto_finalization_retry=True,
|
|
)
|
|
|
|
self.assertEqual(text, "final answer")
|
|
self.assertIn("[STREAMING]", raw_trace)
|
|
self.assertIn("[FINALIZATION ATTEMPT 2/3]", raw_trace)
|
|
self.assertEqual(
|
|
loader.llm.events,
|
|
[("reset", 0), ("completion", 1), ("reset", 1), ("completion", 2)],
|
|
)
|
|
|
|
|
|
class _GlobalCheck(ast.NodeVisitor):
|
|
def __init__(self):
|
|
self.found = False
|
|
|
|
def visit_Global(self, node):
|
|
if "LAST_SAVED_PROMPT" in node.names:
|
|
self.found = True
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|