@shop-os/modelkit 0.4.5 → 0.4.6

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/CHANGELOG.md CHANGED
@@ -9,6 +9,11 @@ header in the release PR.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.4.6] - Unreleased
13
+
14
+ ### Added
15
+ - `ModelKit.resumeVideo(req, jobId)` and `Adapter.resumeVideo` (ark/Seedance only): reconnect to a video job `generateVideo` submitted earlier, by its `jobId`, without submitting a new job. Other adapters throw `unsupported_option`.
16
+
12
17
  ## [0.4.5] - Unreleased
13
18
 
14
19
  ### Fixed
package/README.md CHANGED
@@ -51,6 +51,10 @@ await kit.generateText({
51
51
  await clip.cleanup?.();
52
52
  ```
53
53
 
54
+ A video job that outlives its caller can be rejoined with `resumeVideo`: pass the same
55
+ request and the `jobId` from `generateVideo` and await `job.result()` as usual. Only the
56
+ ark adapter (Seedance) supports it today; every other adapter throws `unsupported_option`.
57
+
54
58
  ### Video tools
55
59
 
56
60
  `video-tool` rows (Topaz upscale, frame interpolation, SDR to HDR on fal) take a clip and
package/dist/index.cjs CHANGED
@@ -3770,6 +3770,11 @@ var arkAdapter = {
3770
3770
  async generateVideo(req, ctx) {
3771
3771
  throwIfAborted(ctx.signal);
3772
3772
  return generateSeedanceVideo(req, ctx);
3773
+ },
3774
+ resumeVideo(req, jobId, ctx) {
3775
+ throwIfAborted(ctx.signal);
3776
+ const key = arkKey(ctx);
3777
+ return seedanceJob(ctx, key, jobId, seedanceUsage(ctx, req, seedanceInitialDuration(req)));
3773
3778
  }
3774
3779
  };
3775
3780
  function arkKey(ctx) {
@@ -3939,6 +3944,9 @@ function seedanceUsage(ctx, req, duration) {
3939
3944
  if (req.resolution !== void 0) usage2.resolution = req.resolution;
3940
3945
  return usage2;
3941
3946
  }
3947
+ function seedanceInitialDuration(req) {
3948
+ return req.durationSeconds ?? 5;
3949
+ }
3942
3950
  function seedanceErrorMessage(error, status) {
3943
3951
  const asString = import_zod7.z.string().safeParse(error);
3944
3952
  if (asString.success) return asString.data;
@@ -3946,6 +3954,78 @@ function seedanceErrorMessage(error, status) {
3946
3954
  if (asObject.success && asObject.data.message !== void 0) return asObject.data.message;
3947
3955
  return `Seedance task ${status}`;
3948
3956
  }
3957
+ function seedanceJob(ctx, key, jobId, usage2) {
3958
+ const modelId = ctx.model.id;
3959
+ const fetchStatus = async () => {
3960
+ const res = await arkFetch(ctx, `${arkBaseUrl(ctx)}${SUBMIT_PATH}/${jobId}`, {
3961
+ headers: { Authorization: `Bearer ${key}` }
3962
+ });
3963
+ if (!res.ok) {
3964
+ if (ctx.call.maxRetries !== 0 && (res.status === 429 || res.status >= 500))
3965
+ return void 0;
3966
+ const text2 = await res.text().catch(() => "");
3967
+ throwArkError(res.status, text2, modelId);
3968
+ }
3969
+ const parsed = SeedanceResponseSchema.safeParse(await res.json());
3970
+ if (!parsed.success) return {};
3971
+ return parsed.data;
3972
+ };
3973
+ return {
3974
+ jobId,
3975
+ async poll() {
3976
+ const polled = await fetchStatus();
3977
+ if (!polled) return { status: "running" };
3978
+ const status = (polled.status ?? "queued").toLowerCase();
3979
+ if (status === "succeeded") return { status: "succeeded", raw: polled };
3980
+ if (status === "failed" || status === "expired" || status === "cancelled") {
3981
+ return { status: "failed", raw: polled };
3982
+ }
3983
+ if (status === "queued" || status === "pending")
3984
+ return { status: "pending", raw: polled };
3985
+ return { status: "running", raw: polled };
3986
+ },
3987
+ async result(opts) {
3988
+ const optSignal = opts?.signal ?? ctx.signal;
3989
+ const pollOpts = {
3990
+ intervalMs: opts?.pollIntervalMs ?? SEEDANCE_POLL_INTERVAL_MS,
3991
+ timeoutMs: opts?.timeoutMs ?? RESULT_TIMEOUT_MS
3992
+ };
3993
+ if (optSignal !== void 0) pollOpts.signal = optSignal;
3994
+ return pollUntil(async () => {
3995
+ const polled = await fetchStatus();
3996
+ if (!polled) return void 0;
3997
+ const status = (polled.status ?? "queued").toLowerCase();
3998
+ if (status === "succeeded") {
3999
+ const url = polled.content?.video_url;
4000
+ if (!url)
4001
+ throwArkError(
4002
+ void 0,
4003
+ "Seedance task succeeded but returned no video_url",
4004
+ modelId
4005
+ );
4006
+ if (polled.duration !== void 0 && polled.duration > 0) {
4007
+ usage2.quantity = polled.duration;
4008
+ usage2.durationSeconds = polled.duration;
4009
+ }
4010
+ const tokens = polled.usage;
4011
+ if (tokens?.total_tokens !== void 0 && tokens.completion_tokens !== void 0) {
4012
+ usage2.inputTokens = tokens.total_tokens - tokens.completion_tokens;
4013
+ usage2.outputTokens = tokens.completion_tokens;
4014
+ }
4015
+ return {
4016
+ video: { url, expiresAt: new Date(Date.now() + VIDEO_URL_TTL_MS) },
4017
+ usage: usage2,
4018
+ raw: polled
4019
+ };
4020
+ }
4021
+ if (status === "failed" || status === "expired" || status === "cancelled") {
4022
+ throwArkError(void 0, seedanceErrorMessage(polled.error, status), modelId);
4023
+ }
4024
+ return void 0;
4025
+ }, pollOpts);
4026
+ }
4027
+ };
4028
+ }
3949
4029
  async function generateSeedanceVideo(req, ctx) {
3950
4030
  const modelId = ctx.model.id;
3951
4031
  const taskType = req.taskType;
@@ -3960,7 +4040,7 @@ async function generateSeedanceVideo(req, ctx) {
3960
4040
  });
3961
4041
  }
3962
4042
  const key = arkKey(ctx);
3963
- const requestedDuration = req.durationSeconds ?? 5;
4043
+ const requestedDuration = seedanceInitialDuration(req);
3964
4044
  let duration = requestedDuration;
3965
4045
  if (taskType === "edit") duration = -1;
3966
4046
  const resolution = req.resolution ?? "720p";
@@ -4041,75 +4121,7 @@ async function generateSeedanceVideo(req, ctx) {
4041
4121
  if (!submit.id) throwArkError(void 0, "Seedance task create returned no id", modelId);
4042
4122
  const jobId = submit.id;
4043
4123
  const usage2 = seedanceUsage(ctx, req, taskType === "edit" ? requestedDuration : duration);
4044
- const fetchStatus = async () => {
4045
- const res = await arkFetch(ctx, `${arkBaseUrl(ctx)}${SUBMIT_PATH}/${jobId}`, {
4046
- headers: { Authorization: `Bearer ${key}` }
4047
- });
4048
- if (!res.ok) {
4049
- if (ctx.call.maxRetries !== 0 && (res.status === 429 || res.status >= 500))
4050
- return void 0;
4051
- const text2 = await res.text().catch(() => "");
4052
- throwArkError(res.status, text2, modelId);
4053
- }
4054
- const parsed = SeedanceResponseSchema.safeParse(await res.json());
4055
- if (!parsed.success) return {};
4056
- return parsed.data;
4057
- };
4058
- return {
4059
- jobId,
4060
- async poll() {
4061
- const polled = await fetchStatus();
4062
- if (!polled) return { status: "running" };
4063
- const status = (polled.status ?? "queued").toLowerCase();
4064
- if (status === "succeeded") return { status: "succeeded", raw: polled };
4065
- if (status === "failed" || status === "expired" || status === "cancelled") {
4066
- return { status: "failed", raw: polled };
4067
- }
4068
- if (status === "queued" || status === "pending")
4069
- return { status: "pending", raw: polled };
4070
- return { status: "running", raw: polled };
4071
- },
4072
- async result(opts) {
4073
- const optSignal = opts?.signal ?? ctx.signal;
4074
- const pollOpts = {
4075
- intervalMs: opts?.pollIntervalMs ?? SEEDANCE_POLL_INTERVAL_MS,
4076
- timeoutMs: opts?.timeoutMs ?? RESULT_TIMEOUT_MS
4077
- };
4078
- if (optSignal !== void 0) pollOpts.signal = optSignal;
4079
- return pollUntil(async () => {
4080
- const polled = await fetchStatus();
4081
- if (!polled) return void 0;
4082
- const status = (polled.status ?? "queued").toLowerCase();
4083
- if (status === "succeeded") {
4084
- const url = polled.content?.video_url;
4085
- if (!url)
4086
- throwArkError(
4087
- void 0,
4088
- "Seedance task succeeded but returned no video_url",
4089
- modelId
4090
- );
4091
- if (polled.duration !== void 0 && polled.duration > 0) {
4092
- usage2.quantity = polled.duration;
4093
- usage2.durationSeconds = polled.duration;
4094
- }
4095
- const tokens = polled.usage;
4096
- if (tokens?.total_tokens !== void 0 && tokens.completion_tokens !== void 0) {
4097
- usage2.inputTokens = tokens.total_tokens - tokens.completion_tokens;
4098
- usage2.outputTokens = tokens.completion_tokens;
4099
- }
4100
- return {
4101
- video: { url, expiresAt: new Date(Date.now() + VIDEO_URL_TTL_MS) },
4102
- usage: usage2,
4103
- raw: polled
4104
- };
4105
- }
4106
- if (status === "failed" || status === "expired" || status === "cancelled") {
4107
- throwArkError(void 0, seedanceErrorMessage(polled.error, status), modelId);
4108
- }
4109
- return void 0;
4110
- }, pollOpts);
4111
- }
4112
- };
4124
+ return seedanceJob(ctx, key, jobId, usage2);
4113
4125
  }
4114
4126
 
4115
4127
  // src/adapters/fal.ts
@@ -9689,6 +9701,33 @@ function createModelKit(config, adapters) {
9689
9701
  result: () => done
9690
9702
  };
9691
9703
  }
9704
+ function wrapVideoJob(model, job, warnings, startedAt) {
9705
+ const result = job.result.bind(job);
9706
+ let settled;
9707
+ return {
9708
+ ...job,
9709
+ warnings,
9710
+ result(opts) {
9711
+ if (settled) return settled;
9712
+ const pending = result(opts).then(
9713
+ async (out) => {
9714
+ const usage2 = timed(
9715
+ { ...out.usage, model: model.id, unit: model.usage.unit },
9716
+ startedAt
9717
+ );
9718
+ await emit(usage2);
9719
+ return { ...out, usage: usage2 };
9720
+ },
9721
+ (cause) => {
9722
+ settled = void 0;
9723
+ throw cause;
9724
+ }
9725
+ );
9726
+ settled = pending;
9727
+ return pending;
9728
+ }
9729
+ };
9730
+ }
9692
9731
  return {
9693
9732
  catalog,
9694
9733
  describe: (id) => catalog.require(id),
@@ -9773,31 +9812,52 @@ function createModelKit(config, adapters) {
9773
9812
  });
9774
9813
  const startedAt = Date.now();
9775
9814
  const job = await adapter.generateVideo(request, ctx);
9776
- const result = job.result.bind(job);
9777
- let settled;
9778
- return {
9779
- ...job,
9780
- warnings,
9781
- result(opts) {
9782
- if (settled) return settled;
9783
- const pending = result(opts).then(
9784
- async (out) => {
9785
- const usage2 = timed(
9786
- { ...out.usage, model: model.id, unit: model.usage.unit },
9787
- startedAt
9788
- );
9789
- await emit(usage2);
9790
- return { ...out, usage: usage2 };
9791
- },
9792
- (cause) => {
9793
- settled = void 0;
9794
- throw cause;
9795
- }
9796
- );
9797
- settled = pending;
9798
- return pending;
9799
- }
9800
- };
9815
+ return wrapVideoJob(model, job, warnings, startedAt);
9816
+ },
9817
+ async resumeVideo(req, jobId, call = {}) {
9818
+ mediaRetryOptions(call);
9819
+ if (!jobId) {
9820
+ throw new ModelKitError({
9821
+ code: "missing_input",
9822
+ message: "resumeVideo needs a jobId",
9823
+ retryable: false
9824
+ });
9825
+ }
9826
+ if (!isCatalogId(req.model)) {
9827
+ throw new ModelKitError({
9828
+ code: "unsupported_option",
9829
+ message: "resumeVideo needs a catalog model id"
9830
+ });
9831
+ }
9832
+ const model = catalog.require(req.model);
9833
+ if (model.type !== "video")
9834
+ throw new ModelKitError({
9835
+ code: "unsupported_option",
9836
+ message: `${model.id} is not a video model`,
9837
+ model: model.id
9838
+ });
9839
+ const { request: input, warnings: inputWarnings } = videoRequestOf({
9840
+ ...req,
9841
+ model: req.model
9842
+ });
9843
+ const { request, warnings } = resolveVideoOptions(
9844
+ model,
9845
+ input,
9846
+ call.onUnsupported ?? "error"
9847
+ );
9848
+ warnings.push(...inputWarnings);
9849
+ logCoercions(logger, model.id, warnings);
9850
+ const signal = combineSignals(req.abortSignal, call.signal);
9851
+ const { adapter, ctx } = context(model, signal ? { ...call, signal } : call, warnings);
9852
+ if (!adapter.resumeVideo)
9853
+ throw new ModelKitError({
9854
+ code: "unsupported_option",
9855
+ message: `${adapter.id} cannot resume a video job`,
9856
+ model: model.id
9857
+ });
9858
+ const startedAt = Date.now();
9859
+ const job = adapter.resumeVideo(request, jobId, ctx);
9860
+ return wrapVideoJob(model, { ...job, jobId }, warnings, startedAt);
9801
9861
  },
9802
9862
  async generateAudio(req, call = {}) {
9803
9863
  mediaRetryOptions(call);