Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2301e15111 |
+21
-15
@@ -164,21 +164,27 @@ class LMStudioImageToText(io.ComfyNode):
|
||||
first_image = image[0] if is_batch else image
|
||||
image_data_url = comfy_image_to_base64_png_url(first_image)
|
||||
|
||||
completion = client.chat.completions.create(
|
||||
model=connection.model,
|
||||
messages=build_chat_messages_with_image(
|
||||
system_prompt=system_prompt,
|
||||
user_prompt=user_prompt,
|
||||
image_data_url=image_data_url,
|
||||
),
|
||||
seed=resolved_seed,
|
||||
temperature=connection.temperature,
|
||||
max_tokens=connection.max_tokens,
|
||||
n=1,
|
||||
)
|
||||
text = extract_chat_completion_text(completion).strip()
|
||||
if not text:
|
||||
raise ValueError("chat.completions fallback returned no text output")
|
||||
try:
|
||||
completion = client.chat.completions.create(
|
||||
model=connection.model,
|
||||
messages=build_chat_messages_with_image(
|
||||
system_prompt=system_prompt,
|
||||
user_prompt=user_prompt,
|
||||
image_data_url=image_data_url,
|
||||
),
|
||||
seed=resolved_seed,
|
||||
temperature=connection.temperature,
|
||||
max_tokens=connection.max_tokens,
|
||||
n=1,
|
||||
)
|
||||
text = extract_chat_completion_text(completion).strip()
|
||||
if not text:
|
||||
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:
|
||||
fallback_reason += (
|
||||
|
||||
@@ -22,15 +22,7 @@ def _query_int(request: web.Request, key: str, default: int) -> int:
|
||||
return value
|
||||
|
||||
|
||||
async def _probe_server(
|
||||
request: web.Request, *, error_prefix: str
|
||||
) -> tuple[str, list[str]] | web.Response:
|
||||
"""Shared work for the models/test endpoints.
|
||||
|
||||
Returns either an error ``web.Response`` (to be returned directly by the
|
||||
caller) or a ``(normalized_url, models)`` tuple for the caller to format
|
||||
into a success response.
|
||||
"""
|
||||
async def _models_handler(request: web.Request) -> web.Response:
|
||||
server_url = (request.query.get("server_url") or "").strip()
|
||||
api_token = (request.query.get("api_token") or "-").strip() or "-"
|
||||
|
||||
@@ -48,20 +40,11 @@ async def _probe_server(
|
||||
return web.json_response(
|
||||
{
|
||||
"ok": False,
|
||||
"error": f"{error_prefix}: {exc}",
|
||||
"error": f"Failed to list models from LMStudio: {exc}",
|
||||
},
|
||||
status=502,
|
||||
)
|
||||
|
||||
return normalized_url, models
|
||||
|
||||
|
||||
async def _models_handler(request: web.Request) -> web.Response:
|
||||
result = await _probe_server(request, error_prefix="Failed to list models from LMStudio")
|
||||
if isinstance(result, web.Response):
|
||||
return result
|
||||
normalized_url, models = result
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
"ok": True,
|
||||
@@ -73,12 +56,29 @@ async def _models_handler(request: web.Request) -> web.Response:
|
||||
|
||||
|
||||
async def _test_handler(request: web.Request) -> web.Response:
|
||||
# The JS always sends the widget value, so the default only matters when the
|
||||
# endpoint is called directly; keep it aligned with ``_models_handler``.
|
||||
result = await _probe_server(request, error_prefix="Connection test failed")
|
||||
if isinstance(result, web.Response):
|
||||
return result
|
||||
normalized_url, models = result
|
||||
server_url = (request.query.get("server_url") or "").strip()
|
||||
api_token = (request.query.get("api_token") or "-").strip() or "-"
|
||||
|
||||
try:
|
||||
# Use the same short default as _models_handler; the JS always sends the
|
||||
# widget value anyway, so this only matters when the endpoint is called directly.
|
||||
timeout_seconds = _query_int(request, "timeout_seconds", 15)
|
||||
normalized_url = normalize_server_url(server_url)
|
||||
models = get_server_models(
|
||||
server_url=normalized_url,
|
||||
api_key=api_token,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return web.json_response({"ok": False, "error": str(exc)}, status=400)
|
||||
except Exception as exc:
|
||||
return web.json_response(
|
||||
{
|
||||
"ok": False,
|
||||
"error": f"Connection test failed: {exc}",
|
||||
},
|
||||
status=502,
|
||||
)
|
||||
|
||||
return web.json_response(
|
||||
{
|
||||
|
||||
@@ -158,3 +158,41 @@ def test_execute_chat_fallback_reports_dropped_frames(
|
||||
assert "via chat.completions" in output.ui.text
|
||||
assert "responses unavailable" 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"
|
||||
|
||||
@@ -161,10 +161,50 @@ 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, "resolve_request_seed", lambda _: 55)
|
||||
|
||||
with pytest.raises(ValueError, match="chat\\.completions fallback returned no text output"):
|
||||
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 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"
|
||||
|
||||
+17
-11
@@ -136,17 +136,23 @@ class LMStudioTextGen(io.ComfyNode):
|
||||
via_endpoint = "chat.completions"
|
||||
fallback_reason = str(responses_error)
|
||||
|
||||
completion = client.chat.completions.create(
|
||||
model=connection.model,
|
||||
messages=build_chat_messages(system_prompt, user_prompt),
|
||||
seed=resolved_seed,
|
||||
temperature=connection.temperature,
|
||||
max_tokens=connection.max_tokens,
|
||||
n=1,
|
||||
)
|
||||
text = extract_chat_completion_text(completion).strip()
|
||||
if not text:
|
||||
raise ValueError("chat.completions fallback returned no text output")
|
||||
try:
|
||||
completion = client.chat.completions.create(
|
||||
model=connection.model,
|
||||
messages=build_chat_messages(system_prompt, user_prompt),
|
||||
seed=resolved_seed,
|
||||
temperature=connection.temperature,
|
||||
max_tokens=connection.max_tokens,
|
||||
n=1,
|
||||
)
|
||||
text = extract_chat_completion_text(completion).strip()
|
||||
if not text:
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user