Add native OpenAI Decisions and OpenRouter Decisions API routing to the image and text Decision Router nodes while keeping local in-process llama.cpp inference as the default. Discover OpenRouter decision models by input modality and decision output capability, then enforce server profile image policy and optional model allowlists both in the UI model catalog and before inference. Keep credentials and endpoints in server-side profiles; apply profile input, image, and timeout limits; never fall back to chat-generated JSON. Preserve saved workflow widget order, release any retained local model when switching to API mode, and normalize typed route choices, probabilities, and confidence into decision_json. Add API setup documentation and adapter/router/security regression coverage. Validation: 240 tests passed across six test modules run individually in bundled ComfyUI Python 3.12; Python and JavaScript syntax checks, API catalog validation, version metadata checks, and git diff --check passed. Provider inference is covered with mocked responses; no authenticated live inference was run.
118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
"""
|
|
{
|
|
"name": "ComfyUI-ThinkingLLM",
|
|
"description": "A multimodal ComfyUI AI node pack with Qwen, Gemma 4, GGUF, Whisper, image and text decision routing, and live thinking output.",
|
|
"author": "goodguy1963",
|
|
"version": "3.0.2",
|
|
"url": "https://github.com/goodguy1963/ComfyUI-ThinkingLLM",
|
|
"category": "image"
|
|
}
|
|
"""
|
|
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
|
|
# Get the directory of the current script
|
|
current_dir = os.path.dirname(__file__)
|
|
sys.path.insert(0, current_dir)
|
|
|
|
NODE_CLASS_MAPPINGS = {}
|
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
|
COMMERCIAL_RELEASE = os.environ.get("THINKINGLLM_COMMERCIAL_RELEASE") == "1"
|
|
WEB_DIRECTORY = "./web_commercial" if COMMERCIAL_RELEASE else "./web"
|
|
COMMERCIAL_MODULES = {
|
|
"AILab_OutputCleaner",
|
|
"AILab_QwenVL",
|
|
"AILab_QwenVL_PromptEnhancer",
|
|
"AILab_StreamDisplay",
|
|
}
|
|
COMMERCIAL_NODE_MODULES = {
|
|
"decision_router",
|
|
"story_split_node",
|
|
"system_prompt_preset",
|
|
"vram_cleanup",
|
|
}
|
|
|
|
LEGACY_NODE_REPLACEMENTS = {
|
|
"AILab_QwenVL": "ThinkingLLM_QwenVL",
|
|
"AILab_QwenVL_Advanced": "ThinkingLLM_QwenVL_Advanced",
|
|
"AILab_QwenVL_GGUF": "ThinkingLLM_QwenVL_GGUF",
|
|
"AILab_QwenVL_GGUF_Advanced": "ThinkingLLM_QwenVL_GGUF_Advanced",
|
|
"AILab_QwenVL_PromptEnhancer": "ThinkingLLM_QwenVL_PromptEnhancer",
|
|
"AILab_QwenVL_GGUF_PromptEnhancer": "ThinkingLLM_QwenVL_GGUF_PromptEnhancer",
|
|
}
|
|
if COMMERCIAL_RELEASE:
|
|
LEGACY_NODE_REPLACEMENTS = {
|
|
old: new for old, new in LEGACY_NODE_REPLACEMENTS.items() if "GGUF" not in old
|
|
}
|
|
|
|
def load_modules_from_directory(directory):
|
|
for file in os.listdir(directory):
|
|
if file.endswith(".py"):
|
|
file_path = os.path.join(directory, file)
|
|
module_name = os.path.basename(file)[:-3]
|
|
if module_name == os.path.basename(__file__)[:-3]:
|
|
continue
|
|
if COMMERCIAL_RELEASE:
|
|
allowed = (
|
|
COMMERCIAL_MODULES
|
|
if os.path.abspath(directory) == os.path.abspath(current_dir)
|
|
else COMMERCIAL_NODE_MODULES
|
|
)
|
|
if module_name not in allowed:
|
|
continue
|
|
|
|
try:
|
|
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
if spec is None or spec.loader is None:
|
|
raise ImportError(f"Unable to create import spec for {module_name}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[module_name] = module
|
|
spec.loader.exec_module(module)
|
|
|
|
if hasattr(module, "NODE_CLASS_MAPPINGS"):
|
|
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
|
|
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"):
|
|
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
|
|
except Exception as e:
|
|
print(f"Error loading module {module_name}: {e}")
|
|
|
|
load_modules_from_directory(current_dir)
|
|
|
|
# Also load from nodes subdirectory
|
|
nodes_dir = os.path.join(current_dir, "nodes")
|
|
if os.path.exists(nodes_dir):
|
|
load_modules_from_directory(nodes_dir)
|
|
|
|
for legacy_node_name in LEGACY_NODE_REPLACEMENTS:
|
|
NODE_CLASS_MAPPINGS.pop(legacy_node_name, None)
|
|
NODE_DISPLAY_NAME_MAPPINGS.pop(legacy_node_name, None)
|
|
|
|
|
|
def _register_legacy_node_replacements():
|
|
try:
|
|
from comfy_api.latest._io import NodeReplace
|
|
from server import PromptServer
|
|
except Exception:
|
|
return
|
|
|
|
manager = getattr(getattr(PromptServer, "instance", None), "node_replace_manager", None)
|
|
if manager is None:
|
|
return
|
|
|
|
for old_node_id, new_node_id in LEGACY_NODE_REPLACEMENTS.items():
|
|
manager.register(NodeReplace(new_node_id=new_node_id, old_node_id=old_node_id))
|
|
|
|
|
|
_register_legacy_node_replacements()
|
|
|
|
NODE_CLASS_MAPPINGS = dict(sorted(NODE_CLASS_MAPPINGS.items(), key=lambda x: NODE_DISPLAY_NAME_MAPPINGS.get(x[0], x[0])))
|
|
NODE_DISPLAY_NAME_MAPPINGS = dict(sorted(NODE_DISPLAY_NAME_MAPPINGS.items(), key=lambda x: x[1]))
|
|
|
|
__all__ = [
|
|
"NODE_CLASS_MAPPINGS",
|
|
"NODE_DISPLAY_NAME_MAPPINGS",
|
|
"LEGACY_NODE_REPLACEMENTS",
|
|
]
|