dreamlayer 0.3.0 → 0.4.0-beta.1
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 +17 -0
- package/dist/cli.js +70 -4
- package/dist/client.d.ts +16 -2
- package/dist/client.js +79 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,3 +107,20 @@ Node.js 22.12 or later.
|
|
|
107
107
|
## License
|
|
108
108
|
|
|
109
109
|
MIT. See LICENSE and NOTICE.
|
|
110
|
+
|
|
111
|
+
## Sprite-sheet beta
|
|
112
|
+
|
|
113
|
+
Sprite requests accept exactly one of `options.animation_prompt` (1–4000 characters) or an `options.action` preset (`walk`, `run`, `idle`). Custom prompts can describe characters, creatures, objects, effects or 360° turntables. `animation_mode` is `loop` or `once`; presets default to loop, custom prompts to once. For a turntable, request a stationary camera and rotating subject. Broad requests do not guarantee correct motion, unseen details or successful effect transparency.
|
|
114
|
+
|
|
115
|
+
Request integer `frame_count` 7–100 (default 12) and `frame_size` 32, 64, 128, 256, 512, 720 or 1080 (default 512). These are square export canvases; a larger export does not guarantee additional detail. Aspect ratio and shared alignment are preserved with transparent padding. A bundle contains transparent PNG frames, sheet, atlas, preview and import instructions, including each frame's playback duration. Choose a repeating loop or a one-time action with a beginning and ending. If the requested number of distinct frames cannot be delivered, the job fails and held credits are returned. Translucent effects can lose detail or fail; small exports are not automatically pixel art.
|
|
116
|
+
|
|
117
|
+
Pricing is unchanged across sizes: frames 1–14 cost $0.14 each; additional frames $0.07 each. One credit is $0.17. Round the complete order upward once to a tenth of a credit. Check `sprite_pricing` in capabilities and approve the quote with `max_credits`. Credits are held during processing, settled after complete delivery and restored on failure/timeout. There is no customer cancellation. Keep the execution ID to resume status. Custom requests need the matching broad-animation server release; older servers reject them. New live generation quality, 100-frame duration and actual cost remain unverified.
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
dreamlayer sprite character.png --action walk --frames 12 --max-credits 9.9 --out walk.zip
|
|
121
|
+
dreamlayer status EXECUTION_ID
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Set `--max-credits` to the amount you approve after checking the current price. The CLI reconnects to existing work if an event stream closes.
|
|
125
|
+
|
|
126
|
+
For affordability, compare the complete rounded quote in **credits** with `available`. One tenth of a credit is $0.017. Promotional and purchased amounts are displayed rounded down separately, so their displayed sum can be 0.1 credit below `available`; stored fractions are preserved. Compare against the combined total, not that sum. The order charge rounds only once, never per frame or per tier.
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spriteCreditPrice } from "./client.js";
|
|
2
3
|
/**
|
|
3
4
|
* DreamLayer CLI.
|
|
4
5
|
*
|
|
@@ -28,6 +29,7 @@ USAGE
|
|
|
28
29
|
dreamlayer edit <image> <prompt> [--out <file>]
|
|
29
30
|
dreamlayer cutout <image> [--out <file>]
|
|
30
31
|
dreamlayer upscale <image> [--out <file>]
|
|
32
|
+
dreamlayer sprite <image> --action <walk|run|idle> [--frames <7–100>] --max-credits <n> [--out <zip>]
|
|
31
33
|
dreamlayer answer <conversation-id> <text> [--image <file>] [--out <file>]
|
|
32
34
|
dreamlayer status <execution-id>
|
|
33
35
|
dreamlayer balance
|
|
@@ -37,6 +39,12 @@ OPTIONS
|
|
|
37
39
|
--out <file> Where to write the image. Default: dreamlayer-<n>.png
|
|
38
40
|
--image <file> Attach an image when answering a question that asks for one
|
|
39
41
|
--aspect <ratio> 1:1, 16:9, 9:16, 4:3, 3:4. Default 1:1
|
|
42
|
+
--action <name> Sprite preset: walk, run, idle (walk if no custom prompt)
|
|
43
|
+
--animation-prompt <text> Custom animation; cannot combine with --action
|
|
44
|
+
--animation-mode <loop|once> Default: loop for presets, once for custom
|
|
45
|
+
--frame-size <px> Square export: 32, 64, 128, 256, 512 (default), 720, 1080
|
|
46
|
+
--frames <n> Frame count: integer 7–100, default 12
|
|
47
|
+
--max-credits <n> Maximum approved charge for the sprite job
|
|
40
48
|
--json Machine-readable output on stdout
|
|
41
49
|
--quiet No progress on stderr
|
|
42
50
|
--idempotency-key <key> Reuse to retry safely after an uncertain response
|
|
@@ -45,13 +53,15 @@ ENVIRONMENT
|
|
|
45
53
|
DREAMLAYER_API_KEY Required. Get one at https://platform.dreamlayer.io
|
|
46
54
|
DREAMLAYER_API_URL Override the endpoint. Default https://api.dreamlayer.io
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
Image operations cost one credit. Sprite pricing is listed in capabilities. A new account starts at zero.
|
|
49
57
|
`;
|
|
50
58
|
class UsageError extends Error {
|
|
51
59
|
}
|
|
52
60
|
function parseOptions(argv) {
|
|
53
61
|
const positional = [];
|
|
54
62
|
const options = {
|
|
63
|
+
maxCredits: 1,
|
|
64
|
+
frameCount: 12,
|
|
55
65
|
out: null,
|
|
56
66
|
image: null,
|
|
57
67
|
aspect: "1:1",
|
|
@@ -71,6 +81,42 @@ function parseOptions(argv) {
|
|
|
71
81
|
throw new UsageError("--out needs a file path");
|
|
72
82
|
options.out = value;
|
|
73
83
|
}
|
|
84
|
+
else if (token === "--action") {
|
|
85
|
+
const value = argv[++i];
|
|
86
|
+
if (value !== "walk" && value !== "run" && value !== "idle")
|
|
87
|
+
throw new UsageError("--action must be walk, run, or idle");
|
|
88
|
+
options.action = value;
|
|
89
|
+
}
|
|
90
|
+
else if (token === "--animation-prompt") {
|
|
91
|
+
const value = argv[++i];
|
|
92
|
+
if (!value?.trim() || [...value].length > 4000)
|
|
93
|
+
throw new UsageError("--animation-prompt needs 1–4000 characters");
|
|
94
|
+
options.animationPrompt = value;
|
|
95
|
+
}
|
|
96
|
+
else if (token === "--animation-mode") {
|
|
97
|
+
const value = argv[++i];
|
|
98
|
+
if (value !== "loop" && value !== "once")
|
|
99
|
+
throw new UsageError("--animation-mode must be loop or once");
|
|
100
|
+
options.animationMode = value;
|
|
101
|
+
}
|
|
102
|
+
else if (token === "--frame-size") {
|
|
103
|
+
const value = Number(argv[++i]);
|
|
104
|
+
if (![32, 64, 128, 256, 512, 720, 1080].includes(value))
|
|
105
|
+
throw new UsageError("--frame-size must be 32, 64, 128, 256, 512, 720 or 1080");
|
|
106
|
+
options.frameSize = value;
|
|
107
|
+
}
|
|
108
|
+
else if (token === "--frames") {
|
|
109
|
+
const value = Number(argv[++i]);
|
|
110
|
+
if (!Number.isInteger(value) || value < 7 || value > 100)
|
|
111
|
+
throw new UsageError("--frames must be an integer from 7 to 100");
|
|
112
|
+
options.frameCount = value;
|
|
113
|
+
}
|
|
114
|
+
else if (token === "--max-credits") {
|
|
115
|
+
const value = Number(argv[++i]);
|
|
116
|
+
if (!Number.isFinite(value) || value < 0.1 || value > 100)
|
|
117
|
+
throw new UsageError("--max-credits must be 0.1 to 100");
|
|
118
|
+
options.maxCredits = value;
|
|
119
|
+
}
|
|
74
120
|
else if (token === "--image") {
|
|
75
121
|
const value = argv[++i];
|
|
76
122
|
if (!value)
|
|
@@ -130,7 +176,7 @@ function defaultOut() {
|
|
|
130
176
|
async function run(api, input, options) {
|
|
131
177
|
const progress = new Progress(!options.quiet && process.stderr.isTTY === true);
|
|
132
178
|
const idempotencyKey = options.idempotencyKey ?? randomUUID();
|
|
133
|
-
const outcome = await consume(api.
|
|
179
|
+
const outcome = await consume(api.follow(input, { idempotencyKey }), progress);
|
|
134
180
|
if (outcome.question) {
|
|
135
181
|
progress.stop();
|
|
136
182
|
if (options.json) {
|
|
@@ -161,7 +207,7 @@ async function run(api, input, options) {
|
|
|
161
207
|
}
|
|
162
208
|
progress.set("Downloading");
|
|
163
209
|
const bytes = await api.download(outcome.asset.download_url);
|
|
164
|
-
const target = options.out ?? defaultOut();
|
|
210
|
+
const target = options.out ?? (input.operation === "sprite_sheet" ? `dreamlayer-${Date.now()}.zip` : defaultOut());
|
|
165
211
|
await writeFile(target, bytes);
|
|
166
212
|
progress.stop();
|
|
167
213
|
if (options.json) {
|
|
@@ -218,7 +264,7 @@ function warnIfOperationsDrifted(capabilities) {
|
|
|
218
264
|
const server = new Set(listed);
|
|
219
265
|
const mine = new Set(KNOWN_OPERATIONS);
|
|
220
266
|
const serverOnly = [...server].filter((o) => !mine.has(o));
|
|
221
|
-
const clientOnly = [...mine].filter((o) => !server.has(o));
|
|
267
|
+
const clientOnly = [...mine].filter((o) => !server.has(o) && o !== "sprite_sheet");
|
|
222
268
|
if (serverOnly.length === 0 && clientOnly.length === 0)
|
|
223
269
|
return;
|
|
224
270
|
process.stderr.write("\nThis CLI and the server disagree about the operation list.\n");
|
|
@@ -250,6 +296,23 @@ async function main(argv) {
|
|
|
250
296
|
}
|
|
251
297
|
const { positional, options } = parseOptions(rest);
|
|
252
298
|
switch (command) {
|
|
299
|
+
case "sprite": {
|
|
300
|
+
if (options.action && options.animationPrompt)
|
|
301
|
+
throw new UsageError("Use either --action or --animation-prompt, not both");
|
|
302
|
+
const file = positional[0];
|
|
303
|
+
if (!file)
|
|
304
|
+
throw new UsageError("sprite needs a reference image");
|
|
305
|
+
const api = client();
|
|
306
|
+
const caps = await api.getCapabilities();
|
|
307
|
+
if (!Array.isArray(caps.operations) || !caps.operations.includes("sprite_sheet"))
|
|
308
|
+
throw new UsageError("sprite beta access is not enabled for this account");
|
|
309
|
+
if (!caps.sprite_pricing)
|
|
310
|
+
throw new UsageError("The server does not support configurable sprite pricing yet");
|
|
311
|
+
const price = spriteCreditPrice(options.frameCount);
|
|
312
|
+
if (options.maxCredits < price)
|
|
313
|
+
throw new UsageError(`Sprite jobs require ${price} credits. Set --max-credits to approve that amount.`);
|
|
314
|
+
return run(api, { operation: "sprite_sheet", input_asset_id: await upload(api, file), options: { ...(options.animationPrompt ? { animation_prompt: options.animationPrompt } : { action: options.action ?? "walk" }), ...(options.animationMode ? { animation_mode: options.animationMode } : {}), ...(options.frameSize ? { frame_size: options.frameSize } : {}), frame_count: options.frameCount }, max_credits: options.maxCredits }, options);
|
|
315
|
+
}
|
|
253
316
|
case "generate": {
|
|
254
317
|
const prompt = positional[0];
|
|
255
318
|
if (!prompt)
|
|
@@ -309,6 +372,9 @@ async function main(argv) {
|
|
|
309
372
|
process.stdout.write(`${balance.available} credits available ` +
|
|
310
373
|
`(${balance.promotional} promotional, ${balance.purchased} purchased)\n`);
|
|
311
374
|
}
|
|
375
|
+
if (!options.json && Math.round(balance.available * 10) > Math.round(balance.promotional * 10) + Math.round(balance.purchased * 10)) {
|
|
376
|
+
process.stdout.write("Use the available total for affordability. Funding balances are rounded down separately; stored fractions are preserved.\n");
|
|
377
|
+
}
|
|
312
378
|
return 0;
|
|
313
379
|
}
|
|
314
380
|
case "capabilities": {
|
package/dist/client.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type ManagedEvent = {
|
|
|
34
34
|
* anyway. Confirmed against the deployment, not the source: the live /openapi.json
|
|
35
35
|
* advertises exactly these four in both ExecuteRequest and ImageJobCreate.
|
|
36
36
|
*/
|
|
37
|
-
export declare const KNOWN_OPERATIONS: readonly ["text_to_image", "image_to_image", "background_remove", "upscale"];
|
|
37
|
+
export declare const KNOWN_OPERATIONS: readonly ["text_to_image", "image_to_image", "background_remove", "upscale", "sprite_sheet"];
|
|
38
38
|
/**
|
|
39
39
|
* Derived from the array above, not written twice.
|
|
40
40
|
*
|
|
@@ -53,7 +53,17 @@ export type ManagedExecuteInput = {
|
|
|
53
53
|
aspect_ratio?: string;
|
|
54
54
|
/** Requires the gateway build that added it. See ManagedOperation. */
|
|
55
55
|
operation?: ManagedOperation;
|
|
56
|
+
options?: {
|
|
57
|
+
action?: "walk" | "run" | "idle";
|
|
58
|
+
animation_prompt?: string;
|
|
59
|
+
animation_mode?: "loop" | "once";
|
|
60
|
+
frame_count?: number;
|
|
61
|
+
frame_size?: 32 | 64 | 128 | 256 | 512 | 720 | 1080;
|
|
62
|
+
};
|
|
63
|
+
max_credits?: number;
|
|
56
64
|
};
|
|
65
|
+
export declare function spriteCreditPrice(frameCount: number): number;
|
|
66
|
+
export declare function validateSpriteInput(input: ManagedExecuteInput): void;
|
|
57
67
|
export type ManagedInputAsset = {
|
|
58
68
|
input_asset_id: string;
|
|
59
69
|
width: number;
|
|
@@ -66,7 +76,7 @@ export type ManagedExecution = {
|
|
|
66
76
|
status: string;
|
|
67
77
|
image_job: Record<string, unknown> | null;
|
|
68
78
|
};
|
|
69
|
-
export declare const PUBLIC_ERROR_REASONS: readonly ["invalid_request", "authentication_failed", "access_denied", "resource_not_found", "insufficient_credits", "conflict", "too_many_active_jobs", "rate_limited", "quota_exceeded", "content_refused", "temporarily_unavailable", "generation_failed"];
|
|
79
|
+
export declare const PUBLIC_ERROR_REASONS: readonly ["invalid_request", "authentication_failed", "access_denied", "resource_not_found", "insufficient_credits", "conflict", "too_many_active_jobs", "rate_limited", "quota_exceeded", "content_refused", "temporarily_unavailable", "generation_failed", "insufficient_frames"];
|
|
70
80
|
export type PublicErrorReason = (typeof PUBLIC_ERROR_REASONS)[number];
|
|
71
81
|
export declare class ApiError extends Error {
|
|
72
82
|
readonly status: number;
|
|
@@ -130,6 +140,10 @@ export declare class ManagedClient {
|
|
|
130
140
|
execute(input: ManagedExecuteInput, options: {
|
|
131
141
|
idempotencyKey: string;
|
|
132
142
|
}): AsyncGenerator<ManagedEvent>;
|
|
143
|
+
/** Follow a durable job across finite streams without submitting it twice. */
|
|
144
|
+
follow(input: ManagedExecuteInput, options: {
|
|
145
|
+
idempotencyKey: string;
|
|
146
|
+
}): AsyncGenerator<ManagedEvent>;
|
|
133
147
|
/** Resume a stream after a drop. Pass the last event id you actually processed. */
|
|
134
148
|
events(executionId: string, lastEventId?: string): AsyncGenerator<ManagedEvent>;
|
|
135
149
|
getCapabilities(): Promise<Record<string, unknown>>;
|
package/dist/client.js
CHANGED
|
@@ -33,7 +33,32 @@ export const KNOWN_OPERATIONS = [
|
|
|
33
33
|
"image_to_image",
|
|
34
34
|
"background_remove",
|
|
35
35
|
"upscale",
|
|
36
|
+
"sprite_sheet",
|
|
36
37
|
];
|
|
38
|
+
export function spriteCreditPrice(frameCount) {
|
|
39
|
+
if (!Number.isInteger(frameCount) || frameCount < 7 || frameCount > 100)
|
|
40
|
+
throw new Error("frame_count must be an integer from 7 to 100");
|
|
41
|
+
const cents = 14 * Math.min(frameCount, 14) + 7 * Math.max(frameCount - 14, 0);
|
|
42
|
+
return Math.ceil(cents * 10 / 17) / 10;
|
|
43
|
+
}
|
|
44
|
+
export function validateSpriteInput(input) {
|
|
45
|
+
if (input.operation !== "sprite_sheet")
|
|
46
|
+
return;
|
|
47
|
+
const options = input.options;
|
|
48
|
+
if (!options || (options.action === undefined) === (options.animation_prompt === undefined))
|
|
49
|
+
throw new Error("Sprite requests require exactly one of options.action or options.animation_prompt");
|
|
50
|
+
if (options.action !== undefined && !["walk", "run", "idle"].includes(options.action))
|
|
51
|
+
throw new Error("Invalid sprite preset");
|
|
52
|
+
if (options.animation_prompt !== undefined && (typeof options.animation_prompt !== "string" || !options.animation_prompt.trim() || [...options.animation_prompt].length > 4000))
|
|
53
|
+
throw new Error("animation_prompt must contain 1–4000 characters");
|
|
54
|
+
if (options.animation_mode !== undefined && !["loop", "once"].includes(options.animation_mode))
|
|
55
|
+
throw new Error("animation_mode must be loop or once");
|
|
56
|
+
const price = spriteCreditPrice(options.frame_count ?? 12);
|
|
57
|
+
if (options.frame_size !== undefined && ![32, 64, 128, 256, 512, 720, 1080].includes(options.frame_size))
|
|
58
|
+
throw new Error("frame_size must be 32, 64, 128, 256, 512, 720 or 1080");
|
|
59
|
+
if (typeof input.max_credits !== "number" || !Number.isFinite(input.max_credits) || input.max_credits < price || input.max_credits > 100)
|
|
60
|
+
throw new Error(`This sprite request requires ${price} credits. Supply a sufficient max_credits limit.`);
|
|
61
|
+
}
|
|
37
62
|
const DIRECT_INPUT_BYTES = 20 * 1024 * 1024;
|
|
38
63
|
const RASTER_INPUT_SUFFIXES = new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
39
64
|
const LEGACY_INPUT_SUFFIXES = new Set([
|
|
@@ -54,6 +79,7 @@ export const PUBLIC_ERROR_REASONS = [
|
|
|
54
79
|
"content_refused",
|
|
55
80
|
"temporarily_unavailable",
|
|
56
81
|
"generation_failed",
|
|
82
|
+
"insufficient_frames",
|
|
57
83
|
];
|
|
58
84
|
const PUBLIC_ERROR_SPECS = {
|
|
59
85
|
invalid_request: { message: "The request could not be validated.", retryable: false },
|
|
@@ -82,6 +108,7 @@ const PUBLIC_ERROR_SPECS = {
|
|
|
82
108
|
message: "The service is temporarily unavailable. Please try again.",
|
|
83
109
|
retryable: true,
|
|
84
110
|
},
|
|
111
|
+
insufficient_frames: { message: "Not enough distinct animation frames. Try a lower frame count.", retryable: false },
|
|
85
112
|
generation_failed: { message: "Image generation failed.", retryable: false },
|
|
86
113
|
};
|
|
87
114
|
const PUBLIC_ERROR_REASON_SET = new Set(PUBLIC_ERROR_REASONS);
|
|
@@ -123,6 +150,7 @@ function defaultCode(reason) {
|
|
|
123
150
|
content_refused: "CONTENT_REFUSED",
|
|
124
151
|
temporarily_unavailable: "SERVICE_UNAVAILABLE",
|
|
125
152
|
generation_failed: "INTERNAL_ERROR",
|
|
153
|
+
insufficient_frames: "INSUFFICIENT_FRAMES",
|
|
126
154
|
};
|
|
127
155
|
return codes[reason];
|
|
128
156
|
}
|
|
@@ -140,6 +168,7 @@ function statusForReason(reason) {
|
|
|
140
168
|
content_refused: 422,
|
|
141
169
|
temporarily_unavailable: 503,
|
|
142
170
|
generation_failed: 500,
|
|
171
|
+
insufficient_frames: 422,
|
|
143
172
|
};
|
|
144
173
|
return statuses[reason];
|
|
145
174
|
}
|
|
@@ -318,14 +347,17 @@ export function managedBalance(value) {
|
|
|
318
347
|
throw new Error("Invalid DreamLayer balance response");
|
|
319
348
|
}
|
|
320
349
|
for (const field of ["promotional", "purchased", "available"]) {
|
|
321
|
-
if (!Number.
|
|
350
|
+
if (typeof value[field] !== "number" || !Number.isFinite(value[field]) || Number(value[field]) < 0 || Number(value[field]) > Number.MAX_SAFE_INTEGER / 10) {
|
|
322
351
|
throw new Error("Invalid DreamLayer balance response");
|
|
323
352
|
}
|
|
324
353
|
}
|
|
325
354
|
if (value.credit_usd !== "0.17" ||
|
|
326
|
-
Number(value.available)
|
|
355
|
+
Math.round(Number(value.available) * 10) < Math.round(Number(value.promotional) * 10) + Math.round(Number(value.purchased) * 10) ||
|
|
356
|
+
Math.round(Number(value.available) * 10) > Math.round(Number(value.promotional) * 10) + Math.round(Number(value.purchased) * 10) + 1) {
|
|
327
357
|
throw new Error("Invalid DreamLayer balance response");
|
|
328
358
|
}
|
|
359
|
+
if ([value.promotional, value.purchased, value.available].some(v => Math.abs(Number(v) * 10 - Math.round(Number(v) * 10)) > 1e-7))
|
|
360
|
+
throw new Error("Invalid DreamLayer balance response");
|
|
329
361
|
return {
|
|
330
362
|
promotional: Number(value.promotional),
|
|
331
363
|
purchased: Number(value.purchased),
|
|
@@ -518,6 +550,7 @@ export class ManagedClient {
|
|
|
518
550
|
* so a caller could not even resume what it had already paid for.
|
|
519
551
|
*/
|
|
520
552
|
async *execute(input, options) {
|
|
553
|
+
validateSpriteInput(input);
|
|
521
554
|
const stream = await this.fetchStream("/v1/execute", {
|
|
522
555
|
method: "POST",
|
|
523
556
|
headers: {
|
|
@@ -529,6 +562,50 @@ export class ManagedClient {
|
|
|
529
562
|
});
|
|
530
563
|
yield* this.parse(stream);
|
|
531
564
|
}
|
|
565
|
+
/** Follow a durable job across finite streams without submitting it twice. */
|
|
566
|
+
async *follow(input, options) {
|
|
567
|
+
let executionId;
|
|
568
|
+
let cursor;
|
|
569
|
+
let stream = this.execute(input, options);
|
|
570
|
+
const deadline = Date.now() + 16 * 60_000;
|
|
571
|
+
let failures = 0;
|
|
572
|
+
while (Date.now() < deadline) {
|
|
573
|
+
try {
|
|
574
|
+
for await (const event of stream) {
|
|
575
|
+
if (event.event === "started")
|
|
576
|
+
executionId = String(event.data.execution_id);
|
|
577
|
+
if (event.id)
|
|
578
|
+
cursor = event.id;
|
|
579
|
+
yield event;
|
|
580
|
+
if (event.event === "done")
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
failures = 0;
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
if (error instanceof StreamIdleError)
|
|
587
|
+
throw error;
|
|
588
|
+
if (!executionId || (error instanceof ApiError && ![429, 500, 502, 503, 504].includes(error.status)) || ++failures > 5)
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
if (!executionId)
|
|
592
|
+
throw new Error("Execution stream ended before an identifier was received; reuse your idempotency key.");
|
|
593
|
+
const state = await this.getExecution(executionId);
|
|
594
|
+
if (["completed", "failed", "cancelled"].includes(state.status)) {
|
|
595
|
+
if (state.status === "completed") {
|
|
596
|
+
const assets = state.image_job?.finished_assets;
|
|
597
|
+
if (!Array.isArray(assets) || assets.length !== 1 || typeof assets[0]?.download_url !== "string")
|
|
598
|
+
throw new Error(`Execution ${executionId} has no downloadable asset yet.`);
|
|
599
|
+
yield managedEvent("asset", null, { asset_id: assets[0].asset_id, download_url: assets[0].download_url });
|
|
600
|
+
}
|
|
601
|
+
yield managedEvent("done", null, { status: state.status });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(5000, 500 * 2 ** failures)));
|
|
605
|
+
stream = this.events(executionId, cursor);
|
|
606
|
+
}
|
|
607
|
+
throw new Error(`Execution ${executionId ?? "unknown"} is still active. Use status to resume; the job has not been cancelled.`);
|
|
608
|
+
}
|
|
532
609
|
/** Resume a stream after a drop. Pass the last event id you actually processed. */
|
|
533
610
|
async *events(executionId, lastEventId) {
|
|
534
611
|
const headers = { Accept: "text/event-stream" };
|