@howells/motif-sdk 5.0.1 → 5.1.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 +2 -0
- package/dist/index.cjs +43 -11
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +43 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ Every Task function returns `Result<TaskOutput, MotifError>` from `neverthrow` a
|
|
|
34
34
|
|
|
35
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
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.
|
|
38
|
+
|
|
37
39
|
## Plan Without Calling fal
|
|
38
40
|
|
|
39
41
|
`plan(task, input, { dryRun: true })` resolves the Model and returns the endpoint, body and projected cost with no I/O and no key.
|
package/dist/index.cjs
CHANGED
|
@@ -2344,6 +2344,16 @@ function endpointFromQueueUrl(url, fallback) {
|
|
|
2344
2344
|
return fallback;
|
|
2345
2345
|
}
|
|
2346
2346
|
}
|
|
2347
|
+
/**
|
|
2348
|
+
* The queue path a submitted run is read back from: fal's app id, its owner and
|
|
2349
|
+
* app, without any route below it. `fal-ai/nano-banana-pro/edit` is submitted
|
|
2350
|
+
* there but read at `fal-ai/nano-banana-pro/requests/<id>`; the full path
|
|
2351
|
+
* answers 405. A live submit learns this from `response_url`; a resume has
|
|
2352
|
+
* only the id, so it derives it.
|
|
2353
|
+
*/
|
|
2354
|
+
function queueAppPath(endpoint) {
|
|
2355
|
+
return endpoint.split("/").slice(0, 2).join("/");
|
|
2356
|
+
}
|
|
2347
2357
|
function parseQueueSubmission(data) {
|
|
2348
2358
|
if (!isRecord$4(data)) return { requestId: "" };
|
|
2349
2359
|
return {
|
|
@@ -4262,27 +4272,36 @@ async function getToolResult(exec, job) {
|
|
|
4262
4272
|
const data = await response.value.json();
|
|
4263
4273
|
return (0, neverthrow.ok)(isRecord$4(data) ? data : {});
|
|
4264
4274
|
}
|
|
4265
|
-
/**
|
|
4266
|
-
async function
|
|
4267
|
-
const job = await submitRequest(exec, prepared);
|
|
4268
|
-
if (job.isErr()) return (0, neverthrow.err)(job.error);
|
|
4275
|
+
/** Poll a submitted job to completion and fetch its result. Submits nothing. */
|
|
4276
|
+
async function awaitQueuedRequest(exec, job, onProgress) {
|
|
4269
4277
|
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
4270
|
-
const status = await checkToolStatus(exec, job
|
|
4278
|
+
const status = await checkToolStatus(exec, job);
|
|
4271
4279
|
if (status.isErr()) return (0, neverthrow.err)(status.error);
|
|
4272
4280
|
onProgress?.(status.value.status, status.value.queuePosition);
|
|
4273
|
-
if (status.value.status === "completed") return (await getToolResult(exec, job
|
|
4281
|
+
if (status.value.status === "completed") return (await getToolResult(exec, job)).map((data) => ({
|
|
4274
4282
|
data,
|
|
4275
|
-
requestId: job.
|
|
4283
|
+
requestId: job.requestId
|
|
4276
4284
|
}));
|
|
4277
4285
|
if (status.value.status === "failed") {
|
|
4278
4286
|
const message = status.value.error ?? "Queued run failed";
|
|
4279
|
-
return (0, neverthrow.err)(new MotifError(message, 0, void 0, job.
|
|
4287
|
+
return (0, neverthrow.err)(new MotifError(message, 0, void 0, job.requestId));
|
|
4280
4288
|
}
|
|
4281
4289
|
await new Promise((resolve) => {
|
|
4282
4290
|
setTimeout(resolve, POLL_INTERVAL_MS);
|
|
4283
4291
|
});
|
|
4284
4292
|
}
|
|
4285
|
-
return (0, neverthrow.err)(new MotifError("Queued run timed out", 0, void 0, job.
|
|
4293
|
+
return (0, neverthrow.err)(new MotifError("Queued run timed out", 0, void 0, job.requestId));
|
|
4294
|
+
}
|
|
4295
|
+
/**
|
|
4296
|
+
* Submit a prepared request, poll it to completion and fetch the result.
|
|
4297
|
+
* `onSubmitted` hears the request id as soon as fal accepts the job, before
|
|
4298
|
+
* any wait, so a caller can keep it and resume the same job later.
|
|
4299
|
+
*/
|
|
4300
|
+
async function runRequestQueued(exec, prepared, onProgress, onSubmitted) {
|
|
4301
|
+
const job = await submitRequest(exec, prepared);
|
|
4302
|
+
if (job.isErr()) return (0, neverthrow.err)(job.error);
|
|
4303
|
+
onSubmitted?.(job.value.requestId);
|
|
4304
|
+
return await awaitQueuedRequest(exec, job.value, onProgress);
|
|
4286
4305
|
}
|
|
4287
4306
|
/** Body of `FalClient.runTool`. */
|
|
4288
4307
|
async function runTool(exec, options) {
|
|
@@ -9898,7 +9917,7 @@ function generationPlan(task, model, config, input) {
|
|
|
9898
9917
|
endpoint: built.endpoint,
|
|
9899
9918
|
prompt: typeof built.body.prompt === "string" ? built.body.prompt : void 0,
|
|
9900
9919
|
provider: "fal",
|
|
9901
|
-
queued: config.useQueue === true || input.onProgress !== void 0
|
|
9920
|
+
queued: config.useQueue === true || input.onProgress !== void 0 || input.onSubmitted !== void 0
|
|
9902
9921
|
});
|
|
9903
9922
|
}
|
|
9904
9923
|
/**
|
|
@@ -10506,7 +10525,19 @@ function createMotif(config = {}) {
|
|
|
10506
10525
|
headers
|
|
10507
10526
|
};
|
|
10508
10527
|
const executor = falRequestExecutor(client.value);
|
|
10509
|
-
return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
|
|
10528
|
+
return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress, input.onSubmitted) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
|
|
10529
|
+
}
|
|
10530
|
+
async function resume(task, input, requestId) {
|
|
10531
|
+
const planned = plan(task, input);
|
|
10532
|
+
if (planned.isErr()) return (0, neverthrow.err)(planned.error);
|
|
10533
|
+
const chosen = planned.value;
|
|
10534
|
+
if (chosen.provider !== "fal") return (0, neverthrow.err)(new MotifError(`Only a fal run can be resumed; ${chosen.model} runs on ${chosen.provider}.`, 0));
|
|
10535
|
+
const client = falClient();
|
|
10536
|
+
if (client.isErr()) return (0, neverthrow.err)(client.error);
|
|
10537
|
+
return (await awaitQueuedRequest(falRequestExecutor(client.value), {
|
|
10538
|
+
endpoint: queueAppPath(chosen.endpoint),
|
|
10539
|
+
requestId
|
|
10540
|
+
}, input.onProgress)).map((value) => falOutput(chosen, value));
|
|
10510
10541
|
}
|
|
10511
10542
|
return {
|
|
10512
10543
|
animate: async (input) => await run("animate", input),
|
|
@@ -10533,6 +10564,7 @@ function createMotif(config = {}) {
|
|
|
10533
10564
|
return client.isErr() ? (0, neverthrow.err)(client.error) : await client.value.deletePayloads(requestId);
|
|
10534
10565
|
},
|
|
10535
10566
|
plan,
|
|
10567
|
+
resume,
|
|
10536
10568
|
run,
|
|
10537
10569
|
stream: taskStreamer(config, plan, context),
|
|
10538
10570
|
async upload(bytes, contentType, fileName) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1202,6 +1202,12 @@ interface TaskInput {
|
|
|
1202
1202
|
* it runs a fal generation through the queue, since only the queue reports.
|
|
1203
1203
|
*/
|
|
1204
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;
|
|
1205
1211
|
}
|
|
1206
1212
|
interface TaskFile {
|
|
1207
1213
|
/** Output key the URL came from, e.g. "image", "masks", "model_glb". */
|
|
@@ -1269,6 +1275,12 @@ interface MotifClient {
|
|
|
1269
1275
|
/** Upload bytes to fal storage; returns the public URL. */
|
|
1270
1276
|
upload: (bytes: Uint8Array, contentType: string, fileName?: string) => Promise<Result$1<string, MotifError>>;
|
|
1271
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>>;
|
|
1272
1284
|
}
|
|
1273
1285
|
/** Create the Task client. Keys fall back to FAL_KEY and OPENAI_API_KEY. */
|
|
1274
1286
|
export declare function createMotif(config?: MotifClientConfig): MotifClient;
|
package/dist/index.d.ts
CHANGED
|
@@ -1202,6 +1202,12 @@ interface TaskInput {
|
|
|
1202
1202
|
* it runs a fal generation through the queue, since only the queue reports.
|
|
1203
1203
|
*/
|
|
1204
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;
|
|
1205
1211
|
}
|
|
1206
1212
|
interface TaskFile {
|
|
1207
1213
|
/** Output key the URL came from, e.g. "image", "masks", "model_glb". */
|
|
@@ -1269,6 +1275,12 @@ interface MotifClient {
|
|
|
1269
1275
|
/** Upload bytes to fal storage; returns the public URL. */
|
|
1270
1276
|
upload: (bytes: Uint8Array, contentType: string, fileName?: string) => Promise<Result$1<string, MotifError>>;
|
|
1271
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>>;
|
|
1272
1284
|
}
|
|
1273
1285
|
/** Create the Task client. Keys fall back to FAL_KEY and OPENAI_API_KEY. */
|
|
1274
1286
|
export declare function createMotif(config?: MotifClientConfig): MotifClient;
|
package/dist/index.js
CHANGED
|
@@ -2343,6 +2343,16 @@ function endpointFromQueueUrl(url, fallback) {
|
|
|
2343
2343
|
return fallback;
|
|
2344
2344
|
}
|
|
2345
2345
|
}
|
|
2346
|
+
/**
|
|
2347
|
+
* The queue path a submitted run is read back from: fal's app id, its owner and
|
|
2348
|
+
* app, without any route below it. `fal-ai/nano-banana-pro/edit` is submitted
|
|
2349
|
+
* there but read at `fal-ai/nano-banana-pro/requests/<id>`; the full path
|
|
2350
|
+
* answers 405. A live submit learns this from `response_url`; a resume has
|
|
2351
|
+
* only the id, so it derives it.
|
|
2352
|
+
*/
|
|
2353
|
+
function queueAppPath(endpoint) {
|
|
2354
|
+
return endpoint.split("/").slice(0, 2).join("/");
|
|
2355
|
+
}
|
|
2346
2356
|
function parseQueueSubmission(data) {
|
|
2347
2357
|
if (!isRecord$4(data)) return { requestId: "" };
|
|
2348
2358
|
return {
|
|
@@ -4261,27 +4271,36 @@ async function getToolResult(exec, job) {
|
|
|
4261
4271
|
const data = await response.value.json();
|
|
4262
4272
|
return ok$1(isRecord$4(data) ? data : {});
|
|
4263
4273
|
}
|
|
4264
|
-
/**
|
|
4265
|
-
async function
|
|
4266
|
-
const job = await submitRequest(exec, prepared);
|
|
4267
|
-
if (job.isErr()) return err$1(job.error);
|
|
4274
|
+
/** Poll a submitted job to completion and fetch its result. Submits nothing. */
|
|
4275
|
+
async function awaitQueuedRequest(exec, job, onProgress) {
|
|
4268
4276
|
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
4269
|
-
const status = await checkToolStatus(exec, job
|
|
4277
|
+
const status = await checkToolStatus(exec, job);
|
|
4270
4278
|
if (status.isErr()) return err$1(status.error);
|
|
4271
4279
|
onProgress?.(status.value.status, status.value.queuePosition);
|
|
4272
|
-
if (status.value.status === "completed") return (await getToolResult(exec, job
|
|
4280
|
+
if (status.value.status === "completed") return (await getToolResult(exec, job)).map((data) => ({
|
|
4273
4281
|
data,
|
|
4274
|
-
requestId: job.
|
|
4282
|
+
requestId: job.requestId
|
|
4275
4283
|
}));
|
|
4276
4284
|
if (status.value.status === "failed") {
|
|
4277
4285
|
const message = status.value.error ?? "Queued run failed";
|
|
4278
|
-
return err$1(new MotifError(message, 0, void 0, job.
|
|
4286
|
+
return err$1(new MotifError(message, 0, void 0, job.requestId));
|
|
4279
4287
|
}
|
|
4280
4288
|
await new Promise((resolve) => {
|
|
4281
4289
|
setTimeout(resolve, POLL_INTERVAL_MS);
|
|
4282
4290
|
});
|
|
4283
4291
|
}
|
|
4284
|
-
return err$1(new MotifError("Queued run timed out", 0, void 0, job.
|
|
4292
|
+
return err$1(new MotifError("Queued run timed out", 0, void 0, job.requestId));
|
|
4293
|
+
}
|
|
4294
|
+
/**
|
|
4295
|
+
* Submit a prepared request, poll it to completion and fetch the result.
|
|
4296
|
+
* `onSubmitted` hears the request id as soon as fal accepts the job, before
|
|
4297
|
+
* any wait, so a caller can keep it and resume the same job later.
|
|
4298
|
+
*/
|
|
4299
|
+
async function runRequestQueued(exec, prepared, onProgress, onSubmitted) {
|
|
4300
|
+
const job = await submitRequest(exec, prepared);
|
|
4301
|
+
if (job.isErr()) return err$1(job.error);
|
|
4302
|
+
onSubmitted?.(job.value.requestId);
|
|
4303
|
+
return await awaitQueuedRequest(exec, job.value, onProgress);
|
|
4285
4304
|
}
|
|
4286
4305
|
/** Body of `FalClient.runTool`. */
|
|
4287
4306
|
async function runTool(exec, options) {
|
|
@@ -9897,7 +9916,7 @@ function generationPlan(task, model, config, input) {
|
|
|
9897
9916
|
endpoint: built.endpoint,
|
|
9898
9917
|
prompt: typeof built.body.prompt === "string" ? built.body.prompt : void 0,
|
|
9899
9918
|
provider: "fal",
|
|
9900
|
-
queued: config.useQueue === true || input.onProgress !== void 0
|
|
9919
|
+
queued: config.useQueue === true || input.onProgress !== void 0 || input.onSubmitted !== void 0
|
|
9901
9920
|
});
|
|
9902
9921
|
}
|
|
9903
9922
|
/**
|
|
@@ -10505,7 +10524,19 @@ function createMotif(config = {}) {
|
|
|
10505
10524
|
headers
|
|
10506
10525
|
};
|
|
10507
10526
|
const executor = falRequestExecutor(client.value);
|
|
10508
|
-
return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
|
|
10527
|
+
return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress, input.onSubmitted) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
|
|
10528
|
+
}
|
|
10529
|
+
async function resume(task, input, requestId) {
|
|
10530
|
+
const planned = plan(task, input);
|
|
10531
|
+
if (planned.isErr()) return err$1(planned.error);
|
|
10532
|
+
const chosen = planned.value;
|
|
10533
|
+
if (chosen.provider !== "fal") return err$1(new MotifError(`Only a fal run can be resumed; ${chosen.model} runs on ${chosen.provider}.`, 0));
|
|
10534
|
+
const client = falClient();
|
|
10535
|
+
if (client.isErr()) return err$1(client.error);
|
|
10536
|
+
return (await awaitQueuedRequest(falRequestExecutor(client.value), {
|
|
10537
|
+
endpoint: queueAppPath(chosen.endpoint),
|
|
10538
|
+
requestId
|
|
10539
|
+
}, input.onProgress)).map((value) => falOutput(chosen, value));
|
|
10509
10540
|
}
|
|
10510
10541
|
return {
|
|
10511
10542
|
animate: async (input) => await run("animate", input),
|
|
@@ -10532,6 +10563,7 @@ function createMotif(config = {}) {
|
|
|
10532
10563
|
return client.isErr() ? err$1(client.error) : await client.value.deletePayloads(requestId);
|
|
10533
10564
|
},
|
|
10534
10565
|
plan,
|
|
10566
|
+
resume,
|
|
10535
10567
|
run,
|
|
10536
10568
|
stream: taskStreamer(config, plan, context),
|
|
10537
10569
|
async upload(bytes, contentType, fileName) {
|