Turn provider content-policy errors into plain-language messages
litellm/provider errors (e.g. Gemini's BadRequestError for safety- filtered generations) were surfacing as raw JSON-in-a-stack-trace, unreadable to non-engineers using these nodes. Catch BadRequestError around every litellm call in both nodes, parse the provider's nested JSON payload, and re-raise as a plain sentence explaining that the provider itself rejected the request for a content-policy reason (not a bug) plus concrete guidance to rephrase the prompt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Bwg5aRmgChpwrVnpNgoQm
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c24e649d1c
commit
64b7889d6b
+75
-16
@@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
from comfy_api.latest import ComfyAPI, InputImpl, io
|
||||
from litellm import aimage_edit, aimage_generation, avideo_content, avideo_generation, avideo_status
|
||||
from litellm import BadRequestError, aimage_edit, aimage_generation, avideo_content, avideo_generation, avideo_status
|
||||
from litellm.interactions import acreate as acreate_interaction
|
||||
|
||||
from .models.azure import AzureModel
|
||||
@@ -15,6 +17,51 @@ api = ComfyAPI()
|
||||
|
||||
_TERMINAL_STATUSES = ("completed", "failed")
|
||||
|
||||
_SAFETY_KEYWORDS = ("safety", "blocked", "policy", "harmful", "flagged", "violat")
|
||||
|
||||
|
||||
def _extract_provider_error(exc_or_text) -> str:
|
||||
"""Pull the human-readable message out of a provider error payload.
|
||||
|
||||
Providers often bury the actual explanation inside a JSON blob nested in
|
||||
the exception text (or, for Gemini specifically, split across separate
|
||||
"message" and "code" fields where "code" is actually a second sentence of
|
||||
prose). Falls back to the raw string if there's nothing to parse out.
|
||||
"""
|
||||
text = str(exc_or_text)
|
||||
match = re.search(r"\{.*\}", text, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
payload = json.loads(match.group(0))
|
||||
except (ValueError, TypeError):
|
||||
payload = None
|
||||
if isinstance(payload, dict):
|
||||
err = payload.get("error", payload)
|
||||
if isinstance(err, dict):
|
||||
parts = [p for p in (err.get("message"), err.get("code")) if isinstance(p, str)]
|
||||
if parts:
|
||||
return " ".join(parts)
|
||||
return text
|
||||
|
||||
|
||||
def _friendly_error_message(detail: str, provider_name: str, kind: str) -> str:
|
||||
"""Rewrite a raw provider error into something a non-engineer can act on."""
|
||||
label = provider_name.title() if provider_name else "The provider"
|
||||
if any(keyword in detail.lower() for keyword in _SAFETY_KEYWORDS):
|
||||
return (
|
||||
f"{label} refused to generate this {kind} because it tripped their content "
|
||||
f"safety filter — this is {label}'s policy, not a ComfyUI or workflow error. "
|
||||
f"Their explanation: \"{detail}\" "
|
||||
"Rephrase the prompt to tone down graphic, sexual, violent, or crude wording "
|
||||
"(this can trigger even for comedic scenes, especially ones involving children, "
|
||||
"injury, or profanity) and run it again."
|
||||
)
|
||||
return f"{label} rejected the {kind} request: {detail}"
|
||||
|
||||
|
||||
def _raise_friendly_api_error(exc: Exception, provider_name: str, kind: str) -> None:
|
||||
raise RuntimeError(_friendly_error_message(_extract_provider_error(exc), provider_name, kind)) from exc
|
||||
|
||||
_PROVIDERS = [GeminiModel, OpenAIModel, AzureModel, RunwayMLModel]
|
||||
_PROVIDERS_BY_NAME = {p.PROVIDER: p for p in _PROVIDERS}
|
||||
|
||||
@@ -153,14 +200,20 @@ class APIVideoGenerate(io.ComfyNode):
|
||||
await asyncio.wait([request], timeout=poll_interval)
|
||||
elapsed = min(elapsed + poll_interval, timeout)
|
||||
await api.execution.set_progress(elapsed, timeout, node_id=node_id)
|
||||
response = await request
|
||||
try:
|
||||
response = await request
|
||||
except BadRequestError as exc:
|
||||
_raise_friendly_api_error(exc, provider_name, "video")
|
||||
await api.execution.set_progress(timeout, timeout, node_id=node_id)
|
||||
video_bytes = provider_cls.extract_video_bytes(response)
|
||||
return io.NodeOutput(InputImpl.VideoFromFile(BytesIO(video_bytes)))
|
||||
|
||||
op = await avideo_generation(
|
||||
model=model, prompt=prompt, timeout=timeout, **auth_kwargs, **extra_kwargs
|
||||
)
|
||||
try:
|
||||
op = await avideo_generation(
|
||||
model=model, prompt=prompt, timeout=timeout, **auth_kwargs, **extra_kwargs
|
||||
)
|
||||
except BadRequestError as exc:
|
||||
_raise_friendly_api_error(exc, provider_name, "video")
|
||||
|
||||
elapsed = 0.0
|
||||
await api.execution.set_progress(0.0, timeout, node_id=node_id)
|
||||
@@ -170,10 +223,13 @@ class APIVideoGenerate(io.ComfyNode):
|
||||
await asyncio.sleep(poll_interval)
|
||||
elapsed += poll_interval
|
||||
await api.execution.set_progress(min(elapsed, timeout), timeout, node_id=node_id)
|
||||
op = await avideo_status(video_id=op.id, timeout=timeout, **auth_kwargs)
|
||||
try:
|
||||
op = await avideo_status(video_id=op.id, timeout=timeout, **auth_kwargs)
|
||||
except BadRequestError as exc:
|
||||
_raise_friendly_api_error(exc, provider_name, "video")
|
||||
|
||||
if op.status == "failed":
|
||||
raise RuntimeError(f"Video generation failed: {op.error}")
|
||||
raise RuntimeError(_friendly_error_message(_extract_provider_error(op.error), provider_name, "video"))
|
||||
|
||||
video_bytes = await avideo_content(video_id=op.id, **auth_kwargs)
|
||||
await api.execution.set_progress(timeout, timeout, node_id=node_id)
|
||||
@@ -264,15 +320,18 @@ class APIImageGenerate(io.ComfyNode):
|
||||
provider_cls = _IMAGE_PROVIDERS_BY_NAME[provider_name]
|
||||
model = f"{provider_name}/{model_id}"
|
||||
|
||||
if source_mode == "text":
|
||||
extra_kwargs = provider_cls.image_kwargs(model_id, settings)
|
||||
response = await aimage_generation(model=model, prompt=prompt, **auth_kwargs, **extra_kwargs)
|
||||
else:
|
||||
extra_kwargs = provider_cls.image_to_image_kwargs(model_id, settings)
|
||||
image = extra_kwargs.pop("image")
|
||||
response = await aimage_edit(
|
||||
model=model, prompt=prompt, image=image, **auth_kwargs, **extra_kwargs
|
||||
)
|
||||
try:
|
||||
if source_mode == "text":
|
||||
extra_kwargs = provider_cls.image_kwargs(model_id, settings)
|
||||
response = await aimage_generation(model=model, prompt=prompt, **auth_kwargs, **extra_kwargs)
|
||||
else:
|
||||
extra_kwargs = provider_cls.image_to_image_kwargs(model_id, settings)
|
||||
image = extra_kwargs.pop("image")
|
||||
response = await aimage_edit(
|
||||
model=model, prompt=prompt, image=image, **auth_kwargs, **extra_kwargs
|
||||
)
|
||||
except BadRequestError as exc:
|
||||
_raise_friendly_api_error(exc, provider_name, "image")
|
||||
|
||||
image_bytes_list = Model.image_bytes_list_from_response(response)
|
||||
tensor = Model.image_bytes_list_to_tensor(image_bytes_list)
|
||||
|
||||
Reference in New Issue
Block a user