每个渠道独立启用、保存连接与默认参数。先保存,再测试连接。生成 API 使用 Bearer Key;本配置页面暂不需要登录。
视频接口:POST /v1/videos。参数 channel 选择 autodl / onethingai;mode 选择 text_to_video / image_to_video / first_last_to_video / ref2va。原生 H3 以 24 fps 生成视频和声音,默认 5 秒、480 × 864、20 步。完整参数与 Node.js 调用说明 ↗
{"channel":"onethingai","model":"minimax-h3","mode":"text_to_video","prompt":"一位青年面对镜头说:你好,很高兴见到你。同步生成清晰中文人声,无字幕。","seconds":5,"response_format":"file"}
已启用的 GPU 渠道模型与本地工作流自动读取。视频推荐选择渠道模型;额外 JSON 可填写 mode、首尾帧或参考素材参数。
GPU 渠道独立配置、MiniMax H3 四种视频模式、Node.js 调用方法见 [渠道接口说明](docs/channels.md)。配置组件位于文档首页最上方;剧芽短剧通过同一 /v1/videos 接口生成与播放视频。
A FastAPI service that fronts a ComfyUI instance (or several) with
a job-queue API. Customers submit a workflow via POST /generate
or one of its variants; the wrapper preprocesses it (downloading
any URL-shaped inputs), submits it to ComfyUI, awaits completion
over the WebSocket, postprocesses the output files (S3 upload,
optional webhook), and returns a structured result envelope.
Source code: [github.com/ai-dock/comfyui-api-wrapper](https://github.com/ai-dock/comfyui-api-wrapper)
request id), sync (POST /generate/sync blocks until done),
and SSE-streaming (POST /generate/stream emits progress
events). Same payload, same Result shape, different delivery.
in input.workflow_json, or a curated modifier class
+ modifications dict that the modifier applies to a baked-in
workflow.
that parses as a URL is downloaded (MD5-hashed cache,
MIME-type-driven extension) into ComfyUI's input/ directory
and replaced with the local filename.
freely): S3 upload (presigned url per output), inline base64
(data per output, opt-in), and fire-and-forget webhook
delivery of a slim summary envelope (HMAC-SHA256 signed when
a secret is configured — see [§ Webhook](#webhook)).
postprocess) with separate worker pools; bounded inbound queue
with HTTP 503 + Retry-After when at capacity.
for parallelism. See [§ Multi-backend](#multi-backend).
OOM-recovery via /free, CUDA-fault → 503 health latch — see
[§ Reliability](#reliability) for the failure modes the wrapper
handles for you.
# 1. Dependencies
apt-get install -y libmagic1 # for python-magic
pip install -r requirements.txt
# 2. Run ComfyUI somewhere (default: 127.0.0.1:8188)
# 3. Run the wrapper
uvicorn main:app --host 0.0.0.0 --port 8000
# 4. Submit a job
curl -X POST http://localhost:8000/generate/sync \
-H 'Content-Type: application/json' \
-d '{"input": {"workflow_json": { ... }}}'
.env.example ships in the repo as a template for environment
configuration. Most defaults are sensible.
The provided zealman-api-docs URL is a panel API, not a native
ComfyUI :8188 endpoint. This checkout is configured for it in the
ignored .env file with `COMFYUI_API_MODE=zealman`. The wrapper submits
to /api/workflow/generate, polls `/api/workflow/result`, and downloads
the returned /output/... files into `COMFYUI_INSTALL_PATH` so the usual
S3, base64, and webhook post-processing continues to work.
First list the saved workflow cards and choose an ID:
curl -sS "$COMFYUI_API_BASE/api/workflow/list"
Then submit using that ID and node-field values from the panel's API
configuration (keys look like 187:text or `3:seed`):
{
"input": {
"workflow_id": "A01-文生图-Qwen2512高清放大",
"input_values": {"187:text": "一只在沙滩上奔跑的小狗", "3:seed": 42},
"return_outputs_as_base64": true
}
}
If ZEALMAN_WORKFLOW_ID is set, `workflow_id` may be omitted. The panel
must have the workflow imported and saved before the wrapper can run it.
Set MEDIA_API_KEYS, then configure one saved workflow ID per media type:
IMAGE_WORKFLOW_ID, `VIDEO_WORKFLOW_ID`, and `AUDIO_WORKFLOW_ID`. The
facade authenticates Authorization: Bearer and translates requests
to the normal queue. Optional _INPUT_TEMPLATE JSON maps request fields
to Zealman node values, for example:
IMAGE_INPUT_TEMPLATE={"187:text":"{prompt}","3:seed":"{seed}"}
For the bundled native Wan video workflow, map duration to frames so the
requested duration is reflected in the generated MP4:
MEDIA_VIDEO_DEFAULT_DURATION=5
MEDIA_VIDEO_FPS=8
VIDEO_INPUT_TEMPLATE={"3:positive_prompt":"{prompt}","3:negative_prompt":"{negative_prompt}","5:seed":"{seed}","4:num_frames":"{num_frames}"}
Supported routes:
POST /v1/images/generations
POST /v1/videos
POST /v1/audio/speech
GET /v1/media/outputs/{request_id}/{filename}
Image and video requests return an OpenAI-style {created, data[]} response.
Audio returns the requested binary format (mp3, `wav`, etc.) when using the
standard format name, or JSON when response_format is `url`/`b64_json`.
Copy .env.example to `.env`, fill in the three workflow IDs and a strong
MEDIA_API_KEYS value, then run:
./restart.sh
The script creates the persistent runtime/ComfyUI input/output directories,
rebuilds the image, restarts the service, and waits for local /healthz without
contacting rented GPUs. Explicit /health still checks upstream availability. The wrapper
listens on WRAPPER_PORT (default `1130`).
In Zealman mode, generated image, video, and audio files are downloaded to
local storage by default. Docker maps the project directory media-output/
to /data/media-output in the container:
MEDIA_OUTPUT_HOST_DIR=./media-output
MEDIA_PUBLIC_BASE_URL=http://127.0.0.1:1130
MEDIA_OUTPUT_PUBLIC=true
Change MEDIA_PUBLIC_BASE_URL to the public domain or port-mapped address of
this wrapper. file, `url`, and the default URL response return the local
file URL; b64_json and `base64` continue to return base64 data. S3 is only
used when configured explicitly in the environment or request payload.
| Endpoint | Method | Purpose |
|---|---|---|
| `/generate` | POST | Submit async — returns 202 + Result with `status: "queued"`. Caller polls `/result/{id}` or supplies a `webhook` for delivery. |
| `/generate/sync` | POST | Submit and block until terminal (`completed`/`failed`/`timeout`/`cancelled`). Cancel-on-disconnect: closing the connection mid-job marks the request cancelled and the worker queue skips remaining stages. |
| `/generate/stream` | POST | Submit and stream Server-Sent Events with progress + queue-position updates, ending with a final `final_result` event. |
| `/result/{id}` | GET | Read a previously-submitted job's current Result. 404 if unknown. |
| `/cancel/{id}` | POST | Soft-cancel: flip the Result status to `cancelled`. The wrapper queue checks this marker before processing each stage and skips ahead; a generation already in flight on ComfyUI is interrupted via ComfyUI's `/api/interrupt`. |
| `/queue-info` | GET | Current sizes of preprocess / generation / postprocess queues. |
| `/health` | GET | Liveness probe (200/503). See [§ Health endpoint](#health-endpoint). |
| `/` | GET | This README rendered as HTML. |
| `/docs` | GET | FastAPI's interactive OpenAPI docs. |
{
"input": {
"request_id": "optional-uuid-v4", // server-generated if omitted
// EITHER raw workflow…
"workflow_json": { /* ComfyUI API-format workflow */ },
// …OR a curated template (mutually exclusive with workflow_json):
"modifier": "ModifierClassName", // e.g. "Image2Image"
"modifications": { "prompt": "...", "seed": 42 },
// Optional: per-request S3 override of the env defaults.
"s3": {
"access_key_id": "...",
"secret_access_key": "...",
"endpoint_url": "https://s3.amazonaws.com",
"bucket_name": "your-bucket",
"region": "us-east-1"
},
// Optional: per-request webhook override. `secret` enables
// HMAC-SHA256 signing for this request (overrides
// `WEBHOOK_SECRET` env default).
"webhook": {
"url": "https://your-endpoint.example.com",
"extra_params": {"any": "context"},
"secret": "shared-secret-for-signing",
"timeout": 30
},
// Optional: inline outputs as base64 in the Result envelope.
// See § Inline base64 output. Default false. The header
// `X-Return-Outputs-As-Base64: 1` flips this server-side.
"return_outputs_as_base64": false
}
}
request_id is optional; the wrapper mints a UUIDv4 if absent.
The same id flows through the pipeline and into the Result.
workflow_json and `modifier` are mutually exclusive — supply
exactly one. modifications only applies in modifier mode.
Returned from /generate/sync (inline), `/result/{id}` (after
async completion), and as the result payload of the final
final_result SSE event:
{
"id": "request-uuid",
"message": "Processing complete.",
"status": "pending|processing|generating|generated|completed|failed|cancelled",
"comfyui_response": { /* ComfyUI's /history/{prompt_id} payload, slim by default — see INCLUDE_COMFYUI_RESPONSE */ },
"output": [
{
"filename": "ComfyUI_00001_.png",
"local_path": "/workspace/ComfyUI/output//ComfyUI_00001_.png",
"subfolder": "",
"type": "output",
"node_id": "9",
"output_type": "images",
"url": "https://your-bucket.../ComfyUI_00001_.png", // only when S3 upload succeeded
"upload_error": "...", // only on S3 upload failure
"data": "iVBORw0KGgoAAAA...", // only when base64 inlining was opted in
"mimetype": "image/png", // accompanies `data`
"error": "..." // only when base64 inlining failed (e.g. file too large)
}
],
"timings": {
"preprocess_ms": 12,
"generation_ms": 18412,
"postprocess_ms": 743
}
}
output[].url is populated only when S3 is configured (env or
per-request) and the upload succeeded. With no S3 configured,
files exist only at local_path inside the wrapper container.
On upload failure upload_error is set and `url` is omitted.
output[].data (base64-encoded bytes) and `output[].mimetype`
are populated only when the customer opted in via either
input.return_outputs_as_base64: true in the body or the
X-Return-Outputs-As-Base64: 1 request header. Files larger
than OUTPUT_BASE64_MAX_BYTES (default 10 MB) are skipped with
output[].error set instead of `data`. Base64 mode coexists
with S3 — both data and `url` are populated when both are
configured.
By default comfyui_response is `{}` to keep wire weight down.
Set INCLUDE_COMFYUI_RESPONSE=true (env) or
X-Include-ComfyUI-Response: 1 (request header) to receive the
full ComfyUI history blob.
Each event is a JSON object on a single data: …\n\n line.
Two event shapes — progress and final:
Progress event (multiple per job, on status / queue-position
change):
{
"request_id": "uuid",
"status": "queued|processing|generating|generated",
"message": "human-readable status",
"timestamp": "2026-05-07T12:34:56.789Z",
"elapsed_time": 45.2,
"queue_position": {
"current_queue": "preprocessing|generation|postprocessing|processing|unknown",
"position": 3,
"queue_size": 5,
"estimated_wait_time": 360 // seconds; rough heuristic (30s/120s/20s per stage)
},
"queue_info": {
"preprocess_queue_size": 2,
"generation_queue_size": 1,
"postprocess_queue_size": 0
},
"output_count": 1 // optional: number of outputs ready so far
}
Final event (emitted once when the job reaches a terminal
state; this is what your client should look for to close the
stream):
{
"request_id": "uuid",
"status": "final_result",
"result": { /* full Result envelope, see above */ },
"elapsed_time": 47.1
}
estimated_wait_time is a rough heuristic — fixed seconds-per-job
multipliers per stage, not a measurement. Treat it as "order of
magnitude" rather than an SLO.
import requests
payload = {"input": {"workflow_json": { /* ... */ }}}
r = requests.post("http://localhost:8000/generate/sync", json=payload)
result = r.json()
print(result["status"], result["output"])
/generate/sync blocks until the job reaches a terminal state.
There is no server-side timeout query parameter — the connection
stays open as long as the worker is making progress.
r = requests.post("http://localhost:8000/generate", json=payload)
job_id = r.json()["id"]
while True:
rr = requests.get(f"http://localhost:8000/result/{job_id}")
data = rr.json()
if data["status"] in ("completed", "failed", "timeout", "cancelled"):
break
time.sleep(2)
Add input.webhook.url to the payload (or set the global
WEBHOOK_URL env). After postprocess the wrapper POSTs a slim
summary to that URL — see [§ Webhook](#webhook) for the body
shape and caveats. Consumers needing the full Result (including
comfyui_response) should fetch it via `GET /result/{id}` from
their webhook handler.
POST /generate/stream returns `text/event-stream`. Use any
SSE client that supports POST + a JSON body — EventSource
is GET-only and will not work here.
import json, requests, sseclient
r = requests.post("http://localhost:8000/generate/stream", json=payload, stream=True)
client = sseclient.SSEClient(r)
for ev in client.events():
data = json.loads(ev.data)
if data["status"] == "final_result":
print("done:", data["result"])
break
pos = data.get("queue_position", {})
if pos.get("position"):
print(f"queue {pos['current_queue']} pos {pos['position']}/{pos['queue_size']}")
JavaScript: use fetch() with `body` and parse the stream
manually, or a library like @microsoft/fetch-event-source that
adds POST support to the SSE pattern.
requests.post(f"http://localhost:8000/cancel/{job_id}")
The next stage boundary (or the generation worker's per-5s cancel
check) sees the marker and skips ahead. An in-flight ComfyUI
generation is interrupted via /api/interrupt. Cancellation is
soft — the request returns cancelled rather than terminating
the connection mid-output.
All configuration is via environment variables (.env is loaded
automatically if present in the working directory). Defaults are
in config/config.py.
| Env | Default | Notes |
| `COMFYUI_API_BASE` | `http://127.0.0.1:8188` | Single-backend HTTP base. Overridden by `COMFYUI_BACKENDS` if that's set. |
| `COMFYUI_BACKENDS` | (unset) | Comma-separated list of backend HTTP bases — see [§ Multi-backend](#multi-backend). When set, the wrapper spawns one generation worker per entry. |
| `COMFYUI_INSTALL_PATH` | `/workspace/ComfyUI` | Root of the ComfyUI install. Used to locate `input/` and `output/`. |
The wrapper waits on ComfyUI's WebSocket for execution events.
On a quiet connection it cross-checks /queue + `/history`
before giving up.
| Env | Default | Notes |
| `WEBSOCKET_INITIAL_TIMEOUT` | `60` | Seconds to wait for the first WS message after submitting the workflow. Bump for cold loads where the first inference step takes longer (large diffusion models on a cold GPU). |
| `WEBSOCKET_MESSAGE_TIMEOUT` | `120` | Seconds between subsequent WS messages before falling back to queue/history polling. |
| `WEBSOCKET_MAX_NO_MESSAGE_RETRIES` | `3` | When polling falls through too, fail the job after this many consecutive timeouts. |
| `WEBSOCKET_MAX_RECONNECTS` | `10` | When the WS closes mid-job and the job is still running on ComfyUI's side, how many times to reconnect before giving up. |
| Env | Default | Notes |
| `PREPROCESS_WORKERS` | `2` | Concurrent preprocess tasks (URL fetch + workflow modification). |
| `GENERATION_WORKERS` | `1` | Ignored when `COMFYUI_BACKENDS` is set — one generation worker per backend. |
| `POSTPROCESS_WORKERS` | `2` | Concurrent postprocess tasks (file move + S3 upload + webhook). |
| `MAX_QUEUE_SIZE` | `100` | Inbound preprocess queue cap. Submits over this return HTTP 503 + `Retry-After: 5`. |
| Env | Default | Notes |
| `INCLUDE_COMFYUI_RESPONSE` | `false` | When `true`, every Result includes the full ComfyUI `/history` blob in `comfyui_response`. Off by default to keep responses small. Per-request override via `X-Include-ComfyUI-Response: 1`. |
The request and response stores back the wrapper's job state.
memory is a per-process dict — fine for a single wrapper
replica; results are lost when the process restarts. redis
shares state across replicas and survives wrapper restarts
within CACHE_TTL.
| Env | Default | Notes |
| `API_CACHE` | `memory` | `memory` or `redis`. |
| `API_CACHE_TTL` | `21600` | Seconds (6 h). How long results stay retrievable via `/result/{id}`. |
| `REDIS_HOST` | `localhost` | Only when `API_CACHE=redis`. |
| `REDIS_PORT` | `6379` | |
| `REDIS_DB` | `0` | |
| `REDIS_PASSWORD` | (empty) | Optional. |
API_CACHE=redis requires the optional `redis` package (already
in requirements.txt). The wrapper passes `REDIS_HOST` /
REDIS_PORT / `REDIS_DB` / `REDIS_PASSWORD` through to aiocache;
keys are namespaced request_store: and `response_store:`.
When opted in, the postprocess worker reads each generated
file and embeds it as base64 under output[].data (with a
sniffed mimetype). Useful for local dev (no S3 setup), small-
image pipelines that don't want a separate fetch round-trip,
and webhook consumers who otherwise get a useless internal
local_path.
Two ways to opt in (per-request):
the body field server-side, so the postprocess flow is
identical either way. Useful for curl and shell pipelines.
| Env | Default | Notes |
| `OUTPUT_BASE64_MAX_BYTES` | `10485760` (10 MB) | Per-file cap. Files larger than this are skipped with `output[*].error` set; the rest still inline. |
Base64 inflates payload size ~33%. The encoded data field
sits in the response store for CACHE_TTL (default 6 h), so
sustained traffic at large image sizes can chew memory — bump
API_CACHE=redis if that's a concern, and tune `CACHE_TTL`.
Coexists with S3: when both are configured, data and `url`
are both populated and the customer picks.
When configured, output[] files are uploaded after generation
and output[].url is populated.
| Env | Notes |
| `S3_ACCESS_KEY_ID` | Required to enable. |
| `S3_SECRET_ACCESS_KEY` | Required to enable. |
| `S3_BUCKET_NAME` | Required to enable. |
| `S3_ENDPOINT_URL` | Optional — for S3-compatible providers (R2, B2, MinIO). Leave unset for AWS S3. |
| `S3_REGION` | |
| `S3_CONNECT_TIMEOUT` | Default `60`. Seconds. |
| `S3_CONNECT_ATTEMPTS` | Default `3`. |
A per-request input.s3 block in the payload overrides these
defaults for that single job.
| Env | Notes |
| `WEBHOOK_URL` | Default webhook endpoint; per-request `input.webhook.url` overrides. |
| `WEBHOOK_SECRET` | Optional. When set, the wrapper signs every outgoing webhook with HMAC-SHA256 over the body bytes and sends `X-Webhook-Signature: sha256= |
| `WEBHOOK_TIMEOUT` | Default `30`. Seconds. |
| Env | Default | Notes |
| `DEBUG` | `false` | When `true`, prints a config summary at startup. |
The wrapper can drive N ComfyUI processes for parallelism — one
per GPU on a multi-GPU host, or several per GPU when the model
fits multiple times in VRAM.
┌── ComfyUI on :8188 (CUDA_VISIBLE_DEVICES=0)
wrapper :8000 ─┬───┼── ComfyUI on :8189 (CUDA_VISIBLE_DEVICES=1)
│ ├── ComfyUI on :8190 (CUDA_VISIBLE_DEVICES=2)
│ └── ComfyUI on :8191 (CUDA_VISIBLE_DEVICES=3)
generation_queue (one consumer per backend)
Set COMFYUI_BACKENDS to a comma-separated list of bases:
COMFYUI_BACKENDS="http://127.0.0.1:8188,http://127.0.0.1:8189"
The wrapper spawns one generation worker per entry; N consumers
reading from the shared generation_queue form a natural pool —
the queue itself routes each job to the first idle worker.
When COMFYUI_BACKENDS is unset, the wrapper falls back to the
single-backend COMFYUI_API_BASE. Existing single-backend
deployments need no changes.
Each ComfyUI process is independently health-checked, gets its
own GPU-fault latch, and runs OOM recovery (/free) in
isolation. Cancellation routes to the right backend
automatically — the worker that holds an in-flight job is the
one that posts /api/interrupt.
The wrapper does not start the ComfyUI processes itself —
operator's responsibility (typically supervisord, systemd, or a
process supervisor that reads container env). Pin each ComfyUI
to its GPU via CUDA_VISIBLE_DEVICES.
When input.webhook.url is set on the request (or the
WEBHOOK_URL env is set globally), the wrapper POSTs a summary
to that URL after postprocess finishes. Body shape:
{
"id": "request-uuid",
"status": "completed",
"message": "Processing complete.",
"output": [ /* same as Result.output */ ],
"timings": { "preprocess_ms": 12, "generation_ms": 18412, "postprocess_ms": 743 },
"extra": { /* whatever the customer put in webhook.extra_params */ }
}
Notes:
default. Set WEBHOOK_SECRET (env, applies globally) or
per-request input.webhook.secret (overrides). When set, the
wrapper sends X-Webhook-Signature: sha256= computed
over the exact JSON body bytes the consumer receives. Per-
request secret takes precedence; supply an empty
per-request secret together with a per-request webhook
block to opt this request out of signing while leaving the
env default in place for other requests.
timeout, no retry. Failures are logged but don't affect the
job. Webhook consumers should be idempotent (the same id
may not be delivered twice in normal operation, but a flaky
network can produce ambiguous outcomes).
not in the webhook body — typically several KB and most
consumers don't want it. Fetch it via GET /result/{id} if
you need it, or set INCLUDE_COMFYUI_RESPONSE=true.
land under the extra key, NOT merged into the top level —
so extra_params: {"status": "anything"} can't clobber the
wrapper's status field. The signature covers the full body
including extra.
Read the raw request body (NOT the parsed JSON dict — body bytes
matter) and recompute the HMAC. Use a constant-time comparison.
Python (FastAPI / Flask):
import hmac, hashlib
WEBHOOK_SECRET = b"shared-secret"
def verify(raw_body: bytes, signature_header: str) -> bool:
if not signature_header.startswith("sha256="):
return False
expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
received = signature_header.split("=", 1)[1]
return hmac.compare_digest(received, expected)
# In a FastAPI route:
async def handler(request: Request):
raw = await request.body()
sig = request.headers.get("x-webhook-signature", "")
if not verify(raw, sig):
raise HTTPException(401, "Invalid webhook signature")
payload = json.loads(raw)
...
Node (Express):
const crypto = require('crypto');
function verify(rawBody, signatureHeader, secret) {
if (!signatureHeader || !signatureHeader.startsWith('sha256=')) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = signatureHeader.slice(7);
// Use timingSafeEqual to avoid timing-attack leakage.
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(received, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
The signature is over the wire bytes, so your handler must read
the raw body before any JSON-parsing middleware mutates it
(Express needs express.raw({type: 'application/json'})).
GET /health walks every backend and returns 200 / 503 + a
JSON body:
{
"status": "healthy",
"cache_type": "memory",
"queues": {"preprocess": 0, "generation": 0, "postprocess": 0},
"backends_healthy": "2/2",
"gpu": {"unrecoverable": false, "reason": ""},
"backends": [
{
"base": "http://127.0.0.1:8188",
"http_ok": true,
"websocket_ok": true,
"system_stats": { /* ComfyUI's /system_stats output */ },
"gpu": {"unrecoverable": false, "reason": ""},
"healthy": true
},
{ /* ... per backend ... */ }
]
}
The endpoint returns 503 when:
access, device-side assert, misaligned address, etc.) — these
don't survive ComfyUI's /free, so the wrapper marks the
backend permanently unhealthy and the orchestrator should
replace the pod.
OOM is not a health failure — the wrapper calls the affected
backend's /free to unload models and continues. The job that
hit OOM still fails (we don't retry inside the wrapper) but
subsequent jobs work.
A short list of failure modes the wrapper handles for you, in
case you've hit them in plain ComfyUI:
/queue + `/history` and reconnects up to
WEBSOCKET_MAX_RECONNECTS times if the job is still alive on
ComfyUI's side. Without this, a transient WS close fails the
job even when ComfyUI is still happily running it.
assigns a new prompt_id on cache hit but doesn't copy the
original outputs into the new history entry. The wrapper
detects this (empty outputs map on a "completed" job),
clears the stale entry from /history, and re-submits to
force a fresh execution.
in the error path, posts to ComfyUI's /free to unload all
models, and keeps the pod alive. The OOM job itself is failed.
memory access, device-side assert, etc. The wrapper latches
this on the affected backend and /health flips to 503. Your
orchestrator should treat that as "replace the pod".
prefers ComfyUI's files array (this run's actual outputs)
over images (which can carry stale references from a cached
prior run) when both are present.
startup probes ComfyUI's HTTP and WebSocket endpoints before
the first POST.
submits return 503 + Retry-After: 5 instead of stacking
forever. Lets the caller back off.
Two ways to specify a workflow:
Send the API-format workflow directly:
{
"input": {
"workflow_json": {
"10": {
"inputs": { "image": "https://example.com/input.jpg", "upload": "image" },
"class_type": "LoadImage"
}
// … rest of workflow
}
}
}
URLs anywhere in the workflow are downloaded automatically (see
[§ Automatic URL fetch](#automatic-url-fetch)).
Bundle a workflow and the parameter-substitution logic into a
Python class derived from BaseModifier:
# modifiers/image2image.py
from modifiers.basemodifier import BaseModifier
import random
class Image2Image(BaseModifier):
WORKFLOW_JSON = "workflows/image2image.json"
async def apply_modifications(self):
self.workflow["3"]["inputs"]["seed"] = await self.modify_workflow_value(
"seed", random.randint(0, 2**32))
self.workflow["6"]["inputs"]["text"] = await self.modify_workflow_value(
"prompt", "")
self.workflow["10"]["inputs"]["image"] = await self.modify_workflow_value(
"input_image", "https://example.com/default.jpg")
await super().apply_modifications() # runs the URL-download pass
Customer payload:
{
"input": {
"modifier": "Image2Image",
"modifications": {
"prompt": "a beautiful sunset",
"seed": 12345,
"input_image": "https://example.com/photo.jpg"
}
}
}
workflow_json and `modifier` are mutually exclusive — supply
exactly one. modifications is silently ignored without
modifier.
BaseModifier.replace_workflow_urls() walks the workflow before
submission. Any string that parses as an HTTP/HTTPS URL is:
1. Downloaded (one-shot httpx GET).
2. MD5-hashed; cached in ComfyUI's input/ directory keyed by
the hash so repeat submissions don't re-download.
3. MIME-type-detected via python-magic for the right file
extension.
4. Replaced in the workflow with the local filename.
Both raw-workflow and modifier-mode submissions go through this
pass.
A second pre-submission pass (BaseModifier.replace_random_ints)
walks the workflow and substitutes the literal string
__RANDOM_INT__ with a fresh `random.randint(0, 232 - 1)` per
request. Most useful for the seed field of `KSampler`-style
nodes — set it once in your stored workflow and every submission
gets a fresh seed without client-side bookkeeping:
"3": {
"inputs": { "seed": "__RANDOM_INT__", "steps": 20, ... },
"class_type": "KSampler"
}
Two substitution shapes:
"__RANDOM_INT__", the result replaces the string with an
actual integer. ComfyUI's seed and similar int-typed inputs
need this — a string would fail the schema validator.
larger value (e.g. "prefix-__RANDOM_INT__"), the result is
still a string but with the placeholder replaced. Useful for
filename prefixes.
Each occurrence is independent — multiple __RANDOM_INT__ fields
in one workflow get distinct random values. Both raw-workflow and
modifier-mode submissions go through this pass.
The HTTP status code on /generate/sync and `/result/{id}`
mirrors the terminal Result.status so callers that key off
the HTTP code (the daemon's sync forwarder, benchmark drivers,
default requests/`fetch` happy-path checks) can't misread a
fast failure as a fast success.
| HTTP | When |
| `200` | Result is `completed`, OR `/result/{id}` lookup returned a non-terminal in-progress state (so polling callers can distinguish "still working" from "done with status X"). |
| `202` | Async submit accepted (returns the request id). |
| `400` | Invalid JSON / Pydantic validation failure. |
| `404` | `/result/{id}` or `/cancel/{id}` for an unknown id. |
| `499` | `/generate/sync` cancel-on-disconnect path, OR a terminal `Result.status: "cancelled"` returned to a polling caller. |
| `500` | Result is `failed` and `message` doesn't match an upstream-connectivity pattern — generation-side error (bad workflow, OOM, etc.). Also: any unexpected wrapper internal error. |
| `502` | Result is `failed` and `message` matches an upstream-connectivity / timeout pattern (`cannot connect`, `failed to post workflow after`, `connection refused`, `websocket timeout`, `timed out`, etc.) — ComfyUI was unreachable or stopped responding. |
| `503` | Inbound queue saturated (`Retry-After: 5`), OR `/health` failed (one or more backends down / GPU latched). |
The full Result envelope (with status, `message`,
comfyui_response, etc.) ships in the body regardless of HTTP
code. Inspect message and `comfyui_response.
the full engine-side reason on a 4xx/5xx.
┌─ preprocess_worker × N (URL fetch, modifier apply)
/generate{,/sync,/stream}
──▶ Pydantic validate ──▶ preprocess_queue
│
▼
generation_worker × len(COMFYUI_BACKENDS)
(POST /prompt → WS wait → /history)
│
▼
postprocess_worker × N
(move outputs, S3 upload, webhook)
│
▼
response_store ──▶ /result, /generate/sync resolve, SSE final_result
The three queues are in-process asyncio.Queues. The Result
envelope flows through the response store (memory or
redis per `API_CACHE`); set Redis if you want results to
survive a wrapper process restart.
pip install -r requirements-dev.txt
pytest # unit tests for the bits that don't need ComfyUI
uvicorn main:app --reload --port 8000
Tests cover the OOM/CUDA error classifiers, the cached-output
validation, and the multi-backend config parser. They don't
exercise the WebSocket path against a real ComfyUI — that's
left for integration testing in a downstream consumer.
and generation. Once a job reaches postprocess (file moves, S3
upload, webhook delivery) cancel is a no-op — the postprocess
worker doesn't check the cancel marker. ComfyUI itself may also
take a few seconds to stop sampling after an /api/interrupt,
so a cancel that arrives mid-generation may complete after an
output has already been written.
seconds. Don't rely on /result/{id} after that window.
If you need files reachable externally, set up S3 (then url
is populated) or run the wrapper next to whatever serves the
files (a sidecar, a shared volume, etc.). The wrapper does not
serve output files directly.
beyond the underlying httpx attempt. Consume webhooks
idempotently.
POST /v1/videos now accepts a public `model` name and resolves the workflow and node parameters automatically. Four public model names resolve six curated video workflows and support synchronous and asynchronous generation, authenticated status/download routes, and discovery through `/v1/models` and `/v1/video-models`. See [视频 API 与模型映射](docs/video-api.md) and [Python client](examples/video_client.py).
For QuantumNous/NewAPI, import [ComfyUI Video task plugin](integrations/newapi/comfyui-video.js) and follow [NewAPI server setup](docs/newapi-video.md). Public model names match the original model families; workflow variants and finetunes are exposed separately in the catalog.