"""Heuristics for LLM errors when image + text (vision) is required but the model rejects it.""" from __future__ import annotations import re # Shown in API/job error text; frontend uses this for a clearer toast title. VISION_LAYOUT_ERROR_MARKER = "TEMPLATE_VISION_MODEL_REQUIRED" VISION_LAYOUT_USER_MESSAGE = ( f"{VISION_LAYOUT_ERROR_MARKER}\n\n" "Generating a custom template layout sends a slide screenshot to your text model, " "so the model must support vision (image + text).\n\n" "The current text model does not accept image inputs. Open Settings, pick a " "vision-capable model for your provider (for example GPT-4o, Claude 3.5 Sonnet or " "newer, or Gemini with image support), save, and try again." ) def _collect_exception_text(exc: BaseException, *, max_chain: int = 10) -> str: parts: list[str] = [] seen: set[int] = set() cur: BaseException | None = exc depth = 0 while cur is not None and depth < max_chain and id(cur) not in seen: seen.add(id(cur)) parts.append(str(cur)) msg = getattr(cur, "message", None) if isinstance(msg, str) and msg.strip(): parts.append(msg) body = getattr(cur, "body", None) if body is not None: parts.append(repr(body)) for attr in ("response", "error", "detail"): obj = getattr(cur, attr, None) if isinstance(obj, str) and obj.strip(): parts.append(obj) elif isinstance(obj, dict): parts.append(repr(obj)) cur = cur.__cause__ or cur.__context__ depth += 1 return " ".join(parts).lower() def is_likely_vision_capability_error(exc: BaseException) -> bool: """ Best-effort detection across OpenAI-compatible, Anthropic, Gemini, and LiteLLM errors when the model rejects multimodal / image content. """ blob = _collect_exception_text(exc) if not blob.strip(): return False strong = ( "image input", "image inputs", "image_url", "input_image", "inline_data", "multimodal", "multi-modal", "vision is not", "does not support images", "does not support image", "cannot accept image", "image content is not supported", "unsupported content type", "model is not multimodal", "text-only model", "text only model", "this model only supports text", "only supports text", "invalid image", "badimage", "no image support", "images are not supported", "image parts", "content blocks of type image", "type 'image'", "modality image", ) if any(s in blob for s in strong): return True if "image" in blob or "picture" in blob or "screenshot" in blob: weak = ( "not supported", "unsupported", "not allowed", "invalid", "cannot", "does not support", "not available", "not enabled", "not accept", "rejected", "forbidden", "bad request", ) if any(w in blob for w in weak): return True if re.search(r"\bimage_url\b", blob) or re.search(r"\binput_image\b", blob): return True return False