Skip to content

Releases: Comfy-Org/comfy-typescript-sdk

v0.5.0 — Job labels

Choose a tag to compare

@vqt123 vqt123 released this 10 Oct 15:46
5f65833

Adds job labels: submit takes metadata to label a job, and listJobs finds jobs by those labels. The full list of changes since 0.4.0 is below (from CHANGELOG.md).

Added

  • The rest of a job's state is readable off the handle. Job held the
    whole v2 job model privately and re-exported four fields, so a caller who
    wanted a run's duration had to cast past private to reach the timestamps.
    Eight read-only accessors now cover the remainder of the wire contract:
    createdAt and expiresAt (Date), startedAt and completedAt
    (Date | null — both are nullable on the wire, and a duration is
    completedAt minus startedAt), progress, queuePosition, metrics and
    urls. They read whatever state the handle currently holds, exactly like
    id/status — nothing re-fetches implicitly — and the object-valued three
    hand back a snapshot copy so editing the result cannot rewrite the handle's
    own links. progress is the same Progress the event stream yields (which
    now also carries currentNodeClass). Comfy Cloud's poll response has been
    reported to carry progress: null even for a running job, so
    job.events() remains the live-progress source there. Where the wire field is nullable, an absent or unusable value
    reads as "none" rather than as an Invalid Date or an empty snapshot; where
    it is required and non-nullable (createdAt, expiresAt, urls) a
    response that omits it raises ComfyError (unexpected_response) instead
    of handing back a Date that silently compares false against everything, or
    a {} typed as a full set of links.
  • Job labels: submit() takes metadata, and client.listJobs() finds
    jobs by it.
    metadata is a map of your own string keys to string values,
    sent as the body's metadata; a submit without it sends the same request
    as before. job.metadata returns the labels (an empty object when there
    are none). client.listJobs({ metadata, limit, signal }) lists your jobs
    newest first, filtered by up to three labels, as an async iterator that
    fetches page by page; a 429 on a page is retried as submit() retries one.
    The SDK also checks each listed job against the filter, so a server that
    ignores it yields only matching jobs. A next_cursor the walk has already
    followed raises ComfyError with code unexpected_response instead of
    fetching the same pages forever.
    A rejected map raises ComfyError with code metadata_invalid. Needs a
    server that supports job metadata: Comfy Cloud answers a labelled submit
    with metadata_not_supported and the list with not_implemented (501),
    and a self-hosted proxy keeps no labels (a filtered list yields nothing
    there).
  • RunJsonResult.replayed / RunBinaryResult.replayed — true when
    Router served the call from its Idempotency-Key record (Idempotent-Replayed)
    rather than by running the model again, so a replayed result can be told from
    a fresh charge: a replay restates the original run and is not billed a second
    time, so a spend tracker must skip it rather than add it up again. Derived
    from the header's PRESENCE, because Router omits it on a fresh run rather
    than sending false. It happens on this SDK's own collect loop (the same-key
    re-send after a paced 409/504) and on a caller's own retry under a
    supplied idempotencyKey. Always false from RequestHandle.get() — the
    queued result route carries no replay marker — so deduplicate re-collection
    by RequestHandle.requestId instead. OPTIONAL on both interfaces for the
    same source-compatibility reason as creditsUsed below; every result this
    SDK returns sets it.
  • creditsUsed on a run result — what Router priced the call at. Both
    arms of RunResult (RunJsonResult and RunBinaryResult) now carry the
    X-Comfy-Credits-Used response header. The queued result
    (RequestHandle.get(), and so comfy.models.subscribe) reads it too, but
    the contract does not declare it on the queued result route, so there it is
    unpinned: expect null, and reconcile spend against the workspace ledger if
    it matters (calling run for the price is a second, billed generation).
    Typed string | null, verbatim off the wire:
    the value is decimal and a caller reconciling money should parse it
    deliberately rather than receive a float this SDK chose the rounding of.
    Three caveats it is worth reading the TSDoc for — it is a price rather than
    a settled ledger entry, null means "not reported" and never "free", and a
    reported "0" is a real cost, so branch on presence (creditsUsed != null)
    rather than on the value being non-zero. A blank header is normalized to
    null for that last reason: passed through, "" would clear a presence
    check and then read as a cost of zero. The field is OPTIONAL on both
    interfaces so that a consumer's own RunJsonResult/RunBinaryResult
    literal — a test double written against 0.4.0, which shipped these
    interfaces without it — keeps compiling; every result this SDK returns sets
    it. Success-only, per the Router contract: the header is written on the path
    that returns a result, so a Router refusal carries no cost. A priced 200
    that the SDK then refuses client-side (response_too_large, a body-read
    timeout, an empty or non-JSON body) throws a ComfyError with no cost on
    it, so reconcile those failures against the workspace ledger.
  • routerErrors.QueueBacklogFull for the queue_backlog_full bucket: a
    queued submit refused with 429 because you already have too many queued
    requests waiting. Nothing was submitted or charged; submit again once some
    of your queued requests finish. It shares 429 with
    ConcurrencyLimitExceeded, and errorType tells them apart.

Changed

  • Breaking: new Comfy() now resolves COMFY_API_KEY, matching the Python
    SDK. The class client takes its credential from the explicit apiKey option,
    then from COMFY_API_KEY in the environment (trimmed; blank counts as unset,
    and it is read per construction), then — targeting Comfy Cloud, which always
    requires one — throws MissingCredentials at construction naming both ways to
    supply it, before any request. comfy.models.* already read that variable;
    the class client did not, so new Comfy() used to send no Authorization
    header and come back with a bare 401 from the server. Pointing
    COMFY_BASE_URL at another deployment keeps the keyless flow: an unresolved
    key there is not an error and means "send no credentials". A COMFY_API_KEY
    set in the environment, though, is now sent to that deployment too, as the
    Python SDK does — unset it for a keyless target. The
    new Comfy({ apiKey: process.env.COMFY_API_KEY }) workaround in the README
    is gone. Note for callers who relied on the old behaviour: new Comfy()
    against Comfy Cloud with no key anywhere now throws locally instead of failing
    on the first call, and an apiKey that is not a string — including null, as
    a JSON config file spells "absent" — now throws a TypeError at
    construction. Pass undefined (or omit the field) to fall back to the
    environment.

Fixed

  • idempotencyKey is now stamped onto every error comfy.models.run and comfy.models.submit throw, including raw transport failures and aborts. Previously only the run response-path ComfyError carried it; undici's transport-failure TypeError ("fetch failed"), an already-aborted signal's AbortError, and the queue path's RouterError (e.g. a bare 502 ProviderError) all escaped without it. On a transport failure the server never minted an X-Comfy-Request-Id, so the key is the only value that correlates the failure to the server-side record. It is now attached as an own idempotencyKey property on the raw throwable (its class, name, message and stack are otherwise untouched), and routerErrors.RouterError gained a typed idempotencyKey field. A failure to collect a generation additionally carries the Retry-After pace Router named, rather than reporting none. Where the throwable is one the caller owns and other calls share — an AbortController's signal.reason, which fetch hands to every concurrent call on that controller — each call receives an equivalent per-call error carrying ITS OWN key instead, so no caller reads a key belonging to another generation and controller.signal.reason is left unmodified. Mirrors the Python SDK's exceptions.translating(idempotency_key=…).
  • The droppedParams doc comments now match the vendored Router contract.
    The TSDoc on parseDroppedParams and RunJsonResult.droppedParams still
    described the pre-sync spec: it called the declared
    X-Comfy-Router-Dropped-Params schema a defect that would be reverted, and
    said only an explicit modelProvider translation could populate the field.
    The spec declares that header as one JSON-encoded string deliberately — each
    entry is a sentence carrying commas of its own — and names an automatic
    fallback_provider retry as a second producer, so a call that never set
    modelProvider can still come back with a non-null droppedParams. Comments
    only; the parsing and the header handling are unchanged.
  • retryAfter on a ComfyError from a Comfy method (submit(),
    client.jobs.get(), asset and output calls) now carries the server's
    Retry-After on every error. Only QueueFull kept it before; every other
    error had null even when the header was sent.
  • submit() waits at least one second before re-sending after a 429. A
    Retry-After: 0 used to re-send at once, over and over, for the whole
    one-minute retry budget.

v0.4.0 — Router alt-provider controls

Choose a tag to compare

@mattmillerai mattmillerai released this 18 Sep 04:48
21454a3

Adds the Comfy Router alt-provider controls to models.run, and the disclosure needed to tell an alt-provider run from a native one. Twin of comfy-sdk v0.4.0 (Python).

Added

  • modelProvider, strictMode and fallbackProvider on RunOptions — sent only when set, so a call that names none appends no query at all and is byte-for-byte the request this route always made.
  • servingProvider and droppedParams on RunResult, beside requestId, populated on every path including the queued one.
  • parseDroppedParams, FALLBACK_PROVIDER_HEADER and DROPPED_PARAMS_HEADER exported.

Why the new RunResult fields

With modelProvider, the response is translated back to the model's own native contract — so data is identical whether an alternate or the native provider served the call. X-Comfy-Router-Fallback-Provider is the only disclosure that they differed, and X-Comfy-Router-Dropped-Params the only disclosure that translating the request onto the alternate's schema could not carry a field.

Fixed

  • strict_mode was rendered with a truthiness test, so the string "false" — truthy in JS, and the exact spelling the sibling fallbackProvider option asks for — inverted the flag that decides whether the body is translated or passed through raw. Now an explicit === true.
  • fallbackProvider now accepts boolean | string and normalises the boolean. The server reads any value other than false as fallback ON, so "False", "0", "no" and "off" all type-checked and silently did the opposite.
  • queue_timeout is now in TERMINAL_ERROR_TYPES. It arrives on a 504, which the status >= 500 rule retried, so an admission timeout burned the whole retry budget re-asking the queue that had just said it could not admit the work.

Note for callers

Recovering a lost generation must resend these controls alongside idempotencyKey. A key's identity covers the query, so replaying without them presents the same key under a different query and leaves the generation uncollectable.

v0.3.0

Choose a tag to compare

@mattmillerai mattmillerai released this 15 Sep 00:40
83c0c87

comfy.models gains queued delivery and model discovery, and run no longer destroys a binary generation on the way back.

⚠️ Breaking

Types. RunResult is now a discriminated union: RunJsonResult (kind: "json") and the new RunBinaryResult (kind: "binary", data a Uint8Array, contentType the partner's own media type). A JSON result's runtime shape only gains kind, so existing code keeps running — but types need a narrowing:

const result = await comfy.models.run("bfl/flux-2-pro", { prompt });
if (result.kind === "json") result.data; // your TData again

Runtime. models.run now caps the response body it will buffer at 64 MiB by default, where it was previously unbounded — a larger result raises response_too_large instead of resolving. Nothing in the catalog returns that much today; if yours does, pass maxBytes: <larger> or maxBytes: null.

Added

  • comfy.models.submit / subscribe / handle — queue a request and collect it later, instead of holding one connection open for the whole generation. submit resolves to a RequestHandle with status(), get(), cancel() and async-iterable events(); subscribe is submit + poll + collect in one call. Preview-gated server-side — outside it, 403 not_enabled arrives as routerErrors.NotEnabled. (#133)
  • comfy.models.schema() / comfy.models.list() — ask Router what it runs and what each model takes. schema() sends a held etag as If-None-Match and resolves a 304 as an explicit { unchanged: true }; list() is async-iterable over the whole catalog, with list().page() for callers driving their own pagination. (#138)
  • maxBytes on models.run — per-call control of the cap above (see Breaking). DEFAULT_MAX_RESPONSE_BYTES is exported; null disables it. A breach raises code: "response_too_large" and is deliberately not retried. (#145)
  • routerErrors.errorFromCompletion(body, requestId) — the typed error a completed queued request reports, or null. (#133)

Fixed

  • A binary 200 no longer throws unexpected_response. The ElevenLabs audio models (elevenlabs/eleven_v3, elevenlabs/eleven_sfx_v2) were unusable: their bytes were UTF-8-decoded and JSON.parsed, destroying the generation after the server had run and billed it. run now reads Content-Type before touching the body. (#139)

Changed

  • A 200 declaring a non-JSON Content-Type is a binary result rather than an error. A 200 declaring none is parsed as JSON if it decodes and parses, and is binary otherwise. (#139)
  • models.run sends Accept: application/json, */*;q=0.9 — JSON still ranked first, but the client no longer claims to reject the binary branch its own contract declares. (#139)

npm i @comfyorg/sdk@0.3.0

Full changelog: v0.2.0...v0.3.0

v0.2.0

Choose a tag to compare

@mattmillerai mattmillerai released this 10 Sep 20:24
183e6c3

comfy.models.run now collects a generation it already dispatched instead of losing it, uploaded assets get a directly-fetchable URL, and a job's execution log can be read on demand.

Highlights

A generation that outlives the connection is collected, not lost. Comfy answers two failures with a Retry-After that means "the generation your Idempotency-Key already names has not finished — wait, then ask again for that one": a 409 carrying concurrency_limit_exceeded (an earlier attempt of this same call is still in flight, which is what a re-send after a dropped connection meets) and a 504 carrying deadline_exceeded (Comfy stopped holding the connection at its own bound while the provider carried on). Both used to be terminal, so a caller lost a generation that had already been dispatched and paid for. run now waits the interval the server named, re-sends the same key, and resolves with the result: no second dispatch, no second charge, nothing to enable. The collect loop has its own budget, retry.collectBudgetMs (default 20 minutes); collectBudgetMs: 0 switches it off alone, retry: false switches off retries and collection together.

The default comfy.models.run deadline is 20 minutes, up from 10. The deadline covers every attempt of a call, and Comfy's own deadline is 10 minutes — so the old default was exactly spent at the moment a deadline_exceeded 504 arrived, and the collect above could never start under it. Successful calls are unaffected; the deadline is a ceiling, not a wait. Pass timeoutMs: 600_000 for the old bound.

Every ComfyError carries retryAfter and idempotencyKey. The server's Retry-After in seconds (null when none was sent), and the key the failed call went out under — including a key the SDK minted for you, which was previously visible nowhere. Together they are what a manual re-ask needs once the collect budget is spent. RouterError gains retryAfter for the same reason.

Asset.getDownloadUrl(). A directly-fetchable URL for an uploaded asset's bytes, mirroring Output.getDownloadUrl(). On Comfy Cloud it is a short-lived signed URL any fetcher can read until expiresAt, which is what lets a local image be passed to a URL-taking image-to-image model: upload it as an asset, resolve its URL, put the URL in the model's input. Matches Asset.get_download_url() in the Python SDK.

Job.getLogs(). The run's captured execution log via GET /api/v2/jobs/{id}/logs, or null when the job has none — today only a job run on a serverless deployment has one; Comfy Cloud captures none. The text is untrusted workflow output and should be rendered as plain text.

Upgrading

No API removals. Two defaults changed on the comfy.models.run path: a 409/504 carrying Retry-After is now collected rather than thrown, and the default deadline is 20 minutes. If you relied on the previous behaviour, pass retry: { collectBudgetMs: 0 } and/or timeoutMs: 600_000.

Full detail in CHANGELOG.md.

v0.1.9

Choose a tag to compare

@mattmillerai mattmillerai released this 04 Sep 17:35
63223f6

One fix: a timeoutMs longer than five minutes is now actually honoured.

Highlights

Long requests no longer die at 300 s. On Node — the only runtime this
package supports — fetch is undici, and undici keeps two timers of its own on
the dispatcher, where no AbortSignal can reach them: headersTimeout and
bodyTimeout, both defaulting to 300 s. Anything asked to wait longer was
silently capped there, and it failed in the worst shape for a caller:
TypeError: fetch failed, with no HTTP status and no server request id to
quote in a support ticket, while the generation had already been dispatched and
billed.

comfy.models.run felt this hardest. It holds one request open for the whole
generation — the server does the provider-side polling inside the call, so
nothing arrives on the connection until the model finishes — and its own
default deadline is 600 s. The default path was capped at half its own
default
; you did not have to ask for anything unusual to hit it.

Both limits are now derived per request from the deadline the caller actually
asked for, and timeoutMs: null disables them the same way it already disabled
the deadline.

Your transport stays yours. A dispatcher already on the request — a
ProxyAgent, an mTLS agent, an egress policy — is delegated to rather than
replaced, so a host that installed one keeps it. A client constructed with its
own fetch is left alone entirely.

No API changes, no breaking changes. Upgrading is a version bump.

Full detail in CHANGELOG.md.

v0.1.8

Choose a tag to compare

@mattmillerai mattmillerai released this 01 Sep 01:31
2bc3f00

The release that makes comfy.models.run work against the live Comfy Router service.

Highlights

models.run reaches Router's real route. POST {routerBaseUrl}/v2/models/{provider}/{model} — the Router service moved its model routes from /v1/models to /v2/models, and against the live service the old path answered a bare 404. The vendored spec/router-openapi.yaml is synced to the same contract and the router-spec contract test re-pins the two together.

One call, native payloads. comfy.models.run(model, input) takes the canonical {provider}/{model} id and the model's own native JSON input, and resolves only when the generation is complete (server-side provider polling included) to { data, requestId }: data is the provider's payload untouched, typed unknown and narrowable with run<T>(...); requestId is the server's X-Comfy-Request-Id, present on thrown ComfyErrors too. An Idempotency-Key goes out on every call. Errors carry Router's own error_type on ComfyError.code — branch on the code.

Full detail in CHANGELOG.md.

v0.1.7

Choose a tag to compare

@wei-hai wei-hai released this 13 Aug 19:36
96c31cb

Added

  • job.getWorkflow() — fetch the workflow behind a job, including one rehydrated by id. Returns the graph and a format discriminator:

    • save — the authoring workflow at the version the job ran, with canvas layout and editor-only nodes intact
    • api — 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 get api today.

  • Asset deletion — Asset.delete() and assets.delete(id), matching the Python SDK.

  • jobId on 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.

  • expiresAt on assets.

Fixed

  • jobId and expiresAt were present on the wire but not exposed by the public wrapper classes, so they were unreachable. Found by end-to-end testing against Comfy Cloud and a self-hosted proxy.

  • getJobWorkflow given a job URL rather than a bare id fetched the job resource instead of its workflow, returning workflow and format as undefined with no error. Unlike getJob/cancelJob/getJobEvents, which receive a pre-built link, there is no urls.workflow — so the sub-resource has to be appended.

Verified

End to end against Comfy Cloud staging and a self-hosted comfy-api-proxy driving a real workflow: submit, poll, jobId on outputs, output download, getWorkflow returning the submitted graph with no extra_data, and the job-URL form above.

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.6

Choose a tag to compare

@wei-hai wei-hai released this 11 Aug 23:21
95d3ea3

Breaking: the base URL moves from a constructor argument to COMFY_BASE_URL

new Comfy() targets 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.

- const client = new Comfy("https://my-deployment.example.com", opts)
+ // COMFY_BASE_URL=https://my-deployment.example.com
+ const client = new Comfy(opts)

TypeScript callers get a compile error on the old form; untyped JavaScript callers get a TypeError rather than a silently ignored argument.

The variable is read on each construction (not at module load), must be an http(s) URL, and unset-or-blank means Comfy Cloud.

ComfyLow (@comfyorg/sdk/low), the documented escape hatch the client is built on, still takes a base URL directly and is unchanged.

Full changelog: v0.1.5...v0.1.6

v0.1.5

Choose a tag to compare

@wei-hai wei-hai released this 30 Jul 20:10
5ca3792

Maintenance release. No API changes — existing code needs no updates.

Packaging

  • Ship an MIT license (the package previously declared none) and add keywords for discoverability.
  • Publish source maps and declare sideEffects: false, so bundlers can tree-shake the SDK and consumers get usable stack traces.

Docs

  • TSDoc for the public API members that had none.
  • README now leads with the same branded header and linked "Related projects" table as the Python and Swift SDKs, so the three SDK READMEs are consistent.

Repo rename

The repository moved from Comfy-Org/ComfyTypeScriptSDK to Comfy-Org/comfy-typescript-sdk, matching the org's comfy-lower-kebab-case convention. GitHub redirects the old URLs.

The npm package name is unchanged (@comfyorg/sdk) — npm i @comfyorg/sdk is unaffected. This release is the first to carry the corrected repository/homepage/bugs URLs in its published metadata.

Internal

  • Added a vitest config so coverage measures hand-written code rather than generated output.

v0.1.4

Choose a tag to compare

@wei-hai wei-hai released this 28 Jul 19:19
2b935f0

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

  • baseUrl now defaults to https://cloud.comfy.org, added as a constructor overload so the options-only form reads naturally. COMFY_CLOUD_BASE_URL is exported for callers who want the value.
  • Spec server URL, the regenerated baseUrl type union, README, and doc comments updated to the new host.
  • Passing an explicit baseUrl still wins — self-hosted and serverless callers are unaffected.

Upgrading

// before
const client = new Comfy("https://api.comfy.org", { apiKey: "..." });

// after — the default is correct, so the host can be dropped
const client = new Comfy({ apiKey: "..." });

Backward compatible for anyone already passing their own host; only api.comfy.org callers must change.