Releases: Comfy-Org/comfy-python-sdk
Release list
v0.3.0
Added
- Queued model surface:
models.submit(),models.subscribe(),models.handle(), and theRequestHandlethey return (status(),get(),cancel(),iter_events()). Sync and async. Gated server side — a caller it is not enabled for gets403NotEnabled. ComfyError.resend_refused—Trueonly on the failure re-raised after a refused same-key resend. Check it before letting an outer retry wrapper re-enterrun(), which would mint a fresh key and bill a second generation.retry.may_have_claimed_key(exc)— whether a failure could have left the key claimed.router_exceptions.error_from_completion()— the typed exception a completed-but-failed queued request reports, orNone.
Changed
models.runnow raises the failure that claimed the key when a same-key retry is refused422 idempotency_key_reuse, with the refusal chained on__cause__and.resend_refusedset.except IdempotencyKeyReuseno longer catches this — catch the real failure (orComfyError) and inspect__cause__.- The substitution only applies when a claim-capable failure preceded the refusal. After a never-delivered transport error or a released
429, a422is a genuine refusal and is raised as itself. Among eligible failures the most recent wins, not the first.
Fixed
- A
409naming no error code raises plainComfyErrorinstead of guessingHashMismatch. Envelopedhash_mismatchand Router-bucketed409s are unaffected.
Full changelog: v0.2.0...v0.3.0
v0.2.0
Uploaded assets get a directly-fetchable URL, and an error answered by something in front of the service now says so — and keeps the one line that explains it.
Highlights
Asset.get_download_url() / AsyncAsset.get_download_url(). A directly-fetchable URL for an uploaded asset's bytes, mirroring Output.get_download_url() (same DownloadUrl, commits the asset first if needed). On Comfy Cloud it is a short-lived signed URL any fetcher can read until expires_at, which is what lets a local image be passed to a URL-taking image-to-image model via client.models.run(): upload the file as an asset, resolve its URL, put the URL in the model's input. The README's "Image to image — upload an asset first" section walks through the flow.
An error nothing in the stack recognised names its status. Such a response now carries .code == "http_<status>" (http_503, http_500) instead of "error". It is reached only after the envelope's own code, Router's error bucket and the status table have all declined, so a bare 401 still maps to Unauthorized and every documented code is untouched. Read it as answered by something in front of Router rather than by the service itself, so no service verdict was reached; retry per your own policy — nothing about what the SDK retries changed.
The body that explains a bare 503 is kept. A load balancer's no healthy upstream or upstream connect error or disconnect/reset before headers arrives as plain text with no JSON and no request id, and used to be discarded with the response. It is now on comfy_low.errors.ApiError.body_excerpt, and str() of the exception you catch — at both the protocol and the SDK layer — reads HTTP 503: no healthy upstream. The excerpt is one line, capped at 256 characters, with control, bidi and zero-width characters replaced, and it is None whenever the response stated a message of its own.
Upgrading
Additive except for one string: code that matched the literal "error" on an unrecognised ComfyError should match code.startswith("http_") instead.
Full detail in CHANGELOG.md.
v0.1.9
The release that makes client.models.run work against the live Comfy Router service, with billing-safe retries and typed Router errors.
Highlights
models.run reaches Router's real route. POST {COMFY_ROUTER_BASE_URL}/v2/models/{provider}/{model} with the partner model's own native JSON as the body, returning the provider's payload as-is. The previous envelope-style call answered 404 against the live service. COMFY_ROUTER_BASE_URL (default https://api.comfy.org) selects the Router deployment, separate from COMFY_BASE_URL; the model argument is the canonical {provider}/{model} id, validated locally.
Typed Router errors on every status. A Router error keeps its own error_type bucket regardless of HTTP status, and models.run raises the matching typed exception — except NotEnabled fires on a not-yet-enrolled account, InvalidInput, ModelNotFound, ConcurrencyLimitExceeded, RateLimited and the rest all catch as themselves instead of surfacing as status-derived aliases. The v2 jobs/assets surface decodes exactly as before.
Billing-safe automatic retry. On by default for models.run: a fresh Idempotency-Key per logical call, sent on every attempt, so a retried call is collected rather than billed twice. Connect-phase failures and paced 429s retry; deterministic refusals don't; unknown-outcome failures (5xx, timeouts) only with retry_possibly_in_flight=True. 60-second elapsed budget, jittered backoff, configurable via Comfy(retry=RetryPolicy(...)) or off with NO_RETRY.
Recoverable failures. Every failed-call exception carries .idempotency_key (replay it to collect an already-billed generation: client.models.run(model, arguments, idempotency_key=exc.idempotency_key)) and .request_id (the server's X-Comfy-Request-Id, the id to quote in a support request).
Credential hygiene. Documented key resolution (api_key= then COMFY_API_KEY), a local MissingApiKey instead of a wasted 401 round trip against Comfy Cloud, keys attached to exactly the two configured origins and never a third, and repr() that never renders a credential.
Full detail in CHANGELOG.md.
v0.1.8
Added
-
Job.get_workflow()/AsyncJob.get_workflow()— fetch the workflow behind a job, including one rehydrated by id. Returns the graph and aformatdiscriminator:save— the authoring workflow at the version the job ran, with canvas layout and editor-only nodes intactapi— the executed API-format graph
Branch on
format; which shape comes back depends on how the job was submitted, not on anything the caller controls. Jobs submitted through this SDK always getapitoday. -
Asset deletion —
Asset.delete()andassets.delete(id). Thanks to @jab416171 for the implementation. -
job_idon outputs and assets — get from an output file back to the job that produced it, without a side table. Absent for uploaded assets, which have no producing job. -
expires_aton assets.
Fixed
job_idandexpires_atwere present on the wire but not exposed by the public wrapper classes, so they were unreachable without touching a private attribute. Found by end-to-end testing against Comfy Cloud and a self-hosted proxy.
Verified
End to end against Comfy Cloud staging and a self-hosted comfy-api-proxy driving a real workflow: submit, poll, job_id on outputs and standalone asset lookups, output download, and get_workflow returning the submitted graph with no extra_data.
Requirements
assets.delete() needs backend support. Comfy Cloud has it. Self-hosted needs a comfy-api-proxy new enough to serve DELETE /api/v2/assets/{id} — older proxies return 405 Method Not Allowed.
v0.1.7
Breaking: the base URL moves from a constructor argument to COMFY_BASE_URL
Comfy() / AsyncComfy() target Comfy Cloud by default. To point the client at another deployment, set the COMFY_BASE_URL environment variable — an arbitrary endpoint is no longer part of the call surface.
- client = Comfy("https://my-deployment.example.com", api_key)
+ # COMFY_BASE_URL=https://my-deployment.example.com
+ client = Comfy(api_key=api_key)api_key is keyword-only now, so the old positional form raises TypeError rather than quietly reading a URL as a key.
The variable is read on each construction (not at import), must be an http(s) URL, and unset-or-blank means Comfy Cloud.
comfy_low, the documented escape hatch the clients are built on, still takes a base URL directly and is unchanged.
There is no 0.1.6 on PyPI — that number was consumed by a release-pipeline failure and never published. This is the first release containing the change above; @comfyorg/sdk ships the same change as 0.1.6.
Full changelog: v0.1.5...v0.1.7
v0.1.5
Maintenance release. No API changes — existing code needs no updates.
Fixes
- Ship
py.typed(PEP 561), so type checkers in consuming projects actually see the SDK's type information. Previously the annotations were shipped but ignored. - Derive
__version__from installed distribution metadata instead of a hardcoded string, so it can no longer drift from the released version.
Packaging
- Ship an MIT license (the package previously declared none) and fill in the empty package metadata.
- Stop sweeping local dev droppings into the sdist — it now contains only what is needed to build and run the tests.
Docs
- Docstrings for the public methods that had none.
- README now leads with the same branded header and linked "Related projects" table as the TypeScript and Swift SDKs, so the three SDK READMEs are consistent.
Repo rename
The repository moved from Comfy-Org/ComfyPythonSDK to Comfy-Org/comfy-python-sdk, matching the org's comfy-lower-kebab-case convention. GitHub redirects the old URLs.
The PyPI package name is unchanged (comfy-sdk) — pip install comfy-sdk is unaffected. This release is the first to carry the corrected repository/issues URLs in its published metadata.
Internal
- CI now catches deprecations, untyped defs, and typos in config.
- Added an SSE conformance leg to the gateway e2e suite, with graceful 501 degradation.
v0.1.4
Comfy Cloud now serves the v2 API on cloud.comfy.org. api.comfy.org continues to serve the node registry.
Breaking
api.comfy.org/api/v2/* no longer responds. If you pass that host explicitly, requests will 404 until you update.
Changes
base_urlnow defaults tohttps://cloud.comfy.org, soComfy(api_key=...)targets Comfy Cloud with no host argument.COMFY_CLOUD_BASE_URLis exported for callers who want the value.- Spec server URL, README, and docstrings updated to the new host.
- Passing an explicit
base_urlstill wins — self-hosted and serverless callers are unaffected.
Upgrading
# before
client = Comfy("https://api.comfy.org", api_key="comfyui-...")
# after — the default is correct, so the host can be dropped
client = Comfy(api_key="comfyui-...")Backward compatible for anyone already passing their own host; only api.comfy.org callers must change.
Verified against production: a real workflow submitted through the default host ran to succeeded and its output downloaded and byte-verified.
v0.1.3
Fixes
- Serverless gateway: follow-up links no longer 404 after submit. A gateway that serves the v2 API under a mount prefix (e.g.
/deployment/{id}/api/v2) returnsjob.urls.*links that already include that prefix. Those links were resolved againstbase_url, which carries the same prefix — doubling it, so the firstJob.refresh()after a successful submit raisedNotFound. Server-returned links (leading slash, containing/api/) now resolve against the origin (scheme + authority); the link is authoritative about its own path. Internal shorthand paths still resolve underbase_url, and Comfy Cloud / self-hosted behavior is unchanged.
Testing
- Adds an env-gated live integration suite (
tests/integration/test_gateway_e2e.py) covering upload → blake3 dedup fast path → img2img submit → poll → output download against a real gateway. Skipped unlessCOMFY_BASE_URL/COMFY_API_KEYare set.
Full changelog: v0.1.2...v0.1.3
v0.1.2
Highlights
- New:
Output.get_download_url()— get a fetchable URL for an output instead of streaming the bytes through your process. On Comfy Cloud / serverless it is a short-lived, self-authorizing signed storage URL (withexpires_at); on a self-hosted proxy it is the content endpoint (expires_at=None). Available on bothOutputandAsyncOutput. - The client now identifies itself via a
User-Agentheader; passclient_info=to attribute your own integration's traffic.
Fixes
- SSE: a read-idle timeout so a stalled stream can no longer hang
events(). - Map entity-specific 404s (
job_not_found/asset_not_found) toNotFound.
Docs
- Document
get_download_url(); correct the API-key placeholder (comfyui-…) and the Cloud host (api.comfy.org).
v0.1.1
Adds an optional `api_key=` parameter to `submit()`/`run()` (sync and async) that authenticates partner (API) nodes in a workflow, sent as `extra_data.api_key_comfy_org`. Omit it (or pass `""`) and no `extra_data` is sent. The key is never logged or persisted and does not participate in idempotency.
Published to PyPI via OIDC trusted publishing.