218 lines
6.2 KiB
Python
218 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,
|
|
}
|
|
|
|
for key, value in kwargs.items():
|
|
if value is None:
|
|
continue
|
|
if isinstance(value, str) and not value:
|
|
continue
|
|
if key == "seed" and value < 0:
|
|
continue
|
|
if key in ("image", "input_image", "image_prompt") and isinstance(value, str):
|
|
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", -1))
|
|
|
|
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
|