@comfyorg/sdk 0.1.1 → 0.1.3
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 +36 -5
- package/dist/low/errors.js +5 -0
- package/dist/low/generated/types.gen.d.ts +9 -1
- package/dist/low/index.d.ts +1 -1
- package/dist/low/sse.d.ts +15 -2
- package/dist/low/sse.js +35 -2
- package/dist/low/transport.d.ts +32 -0
- package/dist/low/transport.js +108 -4
- package/dist/low/version.d.ts +1 -0
- package/dist/low/version.js +2 -0
- package/dist/sdk/client.d.ts +3 -0
- package/dist/sdk/client.js +1 -0
- package/dist/sdk/exceptions.js +5 -0
- package/dist/sdk/outputs.d.ts +14 -0
- package/dist/sdk/outputs.js +13 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,11 +44,11 @@ await job.getOutputs("13")[0].toFile("out.png");
|
|
|
44
44
|
|
|
45
45
|
## Auth, per surface
|
|
46
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: "
|
|
51
|
-
| Serverless | `new Comfy(baseUrl, { apiKey: "
|
|
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: "comfyui-..." })` |
|
|
51
|
+
| Serverless | `new Comfy(baseUrl, { apiKey: "comfyui-..." })` |
|
|
52
52
|
|
|
53
53
|
The client only attaches the `Authorization` header to requests aimed at its
|
|
54
54
|
own `baseUrl`'s origin. If the server hands back an absolute URL on a
|
|
@@ -56,6 +56,11 @@ different host (for example a job's `events`/`cancel` link, or a redirect on
|
|
|
56
56
|
an asset download), the key is not sent there — see "Typed errors" below for
|
|
57
57
|
the exception classes this surface can raise.
|
|
58
58
|
|
|
59
|
+
The SDK identifies itself via `User-Agent` (for support + usage analytics);
|
|
60
|
+
no other data is collected. Pass `clientInfo` to `new Comfy(baseUrl, { ... })`
|
|
61
|
+
to append your own app's name to it, for example when attributing traffic
|
|
62
|
+
from a Worker built on top of this SDK.
|
|
63
|
+
|
|
59
64
|
## Quickstart
|
|
60
65
|
|
|
61
66
|
```ts
|
|
@@ -173,6 +178,32 @@ managing an `AbortController` yourself:
|
|
|
173
178
|
await client.run(wf, { timeoutMs: 60_000 });
|
|
174
179
|
```
|
|
175
180
|
|
|
181
|
+
## Downloading outputs
|
|
182
|
+
|
|
183
|
+
A finished job exposes its results as output handles — `job.outputs`, or
|
|
184
|
+
`job.getOutputs(nodeId)` to filter to one node. Each is an asset you can pull
|
|
185
|
+
down whichever way suits the caller:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const out = job.getOutputs("13")[0];
|
|
189
|
+
await out.toFile("result.png"); // stream to disk
|
|
190
|
+
const data = await out.toBytes(); // buffer into memory
|
|
191
|
+
await out.toFile("head.png", { range: [0, 1023] }); // range-aware: first 1 KiB only
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`getDownloadUrl()` hands back a fetchable URL instead of streaming the bytes
|
|
195
|
+
through your process — give it to a browser, a CDN, or another service:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const { url, expiresAt } = await out.getDownloadUrl();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
On Comfy Cloud / serverless it's a short-lived, **self-authorizing** signed
|
|
202
|
+
storage URL: whoever holds it can read the asset until `expiresAt` with no API
|
|
203
|
+
key of their own. On a self-hosted proxy it's the content endpoint (normal auth
|
|
204
|
+
still applies) and `expiresAt` is `null`. It works on every backend and never
|
|
205
|
+
downloads the bytes first.
|
|
206
|
+
|
|
176
207
|
## Typed errors
|
|
177
208
|
|
|
178
209
|
Protocol-level failures are raised as one exception class per error code, so
|
package/dist/low/errors.js
CHANGED
|
@@ -66,6 +66,11 @@ const BY_CODE = {
|
|
|
66
66
|
queue_full: QueueFull,
|
|
67
67
|
insufficient_credits: InsufficientCredits,
|
|
68
68
|
not_found: NotFound,
|
|
69
|
+
// public-api currently returns entity-specific 404 codes even though the spec
|
|
70
|
+
// documents the generic `not_found`; map them so callers still get a typed
|
|
71
|
+
// NotFound. (Server/spec reconciliation is a separate follow-up.)
|
|
72
|
+
job_not_found: NotFound,
|
|
73
|
+
asset_not_found: NotFound,
|
|
69
74
|
unauthorized: Unauthorized,
|
|
70
75
|
forbidden: Forbidden,
|
|
71
76
|
};
|
|
@@ -372,7 +372,7 @@ export type PostJobsData = {
|
|
|
372
372
|
[key: string]: unknown;
|
|
373
373
|
};
|
|
374
374
|
/**
|
|
375
|
-
*
|
|
375
|
+
* Per-prompt ComfyUI `extra_data`, same shape as Comfy Cloud and local ComfyUI. Closed object: only the enumerated keys are accepted, keeping the contract fully typed. Forwarded to the worker per-prompt, never persisted, and excluded from idempotency comparison.
|
|
376
376
|
*/
|
|
377
377
|
extra_data?: {
|
|
378
378
|
/**
|
|
@@ -446,6 +446,10 @@ export type GetJobErrors = {
|
|
|
446
446
|
* `not_found`.
|
|
447
447
|
*/
|
|
448
448
|
404: ErrorEnvelope;
|
|
449
|
+
/**
|
|
450
|
+
* `rate_limited` — the caller has exceeded the request rate limit for this account. Account/rate-scoped, not job-specific — this can be returned even for a job id the caller doesn't own or that doesn't exist, without revealing which.
|
|
451
|
+
*/
|
|
452
|
+
429: ErrorEnvelope;
|
|
449
453
|
/**
|
|
450
454
|
* `upstream_error` — an unexpected failure reaching or processing the request in this implementation's backing services. The message is always a generic, safe-to-display string; implementation detail (the specific upstream, its error text, transport failures) is never included here — see each implementation's own error-mapping notes. Every operation in this contract can fail this way.
|
|
451
455
|
*/
|
|
@@ -522,6 +526,10 @@ export type CancelJobErrors = {
|
|
|
522
526
|
* `not_found`.
|
|
523
527
|
*/
|
|
524
528
|
404: ErrorEnvelope;
|
|
529
|
+
/**
|
|
530
|
+
* `rate_limited` — the caller has exceeded the request rate limit for this account. Account/rate-scoped, not job-specific — this can be returned even for a job id the caller doesn't own or that doesn't exist, without revealing which.
|
|
531
|
+
*/
|
|
532
|
+
429: ErrorEnvelope;
|
|
525
533
|
/**
|
|
526
534
|
* `upstream_error` — an unexpected failure reaching or processing the request in this implementation's backing services. The message is always a generic, safe-to-display string; implementation detail (the specific upstream, its error text, transport failures) is never included here — see each implementation's own error-mapping notes. Every operation in this contract can fail this way.
|
|
527
535
|
*/
|
package/dist/low/index.d.ts
CHANGED
|
@@ -18,4 +18,4 @@ export * as schemas from "./generated/zod.gen.js";
|
|
|
18
18
|
export * from "./models.js";
|
|
19
19
|
export * from "./errors.js";
|
|
20
20
|
export { type RawEvent, iterateSse } from "./sse.js";
|
|
21
|
-
export { ComfyLow, OPERATION_IDS, OPERATION_METHODS, type ComfyLowOptions, type RequestOptions, } from "./transport.js";
|
|
21
|
+
export { ComfyLow, OPERATION_IDS, OPERATION_METHODS, type ComfyLowOptions, type RequestOptions, type AssetContentUrl, } from "./transport.js";
|
package/dist/low/sse.d.ts
CHANGED
|
@@ -14,11 +14,24 @@ export interface RawEvent {
|
|
|
14
14
|
event: string;
|
|
15
15
|
data: Record<string, unknown>;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Default read-idle timeout for the SSE byte stream. The server sends keepalive
|
|
19
|
+
* comments well within this window, so no *bytes at all* for this long means a
|
|
20
|
+
* silently-stalled ("zombie") connection — open but delivering nothing, not
|
|
21
|
+
* legitimately idle. We error the stream in that case so the caller's
|
|
22
|
+
* reconnect/poll fallback runs (see `Job.events`) instead of hanging forever.
|
|
23
|
+
* The timeout is at the byte level, not the parsed-message level, so keepalive
|
|
24
|
+
* comments (which `eventsource-parser` drops) still count as liveness.
|
|
25
|
+
*/
|
|
26
|
+
export declare const SSE_IDLE_TIMEOUT_MS = 45000;
|
|
17
27
|
/**
|
|
18
28
|
* Decode a fetch response body into a stream of {@link RawEvent}s.
|
|
19
29
|
*
|
|
20
30
|
* No cursor/replay support by design — the contract carries no `id`/
|
|
21
31
|
* `Last-Event-ID`; a blank `data:` frame (no payload) is dropped, matching
|
|
22
|
-
* the Python decoder.
|
|
32
|
+
* the Python decoder. A read-idle timeout (default {@link SSE_IDLE_TIMEOUT_MS})
|
|
33
|
+
* errors a silently-stalled connection instead of blocking forever.
|
|
23
34
|
*/
|
|
24
|
-
export declare function iterateSse(body: ReadableStream<Uint8Array
|
|
35
|
+
export declare function iterateSse(body: ReadableStream<Uint8Array>, options?: {
|
|
36
|
+
idleTimeoutMs?: number;
|
|
37
|
+
}): AsyncGenerator<RawEvent, void, void>;
|
package/dist/low/sse.js
CHANGED
|
@@ -24,15 +24,48 @@ function parseData(raw) {
|
|
|
24
24
|
}
|
|
25
25
|
return { value: parsed };
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Default read-idle timeout for the SSE byte stream. The server sends keepalive
|
|
29
|
+
* comments well within this window, so no *bytes at all* for this long means a
|
|
30
|
+
* silently-stalled ("zombie") connection — open but delivering nothing, not
|
|
31
|
+
* legitimately idle. We error the stream in that case so the caller's
|
|
32
|
+
* reconnect/poll fallback runs (see `Job.events`) instead of hanging forever.
|
|
33
|
+
* The timeout is at the byte level, not the parsed-message level, so keepalive
|
|
34
|
+
* comments (which `eventsource-parser` drops) still count as liveness.
|
|
35
|
+
*/
|
|
36
|
+
export const SSE_IDLE_TIMEOUT_MS = 45_000;
|
|
37
|
+
/** A pass-through stream that errors if no chunk arrives within `ms`. */
|
|
38
|
+
function idleTimeout(ms) {
|
|
39
|
+
let timer;
|
|
40
|
+
const clear = () => {
|
|
41
|
+
if (timer !== undefined)
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
};
|
|
44
|
+
const arm = (controller) => {
|
|
45
|
+
clear();
|
|
46
|
+
timer = setTimeout(() => controller.error(new Error(`SSE idle timeout: no data for ${ms}ms`)), ms);
|
|
47
|
+
};
|
|
48
|
+
return new TransformStream({
|
|
49
|
+
start: arm,
|
|
50
|
+
transform(chunk, controller) {
|
|
51
|
+
arm(controller);
|
|
52
|
+
controller.enqueue(chunk);
|
|
53
|
+
},
|
|
54
|
+
flush: clear,
|
|
55
|
+
cancel: clear,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
27
58
|
/**
|
|
28
59
|
* Decode a fetch response body into a stream of {@link RawEvent}s.
|
|
29
60
|
*
|
|
30
61
|
* No cursor/replay support by design — the contract carries no `id`/
|
|
31
62
|
* `Last-Event-ID`; a blank `data:` frame (no payload) is dropped, matching
|
|
32
|
-
* the Python decoder.
|
|
63
|
+
* the Python decoder. A read-idle timeout (default {@link SSE_IDLE_TIMEOUT_MS})
|
|
64
|
+
* errors a silently-stalled connection instead of blocking forever.
|
|
33
65
|
*/
|
|
34
|
-
export async function* iterateSse(body) {
|
|
66
|
+
export async function* iterateSse(body, options = {}) {
|
|
35
67
|
const stream = body
|
|
68
|
+
.pipeThrough(idleTimeout(options.idleTimeoutMs ?? SSE_IDLE_TIMEOUT_MS))
|
|
36
69
|
.pipeThrough(new TextDecoderStream())
|
|
37
70
|
.pipeThrough(new EventSourceParserStream());
|
|
38
71
|
for await (const message of stream) {
|
package/dist/low/transport.d.ts
CHANGED
|
@@ -38,10 +38,24 @@ export interface RequestOptions {
|
|
|
38
38
|
* must not time out while idle mid-job).
|
|
39
39
|
*/
|
|
40
40
|
timeoutMs?: number | null;
|
|
41
|
+
/** Defaults to `"follow"`; `getAssetContentUrl` passes `"manual"` so it can
|
|
42
|
+
* read a redirect's `Location` instead of following it. */
|
|
43
|
+
redirect?: "follow" | "manual" | "error";
|
|
41
44
|
}
|
|
42
45
|
export interface ComfyLowOptions {
|
|
43
46
|
fetch?: typeof fetch;
|
|
44
47
|
timeoutMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Appended to the default `User-Agent` as `app/{clientInfo}` — lets an
|
|
50
|
+
* app built on this SDK attribute its own traffic in request logs.
|
|
51
|
+
* Opt-in; omitted by default.
|
|
52
|
+
*/
|
|
53
|
+
clientInfo?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Return shape of {@link ComfyLow.getAssetContentUrl}. */
|
|
56
|
+
export interface AssetContentUrl {
|
|
57
|
+
url: string;
|
|
58
|
+
expiresAt: Date | null;
|
|
45
59
|
}
|
|
46
60
|
/** Synchronous protocol bindings — async throughout (JS is async-native). */
|
|
47
61
|
export declare class ComfyLow {
|
|
@@ -49,6 +63,7 @@ export declare class ComfyLow {
|
|
|
49
63
|
private readonly apiKey?;
|
|
50
64
|
private readonly fetchImpl;
|
|
51
65
|
private readonly defaultTimeoutMs;
|
|
66
|
+
private readonly userAgent;
|
|
52
67
|
constructor(baseUrl: string, apiKey?: string, options?: ComfyLowOptions);
|
|
53
68
|
private urlFor;
|
|
54
69
|
/**
|
|
@@ -100,6 +115,23 @@ export declare class ComfyLow {
|
|
|
100
115
|
range?: readonly [number, number];
|
|
101
116
|
signal?: AbortSignal;
|
|
102
117
|
}): Promise<Response>;
|
|
118
|
+
/**
|
|
119
|
+
* `GET /api/v2/assets/{id}/content` with the redirect **not** followed —
|
|
120
|
+
* hands back a directly-fetchable URL instead of the bytes, so a caller
|
|
121
|
+
* (e.g. a serverless/Cloudflare Worker) can pass the URL to a downstream
|
|
122
|
+
* consumer without streaming the body through itself. On an object-storage
|
|
123
|
+
* backend (Cloud/serverless) the server redirects to a short-lived signed
|
|
124
|
+
* URL; that redirect is deliberately not followed (unlike
|
|
125
|
+
* {@link getAssetContent}), both so its `Location` can be read and so this
|
|
126
|
+
* client's bearer token is never attached to the object-storage host. On a
|
|
127
|
+
* self-hosted proxy (which serves the bytes inline, no redirect) this
|
|
128
|
+
* returns the endpoint's own absolute URL instead. Works on every backend
|
|
129
|
+
* and never throws for either shape — only a genuine failure maps to the
|
|
130
|
+
* usual typed error.
|
|
131
|
+
*/
|
|
132
|
+
getAssetContentUrl(assetId: string, options?: {
|
|
133
|
+
signal?: AbortSignal;
|
|
134
|
+
}): Promise<AssetContentUrl>;
|
|
103
135
|
/**
|
|
104
136
|
* `POST /api/v2/jobs`. `extraData` (e.g. the partner-node API key) is a
|
|
105
137
|
* sibling of `workflow` on the wire, per the spec's closed `extra_data`
|
package/dist/low/transport.js
CHANGED
|
@@ -27,11 +27,53 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { errorFromEnvelope } from "./errors.js";
|
|
29
29
|
import { iterateSse } from "./sse.js";
|
|
30
|
+
import { SDK_VERSION } from "./version.js";
|
|
30
31
|
const API_PREFIX = "/api/v2";
|
|
31
32
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
32
33
|
function looksLikePath(value) {
|
|
33
34
|
return value.startsWith("http") || value.startsWith("/");
|
|
34
35
|
}
|
|
36
|
+
function buildUserAgent(clientInfo) {
|
|
37
|
+
const base = `comfy-sdk-typescript/${SDK_VERSION} (node ${process.version})`;
|
|
38
|
+
if (!clientInfo)
|
|
39
|
+
return base;
|
|
40
|
+
// A caller-set token goes verbatim into a header value; reject CR/LF so it
|
|
41
|
+
// can never split/inject headers (undici would reject it anyway, but fail
|
|
42
|
+
// fast with a clear message at construction).
|
|
43
|
+
if (/[\r\n]/.test(clientInfo)) {
|
|
44
|
+
throw new Error("clientInfo must not contain CR or LF characters");
|
|
45
|
+
}
|
|
46
|
+
return `${base} app/${clientInfo}`;
|
|
47
|
+
}
|
|
48
|
+
const GOOG_DATE_RE = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/;
|
|
49
|
+
/**
|
|
50
|
+
* Expiry off a GCS V4 signed URL's `X-Goog-Date` (`YYYYMMDDTHHMMSSZ`, UTC) +
|
|
51
|
+
* `X-Goog-Expires` (seconds) query params. `null` if either is missing or
|
|
52
|
+
* doesn't match — an unrecognized signed-URL flavor degrades to "unknown
|
|
53
|
+
* expiry" rather than throwing.
|
|
54
|
+
*/
|
|
55
|
+
function parseExpiry(url) {
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = new URL(url);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const dateParam = parsed.searchParams.get("X-Goog-Date");
|
|
64
|
+
const expiresParam = parsed.searchParams.get("X-Goog-Expires");
|
|
65
|
+
if (!dateParam || !expiresParam)
|
|
66
|
+
return null;
|
|
67
|
+
const match = GOOG_DATE_RE.exec(dateParam);
|
|
68
|
+
if (!match)
|
|
69
|
+
return null;
|
|
70
|
+
const expiresSeconds = Number.parseInt(expiresParam, 10);
|
|
71
|
+
if (Number.isNaN(expiresSeconds))
|
|
72
|
+
return null;
|
|
73
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
74
|
+
const epochMs = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
|
|
75
|
+
return new Date(epochMs + expiresSeconds * 1000);
|
|
76
|
+
}
|
|
35
77
|
function parseRetryAfter(response) {
|
|
36
78
|
const raw = response.headers.get("Retry-After");
|
|
37
79
|
if (raw === null)
|
|
@@ -45,17 +87,28 @@ export class ComfyLow {
|
|
|
45
87
|
apiKey;
|
|
46
88
|
fetchImpl;
|
|
47
89
|
defaultTimeoutMs;
|
|
90
|
+
userAgent;
|
|
48
91
|
constructor(baseUrl, apiKey, options = {}) {
|
|
49
92
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
50
93
|
this.apiKey = apiKey;
|
|
51
94
|
this.fetchImpl = options.fetch ?? fetch;
|
|
52
95
|
this.defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
96
|
+
this.userAgent = buildUserAgent(options.clientInfo);
|
|
53
97
|
}
|
|
54
98
|
urlFor(path) {
|
|
55
99
|
if (path.startsWith("http"))
|
|
56
100
|
return path;
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
// A server link (job.urls.*, marked by containing /api/) already carries
|
|
102
|
+
// the server's mount prefix, so it resolves against the origin — joining
|
|
103
|
+
// it to baseUrl would double the prefix on a prefix-mounted surface.
|
|
104
|
+
if (path.startsWith("/") && path.includes("/api/")) {
|
|
105
|
+
try {
|
|
106
|
+
return new URL(this.baseUrl).origin + path;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return this.baseUrl + path;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
59
112
|
return this.baseUrl + API_PREFIX + path;
|
|
60
113
|
}
|
|
61
114
|
/**
|
|
@@ -90,6 +143,12 @@ export class ComfyLow {
|
|
|
90
143
|
headers.set(key, value);
|
|
91
144
|
}
|
|
92
145
|
}
|
|
146
|
+
// Identifies this SDK's traffic in request logs (support + adoption
|
|
147
|
+
// metrics, no other data collected) — never overrides a caller-supplied
|
|
148
|
+
// header.
|
|
149
|
+
if (!headers.has("User-Agent")) {
|
|
150
|
+
headers.set("User-Agent", this.userAgent);
|
|
151
|
+
}
|
|
93
152
|
return headers;
|
|
94
153
|
}
|
|
95
154
|
resolveSignal(callerSignal, timeoutMs) {
|
|
@@ -113,7 +172,13 @@ export class ComfyLow {
|
|
|
113
172
|
body = JSON.stringify(options.json);
|
|
114
173
|
}
|
|
115
174
|
const signal = this.resolveSignal(options.signal, options.timeoutMs);
|
|
116
|
-
return this.fetchImpl(url, {
|
|
175
|
+
return this.fetchImpl(url, {
|
|
176
|
+
method,
|
|
177
|
+
headers,
|
|
178
|
+
body,
|
|
179
|
+
signal,
|
|
180
|
+
redirect: options.redirect ?? "follow",
|
|
181
|
+
});
|
|
117
182
|
}
|
|
118
183
|
async parseOrRaise(response, ok) {
|
|
119
184
|
if (ok.includes(response.status)) {
|
|
@@ -137,8 +202,11 @@ export class ComfyLow {
|
|
|
137
202
|
// -- assets -------------------------------------------------------------
|
|
138
203
|
/** `POST /api/v2/assets` — streaming multipart upload. */
|
|
139
204
|
async postAssets(file, contentType, filePath, options = {}) {
|
|
205
|
+
// public-api streams the multipart upload and requires the metadata fields
|
|
206
|
+
// BEFORE the file part (so it can route the file stream without buffering).
|
|
207
|
+
// The `file` part MUST be appended last, or the server rejects with 422
|
|
208
|
+
// "content_type is required and must be sent before the file field".
|
|
140
209
|
const form = new FormData();
|
|
141
|
-
form.append("file", file, filePath);
|
|
142
210
|
form.append("content_type", contentType);
|
|
143
211
|
form.append("file_path", filePath);
|
|
144
212
|
if (options.expectedHash !== undefined) {
|
|
@@ -147,6 +215,7 @@ export class ComfyLow {
|
|
|
147
215
|
for (const tag of options.tags ?? []) {
|
|
148
216
|
form.append("tags", tag);
|
|
149
217
|
}
|
|
218
|
+
form.append("file", file, filePath);
|
|
150
219
|
const headers = {};
|
|
151
220
|
if (options.idempotencyKey) {
|
|
152
221
|
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
@@ -209,6 +278,41 @@ export class ComfyLow {
|
|
|
209
278
|
}
|
|
210
279
|
return response;
|
|
211
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* `GET /api/v2/assets/{id}/content` with the redirect **not** followed —
|
|
283
|
+
* hands back a directly-fetchable URL instead of the bytes, so a caller
|
|
284
|
+
* (e.g. a serverless/Cloudflare Worker) can pass the URL to a downstream
|
|
285
|
+
* consumer without streaming the body through itself. On an object-storage
|
|
286
|
+
* backend (Cloud/serverless) the server redirects to a short-lived signed
|
|
287
|
+
* URL; that redirect is deliberately not followed (unlike
|
|
288
|
+
* {@link getAssetContent}), both so its `Location` can be read and so this
|
|
289
|
+
* client's bearer token is never attached to the object-storage host. On a
|
|
290
|
+
* self-hosted proxy (which serves the bytes inline, no redirect) this
|
|
291
|
+
* returns the endpoint's own absolute URL instead. Works on every backend
|
|
292
|
+
* and never throws for either shape — only a genuine failure maps to the
|
|
293
|
+
* usual typed error.
|
|
294
|
+
*/
|
|
295
|
+
async getAssetContentUrl(assetId, options = {}) {
|
|
296
|
+
const path = `/assets/${encodeURIComponent(assetId)}/content`;
|
|
297
|
+
const response = await this.request("GET", path, {
|
|
298
|
+
signal: options.signal,
|
|
299
|
+
redirect: "manual",
|
|
300
|
+
});
|
|
301
|
+
const location = response.headers.get("Location");
|
|
302
|
+
// Node/undici hands back the real 3xx status with a readable `Location`
|
|
303
|
+
// for a manual redirect; status 0 covers a browser's opaque redirect.
|
|
304
|
+
// We only want the URL, not the bytes — release the response body so
|
|
305
|
+
// undici can reuse the connection instead of pinning it until GC.
|
|
306
|
+
if (location && (response.status === 0 || (response.status >= 300 && response.status < 400))) {
|
|
307
|
+
await response.body?.cancel();
|
|
308
|
+
return { url: location, expiresAt: parseExpiry(location) };
|
|
309
|
+
}
|
|
310
|
+
if (response.status === 200 || response.status === 206) {
|
|
311
|
+
await response.body?.cancel();
|
|
312
|
+
return { url: this.urlFor(path), expiresAt: null };
|
|
313
|
+
}
|
|
314
|
+
return this.parseOrRaise(response, [200, 206]); // always throws here
|
|
315
|
+
}
|
|
212
316
|
// -- jobs -----------------------------------------------------------------
|
|
213
317
|
/**
|
|
214
318
|
* `POST /api/v2/jobs`. `extraData` (e.g. the partner-node API key) is a
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SDK_VERSION = "0.1.3";
|
package/dist/sdk/client.d.ts
CHANGED
|
@@ -37,6 +37,9 @@ export interface ComfyOptions {
|
|
|
37
37
|
apiKey?: string;
|
|
38
38
|
timeoutMs?: number;
|
|
39
39
|
fetch?: ComfyLowOptions["fetch"];
|
|
40
|
+
/** Appended to the SDK's default `User-Agent` as `app/{clientInfo}` — lets
|
|
41
|
+
* an app built on this SDK attribute its own traffic in request logs. */
|
|
42
|
+
clientInfo?: string;
|
|
40
43
|
}
|
|
41
44
|
export declare class Comfy {
|
|
42
45
|
private readonly low;
|
package/dist/sdk/client.js
CHANGED
|
@@ -52,6 +52,7 @@ export class Comfy {
|
|
|
52
52
|
this.low = new ComfyLow(baseUrl, options.apiKey, {
|
|
53
53
|
timeoutMs: options.timeoutMs,
|
|
54
54
|
fetch: options.fetch,
|
|
55
|
+
clientInfo: options.clientInfo,
|
|
55
56
|
});
|
|
56
57
|
this.assets = new AssetFactory(this.low);
|
|
57
58
|
this.workflows = new WorkflowFactory();
|
package/dist/sdk/exceptions.js
CHANGED
|
@@ -79,6 +79,11 @@ const BY_CODE = {
|
|
|
79
79
|
idempotency_key_reuse: IdempotencyKeyReuse,
|
|
80
80
|
insufficient_credits: InsufficientCredits,
|
|
81
81
|
not_found: NotFound,
|
|
82
|
+
// public-api returns entity-specific 404 codes even though the spec documents
|
|
83
|
+
// the generic not_found; map them so a missing job/asset raises the typed
|
|
84
|
+
// NotFound. (Server/spec reconciliation of the code set is a separate follow-up.)
|
|
85
|
+
job_not_found: NotFound,
|
|
86
|
+
asset_not_found: NotFound,
|
|
82
87
|
unauthorized: Unauthorized,
|
|
83
88
|
forbidden: Forbidden,
|
|
84
89
|
};
|
package/dist/sdk/outputs.d.ts
CHANGED
|
@@ -23,4 +23,18 @@ export declare class Output {
|
|
|
23
23
|
toBytes(options?: {
|
|
24
24
|
range?: readonly [number, number];
|
|
25
25
|
}): Promise<Uint8Array>;
|
|
26
|
+
/**
|
|
27
|
+
* A directly-fetchable URL for this output's bytes — a short-lived,
|
|
28
|
+
* self-authorizing bearer credential (readable until `expiresAt`) — so a
|
|
29
|
+
* caller (e.g. a serverless/Cloudflare Worker) can hand the URL to a
|
|
30
|
+
* downstream consumer instead of streaming the bytes through itself. On an
|
|
31
|
+
* object-storage backend (Cloud/serverless) this is a signed URL and
|
|
32
|
+
* `expiresAt` is set; on a self-hosted proxy (which serves the bytes
|
|
33
|
+
* inline) it's this asset's own content URL and `expiresAt` is `null`.
|
|
34
|
+
* Works on every backend and never throws.
|
|
35
|
+
*/
|
|
36
|
+
getDownloadUrl(): Promise<{
|
|
37
|
+
url: string;
|
|
38
|
+
expiresAt: Date | null;
|
|
39
|
+
}>;
|
|
26
40
|
}
|
package/dist/sdk/outputs.js
CHANGED
|
@@ -45,4 +45,17 @@ export class Output {
|
|
|
45
45
|
const response = await this.low.getAssetContent(this.model.id, { range: options.range });
|
|
46
46
|
return new Uint8Array(await response.arrayBuffer());
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A directly-fetchable URL for this output's bytes — a short-lived,
|
|
50
|
+
* self-authorizing bearer credential (readable until `expiresAt`) — so a
|
|
51
|
+
* caller (e.g. a serverless/Cloudflare Worker) can hand the URL to a
|
|
52
|
+
* downstream consumer instead of streaming the bytes through itself. On an
|
|
53
|
+
* object-storage backend (Cloud/serverless) this is a signed URL and
|
|
54
|
+
* `expiresAt` is set; on a self-hosted proxy (which serves the bytes
|
|
55
|
+
* inline) it's this asset's own content URL and `expiresAt` is `null`.
|
|
56
|
+
* Works on every backend and never throws.
|
|
57
|
+
*/
|
|
58
|
+
async getDownloadUrl() {
|
|
59
|
+
return this.low.getAssetContentUrl(this.model.id);
|
|
60
|
+
}
|
|
48
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comfyorg/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "TypeScript SDK for running ComfyUI workflows via the Comfy API v2 (self-hosted, Comfy Cloud, serverless).",
|
|
6
6
|
"homepage": "https://github.com/Comfy-Org/ComfyTypeScriptSDK#readme",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"node": ">=22"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
|
-
"build": "tsc",
|
|
49
|
+
"build": "node scripts/gen-version.mjs && tsc",
|
|
50
50
|
"generate": "openapi-ts",
|
|
51
51
|
"lint": "oxlint .",
|
|
52
52
|
"format": "oxfmt --write .",
|