Files
mamorett-comfyui_siliconflow/api_client.py
T
Mattia Moretti c58a16a401 docs: update README to reflect exhaustive parameter support and new features
Update the README to align with the latest node capabilities, shifting the description from "granular nodes" to "exhaustive control" over all API parameters. Key changes include documenting new features like live tooltips, metadata recovery (actual_seed), and improved batch processing. The node reference was also updated to clearly distinguish between required and optional parameters and to reflect the latest supported models.
2026-05-12 15:18:25 +02:00

215 lines
6.2 KiB
Python

"""
api_client.py — HTTP client for SiliconFlow APIs.
"""
import base64
import io
import json
import time
import urllib.request
import urllib.error
from typing import Optional
from .config import get_api_key, SILICONFLOW_BASE_URL
# Model cache: (timestamp, model_list)
_model_cache: tuple[float, list[str]] | None = None
CACHE_TTL_SECONDS = 300 # 5 minutes
# SiliconFlow task types that correspond to image generation
IMAGE_GENERATION_TASK_TYPES = {
"text2image",
"image2image",
"inpainting",
"image-editing",
}
# Known image generation model prefixes as fallback (strictly spec-compliant)
IMAGE_MODEL_PREFIXES = (
"black-forest-labs/",
"Qwen/",
"Tongyi-MAI/",
)
def _make_request(
method: str,
endpoint: str,
payload: Optional[dict] = None,
timeout: int = 120,
) -> dict:
"""Executes an authenticated HTTP request to SiliconFlow APIs."""
api_key = get_api_key()
url = f"{SILICONFLOW_BASE_URL}{endpoint}"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
}
data = json.dumps(payload).encode("utf-8") if payload else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
raise RuntimeError(
f"[SiliconFlow] HTTP Error {e.code} on {endpoint}:\n{body}"
) from e
except urllib.error.URLError as e:
raise RuntimeError(
f"[SiliconFlow] Network error on {endpoint}: {e.reason}"
) from e
def _is_image_model(model: dict) -> bool:
"""
Determines if a model is for image generation.
"""
# Check direct task_type
task_type = model.get("task_type", "") or ""
if task_type.lower() in {t.lower() for t in IMAGE_GENERATION_TASK_TYPES}:
return True
# Check supportedGenerationMethods array
supported = model.get("supportedGenerationMethods", []) or []
for method in supported:
if any(img_task in str(method).lower() for img_task in ("image", "inpaint")):
return True
# Fallback: heuristics on model id (strict)
model_id = model.get("id", "") or ""
if any(model_id.lower().startswith(p.lower()) for p in IMAGE_MODEL_PREFIXES):
return True
if any(
kw in model_id.lower()
for kw in ("flux", "qwen", "z-image")
):
return True
return False
def fetch_image_models(force_refresh: bool = False) -> list[str]:
"""
Retrieves and filters image generation models from SiliconFlow.
"""
global _model_cache
now = time.time()
if not force_refresh and _model_cache is not None:
cached_time, cached_models = _model_cache
if now - cached_time < CACHE_TTL_SECONDS:
return cached_models
all_models = []
page = 1
page_size = 100
while True:
try:
resp = _make_request(
"GET",
f"/models?type=image&page={page}&page_size={page_size}",
timeout=30,
)
except Exception:
resp = _make_request("GET", "/models", timeout=30)
raw = resp.get("data", resp.get("models", []))
all_models.extend(raw)
break
raw = resp.get("data", [])
if not raw:
break
all_models.extend(raw)
total = resp.get("total", 0)
if len(all_models) >= total or len(raw) < page_size:
break
page += 1
image_models = [
m["id"] for m in all_models if isinstance(m, dict) and _is_image_model(m) and m.get("id")
]
image_models = sorted(set(image_models))
if not image_models:
image_models = [
"black-forest-labs/FLUX.1-dev",
"black-forest-labs/FLUX.1-schnell",
"black-forest-labs/FLUX.1-pro",
"black-forest-labs/FLUX-1.1-pro",
"black-forest-labs/FLUX-1.1-pro-Ultra",
"Qwen/Qwen-Image",
"Tongyi-MAI/Z-Image-Turbo",
]
_model_cache = (now, image_models)
return image_models
def run_inference(
model: str,
prompt: str,
**kwargs
) -> tuple[list[bytes], int]:
"""
Executes image inference via SiliconFlow.
Returns (list of image bytes, seed used).
"""
payload: dict = {
"model": model,
"prompt": prompt,
}
# Add all supported parameters
for key, value in kwargs.items():
if value is not None:
if key in ("image", "input_image", "image_prompt") and isinstance(value, str):
# Handle base64 prefixing if not already present
if not value.startswith("data:"):
payload[key] = f"data:image/png;base64,{value}"
else:
payload[key] = value
else:
payload[key] = value
resp = _make_request("POST", "/images/generations", payload=payload, timeout=120)
images_data = resp.get("images", [])
if not images_data:
raise RuntimeError(f"[SiliconFlow] No image in response: {resp}")
results = []
for item in images_data:
if isinstance(item, dict):
if "b64_json" in item:
results.append(base64.b64decode(item["b64_json"]))
elif "url" in item:
results.append(_download_image(item["url"]))
elif isinstance(item, str):
if item.startswith("http"):
results.append(_download_image(item))
else:
results.append(base64.b64decode(item))
# Extract seed from response
actual_seed = resp.get("seed", kwargs.get("seed", 0))
return results, actual_seed
def _download_image(url: str) -> bytes:
"""Downloads an image from URL and returns the bytes."""
try:
req = urllib.request.Request(url, headers={"User-Agent": "ComfyUI-SiliconFlow/1.0"})
with urllib.request.urlopen(req, timeout=60) as resp:
return resp.read()
except Exception as e:
raise RuntimeError(f"[SiliconFlow] Error downloading image from {url}: {e}") from e