Compare commits

..
Author SHA1 Message Date
Gerald LonlasandClaude Opus 4.7 2301e15111 fix: include both endpoint errors when chat.completions fallback also fails
When the responses endpoint fails and the chat.completions fallback also
fails (raises or returns empty), the original responses error was captured
in fallback_reason but never surfaced. Wrap the fallback call in its own
try/except and re-raise a RuntimeError that chains both errors with
context, so users see why both endpoints failed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:46:20 +08:00
5 changed files with 142 additions and 52 deletions
+21 -15
View File
@@ -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 += (
+25 -25
View File
@@ -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(
{
+38
View File
@@ -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"
+41 -1
View File
@@ -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
View File
@@ -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)