Skip to main content

Local proxy exposing the Comfy API v2 in front of a self-hosted ComfyUI instance.

Project description

comfy-api-proxy

CI

A local service that puts the Comfy API v2 in front of a self-hosted ComfyUI instance. The same client SDK code that talks to Comfy Cloud can drive a ComfyUI on your own machine instead — and, since this proxy is Python + aiohttp (the same stack as ComfyUI core), the same adapter can later move into core itself. That's the "one contract, three surfaces" idea: Comfy Cloud, this proxy, and (eventually) ComfyUI core all speak the same /api/v2/ shape, so integrator code doesn't need to fork depending on where it's pointed.

Requirements & install

  • Python 3.10+ (CI runs 3.10, 3.11, and 3.12 on every pull request).

  • Install from PyPI:

    pip install comfy-api-proxy
    

Quickstart

Against a real ComfyUI

Point the proxy at an already-running ComfyUI and it serves /api/v2/* on its own port:

# Start in the background (defaults: ComfyUI at 127.0.0.1:8188, serve on :8189):
comfy-api-proxy start
comfy-api-proxy status
comfy-api-proxy stop

# Point it elsewhere / co-locate with ComfyUI to enable model-directory uploads:
comfy-api-proxy start --comfyui http://127.0.0.1:8188 --port 8189 \
  --comfyui-base-dir /path/to/ComfyUI

# Or run in the foreground (Ctrl+C to stop):
comfy-api-proxy run

No-GPU demo

demo/fake_comfyui.py is a stand-in ComfyUI — just enough of the native /prompt / /history / /queue / /view / /ws surface to run a workflow end to end without a GPU (or ComfyUI installed at all):

python demo/fake_comfyui.py &          # a stand-in ComfyUI on :8188
comfy-api-proxy &                      # the proxy on :8189
python demo/run_demo.py                # submit → wait → download

demo/run_demo.py drives the proxy through the real Python SDK (comfy_sdk.Comfy) — the same client code you'd point at Comfy Cloud — and imports it from a sibling checkout, so clone the SDK repo (see SDKs and the API contract below) next to this one before running the demo:

some-parent-dir/
├── comfy-api-proxy/   (this repo)
└── ComfyPythonSDK/

CI never depends on that checkout being present: the test suite (tests/test_smoke.py) drives the proxy's own HTTP surface directly with only the standard library, so pytest works with nothing but this repo installed.

The /api/v2/ surface

Wraps ComfyUI's native HTTP + WebSocket API one-to-one:

v2 operation Backed by
POST /api/v2/jobs Resolves any core/ASSET reference in the workflow to the filename ComfyUI expects, then POST /prompt
GET /api/v2/jobs/{id} GET /history/{id} (+ /queue while queued/running) — the authoritative, pollable state
POST /api/v2/jobs/{id}/cancel ComfyUI's atomic POST /api/jobs/{id}/cancel
GET /api/v2/jobs/{id}/events Server-Sent Events, driven by ComfyUI's /ws (the only live signal ComfyUI exposes)
POST /api/v2/assets Multipart upload; blake3-hashed and deduped locally; routed to ComfyUI's /upload/image for workflow inputs, or placed directly in a model directory (see below) for model weights
POST /api/v2/assets/from-hash, HEAD /api/v2/assets/by-hash/{hash} Local hash index
GET /api/v2/assets/{id}, GET /api/v2/assets/{id}/content Local index / ComfyUI /view, Range-capable

Poll-first, same as the canonical contract: GET /api/v2/jobs/{id} is always the source of truth for a job's state; the SSE stream (GET /api/v2/jobs/{id}/events) is a live convenience layered on top of it — a client that never opens it still sees the same state by polling. See Live events (SSE) below for what the stream carries and its concurrent-connection limit.

Partner (API) node auth — extra_data

Workflows that use partner/API nodes (Gemini, etc.) need a Comfy API key to authenticate them. Pass it alongside the workflow on submit:

POST /api/v2/jobs
{
  "workflow": { /* API-format graph */ },
  "extra_data": { "api_key_comfy_org": "comfyui-…" }
}

extra_data is a closed, typed object — the only accepted field is api_key_comfy_org (any other shape is rejected 400 invalid_request). One key authenticates every partner node in the workflow. The proxy forwards it verbatim onto ComfyUI's /prompt call and never stores or logs it (ComfyUI likewise strips it from history). Omit extra_data entirely when you have no key.

Model-file uploads (checkpoints/, loras/, vae/, ...)

ComfyUI's own /upload/image only understands input/output/temp — it has no endpoint for placing a file into a model directory. This proxy can do that itself, but only when it is co-located with ComfyUI (same host, sharing a filesystem) and started with --comfyui-base-dir pointing at the ComfyUI install root. Without that flag, model-directory uploads are rejected with a clear error; workflow-input uploads (images, etc.) work either way.

When enabled, a model upload must clear all of the following before a byte touches disk:

  • safetensors-only, verified by parsing the file's own header (the length-prefixed JSON tensor index) — never a pickle/torch.load path.
  • Allowlisted destination roots only — the real ComfyUI model directories (checkpoints, loras, vae, controlnet, ...). configs and custom_nodes are deliberately excluded even though ComfyUI itself has directories by those names, since one holds arbitrary YAML and the other arbitrary Python.
  • No path traversal, including through a symlink — the resolved, real (symlink-followed) destination path must still land inside the configured model directory.
  • Atomic, no-clobber writes — a temp file plus O_EXCL on the final destination, so two uploads can never race into a torn or silently overwritten file.

Once placed, the asset's file_path — and the value substituted for any core/ASSET reference to it in a submitted workflow — is the filename relative to the model-root directory (e.g. my_model.safetensors, not checkpoints/my_model.safetensors). That matches how ComfyUI's own combo widgets/loaders reference a model internally; a category-qualified path would be rejected as an unknown filename.

Live events (SSE)

GET /api/v2/jobs/{id}/events opens one WebSocket connection to ComfyUI, performs its feature_flags handshake, and translates the native progress/progress_state/preview/terminal messages into the v2 SSE event catalog (status, progress, preview, output), throttled to ~2 progress/preview events per second. If ComfyUI's WebSocket is unreachable, the stream falls back to polling /history so it still resolves to an authoritative terminal status rather than failing outright.

A proxy instance also caps concurrent event streams (8 by default, since each one holds open a ComfyUI WebSocket connection). Past that limit, a new stream request gets 429 too_many_streams with a Retry-After hint instead of queuing or degrading — GET /api/v2/jobs/{id} polling is always available regardless, and a slot frees up as soon as the stream it belongs to ends (the job finishes, or the client disconnects).

Security defaults

Everything here is on by default — no flags needed to get to the safe configuration:

  • Binds to 127.0.0.1 only. Widening --host to a non-loopback address is refused (the process exits with an error) unless --token is set, or --allow-insecure-bind is passed to explicitly opt out of that guard.
  • An optional static bearer token (--token) gates all of /api/v2/* when configured; unset by default, since a self-hosted single-user ComfyUI usually has nothing to authenticate against.
  • A default-on origin-check middleware — ported from ComfyUI core's own create_origin_only_middleware — rejects cross-site browser requests even when nothing else is configured, closing the DNS-rebinding / drive-by-CSRF hole any unauthenticated localhost server is exposed to.
  • Model-file uploads are safetensors-only, with path-traversal and symlink-escape guards (see Model-file uploads above) — and are rejected outright unless the proxy was started co-located with --comfyui-base-dir.

CLI reference

comfy-api-proxy --comfyui http://127.0.0.1:8188 --port 8189 [options]
Flag Default What it does
--comfyui http://127.0.0.1:8188 Base URL of the self-hosted ComfyUI to proxy.
--host 127.0.0.1 Address to bind. Widening past loopback requires --token or --allow-insecure-bind (see Security defaults).
--port 8189 Port to serve the v2 API on.
--token (unset) Require Authorization: Bearer <token> on every /api/v2/* request.
--comfyui-base-dir (unset) Filesystem root of a co-located ComfyUI install. Required to enable direct model-directory placement of model-file uploads; without it, model uploads are rejected (workflow-input uploads still work).
--max-upload-mb 100 Max single-request upload size, in MB.
--allow-insecure-bind false Permit binding a non-loopback --host without a --token. Unsafe — exposes an unauthenticated proxy to the network.

SDKs and the API contract

Any real integration — and demo/run_demo.py — uses the same client SDKs Comfy Cloud users use, just pointed at this proxy's --host:--port instead of api.comfy.org / cloud.comfy.org:

spec/openapi.yaml in this repo is a synced, filtered copy of that same Comfy API v2 contract — see spec/README.md for what "filtered" means, and Development below for how it's kept in sync.

Development

pip install -e ".[dev]"
ruff check .            # lint
ruff format --check .   # format check
mypy src/comfy_api_proxy   # type-check (lenient - see pyproject.toml)
pytest -v                # unit + end-to-end tests
python3 scripts/generate_models.py && git diff --exit-code src/comfy_api_proxy/schemas/_generated.py
                          # spec-drift check (also runs in CI)

These are exactly the checks CI runs (.github/workflows/ci.yml), each as its own job — lint/format, type-check, spec-drift, and test — with the test job running across Python 3.10, 3.11, and 3.12.

tests/test_smoke.py and tests/test_endpoints.py start the fake ComfyUI stand-in and the real proxy as subprocesses and drive both over plain HTTP (standard library only — no SDK, no third-party client, no dependency on another repo's credentials), covering upload → core/ASSET-reference → run → download, cancel, from-hash/by-hash, the SSE stream (including its concurrent-stream cap), and the model-placement security guards.

Keeping spec/openapi.yaml in sync

The vendored spec is generated, one-way (upstream → here), and never hand-edited. scripts/sync-spec.sh fetches the canonical spec from wherever it's passed (a local path or a URL), runs it through scripts/filter_openapi.py — which strips anything internal-only before a byte lands in this public repo — and writes the result to spec/openapi.yaml plus a spec/VERSION provenance pin. scripts/generate_models.py then regenerates the pydantic models (src/comfy_api_proxy/schemas/_generated.py) that tests/test_schema_conformance.py validates real handler responses against — those models are used only in tests, never on the request-handling hot path. CI's spec-drift job re-runs the generator and fails the build if the checked-in models don't match, so a spec sync without a regeneration gets caught immediately instead of drifting silently. See spec/README.md and docs/sync-workflow.md for the full design.

Scope

Implemented: submit (with core/ASSET resolution), poll, cancel, live SSE events, asset upload/download, from-hash/by-hash dedup, and guarded model-directory placement.

Known limitations:

  • State is in-memory only. The asset index and job store live in memory (as does ComfyUI's own history), so all state is lost on restart. A job id or asset id issued before a restart no longer resolves afterward — this includes the HMAC-signed stateless output ids, since the signing secret is regenerated per process. A client that persists an id across a proxy restart must expect a 404; a durable store is a follow-up.
  • Idempotency-Key dedup is in-memory. A reused key is rejected (422 idempotency_key_reuse — keys are single-use, no replay), but the claim set lives in the process and is cleared on restart, so a key reused across a proxy restart is not detected. A durable store is a follow-up.
  • Uploads are not zero-copy. Large uploads are read fully into memory / a temp file rather than true streaming.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

comfy_api_proxy-0.1.1.tar.gz (246.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

comfy_api_proxy-0.1.1-py3-none-any.whl (45.6 kB view details)

Uploaded Python 3

File details

Details for the file comfy_api_proxy-0.1.1.tar.gz.

File metadata

  • Download URL: comfy_api_proxy-0.1.1.tar.gz
  • Upload date:
  • Size: 246.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for comfy_api_proxy-0.1.1.tar.gz
Algorithm Hash digest
SHA256 475b454375907c379b0bd601603eb100b23e255c7478dcb145c09f310d5a499f
MD5 af42c891a3b3e175a855437d3364281c
BLAKE2b-256 3372bd41c57e64305d45d68abb7275264dd38e368fdf93c681643cdf12d2b8d7

See more details on using hashes here.

Provenance

The following attestation bundles were made for comfy_api_proxy-0.1.1.tar.gz:

Publisher: publish.yml on Comfy-Org/comfy-api-proxy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file comfy_api_proxy-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for comfy_api_proxy-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a2af153d9d786d27afa94dfc26010bc0e4a6db7bcbb88ad673ef98249e647137
MD5 a8fd6060b83cde6aab58fa340bb27970
BLAKE2b-256 6753be3b44705330443f526abd1046ecc625e44116aca6c5c2c7f1a5c27ca7dd

See more details on using hashes here.

Provenance

The following attestation bundles were made for comfy_api_proxy-0.1.1-py3-none-any.whl:

Publisher: publish.yml on Comfy-Org/comfy-api-proxy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page