Compare commits

..
Author SHA1 Message Date
Gerald LonlasandClaude Opus 4.7 a6d471fc9b fix: accept extra kwargs in LMStudioConnect.validate_inputs
ComfyUI can pass any of a node's schema inputs to validate_inputs by
name, depending on the schema version. The current signature only
accepts server_url, timeout_seconds, and max_tokens, so extra inputs
(api_token, model, reasoning_enabled, ...) would raise TypeError before
validation ever runs.

Add **_: Any to absorb any additional inputs ComfyUI provides while
keeping the existing validation logic intact.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:44:57 +08:00
6 changed files with 57 additions and 118 deletions
+9 -1
View File
@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from comfy_api.latest import io, ui from comfy_api.latest import io, ui
from .client import ( from .client import (
@@ -115,7 +117,13 @@ class LMStudioConnect(io.ComfyNode):
) )
@classmethod @classmethod
def validate_inputs(cls, server_url: str, timeout_seconds: int, max_tokens: int) -> bool | str: def validate_inputs(
cls,
server_url: str,
timeout_seconds: int,
max_tokens: int,
**_: Any,
) -> bool | str:
try: try:
normalize_server_url(server_url) normalize_server_url(server_url)
except ValueError as exc: except ValueError as exc:
+15 -21
View File
@@ -164,27 +164,21 @@ class LMStudioImageToText(io.ComfyNode):
first_image = image[0] if is_batch else image first_image = image[0] if is_batch else image
image_data_url = comfy_image_to_base64_png_url(first_image) image_data_url = comfy_image_to_base64_png_url(first_image)
try: completion = client.chat.completions.create(
completion = client.chat.completions.create( model=connection.model,
model=connection.model, messages=build_chat_messages_with_image(
messages=build_chat_messages_with_image( system_prompt=system_prompt,
system_prompt=system_prompt, user_prompt=user_prompt,
user_prompt=user_prompt, image_data_url=image_data_url,
image_data_url=image_data_url, ),
), seed=resolved_seed,
seed=resolved_seed, temperature=connection.temperature,
temperature=connection.temperature, max_tokens=connection.max_tokens,
max_tokens=connection.max_tokens, n=1,
n=1, )
) text = extract_chat_completion_text(completion).strip()
text = extract_chat_completion_text(completion).strip() if not text:
if not text: raise ValueError("chat.completions fallback returned no text output")
raise ValueError("chat.completions fallback returned no text output")
except Exception as chat_error:
raise RuntimeError(
f"Both endpoints failed. responses error: {fallback_reason}. "
f"chat.completions error: {chat_error}"
) from chat_error
if batch_size > 1: if batch_size > 1:
fallback_reason += ( fallback_reason += (
+21
View File
@@ -49,6 +49,27 @@ def test_validate_inputs() -> None:
assert cls.validate_inputs("http://127.0.0.1:1234", 10, 0) == "max_tokens must be >= 1" assert cls.validate_inputs("http://127.0.0.1:1234", 10, 0) == "max_tokens must be >= 1"
def test_validate_inputs_accepts_extra_kwargs() -> None:
connect_node = _connect_module()
cls = connect_node.LMStudioConnect
# ComfyUI may pass any of the schema inputs by name; the method must tolerate them.
assert (
cls.validate_inputs(
"http://127.0.0.1:1234",
10,
128,
api_token="-",
model="placeholder",
reasoning_enabled=False,
use_tooling_mcp=False,
temperature=0.7,
test_connectivity=True,
)
is True
)
def test_execute_with_placeholder_model_uses_discovered_default( def test_execute_with_placeholder_model_uses_discovered_default(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
) -> None: ) -> None:
-38
View File
@@ -158,41 +158,3 @@ def test_execute_chat_fallback_reports_dropped_frames(
assert "via chat.completions" in output.ui.text assert "via chat.completions" in output.ui.text
assert "responses unavailable" in output.ui.text assert "responses unavailable" in output.ui.text
assert "additional frame(s) were dropped" in output.ui.text assert "additional frame(s) were dropped" in output.ui.text
def test_execute_chains_both_errors_when_chat_completions_also_raises(
monkeypatch: pytest.MonkeyPatch,
) -> None:
image_node = _image_module()
def raise_responses(**kwargs):
raise RuntimeError("responses boom")
def raise_chat(**kwargs):
raise RuntimeError("chat boom")
fake_client = SimpleNamespace(
responses=SimpleNamespace(create=raise_responses),
chat=SimpleNamespace(completions=SimpleNamespace(create=raise_chat)),
)
monkeypatch.setattr(image_node, "resolve_request_seed", lambda _: 41)
monkeypatch.setattr(image_node, "create_openai_client", lambda **_: fake_client)
monkeypatch.setattr(
image_node, "comfy_image_to_base64_png_url", lambda _img: "data:image/png;base64,xxx"
)
with pytest.raises(RuntimeError) as exc_info:
image_node.LMStudioImageToText.execute(
connection=_connection_payload(),
image=_single_image(),
system_prompt="sys",
user_prompt="describe",
seed=0,
)
message = str(exc_info.value)
assert "Both endpoints failed" in message
assert "responses boom" in message
assert "chat boom" in message
assert isinstance(exc_info.value.__cause__, RuntimeError)
assert str(exc_info.value.__cause__) == "chat boom"
+1 -41
View File
@@ -161,50 +161,10 @@ def test_execute_raises_if_both_endpoints_return_no_text(
monkeypatch.setattr(text_gen_node, "create_openai_client", lambda **_: fake_client) monkeypatch.setattr(text_gen_node, "create_openai_client", lambda **_: fake_client)
monkeypatch.setattr(text_gen_node, "resolve_request_seed", lambda _: 55) monkeypatch.setattr(text_gen_node, "resolve_request_seed", lambda _: 55)
with pytest.raises(RuntimeError) as exc_info: with pytest.raises(ValueError, match="chat\\.completions fallback returned no text output"):
text_gen_node.LMStudioTextGen.execute( text_gen_node.LMStudioTextGen.execute(
connection=_connection_payload(), connection=_connection_payload(),
system_prompt="sys", system_prompt="sys",
user_prompt="hello", user_prompt="hello",
seed=1, seed=1,
) )
message = str(exc_info.value)
assert "Both endpoints failed" in message
assert "responses endpoint returned no text output" in message
assert "chat.completions fallback returned no text output" in message
def test_execute_chains_both_errors_when_chat_completions_also_raises(
monkeypatch: pytest.MonkeyPatch,
) -> None:
text_gen_node = _text_module()
def raise_responses(**kwargs):
raise RuntimeError("responses boom")
def raise_chat(**kwargs):
raise RuntimeError("chat boom")
fake_client = SimpleNamespace(
responses=SimpleNamespace(create=raise_responses),
chat=SimpleNamespace(completions=SimpleNamespace(create=raise_chat)),
)
monkeypatch.setattr(text_gen_node, "create_openai_client", lambda **_: fake_client)
monkeypatch.setattr(text_gen_node, "resolve_request_seed", lambda _: 55)
with pytest.raises(RuntimeError) as exc_info:
text_gen_node.LMStudioTextGen.execute(
connection=_connection_payload(),
system_prompt="sys",
user_prompt="hello",
seed=1,
)
message = str(exc_info.value)
assert "Both endpoints failed" in message
assert "responses boom" in message
assert "chat boom" in message
assert isinstance(exc_info.value.__cause__, RuntimeError)
assert str(exc_info.value.__cause__) == "chat boom"
+11 -17
View File
@@ -136,23 +136,17 @@ class LMStudioTextGen(io.ComfyNode):
via_endpoint = "chat.completions" via_endpoint = "chat.completions"
fallback_reason = str(responses_error) fallback_reason = str(responses_error)
try: completion = client.chat.completions.create(
completion = client.chat.completions.create( model=connection.model,
model=connection.model, messages=build_chat_messages(system_prompt, user_prompt),
messages=build_chat_messages(system_prompt, user_prompt), seed=resolved_seed,
seed=resolved_seed, temperature=connection.temperature,
temperature=connection.temperature, max_tokens=connection.max_tokens,
max_tokens=connection.max_tokens, n=1,
n=1, )
) text = extract_chat_completion_text(completion).strip()
text = extract_chat_completion_text(completion).strip() if not text:
if not text: raise ValueError("chat.completions fallback returned no text output")
raise ValueError("chat.completions fallback returned no text output")
except Exception as chat_error:
raise RuntimeError(
f"Both endpoints failed. responses error: {fallback_reason}. "
f"chat.completions error: {chat_error}"
) from chat_error
text = strip_think_content(text) text = strip_think_content(text)