BYOK OpenAI gpt-image generation and editing nodes for ComfyUI — calls go directly to the OpenAI API with your own key (no proxy, no credit markup). - Two nodes: "Imagent: OpenAI Image" (text-to-image) and "Imagent: OpenAI Image Edit" (edit / inpaint / multi-reference compose). - Models gpt-image-2 / gpt-image-1.5 / gpt-image-1 with a per-model dynamic UI (comfy_api DynamicCombo): size, background, custom WxH, and input_fidelity are revealed only for the models that support them. - Edit node accepts an auto-growing set of reference images (up to 16) plus an optional inpaint mask; generate supports batches up to 8. - API key via OPENAI_API_KEY or a local config.json. Requires ComfyUI >= 0.23.0.
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""OpenAI API key resolution, model catalog, and client construction.
|
|
|
|
Public entry points: resolve_api_key(), get_client(), friendly_error(); plus the
|
|
MODELS list and DEFAULT_MODEL constant.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from openai import OpenAI
|
|
|
|
log = logging.getLogger("imagent")
|
|
|
|
# config.json lives at the repo root, next to this package directory.
|
|
_CONFIG_PATH = Path(__file__).resolve().parent.parent / "config.json"
|
|
|
|
# Current OpenAI image models, verified 2026-06-07. Curated to the three current
|
|
# GPT image generations (1, 1.5, 2); dated snapshots and -mini are intentionally
|
|
# omitted. DALL-E excluded (shut down 2026-05-12).
|
|
MODELS = [
|
|
"gpt-image-2",
|
|
"gpt-image-1.5",
|
|
"gpt-image-1",
|
|
]
|
|
DEFAULT_MODEL = "gpt-image-2"
|
|
|
|
_PLACEHOLDERS = {"", "sk-...", "your_openai_api_key_here"}
|
|
|
|
|
|
def resolve_api_key() -> str | None:
|
|
"""Return the API key from env (preferred) or config.json, else None."""
|
|
env_key = os.environ.get("OPENAI_API_KEY")
|
|
if env_key and env_key not in _PLACEHOLDERS:
|
|
return env_key
|
|
try:
|
|
if _CONFIG_PATH.exists():
|
|
cfg = json.loads(_CONFIG_PATH.read_text())
|
|
file_key = cfg.get("OPENAI_API_KEY", "")
|
|
if isinstance(file_key, str) and file_key and file_key not in _PLACEHOLDERS:
|
|
return file_key
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
log.warning("imagent: could not read %s: %s", _CONFIG_PATH, exc)
|
|
return None
|
|
|
|
|
|
def get_client() -> "OpenAI | None":
|
|
"""Construct an OpenAI client, or None if no key is available."""
|
|
key = resolve_api_key()
|
|
if not key:
|
|
return None
|
|
from openai import OpenAI
|
|
return OpenAI(api_key=key)
|
|
|
|
|
|
def friendly_error(exc: Exception) -> str:
|
|
"""Turn an OpenAI exception into an actionable message.
|
|
|
|
Prefers the exception's HTTP status_code (OpenAI APIError exposes it) and
|
|
falls back to anchored substrings, so unrelated text containing 'verif' or
|
|
'429' is not misclassified.
|
|
"""
|
|
msg = str(exc)
|
|
lower = msg.lower()
|
|
status = getattr(exc, "status_code", None)
|
|
is_verif = "organization verification" in lower or "must be verified" in lower
|
|
if status == 403 or "error code: 403" in lower or is_verif:
|
|
return ("Error: 403 — your OpenAI org may need API Organization "
|
|
"Verification before gpt-image models can be used. Details: " + msg)
|
|
if status == 429 or "error code: 429" in lower:
|
|
return "Error: 429 — rate limit / quota exceeded. Details: " + msg
|
|
return f"Error: {msg}"
|