@comfyorg/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/low/errors.d.ts +73 -0
- package/dist/low/errors.js +104 -0
- package/dist/low/generated/index.d.ts +1 -0
- package/dist/low/generated/index.js +2 -0
- package/dist/low/generated/types.gen.d.ts +528 -0
- package/dist/low/generated/types.gen.js +2 -0
- package/dist/low/generated/zod.gen.d.ts +455 -0
- package/dist/low/generated/zod.gen.js +204 -0
- package/dist/low/index.d.ts +21 -0
- package/dist/low/index.js +21 -0
- package/dist/low/models.d.ts +47 -0
- package/dist/low/models.js +16 -0
- package/dist/low/sse.d.ts +24 -0
- package/dist/low/sse.js +44 -0
- package/dist/low/transport.d.ts +128 -0
- package/dist/low/transport.js +285 -0
- package/dist/sdk/abortable-sleep.d.ts +13 -0
- package/dist/sdk/abortable-sleep.js +31 -0
- package/dist/sdk/assets.d.ts +74 -0
- package/dist/sdk/assets.js +200 -0
- package/dist/sdk/client.d.ts +70 -0
- package/dist/sdk/client.js +120 -0
- package/dist/sdk/core.d.ts +33 -0
- package/dist/sdk/core.js +89 -0
- package/dist/sdk/events.d.ts +60 -0
- package/dist/sdk/events.js +84 -0
- package/dist/sdk/exceptions.d.ts +79 -0
- package/dist/sdk/exceptions.js +114 -0
- package/dist/sdk/hashing.d.ts +14 -0
- package/dist/sdk/hashing.js +28 -0
- package/dist/sdk/index.d.ts +13 -0
- package/dist/sdk/index.js +12 -0
- package/dist/sdk/jobs.d.ts +47 -0
- package/dist/sdk/jobs.js +145 -0
- package/dist/sdk/outputs.d.ts +26 -0
- package/dist/sdk/outputs.js +48 -0
- package/dist/sdk/workflows.d.ts +32 -0
- package/dist/sdk/workflows.js +45 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# @comfyorg/sdk (TypeScript)
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for running ComfyUI workflows via the **Comfy API v2**. The
|
|
4
|
+
same code runs against three surfaces — a self-hosted ComfyUI (through
|
|
5
|
+
[`comfy-api-proxy`](https://github.com/Comfy-Org/comfy-api-proxy)), Comfy
|
|
6
|
+
Cloud, or a serverless deployment — changing only the base URL and an
|
|
7
|
+
optional API key. It mirrors the behavior of the
|
|
8
|
+
[Python SDK](https://github.com/Comfy-Org/ComfyPythonSDK) (`comfy-sdk`):
|
|
9
|
+
upload/dedup inputs, submit a workflow, wait for it, download the outputs —
|
|
10
|
+
collapsed here to a single async client (no separate sync/async API;
|
|
11
|
+
JavaScript is async-native).
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Comfy } from "@comfyorg/sdk";
|
|
15
|
+
|
|
16
|
+
const client = new Comfy("http://127.0.0.1:8189"); // local proxy, no key needed
|
|
17
|
+
// const client = new Comfy("https://api.comfy.org", { apiKey: "..." }); // Comfy Cloud
|
|
18
|
+
|
|
19
|
+
const wf = await client.workflows.fromFile("workflow_api.json");
|
|
20
|
+
const asset = client.assets.fromFile("photo.png"); // lazy; hashed + uploaded on first use
|
|
21
|
+
wf.setInput("10", "image", asset);
|
|
22
|
+
|
|
23
|
+
const job = await client.run(wf); // submit, then poll to a terminal state
|
|
24
|
+
await job.getOutputs("13")[0].toFile("out.png");
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- **Node >=22.** Node 20 reached end-of-life; browser support is out of
|
|
30
|
+
scope for v1.
|
|
31
|
+
- **Install:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm i @comfyorg/sdk
|
|
35
|
+
pnpm add @comfyorg/sdk
|
|
36
|
+
yarn add @comfyorg/sdk
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Releases are published to npm from a GitHub Release (tag `vX.Y.Z`) by
|
|
40
|
+
[`.github/workflows/publish.yml`](.github/workflows/publish.yml). To build
|
|
41
|
+
from source instead (for local development, or to track an unreleased
|
|
42
|
+
commit), clone this repo, `pnpm install`, `pnpm build`, and reference the
|
|
43
|
+
built `dist/`.
|
|
44
|
+
|
|
45
|
+
## Auth, per surface
|
|
46
|
+
|
|
47
|
+
| Surface | Auth |
|
|
48
|
+
| ------------------------------------------------------------------ | ------------------------------------------ |
|
|
49
|
+
| Self-hosted proxy (`comfy-api-proxy` in front of your own ComfyUI) | none — do **not** pass `apiKey` |
|
|
50
|
+
| Comfy Cloud | `new Comfy(baseUrl, { apiKey: "ck_..." })` |
|
|
51
|
+
| Serverless | `new Comfy(baseUrl, { apiKey: "ck_..." })` |
|
|
52
|
+
|
|
53
|
+
The client only attaches the `Authorization` header to requests aimed at its
|
|
54
|
+
own `baseUrl`'s origin. If the server hands back an absolute URL on a
|
|
55
|
+
different host (for example a job's `events`/`cancel` link, or a redirect on
|
|
56
|
+
an asset download), the key is not sent there — see "Typed errors" below for
|
|
57
|
+
the exception classes this surface can raise.
|
|
58
|
+
|
|
59
|
+
## Quickstart
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { Comfy } from "@comfyorg/sdk";
|
|
63
|
+
|
|
64
|
+
const client = new Comfy("http://127.0.0.1:8189");
|
|
65
|
+
|
|
66
|
+
const wf = await client.workflows.fromFile("workflow_api.json");
|
|
67
|
+
const job = await client.run(wf); // submit + poll to a terminal state; throws on failure
|
|
68
|
+
const outputs = job.getOutputs("13"); // outputs produced by node "13"
|
|
69
|
+
await outputs[0].toFile("out.png");
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`run()` submits and polls to completion in one call. If you want to act on
|
|
73
|
+
the job in between (read `job.status`, stream progress, cancel it), use
|
|
74
|
+
`submit()` and drive the job yourself:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const job = await client.submit(wf);
|
|
78
|
+
await job.wait(); // poll to terminal (adaptive backoff); or call job.refresh() yourself
|
|
79
|
+
console.log(job.status, job.outputs);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Assets and `core/ASSET`
|
|
83
|
+
|
|
84
|
+
`client.assets.fromFile(path)` / `client.assets.fromBytes(data, options)`
|
|
85
|
+
return a **lazy** asset handle: nothing is hashed or uploaded until the
|
|
86
|
+
handle is actually used. Embed the handle directly in a workflow input with
|
|
87
|
+
`wf.setInput(...)`:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const asset = client.assets.fromFile("photo.png");
|
|
91
|
+
wf.setInput("10", "image", asset);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
On submit, the SDK walks the workflow graph, finds every embedded handle,
|
|
95
|
+
and for each one: hashes the bytes locally (blake3, via
|
|
96
|
+
[`hash-wasm`](https://www.npmjs.com/package/hash-wasm) — pure WebAssembly,
|
|
97
|
+
no native addon), probes the server's dedup fast path, and only streams a
|
|
98
|
+
full upload if the server does not already have those bytes. Each handle is
|
|
99
|
+
then substituted in place with a `core/ASSET` reference
|
|
100
|
+
(`{ __type: "core/ASSET", info: { id, hash, file_path } }`) before the
|
|
101
|
+
workflow is sent. Re-running a script against unchanged files re-uploads
|
|
102
|
+
nothing.
|
|
103
|
+
|
|
104
|
+
`client.assets` also has `fromStream`, `fromUrl`, and `get(assetId)` (to
|
|
105
|
+
rehydrate a handle for an asset that is already committed) for less common
|
|
106
|
+
cases — see the type definitions for details.
|
|
107
|
+
|
|
108
|
+
## Live progress
|
|
109
|
+
|
|
110
|
+
`job.events()` is a typed, auto-reconnecting async iterator over the job's
|
|
111
|
+
live event stream:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const job = await client.submit(wf);
|
|
115
|
+
for await (const event of job.events()) {
|
|
116
|
+
switch (event.kind) {
|
|
117
|
+
case "progress":
|
|
118
|
+
console.log(event.value);
|
|
119
|
+
break;
|
|
120
|
+
case "outputReady":
|
|
121
|
+
await event.output.toFile(`${event.output.name}`);
|
|
122
|
+
break;
|
|
123
|
+
case "statusChange":
|
|
124
|
+
if (event.status === "succeeded") break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The stream carries no replay cursor, so a dropped connection is reconnected
|
|
130
|
+
from "now," not replayed from the start. Polling stays the source of truth
|
|
131
|
+
for whether the job is actually done: if the stream is throttled, drops
|
|
132
|
+
permanently, or never even connects, `events()` falls back to polling
|
|
133
|
+
`GET /jobs/{id}` to detect the terminal state, so consumers never hang
|
|
134
|
+
waiting on a stream that isn't coming back. If you only care about the
|
|
135
|
+
final result, `run()`/`job.wait()` (poll-only, no SSE) is simpler.
|
|
136
|
+
|
|
137
|
+
## Cancellation and timeouts
|
|
138
|
+
|
|
139
|
+
`submit`, `run`, `wait`, `events`, and `cancel` all accept an `AbortSignal`,
|
|
140
|
+
which stops both the in-flight request _and_ any internal wait (the queue-full
|
|
141
|
+
retry pause, the poll backoff, the SSE reconnect pause) — an abort takes
|
|
142
|
+
effect immediately rather than only after the current network call returns:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
setTimeout(() => controller.abort(), 30_000); // give up after 30s
|
|
147
|
+
|
|
148
|
+
const job = await client.submit(wf, { signal: controller.signal });
|
|
149
|
+
await job.wait(undefined, controller.signal);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`run()` also takes a plain `timeoutMs` if you just want a deadline without
|
|
153
|
+
managing an `AbortController` yourself:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
await client.run(wf, { timeoutMs: 60_000 });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Typed errors
|
|
160
|
+
|
|
161
|
+
Protocol-level failures are raised as one exception class per error code, so
|
|
162
|
+
you can catch what you actually expect instead of string-matching messages:
|
|
163
|
+
|
|
164
|
+
- `Unauthorized`, `Forbidden`, `NotFound`
|
|
165
|
+
- `InvalidWorkflow` (and `WorkflowFormatUi`, for submitting a UI-export
|
|
166
|
+
instead of an API-format graph)
|
|
167
|
+
- `MissingAsset` — a `core/ASSET` reference the server couldn't resolve
|
|
168
|
+
- `HashMismatch` — uploaded bytes didn't match the declared hash
|
|
169
|
+
- `BlobNotFound`
|
|
170
|
+
- `IdempotencyKeyReuse` — the `Idempotency-Key` was reused. `submit()` (and
|
|
171
|
+
`run()`) attach a fresh key to every call, so an accidental exact resend never
|
|
172
|
+
runs the workflow twice. Keys are single-use (reject-on-duplicate, no replay),
|
|
173
|
+
so reusing your own explicit `idempotencyKey` throws this. After an ambiguous
|
|
174
|
+
failure, poll or list your jobs instead of resubmitting with the same key.
|
|
175
|
+
- `InsufficientCredits`
|
|
176
|
+
- `QueueFull` (carries `retryAfter`; `submit()` already retries this one
|
|
177
|
+
transparently)
|
|
178
|
+
- `JobFailed` — a job reached a non-`succeeded` terminal state (carries the
|
|
179
|
+
node-level `error` detail when the platform provided one)
|
|
180
|
+
|
|
181
|
+
All extend a shared `ComfyError` (`code`, `httpStatus`, `details`).
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { JobFailed, MissingAsset } from "@comfyorg/sdk";
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await client.run(wf);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err instanceof JobFailed) {
|
|
190
|
+
console.error(err.error); // { code, message, node_id, class_type, traceback } | null
|
|
191
|
+
} else if (err instanceof MissingAsset) {
|
|
192
|
+
console.error("asset reference was not usable:", err.details);
|
|
193
|
+
} else {
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Two layers
|
|
200
|
+
|
|
201
|
+
- **`@comfyorg/sdk`** — the idiomatic client above: asset dedup/upload,
|
|
202
|
+
`core/ASSET` substitution, idempotent submit with queue-full backoff,
|
|
203
|
+
poll-authoritative job completion, typed SSE events, range-aware
|
|
204
|
+
downloads, and typed errors.
|
|
205
|
+
- **`@comfyorg/sdk/low`** — generated types + [Zod](https://zod.dev) schemas
|
|
206
|
+
plus a hand-written `fetch` transport (`ComfyLow`) with one method per API
|
|
207
|
+
operation and the escape hatches the SDK layer is built on: raw `Response`
|
|
208
|
+
access, unbuffered streaming bodies (for SSE and range downloads), a
|
|
209
|
+
streaming multipart upload body, and per-request `AbortSignal`/timeout.
|
|
210
|
+
Use this directly if you need lower-level control.
|
|
211
|
+
|
|
212
|
+
The generated part of `low` (`src/low/generated/*`) is produced by
|
|
213
|
+
[`@hey-api/openapi-ts`](https://heyapi.dev) from `spec/openapi.yaml`, a
|
|
214
|
+
vendored, filtered copy of the canonical Comfy API v2 contract (see
|
|
215
|
+
`spec/README.md`). Regenerate it with `pnpm generate` after the spec
|
|
216
|
+
changes; CI fails if the generated code has drifted from the spec.
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pnpm install --frozen-lockfile
|
|
222
|
+
pnpm lint # oxlint
|
|
223
|
+
pnpm format:check # oxfmt --check
|
|
224
|
+
pnpm typecheck # tsc --noEmit
|
|
225
|
+
pnpm test # vitest run
|
|
226
|
+
pnpm build # tsc -> dist/
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Other useful scripts:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pnpm generate # regenerate src/low/generated/* from spec/openapi.yaml
|
|
233
|
+
pnpm format # oxfmt --write
|
|
234
|
+
pnpm test:coverage # vitest run --coverage
|
|
235
|
+
pnpm check:spec-drift # fails if src/low/generated/* is stale vs. the spec
|
|
236
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comfy SDK — the idiomatic TypeScript client for the Comfy API v2.
|
|
3
|
+
*
|
|
4
|
+
* Runs an API-format workflow against any Comfy API v2 surface (self-hosted
|
|
5
|
+
* ComfyUI through `comfy-api-proxy`, Comfy Cloud, or a serverless
|
|
6
|
+
* deployment) — the only per-surface difference is the base URL and an
|
|
7
|
+
* optional key. This is the `sdk` (idiomatic) layer; the generated
|
|
8
|
+
* types/validators + thin transport live under `@comfyorg/sdk/low` for
|
|
9
|
+
* advanced/escape-hatch use. Behaviorally mirrors the Python SDK
|
|
10
|
+
* (`Comfy-Org/ComfyPythonSDK`), collapsed to one async client (JS is
|
|
11
|
+
* async-native — no sync/async split).
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { Comfy } from "@comfyorg/sdk";
|
|
15
|
+
*
|
|
16
|
+
* const client = new Comfy("http://127.0.0.1:8189"); // local proxy
|
|
17
|
+
* // const client = new Comfy("https://api.comfy.org", { apiKey: "..." }) // Comfy Cloud
|
|
18
|
+
*
|
|
19
|
+
* const wf = await client.workflows.fromFile("workflow_api.json");
|
|
20
|
+
* const asset = client.assets.fromFile("photo.png"); // lazy; uploaded on use
|
|
21
|
+
* wf.setInput("10", "image", asset);
|
|
22
|
+
*
|
|
23
|
+
* const job = await client.run(wf);
|
|
24
|
+
* await job.getOutputs("13")[0].toFile("out.png");
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export * from "./sdk/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comfy SDK — the idiomatic TypeScript client for the Comfy API v2.
|
|
3
|
+
*
|
|
4
|
+
* Runs an API-format workflow against any Comfy API v2 surface (self-hosted
|
|
5
|
+
* ComfyUI through `comfy-api-proxy`, Comfy Cloud, or a serverless
|
|
6
|
+
* deployment) — the only per-surface difference is the base URL and an
|
|
7
|
+
* optional key. This is the `sdk` (idiomatic) layer; the generated
|
|
8
|
+
* types/validators + thin transport live under `@comfyorg/sdk/low` for
|
|
9
|
+
* advanced/escape-hatch use. Behaviorally mirrors the Python SDK
|
|
10
|
+
* (`Comfy-Org/ComfyPythonSDK`), collapsed to one async client (JS is
|
|
11
|
+
* async-native — no sync/async split).
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { Comfy } from "@comfyorg/sdk";
|
|
15
|
+
*
|
|
16
|
+
* const client = new Comfy("http://127.0.0.1:8189"); // local proxy
|
|
17
|
+
* // const client = new Comfy("https://api.comfy.org", { apiKey: "..." }) // Comfy Cloud
|
|
18
|
+
*
|
|
19
|
+
* const wf = await client.workflows.fromFile("workflow_api.json");
|
|
20
|
+
* const asset = client.assets.fromFile("photo.png"); // lazy; uploaded on use
|
|
21
|
+
* wf.setInput("10", "image", asset);
|
|
22
|
+
*
|
|
23
|
+
* const job = await client.run(wf);
|
|
24
|
+
* await job.getOutputs("13")[0].toFile("out.png");
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export * from "./sdk/index.js";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared error envelope mapped to a typed exception per `code`.
|
|
3
|
+
*
|
|
4
|
+
* This is the `low` (protocol) view of errors: one class per documented error
|
|
5
|
+
* `code`, plus a fallback. The `sdk` layer re-raises these as its own
|
|
6
|
+
* idiomatic exceptions where it adds value (e.g. `JobFailed` carrying node
|
|
7
|
+
* details), but the protocol codes are defined here so the generated layer
|
|
8
|
+
* has a stable, typed error surface. Mirrors `comfy_low.errors` in the
|
|
9
|
+
* Python SDK.
|
|
10
|
+
*/
|
|
11
|
+
export interface ApiErrorOptions {
|
|
12
|
+
code?: string;
|
|
13
|
+
httpStatus: number;
|
|
14
|
+
details?: Record<string, unknown> | null;
|
|
15
|
+
retryAfter?: number | null;
|
|
16
|
+
}
|
|
17
|
+
export declare class ApiError extends Error {
|
|
18
|
+
static readonly code: string;
|
|
19
|
+
readonly code: string;
|
|
20
|
+
readonly httpStatus: number;
|
|
21
|
+
readonly details: Record<string, unknown> | null;
|
|
22
|
+
readonly retryAfter: number | null;
|
|
23
|
+
constructor(message: string, options: ApiErrorOptions);
|
|
24
|
+
}
|
|
25
|
+
export declare class InvalidWorkflow extends ApiError {
|
|
26
|
+
static readonly code = "invalid_workflow";
|
|
27
|
+
}
|
|
28
|
+
export declare class WorkflowFormatUi extends ApiError {
|
|
29
|
+
static readonly code = "workflow_format_ui";
|
|
30
|
+
}
|
|
31
|
+
export declare class MissingAsset extends ApiError {
|
|
32
|
+
static readonly code = "missing_asset";
|
|
33
|
+
}
|
|
34
|
+
export declare class HashMismatch extends ApiError {
|
|
35
|
+
static readonly code = "hash_mismatch";
|
|
36
|
+
}
|
|
37
|
+
export declare class BlobNotFound extends ApiError {
|
|
38
|
+
static readonly code = "blob_not_found";
|
|
39
|
+
}
|
|
40
|
+
export declare class IdempotencyKeyReuse extends ApiError {
|
|
41
|
+
static readonly code = "idempotency_key_reuse";
|
|
42
|
+
}
|
|
43
|
+
export declare class QueueFull extends ApiError {
|
|
44
|
+
static readonly code = "queue_full";
|
|
45
|
+
}
|
|
46
|
+
export declare class InsufficientCredits extends ApiError {
|
|
47
|
+
static readonly code = "insufficient_credits";
|
|
48
|
+
}
|
|
49
|
+
export declare class NotFound extends ApiError {
|
|
50
|
+
static readonly code = "not_found";
|
|
51
|
+
}
|
|
52
|
+
export declare class Unauthorized extends ApiError {
|
|
53
|
+
static readonly code = "unauthorized";
|
|
54
|
+
}
|
|
55
|
+
export declare class Forbidden extends ApiError {
|
|
56
|
+
static readonly code = "forbidden";
|
|
57
|
+
}
|
|
58
|
+
interface ErrorEnvelopeBody {
|
|
59
|
+
error?: {
|
|
60
|
+
code?: string;
|
|
61
|
+
message?: string;
|
|
62
|
+
details?: Record<string, unknown> | null;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the typed exception for an error response. Falls back to a
|
|
67
|
+
* status-derived code when the body is missing or not a well-formed
|
|
68
|
+
* envelope, so a bare 401 with no JSON still maps to `Unauthorized`.
|
|
69
|
+
*/
|
|
70
|
+
export declare function errorFromEnvelope(httpStatus: number, body: ErrorEnvelopeBody | null | undefined, options?: {
|
|
71
|
+
retryAfter?: number | null;
|
|
72
|
+
}): ApiError;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared error envelope mapped to a typed exception per `code`.
|
|
3
|
+
*
|
|
4
|
+
* This is the `low` (protocol) view of errors: one class per documented error
|
|
5
|
+
* `code`, plus a fallback. The `sdk` layer re-raises these as its own
|
|
6
|
+
* idiomatic exceptions where it adds value (e.g. `JobFailed` carrying node
|
|
7
|
+
* details), but the protocol codes are defined here so the generated layer
|
|
8
|
+
* has a stable, typed error surface. Mirrors `comfy_low.errors` in the
|
|
9
|
+
* Python SDK.
|
|
10
|
+
*/
|
|
11
|
+
export class ApiError extends Error {
|
|
12
|
+
static code = "error";
|
|
13
|
+
code;
|
|
14
|
+
httpStatus;
|
|
15
|
+
details;
|
|
16
|
+
retryAfter;
|
|
17
|
+
constructor(message, options) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = new.target.name;
|
|
20
|
+
this.code = options.code ?? new.target.code;
|
|
21
|
+
this.httpStatus = options.httpStatus;
|
|
22
|
+
this.details = options.details ?? null;
|
|
23
|
+
this.retryAfter = options.retryAfter ?? null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class InvalidWorkflow extends ApiError {
|
|
27
|
+
static code = "invalid_workflow";
|
|
28
|
+
}
|
|
29
|
+
export class WorkflowFormatUi extends ApiError {
|
|
30
|
+
static code = "workflow_format_ui";
|
|
31
|
+
}
|
|
32
|
+
export class MissingAsset extends ApiError {
|
|
33
|
+
static code = "missing_asset";
|
|
34
|
+
}
|
|
35
|
+
export class HashMismatch extends ApiError {
|
|
36
|
+
static code = "hash_mismatch";
|
|
37
|
+
}
|
|
38
|
+
export class BlobNotFound extends ApiError {
|
|
39
|
+
static code = "blob_not_found";
|
|
40
|
+
}
|
|
41
|
+
export class IdempotencyKeyReuse extends ApiError {
|
|
42
|
+
static code = "idempotency_key_reuse";
|
|
43
|
+
}
|
|
44
|
+
export class QueueFull extends ApiError {
|
|
45
|
+
static code = "queue_full";
|
|
46
|
+
}
|
|
47
|
+
export class InsufficientCredits extends ApiError {
|
|
48
|
+
static code = "insufficient_credits";
|
|
49
|
+
}
|
|
50
|
+
export class NotFound extends ApiError {
|
|
51
|
+
static code = "not_found";
|
|
52
|
+
}
|
|
53
|
+
export class Unauthorized extends ApiError {
|
|
54
|
+
static code = "unauthorized";
|
|
55
|
+
}
|
|
56
|
+
export class Forbidden extends ApiError {
|
|
57
|
+
static code = "forbidden";
|
|
58
|
+
}
|
|
59
|
+
const BY_CODE = {
|
|
60
|
+
invalid_workflow: InvalidWorkflow,
|
|
61
|
+
workflow_format_ui: WorkflowFormatUi,
|
|
62
|
+
missing_asset: MissingAsset,
|
|
63
|
+
hash_mismatch: HashMismatch,
|
|
64
|
+
blob_not_found: BlobNotFound,
|
|
65
|
+
idempotency_key_reuse: IdempotencyKeyReuse,
|
|
66
|
+
queue_full: QueueFull,
|
|
67
|
+
insufficient_credits: InsufficientCredits,
|
|
68
|
+
not_found: NotFound,
|
|
69
|
+
unauthorized: Unauthorized,
|
|
70
|
+
forbidden: Forbidden,
|
|
71
|
+
};
|
|
72
|
+
const CODE_BY_STATUS = {
|
|
73
|
+
401: "unauthorized",
|
|
74
|
+
402: "insufficient_credits",
|
|
75
|
+
403: "forbidden",
|
|
76
|
+
404: "not_found",
|
|
77
|
+
409: "hash_mismatch",
|
|
78
|
+
422: "invalid_workflow",
|
|
79
|
+
429: "queue_full",
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Build the typed exception for an error response. Falls back to a
|
|
83
|
+
* status-derived code when the body is missing or not a well-formed
|
|
84
|
+
* envelope, so a bare 401 with no JSON still maps to `Unauthorized`.
|
|
85
|
+
*/
|
|
86
|
+
export function errorFromEnvelope(httpStatus, body, options = {}) {
|
|
87
|
+
const err = body && typeof body === "object" ? body.error : undefined;
|
|
88
|
+
let code = err && typeof err === "object" ? err.code : undefined;
|
|
89
|
+
let message = err && typeof err === "object" ? err.message : undefined;
|
|
90
|
+
const details = err && typeof err === "object" ? err.details : undefined;
|
|
91
|
+
if (!code) {
|
|
92
|
+
code = CODE_BY_STATUS[httpStatus] ?? "error";
|
|
93
|
+
}
|
|
94
|
+
if (!message) {
|
|
95
|
+
message = `HTTP ${httpStatus}`;
|
|
96
|
+
}
|
|
97
|
+
const cls = BY_CODE[code] ?? ApiError;
|
|
98
|
+
return new cls(message, {
|
|
99
|
+
code,
|
|
100
|
+
httpStatus,
|
|
101
|
+
details: details && typeof details === "object" ? details : null,
|
|
102
|
+
retryAfter: options.retryAfter ?? null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { Asset, AssetFromHashData, AssetFromHashError, AssetFromHashErrors, AssetFromHashResponse, AssetFromHashResponses, AssetId, BlakeHash, CancelJobData, CancelJobError, CancelJobErrors, CancelJobResponse, CancelJobResponses, ClientOptions, ErrorEnvelope, GetAssetContentData, GetAssetContentError, GetAssetContentErrors, GetAssetContentResponse, GetAssetContentResponses, GetAssetData, GetAssetError, GetAssetErrors, GetAssetResponse, GetAssetResponses, GetJobData, GetJobError, GetJobErrors, GetJobEventsData, GetJobEventsError, GetJobEventsErrors, GetJobEventsResponse, GetJobEventsResponses, GetJobResponse, GetJobResponses, HeadAssetByHashData, HeadAssetByHashError, HeadAssetByHashErrors, HeadAssetByHashResponses, IdempotencyKey, Job, JobError, JobId, JobStatus, JobUrls, Output, OutputType, PostAssetsData, PostAssetsError, PostAssetsErrors, PostAssetsResponse, PostAssetsResponses, PostJobsData, PostJobsError, PostJobsErrors, PostJobsResponse, PostJobsResponses, Progress } from './types.gen.js';
|