Files
ycyy-ComfyUI-YCYY-API/utils/request_utils.py
T
2026-08-27 10:37:17 +08:00

131 lines
5.2 KiB
Python

"""Shared request and media helpers for OpenAI-compatible nodes."""
import base64
import json
from io import BytesIO
from urllib.parse import urlsplit, urlunsplit
import requests
from .config_utils import get_config_section
class ToolChoiceRejected(RuntimeError):
"""The provider explicitly rejected the request's tool_choice field."""
class FunctionToolsRejected(RuntimeError):
"""The provider explicitly rejected the requested function tool schema."""
def post_openai_json(endpoint, headers, payload, timeout, proxies):
"""POST an OpenAI-compatible JSON request with stable error classification."""
response = requests.post(
endpoint, headers=headers, json=payload, timeout=timeout, proxies=proxies
)
if response.status_code < 200 or response.status_code >= 300:
detail = response.text[:1000]
lower_detail = detail.lower()
choice_rejected = (
response.status_code in (400, 404, 415, 422)
and "tool_choice" in payload
and any(marker in lower_detail for marker in ("tool_choice", "tool choice"))
and any(
marker in lower_detail
for marker in (
"unsupported", "unknown", "unrecognized", "invalid",
"not support", "not allowed", "not permitted", "extra field",
)
)
)
if choice_rejected:
raise ToolChoiceRejected(
f"API request rejected tool_choice ({response.status_code}): {detail}"
)
tool_error = response.status_code in (400, 404, 415, 422) and any(
marker in lower_detail
for marker in ("tool", "function", "unsupported", "unknown field")
)
if payload.get("tools") and tool_error:
raise FunctionToolsRejected(
f"API request failed ({response.status_code}); this API/model may not support "
f"the requested Skill tools: {detail}"
)
raise RuntimeError(f"API request failed ({response.status_code}): {detail}")
if not response.text.strip():
raise ValueError("API returned an empty response")
try:
return response.json()
except ValueError as exc:
raise ValueError("API returned invalid JSON") from exc
def parse_json_options(options_json):
if not options_json or not str(options_json).strip():
return {}
try:
value = json.loads(options_json)
except (TypeError, json.JSONDecodeError) as exc:
raise ValueError(f"Advanced options must be valid JSON: {exc}")
if not isinstance(value, dict):
raise ValueError("Advanced options JSON must be a top-level object")
return value
def resolve_endpoint(base_url, api_protocol):
if not isinstance(base_url, str) or not base_url.strip():
raise ValueError("base_url cannot be empty")
if api_protocol not in ("openai-completions", "openai-responses"):
raise ValueError(f"Unsupported API protocol: {api_protocol}")
raw = base_url.strip()
parts = urlsplit(raw)
path = parts.path.rstrip("/")
lower_path = path.lower()
if lower_path.endswith("/chat/completions"):
if api_protocol != "openai-completions":
raise ValueError("base_url points to /chat/completions but protocol is openai-responses")
endpoint_path = path
elif lower_path.endswith("/responses"):
if api_protocol != "openai-responses":
raise ValueError("base_url points to /responses but protocol is openai-completions")
endpoint_path = path
else:
suffix = "/chat/completions" if api_protocol == "openai-completions" else "/responses"
endpoint_path = f"{path}{suffix}" if path else suffix
return urlunsplit((parts.scheme, parts.netloc, endpoint_path, parts.query, parts.fragment))
def get_proxy_config(proxy_options=None):
config = proxy_options if proxy_options is not None else get_config_section("proxy")
if not isinstance(config, dict) or not config.get("enable", False):
return None
proxies = {}
for key in ("http", "https"):
value = config.get(key, "")
if isinstance(value, str) and value.strip():
proxies[key] = value.strip()
return proxies or None
def video_to_data_uri(video, max_bytes=64 * 1024 * 1024):
"""Read a ComfyUI VideoInput into a bounded base64 data URI."""
if video is None or not hasattr(video, "get_stream_source"):
raise ValueError("Invalid video input")
source = video.get_stream_source()
if isinstance(source, BytesIO):
source.seek(0)
data = source.read(max_bytes + 1)
else:
with open(source, "rb") as handle:
data = handle.read(max_bytes + 1)
if len(data) > max_bytes:
raise ValueError(f"Video exceeds the maximum size of {max_bytes} bytes")
if isinstance(source, BytesIO):
source.seek(0)
mime = getattr(video, "container", None)
mime = str(mime).lower() if mime else ""
mime = {"mp4": "video/mp4", "webm": "video/webm", "mkv": "video/x-matroska"}.get(mime, mime)
if not mime.startswith("video/"):
mime = "video/mp4"
return f"data:{mime};base64,{base64.b64encode(data).decode('ascii')}"