@howells/motif-sdk 5.0.0 → 5.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 +3 -1
- package/dist/index.cjs +33 -11
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +33 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,9 @@ console.log(result.value.model, result.value.files[0]?.url, result.value.cost);
|
|
|
32
32
|
|
|
33
33
|
Every Task function returns `Result<TaskOutput, MotifError>` from `neverthrow` and does not throw for fal request failures; check `isErr()` / `isOk()`.
|
|
34
34
|
|
|
35
|
-
Pass `onProgress(status, queuePosition)` to hear fal's queue state while a run waits: `"queued"` with its place in line, then `"
|
|
35
|
+
Pass `onProgress(status, queuePosition)` to hear fal's queue state while a run waits: `"queued"` with its place in line, then `"processing"`, then `"completed"` (or `"failed"`). Passing it sends the run through fal's queue, since only the queue reports state. Models without streaming report state, not a percentage.
|
|
36
|
+
|
|
37
|
+
Pass `onSubmitted(requestId)` to hear the request id the moment fal's queue accepts the run, before any wait. Keep it: after a restart, `motif.resume(task, input, requestId)` waits for that same job and returns its output, submitting nothing and so paying nothing twice. Passing `onSubmitted` also queues the run.
|
|
36
38
|
|
|
37
39
|
## Plan Without Calling fal
|
|
38
40
|
|
package/dist/index.cjs
CHANGED
|
@@ -4262,27 +4262,36 @@ async function getToolResult(exec, job) {
|
|
|
4262
4262
|
const data = await response.value.json();
|
|
4263
4263
|
return (0, neverthrow.ok)(isRecord$4(data) ? data : {});
|
|
4264
4264
|
}
|
|
4265
|
-
/**
|
|
4266
|
-
async function
|
|
4267
|
-
const job = await submitRequest(exec, prepared);
|
|
4268
|
-
if (job.isErr()) return (0, neverthrow.err)(job.error);
|
|
4265
|
+
/** Poll a submitted job to completion and fetch its result. Submits nothing. */
|
|
4266
|
+
async function awaitQueuedRequest(exec, job, onProgress) {
|
|
4269
4267
|
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
4270
|
-
const status = await checkToolStatus(exec, job
|
|
4268
|
+
const status = await checkToolStatus(exec, job);
|
|
4271
4269
|
if (status.isErr()) return (0, neverthrow.err)(status.error);
|
|
4272
4270
|
onProgress?.(status.value.status, status.value.queuePosition);
|
|
4273
|
-
if (status.value.status === "completed") return (await getToolResult(exec, job
|
|
4271
|
+
if (status.value.status === "completed") return (await getToolResult(exec, job)).map((data) => ({
|
|
4274
4272
|
data,
|
|
4275
|
-
requestId: job.
|
|
4273
|
+
requestId: job.requestId
|
|
4276
4274
|
}));
|
|
4277
4275
|
if (status.value.status === "failed") {
|
|
4278
4276
|
const message = status.value.error ?? "Queued run failed";
|
|
4279
|
-
return (0, neverthrow.err)(new MotifError(message, 0, void 0, job.
|
|
4277
|
+
return (0, neverthrow.err)(new MotifError(message, 0, void 0, job.requestId));
|
|
4280
4278
|
}
|
|
4281
4279
|
await new Promise((resolve) => {
|
|
4282
4280
|
setTimeout(resolve, POLL_INTERVAL_MS);
|
|
4283
4281
|
});
|
|
4284
4282
|
}
|
|
4285
|
-
return (0, neverthrow.err)(new MotifError("Queued run timed out", 0, void 0, job.
|
|
4283
|
+
return (0, neverthrow.err)(new MotifError("Queued run timed out", 0, void 0, job.requestId));
|
|
4284
|
+
}
|
|
4285
|
+
/**
|
|
4286
|
+
* Submit a prepared request, poll it to completion and fetch the result.
|
|
4287
|
+
* `onSubmitted` hears the request id as soon as fal accepts the job, before
|
|
4288
|
+
* any wait, so a caller can keep it and resume the same job later.
|
|
4289
|
+
*/
|
|
4290
|
+
async function runRequestQueued(exec, prepared, onProgress, onSubmitted) {
|
|
4291
|
+
const job = await submitRequest(exec, prepared);
|
|
4292
|
+
if (job.isErr()) return (0, neverthrow.err)(job.error);
|
|
4293
|
+
onSubmitted?.(job.value.requestId);
|
|
4294
|
+
return await awaitQueuedRequest(exec, job.value, onProgress);
|
|
4286
4295
|
}
|
|
4287
4296
|
/** Body of `FalClient.runTool`. */
|
|
4288
4297
|
async function runTool(exec, options) {
|
|
@@ -9898,7 +9907,7 @@ function generationPlan(task, model, config, input) {
|
|
|
9898
9907
|
endpoint: built.endpoint,
|
|
9899
9908
|
prompt: typeof built.body.prompt === "string" ? built.body.prompt : void 0,
|
|
9900
9909
|
provider: "fal",
|
|
9901
|
-
queued: config.useQueue === true || input.onProgress !== void 0
|
|
9910
|
+
queued: config.useQueue === true || input.onProgress !== void 0 || input.onSubmitted !== void 0
|
|
9902
9911
|
});
|
|
9903
9912
|
}
|
|
9904
9913
|
/**
|
|
@@ -10506,7 +10515,19 @@ function createMotif(config = {}) {
|
|
|
10506
10515
|
headers
|
|
10507
10516
|
};
|
|
10508
10517
|
const executor = falRequestExecutor(client.value);
|
|
10509
|
-
return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
|
|
10518
|
+
return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress, input.onSubmitted) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
|
|
10519
|
+
}
|
|
10520
|
+
async function resume(task, input, requestId) {
|
|
10521
|
+
const planned = plan(task, input);
|
|
10522
|
+
if (planned.isErr()) return (0, neverthrow.err)(planned.error);
|
|
10523
|
+
const chosen = planned.value;
|
|
10524
|
+
if (chosen.provider !== "fal") return (0, neverthrow.err)(new MotifError(`Only a fal run can be resumed; ${chosen.model} runs on ${chosen.provider}.`, 0));
|
|
10525
|
+
const client = falClient();
|
|
10526
|
+
if (client.isErr()) return (0, neverthrow.err)(client.error);
|
|
10527
|
+
return (await awaitQueuedRequest(falRequestExecutor(client.value), {
|
|
10528
|
+
endpoint: chosen.endpoint,
|
|
10529
|
+
requestId
|
|
10530
|
+
}, input.onProgress)).map((value) => falOutput(chosen, value));
|
|
10510
10531
|
}
|
|
10511
10532
|
return {
|
|
10512
10533
|
animate: async (input) => await run("animate", input),
|
|
@@ -10533,6 +10554,7 @@ function createMotif(config = {}) {
|
|
|
10533
10554
|
return client.isErr() ? (0, neverthrow.err)(client.error) : await client.value.deletePayloads(requestId);
|
|
10534
10555
|
},
|
|
10535
10556
|
plan,
|
|
10557
|
+
resume,
|
|
10536
10558
|
run,
|
|
10537
10559
|
stream: taskStreamer(config, plan, context),
|
|
10538
10560
|
async upload(bytes, contentType, fileName) {
|
package/dist/index.d.cts
CHANGED
|
@@ -262,6 +262,15 @@ interface ImageSizeBounds {
|
|
|
262
262
|
/** Both edges must be multiples of this. Motif uses 16 when absent. */
|
|
263
263
|
multipleOf?: number;
|
|
264
264
|
}
|
|
265
|
+
interface JobStatus {
|
|
266
|
+
error?: string;
|
|
267
|
+
logs?: {
|
|
268
|
+
message: string;
|
|
269
|
+
timestamp: string;
|
|
270
|
+
}[];
|
|
271
|
+
queuePosition?: number;
|
|
272
|
+
status: "queued" | "processing" | "completed" | "failed";
|
|
273
|
+
}
|
|
265
274
|
/** ─── Configuration ──────────────────────────────────────────── */
|
|
266
275
|
/** The network seam: a `fetch`-shaped function. */
|
|
267
276
|
type FalFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
@@ -1189,10 +1198,16 @@ interface TaskInput {
|
|
|
1189
1198
|
ephemeral?: boolean;
|
|
1190
1199
|
/**
|
|
1191
1200
|
* Called on each poll of fal's queue with its status ("queued",
|
|
1192
|
-
* "
|
|
1201
|
+
* "processing", "completed", "failed") and, while queued, the place in line. Passing
|
|
1193
1202
|
* it runs a fal generation through the queue, since only the queue reports.
|
|
1194
1203
|
*/
|
|
1195
|
-
onProgress?: (status:
|
|
1204
|
+
onProgress?: (status: JobStatus["status"], queuePosition?: number) => void;
|
|
1205
|
+
/**
|
|
1206
|
+
* Called once, as soon as fal's queue accepts the run, with its request id
|
|
1207
|
+
* and before any wait. Keep it to `resume` the same job after a restart
|
|
1208
|
+
* rather than submitting, and paying for, a second. Passing it queues the run.
|
|
1209
|
+
*/
|
|
1210
|
+
onSubmitted?: (requestId: string) => void;
|
|
1196
1211
|
}
|
|
1197
1212
|
interface TaskFile {
|
|
1198
1213
|
/** Output key the URL came from, e.g. "image", "masks", "model_glb". */
|
|
@@ -1260,6 +1275,12 @@ interface MotifClient {
|
|
|
1260
1275
|
/** Upload bytes to fal storage; returns the public URL. */
|
|
1261
1276
|
upload: (bytes: Uint8Array, contentType: string, fileName?: string) => Promise<Result$1<string, MotifError>>;
|
|
1262
1277
|
deletePayloads: (requestId: string) => Promise<Result$1<void, MotifError>>;
|
|
1278
|
+
/**
|
|
1279
|
+
* Read a queued run already submitted, by the request id `onSubmitted` gave:
|
|
1280
|
+
* wait for it and return its output, submitting nothing. Pass the same task
|
|
1281
|
+
* and input the run was made with, so the endpoint and costing match.
|
|
1282
|
+
*/
|
|
1283
|
+
resume: (task: TaskId, input: TaskInput, requestId: string) => Promise<Result$1<TaskOutput, MotifError>>;
|
|
1263
1284
|
}
|
|
1264
1285
|
/** Create the Task client. Keys fall back to FAL_KEY and OPENAI_API_KEY. */
|
|
1265
1286
|
export declare function createMotif(config?: MotifClientConfig): MotifClient;
|
package/dist/index.d.ts
CHANGED
|
@@ -262,6 +262,15 @@ interface ImageSizeBounds {
|
|
|
262
262
|
/** Both edges must be multiples of this. Motif uses 16 when absent. */
|
|
263
263
|
multipleOf?: number;
|
|
264
264
|
}
|
|
265
|
+
interface JobStatus {
|
|
266
|
+
error?: string;
|
|
267
|
+
logs?: {
|
|
268
|
+
message: string;
|
|
269
|
+
timestamp: string;
|
|
270
|
+
}[];
|
|
271
|
+
queuePosition?: number;
|
|
272
|
+
status: "queued" | "processing" | "completed" | "failed";
|
|
273
|
+
}
|
|
265
274
|
/** ─── Configuration ──────────────────────────────────────────── */
|
|
266
275
|
/** The network seam: a `fetch`-shaped function. */
|
|
267
276
|
type FalFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
@@ -1189,10 +1198,16 @@ interface TaskInput {
|
|
|
1189
1198
|
ephemeral?: boolean;
|
|
1190
1199
|
/**
|
|
1191
1200
|
* Called on each poll of fal's queue with its status ("queued",
|
|
1192
|
-
* "
|
|
1201
|
+
* "processing", "completed", "failed") and, while queued, the place in line. Passing
|
|
1193
1202
|
* it runs a fal generation through the queue, since only the queue reports.
|
|
1194
1203
|
*/
|
|
1195
|
-
onProgress?: (status:
|
|
1204
|
+
onProgress?: (status: JobStatus["status"], queuePosition?: number) => void;
|
|
1205
|
+
/**
|
|
1206
|
+
* Called once, as soon as fal's queue accepts the run, with its request id
|
|
1207
|
+
* and before any wait. Keep it to `resume` the same job after a restart
|
|
1208
|
+
* rather than submitting, and paying for, a second. Passing it queues the run.
|
|
1209
|
+
*/
|
|
1210
|
+
onSubmitted?: (requestId: string) => void;
|
|
1196
1211
|
}
|
|
1197
1212
|
interface TaskFile {
|
|
1198
1213
|
/** Output key the URL came from, e.g. "image", "masks", "model_glb". */
|
|
@@ -1260,6 +1275,12 @@ interface MotifClient {
|
|
|
1260
1275
|
/** Upload bytes to fal storage; returns the public URL. */
|
|
1261
1276
|
upload: (bytes: Uint8Array, contentType: string, fileName?: string) => Promise<Result$1<string, MotifError>>;
|
|
1262
1277
|
deletePayloads: (requestId: string) => Promise<Result$1<void, MotifError>>;
|
|
1278
|
+
/**
|
|
1279
|
+
* Read a queued run already submitted, by the request id `onSubmitted` gave:
|
|
1280
|
+
* wait for it and return its output, submitting nothing. Pass the same task
|
|
1281
|
+
* and input the run was made with, so the endpoint and costing match.
|
|
1282
|
+
*/
|
|
1283
|
+
resume: (task: TaskId, input: TaskInput, requestId: string) => Promise<Result$1<TaskOutput, MotifError>>;
|
|
1263
1284
|
}
|
|
1264
1285
|
/** Create the Task client. Keys fall back to FAL_KEY and OPENAI_API_KEY. */
|
|
1265
1286
|
export declare function createMotif(config?: MotifClientConfig): MotifClient;
|
package/dist/index.js
CHANGED
|
@@ -4261,27 +4261,36 @@ async function getToolResult(exec, job) {
|
|
|
4261
4261
|
const data = await response.value.json();
|
|
4262
4262
|
return ok$1(isRecord$4(data) ? data : {});
|
|
4263
4263
|
}
|
|
4264
|
-
/**
|
|
4265
|
-
async function
|
|
4266
|
-
const job = await submitRequest(exec, prepared);
|
|
4267
|
-
if (job.isErr()) return err$1(job.error);
|
|
4264
|
+
/** Poll a submitted job to completion and fetch its result. Submits nothing. */
|
|
4265
|
+
async function awaitQueuedRequest(exec, job, onProgress) {
|
|
4268
4266
|
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
4269
|
-
const status = await checkToolStatus(exec, job
|
|
4267
|
+
const status = await checkToolStatus(exec, job);
|
|
4270
4268
|
if (status.isErr()) return err$1(status.error);
|
|
4271
4269
|
onProgress?.(status.value.status, status.value.queuePosition);
|
|
4272
|
-
if (status.value.status === "completed") return (await getToolResult(exec, job
|
|
4270
|
+
if (status.value.status === "completed") return (await getToolResult(exec, job)).map((data) => ({
|
|
4273
4271
|
data,
|
|
4274
|
-
requestId: job.
|
|
4272
|
+
requestId: job.requestId
|
|
4275
4273
|
}));
|
|
4276
4274
|
if (status.value.status === "failed") {
|
|
4277
4275
|
const message = status.value.error ?? "Queued run failed";
|
|
4278
|
-
return err$1(new MotifError(message, 0, void 0, job.
|
|
4276
|
+
return err$1(new MotifError(message, 0, void 0, job.requestId));
|
|
4279
4277
|
}
|
|
4280
4278
|
await new Promise((resolve) => {
|
|
4281
4279
|
setTimeout(resolve, POLL_INTERVAL_MS);
|
|
4282
4280
|
});
|
|
4283
4281
|
}
|
|
4284
|
-
return err$1(new MotifError("Queued run timed out", 0, void 0, job.
|
|
4282
|
+
return err$1(new MotifError("Queued run timed out", 0, void 0, job.requestId));
|
|
4283
|
+
}
|
|
4284
|
+
/**
|
|
4285
|
+
* Submit a prepared request, poll it to completion and fetch the result.
|
|
4286
|
+
* `onSubmitted` hears the request id as soon as fal accepts the job, before
|
|
4287
|
+
* any wait, so a caller can keep it and resume the same job later.
|
|
4288
|
+
*/
|
|
4289
|
+
async function runRequestQueued(exec, prepared, onProgress, onSubmitted) {
|
|
4290
|
+
const job = await submitRequest(exec, prepared);
|
|
4291
|
+
if (job.isErr()) return err$1(job.error);
|
|
4292
|
+
onSubmitted?.(job.value.requestId);
|
|
4293
|
+
return await awaitQueuedRequest(exec, job.value, onProgress);
|
|
4285
4294
|
}
|
|
4286
4295
|
/** Body of `FalClient.runTool`. */
|
|
4287
4296
|
async function runTool(exec, options) {
|
|
@@ -9897,7 +9906,7 @@ function generationPlan(task, model, config, input) {
|
|
|
9897
9906
|
endpoint: built.endpoint,
|
|
9898
9907
|
prompt: typeof built.body.prompt === "string" ? built.body.prompt : void 0,
|
|
9899
9908
|
provider: "fal",
|
|
9900
|
-
queued: config.useQueue === true || input.onProgress !== void 0
|
|
9909
|
+
queued: config.useQueue === true || input.onProgress !== void 0 || input.onSubmitted !== void 0
|
|
9901
9910
|
});
|
|
9902
9911
|
}
|
|
9903
9912
|
/**
|
|
@@ -10505,7 +10514,19 @@ function createMotif(config = {}) {
|
|
|
10505
10514
|
headers
|
|
10506
10515
|
};
|
|
10507
10516
|
const executor = falRequestExecutor(client.value);
|
|
10508
|
-
return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
|
|
10517
|
+
return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress, input.onSubmitted) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
|
|
10518
|
+
}
|
|
10519
|
+
async function resume(task, input, requestId) {
|
|
10520
|
+
const planned = plan(task, input);
|
|
10521
|
+
if (planned.isErr()) return err$1(planned.error);
|
|
10522
|
+
const chosen = planned.value;
|
|
10523
|
+
if (chosen.provider !== "fal") return err$1(new MotifError(`Only a fal run can be resumed; ${chosen.model} runs on ${chosen.provider}.`, 0));
|
|
10524
|
+
const client = falClient();
|
|
10525
|
+
if (client.isErr()) return err$1(client.error);
|
|
10526
|
+
return (await awaitQueuedRequest(falRequestExecutor(client.value), {
|
|
10527
|
+
endpoint: chosen.endpoint,
|
|
10528
|
+
requestId
|
|
10529
|
+
}, input.onProgress)).map((value) => falOutput(chosen, value));
|
|
10509
10530
|
}
|
|
10510
10531
|
return {
|
|
10511
10532
|
animate: async (input) => await run("animate", input),
|
|
@@ -10532,6 +10553,7 @@ function createMotif(config = {}) {
|
|
|
10532
10553
|
return client.isErr() ? err$1(client.error) : await client.value.deletePayloads(requestId);
|
|
10533
10554
|
},
|
|
10534
10555
|
plan,
|
|
10556
|
+
resume,
|
|
10535
10557
|
run,
|
|
10536
10558
|
stream: taskStreamer(config, plan, context),
|
|
10537
10559
|
async upload(bytes, contentType, fileName) {
|