From e5486eadfcf14131da5e6423bec91a4eeb256c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=91=E6=9E=97=5FCyril=20Pilgrim?= Date: Tue, 3 Mar 2026 11:20:18 +0800 Subject: [PATCH] feat(nodes): refactor openai adapter and improve user experience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: 1. Clarify node classification by moving from 'OpenAI' to 'LLM' category. 2. Improve default prompt/system_prompt for better UX. 3. Resolve ambiguity in configuration passthrough from LLMs Loader node. Implementation: 1. Updated CATEGORY to ๐ŸšฆComfyUI_LLMs_Toolkit/LLM. 2. Changed default system_prompt to English and set default prompt to 'hello'. 3. Added 'LLM_CONFIG (from input)' option to provider and model dropdowns to explicitly handle external configuration. 4. Fixed a bug where unresolved 'model' parameter was sent to the API instead of 'actual_model'. --- nodes/openai_compatible.py | 64 ++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/nodes/openai_compatible.py b/nodes/openai_compatible.py index 31caee5..64b1ab3 100644 --- a/nodes/openai_compatible.py +++ b/nodes/openai_compatible.py @@ -108,12 +108,10 @@ class OpenAICompatibleLoader: providers = _get_providers() provider_names = [p['name'] for p in providers] - provider_names.append("Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ") + provider_names.append("LLM_CONFIG (from input)") - # We need a massive list of models across all providers to satisfy comfyui's static dropdowns, - # but the UI will show/hide them based on provider selection (client-side). - # For simplicity in Python, we combine them all + Add Custom option. - all_models = ["Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ"] + # Collect all models from providers for the dropdown + all_models = ["Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ", "LLM_CONFIG (from input)"] for p in providers: for m in p.get("models", []): if m not in all_models: @@ -121,10 +119,10 @@ class OpenAICompatibleLoader: return { "required": { - "provider": (provider_names, {"default": provider_names[0] if provider_names else "Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ"}), + "provider": (provider_names, {"default": provider_names[0] if provider_names else "LLM_CONFIG (from input)"}), "model": (all_models, {"default": all_models[1] if len(all_models) > 1 else "Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ"}), - "system_prompt": ("STRING", {"default": "ไฝ ๆ˜ฏไธ€ไธช AI ๅŠฉๆ‰‹", "multiline": True}), - "prompt": ("STRING", {"multiline": True, "default": ""}), + "system_prompt": ("STRING", {"default": "You are an AI assistant", "multiline": True}), + "prompt": ("STRING", {"multiline": True, "default": "hello"}), }, "optional": { "llm_config": ("LLM_CONFIG",), # Keep for backward compatibility @@ -139,7 +137,7 @@ class OpenAICompatibleLoader: RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("response", "reasoning") FUNCTION = "generate" - CATEGORY = "๐ŸšฆComfyUI_LLMs_Toolkit/OpenAI" + CATEGORY = "๐ŸšฆComfyUI_LLMs_Toolkit/LLM" def __init__(self): self.conversation_history: List[Dict[str, Any]] = [] @@ -219,8 +217,6 @@ class OpenAICompatibleLoader: def _get_provider_config(self, provider_choice: str) -> Optional[Dict[str, Any]]: """Find the provider config by its dropdown label.""" - if provider_choice == "Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ": - return None providers = _get_providers() for p in providers: @@ -233,8 +229,8 @@ class OpenAICompatibleLoader: def generate( self, - provider: str = "Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ", - model: str = "Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ", + provider: str = "", + model: str = "", prompt: str = "", system_prompt: Optional[str] = None, llm_config: Optional[Dict[str, Any]] = None, @@ -247,14 +243,28 @@ class OpenAICompatibleLoader: """Main generation entry point with graceful degradation.""" # โ”€โ”€ Configuration Resolution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + _FROM_INPUT = "LLM_CONFIG (from input)" + api_key = "" base_url = "" - actual_model = model + actual_model = "" if model in ("Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ", _FROM_INPUT, "") else model provider_id = "custom" provider_name = "Custom Endpoint" - - # 1. New Provider Manager Configuration - if provider != "Custom/ๆ‰‹ๅŠจ่พ“ๅ…ฅ": + + if provider == _FROM_INPUT: + # Mode 1: All config comes from LLM_CONFIG input node + if not llm_config: + return self._error_result( + 'Provider is set to "LLM_CONFIG (from input)" but no LLM_CONFIG node is connected. ' + 'Please connect a LLMs Loader node or select a different provider.' + ) + api_key = llm_config.get("api_key", "") + base_url = llm_config.get("base_url", "") + actual_model = llm_config.get("model", "") or actual_model + provider_id = llm_config.get("provider", "custom") + provider_name = provider_id + else: + # Mode 2: Config from Provider Manager (providers.json) p_config = self._get_provider_config(provider) if p_config: provider_id = p_config["id"] @@ -262,14 +272,6 @@ class OpenAICompatibleLoader: api_key = p_config.get("apiKey", "") or api_key base_url = p_config.get("apiHost", "") or base_url - # 2. Legacy fallback from LLM_CONFIG node if connected - if llm_config: - api_key = api_key or llm_config.get("api_key", "") - base_url = base_url or llm_config.get("base_url", "") - actual_model = actual_model or llm_config.get("model", "") - provider_id = llm_config.get("provider", provider_id) - provider_name = getattr(llm_config, "name", provider_id) - # โ”€โ”€ Input validation (fail fast, don't waste API quota) โ”€โ”€โ”€โ”€โ”€โ”€ if not prompt or not prompt.strip(): return self._error_result("่พ“ๅ…ฅ็š„ Prompt ไธบ็ฉบ๏ผŒ่ฏทๅกซๅ†™ๅ†…ๅฎนๅŽ้‡่ฏ•") @@ -300,9 +302,9 @@ class OpenAICompatibleLoader: image_input = stripped # โ”€โ”€ Logging โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - print(f"{self.TAG} ไฝฟ็”จ้…็ฝฎ: {provider_name} / {model}") + print(f"{self.TAG} ไฝฟ็”จ้…็ฝฎ: {provider_name} / {actual_model}") start = self._log_start( - provider_name, model, system_prompt, prompt, image_input + provider_name, actual_model, system_prompt, prompt, image_input ) # โ”€โ”€ Build messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -311,7 +313,7 @@ class OpenAICompatibleLoader: messages = self._apply_memory(messages, enable_memory) # โ”€โ”€ o1/o3 System Role Downgrade (Compatibility) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - if re.search(r'o[1-3]', model): + if re.search(r'o[1-3]', actual_model): for i, msg in enumerate(messages): if msg["role"] == "system": messages[i] = {"role": "user", "content": msg["content"]} @@ -320,7 +322,7 @@ class OpenAICompatibleLoader: # โ”€โ”€ Build payload (clean, standard fields) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ payload = { - "model": model, + "model": actual_model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, @@ -372,8 +374,8 @@ class OpenAICompatibleLoader: except Exception as e: elapsed_ms = int((time.time() - start) * 1000) - err = classify_error(e, provider_name, model, request_size_mb, elapsed_ms) - log_error(err, provider_name, model, request_size_mb, elapsed_ms) + err = classify_error(e, provider_name, actual_model, request_size_mb, elapsed_ms) + log_error(err, provider_name, actual_model, request_size_mb, elapsed_ms) # Graceful degradation: return error text instead of crashing return self._error_result(err.user_message())