@shop-os/modelkit 0.4.4 → 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/dist/index.d.cts CHANGED
@@ -1144,6 +1144,8 @@ interface Adapter {
1144
1144
  id: string;
1145
1145
  generateImage?(req: ImageRequest, ctx: AdapterContext): Promise<ImageResult>;
1146
1146
  generateVideo?(req: VideoRequest, ctx: AdapterContext): Promise<VideoJob>;
1147
+ /** Reconnect to a job `generateVideo` submitted earlier; never submits. */
1148
+ resumeVideo?(req: VideoRequest, jobId: string, ctx: AdapterContext): VideoJob;
1147
1149
  generateAudio?(req: AudioRequest, ctx: AdapterContext): Promise<AudioResult>;
1148
1150
  /** Video tools run as provider jobs the caller tracks; see ModelKit.submitVideoTool. */
1149
1151
  submitVideoTool?(req: VideoToolRequest, ctx: AdapterContext, opts: {
@@ -1350,6 +1352,12 @@ interface ModelKit extends TextMethods {
1350
1352
  * `model` is a catalog id or an AI SDK video model instance.
1351
1353
  */
1352
1354
  generateVideo(req: VideoCallOptions, call?: CallContext): Promise<VideoCallJob>;
1355
+ /**
1356
+ * Reconnect to a video job submitted earlier with `generateVideo` (same request), by its
1357
+ * `jobId`. Never submits a new job. Await `job.result()` as usual; usage is emitted once.
1358
+ * A resumed job reports only the resumed wait in `latencyMs`.
1359
+ */
1360
+ resumeVideo(req: VideoCallOptions, jobId: string, call?: CallContext): Promise<VideoCallJob>;
1353
1361
  /** AI SDK LanguageModel for chat models. Pair with the catalog's providerOptions. */
1354
1362
  languageModel(modelId: string, call?: CallContext): LanguageModelHandle;
1355
1363
  /** Usage-event `provider` for a catalog model, without building it: "bedrock" on Bedrock, else the vendor. */
package/dist/index.d.ts CHANGED
@@ -1144,6 +1144,8 @@ interface Adapter {
1144
1144
  id: string;
1145
1145
  generateImage?(req: ImageRequest, ctx: AdapterContext): Promise<ImageResult>;
1146
1146
  generateVideo?(req: VideoRequest, ctx: AdapterContext): Promise<VideoJob>;
1147
+ /** Reconnect to a job `generateVideo` submitted earlier; never submits. */
1148
+ resumeVideo?(req: VideoRequest, jobId: string, ctx: AdapterContext): VideoJob;
1147
1149
  generateAudio?(req: AudioRequest, ctx: AdapterContext): Promise<AudioResult>;
1148
1150
  /** Video tools run as provider jobs the caller tracks; see ModelKit.submitVideoTool. */
1149
1151
  submitVideoTool?(req: VideoToolRequest, ctx: AdapterContext, opts: {
@@ -1350,6 +1352,12 @@ interface ModelKit extends TextMethods {
1350
1352
  * `model` is a catalog id or an AI SDK video model instance.
1351
1353
  */
1352
1354
  generateVideo(req: VideoCallOptions, call?: CallContext): Promise<VideoCallJob>;
1355
+ /**
1356
+ * Reconnect to a video job submitted earlier with `generateVideo` (same request), by its
1357
+ * `jobId`. Never submits a new job. Await `job.result()` as usual; usage is emitted once.
1358
+ * A resumed job reports only the resumed wait in `latencyMs`.
1359
+ */
1360
+ resumeVideo(req: VideoCallOptions, jobId: string, call?: CallContext): Promise<VideoCallJob>;
1353
1361
  /** AI SDK LanguageModel for chat models. Pair with the catalog's providerOptions. */
1354
1362
  languageModel(modelId: string, call?: CallContext): LanguageModelHandle;
1355
1363
  /** Usage-event `provider` for a catalog model, without building it: "bedrock" on Bedrock, else the vendor. */
package/dist/index.js CHANGED
@@ -3651,6 +3651,11 @@ var arkAdapter = {
3651
3651
  async generateVideo(req, ctx) {
3652
3652
  throwIfAborted(ctx.signal);
3653
3653
  return generateSeedanceVideo(req, ctx);
3654
+ },
3655
+ resumeVideo(req, jobId, ctx) {
3656
+ throwIfAborted(ctx.signal);
3657
+ const key = arkKey(ctx);
3658
+ return seedanceJob(ctx, key, jobId, seedanceUsage(ctx, req, seedanceInitialDuration(req)));
3654
3659
  }
3655
3660
  };
3656
3661
  function arkKey(ctx) {
@@ -3820,6 +3825,9 @@ function seedanceUsage(ctx, req, duration) {
3820
3825
  if (req.resolution !== void 0) usage2.resolution = req.resolution;
3821
3826
  return usage2;
3822
3827
  }
3828
+ function seedanceInitialDuration(req) {
3829
+ return req.durationSeconds ?? 5;
3830
+ }
3823
3831
  function seedanceErrorMessage(error, status) {
3824
3832
  const asString = z7.string().safeParse(error);
3825
3833
  if (asString.success) return asString.data;
@@ -3827,6 +3835,78 @@ function seedanceErrorMessage(error, status) {
3827
3835
  if (asObject.success && asObject.data.message !== void 0) return asObject.data.message;
3828
3836
  return `Seedance task ${status}`;
3829
3837
  }
3838
+ function seedanceJob(ctx, key, jobId, usage2) {
3839
+ const modelId = ctx.model.id;
3840
+ const fetchStatus = async () => {
3841
+ const res = await arkFetch(ctx, `${arkBaseUrl(ctx)}${SUBMIT_PATH}/${jobId}`, {
3842
+ headers: { Authorization: `Bearer ${key}` }
3843
+ });
3844
+ if (!res.ok) {
3845
+ if (ctx.call.maxRetries !== 0 && (res.status === 429 || res.status >= 500))
3846
+ return void 0;
3847
+ const text2 = await res.text().catch(() => "");
3848
+ throwArkError(res.status, text2, modelId);
3849
+ }
3850
+ const parsed = SeedanceResponseSchema.safeParse(await res.json());
3851
+ if (!parsed.success) return {};
3852
+ return parsed.data;
3853
+ };
3854
+ return {
3855
+ jobId,
3856
+ async poll() {
3857
+ const polled = await fetchStatus();
3858
+ if (!polled) return { status: "running" };
3859
+ const status = (polled.status ?? "queued").toLowerCase();
3860
+ if (status === "succeeded") return { status: "succeeded", raw: polled };
3861
+ if (status === "failed" || status === "expired" || status === "cancelled") {
3862
+ return { status: "failed", raw: polled };
3863
+ }
3864
+ if (status === "queued" || status === "pending")
3865
+ return { status: "pending", raw: polled };
3866
+ return { status: "running", raw: polled };
3867
+ },
3868
+ async result(opts) {
3869
+ const optSignal = opts?.signal ?? ctx.signal;
3870
+ const pollOpts = {
3871
+ intervalMs: opts?.pollIntervalMs ?? SEEDANCE_POLL_INTERVAL_MS,
3872
+ timeoutMs: opts?.timeoutMs ?? RESULT_TIMEOUT_MS
3873
+ };
3874
+ if (optSignal !== void 0) pollOpts.signal = optSignal;
3875
+ return pollUntil(async () => {
3876
+ const polled = await fetchStatus();
3877
+ if (!polled) return void 0;
3878
+ const status = (polled.status ?? "queued").toLowerCase();
3879
+ if (status === "succeeded") {
3880
+ const url = polled.content?.video_url;
3881
+ if (!url)
3882
+ throwArkError(
3883
+ void 0,
3884
+ "Seedance task succeeded but returned no video_url",
3885
+ modelId
3886
+ );
3887
+ if (polled.duration !== void 0 && polled.duration > 0) {
3888
+ usage2.quantity = polled.duration;
3889
+ usage2.durationSeconds = polled.duration;
3890
+ }
3891
+ const tokens = polled.usage;
3892
+ if (tokens?.total_tokens !== void 0 && tokens.completion_tokens !== void 0) {
3893
+ usage2.inputTokens = tokens.total_tokens - tokens.completion_tokens;
3894
+ usage2.outputTokens = tokens.completion_tokens;
3895
+ }
3896
+ return {
3897
+ video: { url, expiresAt: new Date(Date.now() + VIDEO_URL_TTL_MS) },
3898
+ usage: usage2,
3899
+ raw: polled
3900
+ };
3901
+ }
3902
+ if (status === "failed" || status === "expired" || status === "cancelled") {
3903
+ throwArkError(void 0, seedanceErrorMessage(polled.error, status), modelId);
3904
+ }
3905
+ return void 0;
3906
+ }, pollOpts);
3907
+ }
3908
+ };
3909
+ }
3830
3910
  async function generateSeedanceVideo(req, ctx) {
3831
3911
  const modelId = ctx.model.id;
3832
3912
  const taskType = req.taskType;
@@ -3841,7 +3921,7 @@ async function generateSeedanceVideo(req, ctx) {
3841
3921
  });
3842
3922
  }
3843
3923
  const key = arkKey(ctx);
3844
- const requestedDuration = req.durationSeconds ?? 5;
3924
+ const requestedDuration = seedanceInitialDuration(req);
3845
3925
  let duration = requestedDuration;
3846
3926
  if (taskType === "edit") duration = -1;
3847
3927
  const resolution = req.resolution ?? "720p";
@@ -3922,75 +4002,7 @@ async function generateSeedanceVideo(req, ctx) {
3922
4002
  if (!submit.id) throwArkError(void 0, "Seedance task create returned no id", modelId);
3923
4003
  const jobId = submit.id;
3924
4004
  const usage2 = seedanceUsage(ctx, req, taskType === "edit" ? requestedDuration : duration);
3925
- const fetchStatus = async () => {
3926
- const res = await arkFetch(ctx, `${arkBaseUrl(ctx)}${SUBMIT_PATH}/${jobId}`, {
3927
- headers: { Authorization: `Bearer ${key}` }
3928
- });
3929
- if (!res.ok) {
3930
- if (ctx.call.maxRetries !== 0 && (res.status === 429 || res.status >= 500))
3931
- return void 0;
3932
- const text2 = await res.text().catch(() => "");
3933
- throwArkError(res.status, text2, modelId);
3934
- }
3935
- const parsed = SeedanceResponseSchema.safeParse(await res.json());
3936
- if (!parsed.success) return {};
3937
- return parsed.data;
3938
- };
3939
- return {
3940
- jobId,
3941
- async poll() {
3942
- const polled = await fetchStatus();
3943
- if (!polled) return { status: "running" };
3944
- const status = (polled.status ?? "queued").toLowerCase();
3945
- if (status === "succeeded") return { status: "succeeded", raw: polled };
3946
- if (status === "failed" || status === "expired" || status === "cancelled") {
3947
- return { status: "failed", raw: polled };
3948
- }
3949
- if (status === "queued" || status === "pending")
3950
- return { status: "pending", raw: polled };
3951
- return { status: "running", raw: polled };
3952
- },
3953
- async result(opts) {
3954
- const optSignal = opts?.signal ?? ctx.signal;
3955
- const pollOpts = {
3956
- intervalMs: opts?.pollIntervalMs ?? SEEDANCE_POLL_INTERVAL_MS,
3957
- timeoutMs: opts?.timeoutMs ?? RESULT_TIMEOUT_MS
3958
- };
3959
- if (optSignal !== void 0) pollOpts.signal = optSignal;
3960
- return pollUntil(async () => {
3961
- const polled = await fetchStatus();
3962
- if (!polled) return void 0;
3963
- const status = (polled.status ?? "queued").toLowerCase();
3964
- if (status === "succeeded") {
3965
- const url = polled.content?.video_url;
3966
- if (!url)
3967
- throwArkError(
3968
- void 0,
3969
- "Seedance task succeeded but returned no video_url",
3970
- modelId
3971
- );
3972
- if (polled.duration !== void 0 && polled.duration > 0) {
3973
- usage2.quantity = polled.duration;
3974
- usage2.durationSeconds = polled.duration;
3975
- }
3976
- const tokens = polled.usage;
3977
- if (tokens?.total_tokens !== void 0 && tokens.completion_tokens !== void 0) {
3978
- usage2.inputTokens = tokens.total_tokens - tokens.completion_tokens;
3979
- usage2.outputTokens = tokens.completion_tokens;
3980
- }
3981
- return {
3982
- video: { url, expiresAt: new Date(Date.now() + VIDEO_URL_TTL_MS) },
3983
- usage: usage2,
3984
- raw: polled
3985
- };
3986
- }
3987
- if (status === "failed" || status === "expired" || status === "cancelled") {
3988
- throwArkError(void 0, seedanceErrorMessage(polled.error, status), modelId);
3989
- }
3990
- return void 0;
3991
- }, pollOpts);
3992
- }
3993
- };
4005
+ return seedanceJob(ctx, key, jobId, usage2);
3994
4006
  }
3995
4007
 
3996
4008
  // src/adapters/fal.ts
@@ -4132,6 +4144,20 @@ import { z as z11 } from "zod";
4132
4144
 
4133
4145
  // src/catalog/video-tools-spec.ts
4134
4146
  import { z as z10 } from "zod";
4147
+
4148
+ // src/video-length.ts
4149
+ function tenths(seconds2) {
4150
+ return Math.round(seconds2 * 10) / 10;
4151
+ }
4152
+ function overMax(seconds2, max) {
4153
+ return seconds2 > max && tenths(seconds2) > max;
4154
+ }
4155
+ function overTotal(lengths, total) {
4156
+ const sum = (values) => values.reduce((acc, len) => acc + len, 0);
4157
+ return sum(lengths) > total && tenths(sum(lengths.map(tenths))) > total;
4158
+ }
4159
+
4160
+ // src/catalog/video-tools-spec.ts
4135
4161
  var RangeSchema2 = z10.object({ min: z10.number(), max: z10.number() });
4136
4162
  var ToolSchema = z10.object({
4137
4163
  kind: z10.enum(["upscale", "interpolate", "hdr"]),
@@ -4175,7 +4201,7 @@ function planVideoTool(row, req) {
4175
4201
  return fail("The source video's width, height and duration are required");
4176
4202
  }
4177
4203
  const maxSeconds = row.constraints.sourceVideoSeconds?.max;
4178
- if (maxSeconds !== void 0 && durationSeconds > maxSeconds) {
4204
+ if (maxSeconds !== void 0 && overMax(durationSeconds, maxSeconds)) {
4179
4205
  return fail(`${row.label} takes source videos up to ${maxSeconds} seconds.`);
4180
4206
  }
4181
4207
  const base = {
@@ -8579,7 +8605,7 @@ function videoLengthViolation(model, input) {
8579
8605
  const lengths = (input.referenceVideos ?? []).map(knownLength);
8580
8606
  if (limits?.max !== void 0) {
8581
8607
  const max = limits.max;
8582
- const i = lengths.findIndex((len) => len !== void 0 && len > max);
8608
+ const i = lengths.findIndex((len) => len !== void 0 && overMax(len, max));
8583
8609
  if (i >= 0)
8584
8610
  return `${model.label} takes reference videos up to ${String(max)} s each; video ${i + 1} is ${seconds(lengths[i], max)} s.`;
8585
8611
  }
@@ -8591,14 +8617,17 @@ function videoLengthViolation(model, input) {
8591
8617
  const source = knownLength(input.sourceVideo);
8592
8618
  const sourceLimits = model.constraints.sourceVideoSeconds;
8593
8619
  if (limits?.total !== void 0) {
8594
- const refSum = lengths.reduce((acc, len) => acc + (len ?? 0), 0);
8595
- const sum = refSum + (sourceLimits?.inReferenceTotal ? source ?? 0 : 0);
8596
- if (sum > limits.total)
8620
+ const counted = [
8621
+ ...lengths.filter((len) => len !== void 0),
8622
+ ...sourceLimits?.inReferenceTotal && source !== void 0 ? [source] : []
8623
+ ];
8624
+ const sum = counted.reduce((acc, len) => acc + len, 0);
8625
+ if (overTotal(counted, limits.total))
8597
8626
  return `${model.label} takes reference videos up to ${String(limits.total)} s combined; these add up to ${seconds(sum, limits.total)} s.`;
8598
8627
  }
8599
8628
  if (source === void 0) return void 0;
8600
8629
  const sourceMin = effectiveMin(sourceLimits, input.taskType);
8601
- if (sourceLimits?.max !== void 0 && source > sourceLimits.max)
8630
+ if (sourceLimits?.max !== void 0 && overMax(source, sourceLimits.max))
8602
8631
  return `${model.label} takes a source video up to ${String(sourceLimits.max)} s; this one is ${seconds(source, sourceLimits.max)} s.`;
8603
8632
  if (sourceMin !== void 0 && source < sourceMin)
8604
8633
  return `${model.label} takes a source video of at least ${String(sourceMin)} s; this one is ${seconds(source, sourceMin)} s.`;
@@ -9562,6 +9591,33 @@ function createModelKit(config, adapters) {
9562
9591
  result: () => done
9563
9592
  };
9564
9593
  }
9594
+ function wrapVideoJob(model, job, warnings, startedAt) {
9595
+ const result = job.result.bind(job);
9596
+ let settled;
9597
+ return {
9598
+ ...job,
9599
+ warnings,
9600
+ result(opts) {
9601
+ if (settled) return settled;
9602
+ const pending = result(opts).then(
9603
+ async (out) => {
9604
+ const usage2 = timed(
9605
+ { ...out.usage, model: model.id, unit: model.usage.unit },
9606
+ startedAt
9607
+ );
9608
+ await emit(usage2);
9609
+ return { ...out, usage: usage2 };
9610
+ },
9611
+ (cause) => {
9612
+ settled = void 0;
9613
+ throw cause;
9614
+ }
9615
+ );
9616
+ settled = pending;
9617
+ return pending;
9618
+ }
9619
+ };
9620
+ }
9565
9621
  return {
9566
9622
  catalog,
9567
9623
  describe: (id) => catalog.require(id),
@@ -9646,31 +9702,52 @@ function createModelKit(config, adapters) {
9646
9702
  });
9647
9703
  const startedAt = Date.now();
9648
9704
  const job = await adapter.generateVideo(request, ctx);
9649
- const result = job.result.bind(job);
9650
- let settled;
9651
- return {
9652
- ...job,
9653
- warnings,
9654
- result(opts) {
9655
- if (settled) return settled;
9656
- const pending = result(opts).then(
9657
- async (out) => {
9658
- const usage2 = timed(
9659
- { ...out.usage, model: model.id, unit: model.usage.unit },
9660
- startedAt
9661
- );
9662
- await emit(usage2);
9663
- return { ...out, usage: usage2 };
9664
- },
9665
- (cause) => {
9666
- settled = void 0;
9667
- throw cause;
9668
- }
9669
- );
9670
- settled = pending;
9671
- return pending;
9672
- }
9673
- };
9705
+ return wrapVideoJob(model, job, warnings, startedAt);
9706
+ },
9707
+ async resumeVideo(req, jobId, call = {}) {
9708
+ mediaRetryOptions(call);
9709
+ if (!jobId) {
9710
+ throw new ModelKitError({
9711
+ code: "missing_input",
9712
+ message: "resumeVideo needs a jobId",
9713
+ retryable: false
9714
+ });
9715
+ }
9716
+ if (!isCatalogId(req.model)) {
9717
+ throw new ModelKitError({
9718
+ code: "unsupported_option",
9719
+ message: "resumeVideo needs a catalog model id"
9720
+ });
9721
+ }
9722
+ const model = catalog.require(req.model);
9723
+ if (model.type !== "video")
9724
+ throw new ModelKitError({
9725
+ code: "unsupported_option",
9726
+ message: `${model.id} is not a video model`,
9727
+ model: model.id
9728
+ });
9729
+ const { request: input, warnings: inputWarnings } = videoRequestOf({
9730
+ ...req,
9731
+ model: req.model
9732
+ });
9733
+ const { request, warnings } = resolveVideoOptions(
9734
+ model,
9735
+ input,
9736
+ call.onUnsupported ?? "error"
9737
+ );
9738
+ warnings.push(...inputWarnings);
9739
+ logCoercions(logger, model.id, warnings);
9740
+ const signal = combineSignals(req.abortSignal, call.signal);
9741
+ const { adapter, ctx } = context(model, signal ? { ...call, signal } : call, warnings);
9742
+ if (!adapter.resumeVideo)
9743
+ throw new ModelKitError({
9744
+ code: "unsupported_option",
9745
+ message: `${adapter.id} cannot resume a video job`,
9746
+ model: model.id
9747
+ });
9748
+ const startedAt = Date.now();
9749
+ const job = adapter.resumeVideo(request, jobId, ctx);
9750
+ return wrapVideoJob(model, { ...job, jobId }, warnings, startedAt);
9674
9751
  },
9675
9752
  async generateAudio(req, call = {}) {
9676
9753
  mediaRetryOptions(call);