From 674454d539d87ab1b687e45bc22336e84d050473 Mon Sep 17 00:00:00 2001 From: nuck Date: Mon, 2 Feb 2026 14:51:39 -0500 Subject: [PATCH 1/2] Improve Gemini safety error reporting --- comfy_api_nodes/apis/__init__.py | 4 +-- comfy_api_nodes/apis/gemini.py | 2 +- comfy_api_nodes/nodes_gemini.py | 62 ++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/comfy_api_nodes/apis/__init__.py b/comfy_api_nodes/apis/__init__.py index ee2aa1ce6..c1cbd68ea 100644 --- a/comfy_api_nodes/apis/__init__.py +++ b/comfy_api_nodes/apis/__init__.py @@ -5749,8 +5749,8 @@ class EasyInputMessage(BaseModel): class GeminiContent(BaseModel): - parts: List[GeminiPart] - role: Role1 = Field(..., examples=['user']) + parts: List[GeminiPart] = Field(default_factory=list) + role: Optional[Role1] = Field(None, examples=['user']) class GeminiGenerateContentRequest(BaseModel): diff --git a/comfy_api_nodes/apis/gemini.py b/comfy_api_nodes/apis/gemini.py index d81337dae..d65122f14 100644 --- a/comfy_api_nodes/apis/gemini.py +++ b/comfy_api_nodes/apis/gemini.py @@ -75,7 +75,7 @@ class GeminiTextPart(BaseModel): class GeminiContent(BaseModel): parts: list[GeminiPart] = Field([]) - role: GeminiRole = Field(..., examples=["user"]) + role: GeminiRole | None = Field(None, examples=["user"]) class GeminiSystemInstructionContent(BaseModel): diff --git a/comfy_api_nodes/nodes_gemini.py b/comfy_api_nodes/nodes_gemini.py index 3b31caa7b..6a948ed62 100644 --- a/comfy_api_nodes/nodes_gemini.py +++ b/comfy_api_nodes/nodes_gemini.py @@ -119,6 +119,45 @@ async def create_image_parts( return image_parts +def _summarize_gemini_response_issues(response: GeminiGenerateContentResponse) -> str: + details: list[str] = [] + if response.promptFeedback and response.promptFeedback.blockReason: + msg = f"promptFeedback.blockReason={response.promptFeedback.blockReason}" + if response.promptFeedback.blockReasonMessage: + msg = f"{msg} ({response.promptFeedback.blockReasonMessage})" + details.append(msg) + + finish_reasons = sorted( + { + candidate.finishReason + for candidate in (response.candidates or []) + if candidate.finishReason + } + ) + if finish_reasons: + details.append(f"finishReason(s)={', '.join(finish_reasons)}") + + safety_ratings: set[str] = set() + for candidate in response.candidates or []: + for rating in candidate.safetyRatings or []: + if rating.category and rating.probability: + safety_ratings.add(f"{rating.category}:{rating.probability}") + elif rating.category: + safety_ratings.add(str(rating.category)) + if safety_ratings: + details.append(f"safetyRatings={', '.join(sorted(safety_ratings))}") + + candidates = response.candidates or [] + if candidates: + missing_content = sum( + 1 for candidate in candidates if candidate.content is None or candidate.content.parts is None + ) + if missing_content: + details.append(f"candidates_missing_content={missing_content}/{len(candidates)}") + + return "; ".join(details) + + def get_parts_by_type(response: GeminiGenerateContentResponse, part_type: Literal["text"] | str) -> list[GeminiPart]: """ Filter response parts by their type. @@ -156,8 +195,15 @@ def get_parts_by_type(response: GeminiGenerateContentResponse, part_type: Litera elif part.fileData and part.fileData.mimeType == part_type: parts.append(part) - if not parts and blocked_reasons: - raise ValueError(f"Gemini API blocked the request. Reasons: {blocked_reasons}") + if not parts: + if blocked_reasons: + raise ValueError(f"Gemini API blocked the request. Reasons: {blocked_reasons}") + details = _summarize_gemini_response_issues(response) + if details: + raise ValueError( + f"Gemini API returned no {part_type} parts. Details: {details}. " + "If you are using the `IMAGE` modality, try `IMAGE+TEXT` to see why image generation failed." + ) return parts @@ -187,7 +233,17 @@ async def get_image_from_response(response: GeminiGenerateContentResponse) -> In returned_image = await download_url_to_image_tensor(part.fileData.fileUri) image_tensors.append(returned_image) if len(image_tensors) == 0: - return torch.zeros((1, 1024, 1024, 4)) + details = _summarize_gemini_response_issues(response) + if details: + raise ValueError( + "Gemini API returned no image parts. " + f"Details: {details}. " + "If you are using the `IMAGE` modality, try `IMAGE+TEXT` to see why image generation failed." + ) + raise ValueError( + "Gemini API returned no image parts. " + "If you are using the `IMAGE` modality, try `IMAGE+TEXT` to see why image generation failed." + ) return torch.cat(image_tensors, dim=0) From 5cedd0cb5a09be5154b0646f25aa95e67766a09d Mon Sep 17 00:00:00 2001 From: nuck Date: Mon, 2 Feb 2026 21:28:47 -0500 Subject: [PATCH 2/2] Allow image-only Gemini responses --- comfy_api_nodes/nodes_gemini.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/comfy_api_nodes/nodes_gemini.py b/comfy_api_nodes/nodes_gemini.py index 6a948ed62..78881c3e9 100644 --- a/comfy_api_nodes/nodes_gemini.py +++ b/comfy_api_nodes/nodes_gemini.py @@ -198,12 +198,18 @@ def get_parts_by_type(response: GeminiGenerateContentResponse, part_type: Litera if not parts: if blocked_reasons: raise ValueError(f"Gemini API blocked the request. Reasons: {blocked_reasons}") + if part_type == "text": + return [] details = _summarize_gemini_response_issues(response) if details: raise ValueError( f"Gemini API returned no {part_type} parts. Details: {details}. " "If you are using the `IMAGE` modality, try `IMAGE+TEXT` to see why image generation failed." ) + raise ValueError( + f"Gemini API returned no {part_type} parts. " + "If you are using the `IMAGE` modality, try `IMAGE+TEXT` to see why image generation failed." + ) return parts