@shop-os/modelkit 0.7.2 → 0.7.3

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,22 @@ header in the release PR.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.7.3] - Unreleased
13
+
14
+ ENG-3272 Luma code review follow-ups.
15
+
16
+ ### Changed
17
+ - `ray-3.2` (generate) returns the output as `{ url, expiresAt }` instead of downloading it: its 5 s bill needs no bytes. Luma's URL lasts 1 hour; `expiresAt` is 55 minutes after the result. Edit and reframe still download the MP4 to read its length, and that download is now retried.
18
+ - A URL source for edit or reframe sends `media_type` from its extension (`.mov` is `video/quicktime`, `.webm` is `video/webm`), else `video/mp4`.
19
+ - Luma receives the `providerModelId` lowercased, the same spelling the row checks accept.
20
+
21
+ ### Fixed
22
+ - An edit or reframe whose MP4 length cannot be read bills the caller's measured `durationSeconds` for an inline (`bytes`) source too, not only for a URL source. Before, it billed 5 s.
23
+ - The MP4 length reader reads boxes with a 64-bit size or a size of 0 (box to end of file), so a `moov` after a large `mdat` is found.
24
+ - A completed video takes only an output typed `video` (or untyped); an output of another type is no longer downloaded as the video.
25
+ - `result({ signal })` also cancels the poll requests and the download, not only the wait between polls.
26
+ - A 200 response that is not JSON is a `ModelKitError`: on submit `provider_rejected`, on a poll a transient `network` error that keeps polling.
27
+
12
28
  ## [0.7.2] - Unreleased
13
29
 
14
30
  ENG-3272 review fixes. Merged as a second 0.7.1 after FlashVSR's 0.7.1 had published, so npm never got it; released as 0.7.2.
package/dist/index.cjs CHANGED
@@ -13808,24 +13808,24 @@ var import_zod25 = require("zod");
13808
13808
  function mp4DurationSeconds(bytes) {
13809
13809
  const moov = findBox(bytes, 0, bytes.length, "moov");
13810
13810
  if (!moov) return void 0;
13811
- const mvhd = findBox(bytes, moov.start + 8, moov.end, "mvhd");
13811
+ const mvhd = findBox(bytes, moov.body, moov.end, "mvhd");
13812
13812
  if (!mvhd) return void 0;
13813
13813
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
13814
- const size = mvhd.end - mvhd.start;
13815
- if (size < 8 + 4) return void 0;
13816
- const version = view.getUint8(mvhd.start + 8);
13814
+ const size = mvhd.end - mvhd.body;
13815
+ if (size < 4) return void 0;
13816
+ const version = view.getUint8(mvhd.body);
13817
13817
  if (version === 0) {
13818
- if (size < 8 + 4 + 4 + 4 + 4 + 4) return void 0;
13819
- const timescale = view.getUint32(mvhd.start + 8 + 4 + 4 + 4);
13820
- const duration = view.getUint32(mvhd.start + 8 + 4 + 4 + 4 + 4);
13818
+ if (size < 4 + 4 + 4 + 4 + 4) return void 0;
13819
+ const timescale = view.getUint32(mvhd.body + 4 + 4 + 4);
13820
+ const duration = view.getUint32(mvhd.body + 4 + 4 + 4 + 4);
13821
13821
  if (timescale === 0) return void 0;
13822
13822
  return duration / timescale;
13823
13823
  }
13824
13824
  if (version === 1) {
13825
- if (size < 8 + 4 + 8 + 8 + 4 + 8) return void 0;
13826
- const timescale = view.getUint32(mvhd.start + 8 + 4 + 8 + 8);
13827
- const high = view.getUint32(mvhd.start + 8 + 4 + 8 + 8 + 4);
13828
- const low = view.getUint32(mvhd.start + 8 + 4 + 8 + 8 + 4 + 4);
13825
+ if (size < 4 + 8 + 8 + 4 + 8) return void 0;
13826
+ const timescale = view.getUint32(mvhd.body + 4 + 8 + 8);
13827
+ const high = view.getUint32(mvhd.body + 4 + 8 + 8 + 4);
13828
+ const low = view.getUint32(mvhd.body + 4 + 8 + 8 + 4 + 4);
13829
13829
  if (timescale === 0) return void 0;
13830
13830
  return (high * 2 ** 32 + low) / timescale;
13831
13831
  }
@@ -13835,15 +13835,23 @@ function findBox(bytes, from, to, type) {
13835
13835
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
13836
13836
  let offset = from;
13837
13837
  while (offset + 8 <= to) {
13838
- const size = view.getUint32(offset);
13838
+ let size = view.getUint32(offset);
13839
+ let header = 8;
13839
13840
  const name = String.fromCharCode(
13840
13841
  view.getUint8(offset + 4),
13841
13842
  view.getUint8(offset + 5),
13842
13843
  view.getUint8(offset + 6),
13843
13844
  view.getUint8(offset + 7)
13844
13845
  );
13845
- if (size < 8 || offset + size > to) return void 0;
13846
- if (name === type) return { start: offset, end: offset + size };
13846
+ if (size === 1) {
13847
+ if (offset + 16 > to) return void 0;
13848
+ size = view.getUint32(offset + 8) * 2 ** 32 + view.getUint32(offset + 12);
13849
+ header = 16;
13850
+ } else if (size === 0) {
13851
+ size = to - offset;
13852
+ }
13853
+ if (size < header || offset + size > to) return void 0;
13854
+ if (name === type) return { body: offset + header, end: offset + size };
13847
13855
  offset += size;
13848
13856
  }
13849
13857
  return void 0;
@@ -13856,6 +13864,7 @@ var IMAGE_TIMEOUT_MS = 5 * 6e4;
13856
13864
  var VIDEO_POLL_INTERVAL_MS = 5e3;
13857
13865
  var VIDEO_TIMEOUT_MS = 20 * 6e4;
13858
13866
  var PROMPT_MAX_CHARS = 6e3;
13867
+ var OUTPUT_URL_TTL_MS = 55 * 6e4;
13859
13868
  var LUMA_IMAGE_MODEL_IDS = /* @__PURE__ */ new Set(["uni-1", "uni-1-max"]);
13860
13869
  var LUMA_VIDEO_MODEL_ID = "ray-3.2";
13861
13870
  var LUMA_VIDEO_MODES = /* @__PURE__ */ new Map([
@@ -13874,6 +13883,9 @@ function lumaRowRefusal(row2) {
13874
13883
  }
13875
13884
  return `luma has no ${row2.type} rows`;
13876
13885
  }
13886
+ function lumaModel(ctx) {
13887
+ return ctx.model.providerModelId.toLowerCase();
13888
+ }
13877
13889
  function lumaBase(ctx) {
13878
13890
  return ctx.endpoints?.luma ?? DEFAULT_BASE_URL;
13879
13891
  }
@@ -13916,8 +13928,23 @@ function imageRef(ref) {
13916
13928
  if ("url" in ref) return { url: ref.url };
13917
13929
  return { data: mediaRefToBase64(ref.bytes), media_type: ref.mime };
13918
13930
  }
13931
+ var VIDEO_TYPES_BY_EXTENSION = /* @__PURE__ */ new Map([
13932
+ ["mp4", "video/mp4"],
13933
+ ["m4v", "video/mp4"],
13934
+ ["mov", "video/quicktime"],
13935
+ ["webm", "video/webm"]
13936
+ ]);
13937
+ function videoUrlType(url) {
13938
+ let path = url;
13939
+ try {
13940
+ path = new URL(url).pathname;
13941
+ } catch {
13942
+ }
13943
+ const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
13944
+ return VIDEO_TYPES_BY_EXTENSION.get(extension) ?? "video/mp4";
13945
+ }
13919
13946
  function videoRef(ref) {
13920
- if ("url" in ref) return { url: ref.url, media_type: "video/mp4" };
13947
+ if ("url" in ref) return { url: ref.url, media_type: videoUrlType(ref.url) };
13921
13948
  const mediaType = ref.mime.startsWith("video/") ? ref.mime : "video/mp4";
13922
13949
  return { data: mediaRefToBase64(ref.bytes), media_type: mediaType };
13923
13950
  }
@@ -14081,14 +14108,22 @@ async function lumaRequest(ctx, url, body) {
14081
14108
  }
14082
14109
  return res;
14083
14110
  }
14111
+ async function parseGeneration(res) {
14112
+ try {
14113
+ const parsed = lumaGenerationSchema.safeParse(await res.json());
14114
+ return parsed.success ? parsed.data : void 0;
14115
+ } catch {
14116
+ return "not_json";
14117
+ }
14118
+ }
14084
14119
  async function submitGeneration(ctx, body) {
14085
14120
  const retryOptions = mediaRetryOptions(ctx.call, ctx.signal);
14086
14121
  const res = await withRetry(
14087
14122
  () => lumaRequest(ctx, `${lumaBase(ctx)}/v1/generations`, JSON.stringify(body)),
14088
14123
  retryOptions
14089
14124
  );
14090
- const parsed = lumaGenerationSchema.safeParse(await res.json());
14091
- if (!parsed.success) {
14125
+ const generation = await parseGeneration(res);
14126
+ if (generation === void 0 || generation === "not_json") {
14092
14127
  throw new ModelKitError({
14093
14128
  code: "provider_rejected",
14094
14129
  message: "Luma submit returned no generation id",
@@ -14097,13 +14132,20 @@ async function submitGeneration(ctx, body) {
14097
14132
  retryable: false
14098
14133
  });
14099
14134
  }
14100
- return parsed.data;
14135
+ return generation;
14101
14136
  }
14102
14137
  async function readGeneration(ctx, id) {
14103
14138
  const res = await lumaRequest(ctx, `${lumaBase(ctx)}/v1/generations/${id}`);
14104
- const payload = JsonValueSchema.parse(await res.json());
14105
- const parsed = lumaGenerationSchema.safeParse(payload);
14106
- if (!parsed.success) {
14139
+ const generation = await parseGeneration(res);
14140
+ if (generation === "not_json") {
14141
+ throw new ModelKitError({
14142
+ code: "network",
14143
+ message: "Luma returned a poll response that is not JSON",
14144
+ provider: "luma",
14145
+ model: ctx.model.id
14146
+ });
14147
+ }
14148
+ if (generation === void 0) {
14107
14149
  throw new ModelKitError({
14108
14150
  code: "provider_rejected",
14109
14151
  message: "Luma returned an unreadable generation",
@@ -14112,14 +14154,12 @@ async function readGeneration(ctx, id) {
14112
14154
  retryable: false
14113
14155
  });
14114
14156
  }
14115
- return parsed.data;
14157
+ return generation;
14116
14158
  }
14117
14159
  function outputUrl(generation, type) {
14118
14160
  const outputs = generation.output ?? [];
14119
- const match = outputs.find((item) => item.type === type && item.url);
14120
- if (match?.url) return match.url;
14121
- const fallback = outputs.find((item) => item.url);
14122
- return fallback?.url;
14161
+ const match = outputs.find((item) => item.type === type && item.url) ?? outputs.find((item) => item.type === void 0 && item.url);
14162
+ return match?.url;
14123
14163
  }
14124
14164
  async function readGenerationOnce(ctx, id) {
14125
14165
  try {
@@ -14158,21 +14198,22 @@ async function checkGeneration(ctx, id, type) {
14158
14198
  return void 0;
14159
14199
  }
14160
14200
  async function downloadBytes(ctx, url, fallback) {
14161
- let bytes;
14162
- let header;
14163
- try {
14164
- const out = await mediaRefToBytes({ url }, ctx.fetch, {
14165
- ...ctx.signal !== void 0 && { signal: ctx.signal },
14166
- provider: "luma",
14167
- model: ctx.model.id
14168
- });
14169
- bytes = out.bytes;
14170
- header = out.mime;
14171
- } catch (cause) {
14172
- throw classifyThrown("luma", cause, ctx.model.id);
14173
- }
14201
+ const { bytes, mime: header } = await withRetry(
14202
+ async () => {
14203
+ try {
14204
+ return await mediaRefToBytes({ url }, ctx.fetch, {
14205
+ ...ctx.signal !== void 0 && { signal: ctx.signal },
14206
+ provider: "luma",
14207
+ model: ctx.model.id
14208
+ });
14209
+ } catch (cause) {
14210
+ throw classifyThrown("luma", cause, ctx.model.id);
14211
+ }
14212
+ },
14213
+ mediaRetryOptions(ctx.call, ctx.signal)
14214
+ );
14174
14215
  const kind = fallback.split("/")[0];
14175
- const usable = header !== void 0 && header.startsWith(`${kind}/`);
14216
+ const usable = header.startsWith(`${kind}/`);
14176
14217
  return { bytes, mime: usable ? header : fallback };
14177
14218
  }
14178
14219
  function imageUsage2(ctx) {
@@ -14187,15 +14228,12 @@ function imageUsage2(ctx) {
14187
14228
  }
14188
14229
  var GENERATE_SECONDS = 5;
14189
14230
  var MAX_PLAUSIBLE_SECONDS = 60;
14190
- function billedSeconds(req, mode, bytes) {
14191
- if (mode === "generate") return GENERATE_SECONDS;
14231
+ function editedSeconds(req, bytes) {
14192
14232
  const measured = mp4DurationSeconds(bytes);
14193
- const source2 = req.sourceVideo;
14194
- const length = measured !== void 0 && measured > 0 && measured <= MAX_PLAUSIBLE_SECONDS ? measured : source2 !== void 0 && "url" in source2 ? source2.durationSeconds : void 0;
14233
+ const length = measured !== void 0 && measured > 0 && measured <= MAX_PLAUSIBLE_SECONDS ? measured : req.sourceVideo?.durationSeconds;
14195
14234
  return Math.max(length ?? 0, GENERATE_SECONDS);
14196
14235
  }
14197
- function videoUsage4(ctx, req, mode, bytes) {
14198
- const seconds3 = billedSeconds(req, mode, bytes);
14236
+ function videoUsage4(ctx, req, mode, seconds3) {
14199
14237
  const event = {
14200
14238
  model: ctx.model.id,
14201
14239
  provider: "luma",
@@ -14210,7 +14248,7 @@ function videoUsage4(ctx, req, mode, bytes) {
14210
14248
  }
14211
14249
  function imageBody(req, ctx, outputFormat) {
14212
14250
  const prompt = checkPrompt(req, ctx);
14213
- const model = ctx.model.providerModelId;
14251
+ const model = lumaModel(ctx);
14214
14252
  const ratio = req.aspectRatio !== "adaptive" ? req.aspectRatio : void 0;
14215
14253
  const [source2, ...rest] = req.references ?? [];
14216
14254
  if (source2 === void 0) {
@@ -14238,7 +14276,7 @@ function imageBody(req, ctx, outputFormat) {
14238
14276
  }
14239
14277
  function videoBody(req, ctx, mode) {
14240
14278
  const prompt = checkPrompt(req, ctx);
14241
- const model = ctx.model.providerModelId;
14279
+ const model = lumaModel(ctx);
14242
14280
  const hasReferences = (req.referenceImages ?? []).length + (req.referenceVideos ?? []).length + (req.referenceAudios ?? []).length > 0;
14243
14281
  if (hasReferences) {
14244
14282
  refuseOrDrop(
@@ -14320,7 +14358,7 @@ function videoBody(req, ctx, mode) {
14320
14358
  async function generateLumaImage(req, ctx) {
14321
14359
  throwIfAborted(ctx.signal);
14322
14360
  lumaKey(ctx);
14323
- if (!LUMA_IMAGE_MODEL_IDS.has(ctx.model.providerModelId.toLowerCase())) {
14361
+ if (!LUMA_IMAGE_MODEL_IDS.has(lumaModel(ctx))) {
14324
14362
  throw new ModelKitError({
14325
14363
  code: "unsupported_option",
14326
14364
  message: `luma has no image mapping for "${ctx.model.providerModelId}"`,
@@ -14353,16 +14391,25 @@ function lumaVideoJob(ctx, req, mode, id) {
14353
14391
  return { status: "pending" };
14354
14392
  },
14355
14393
  async result(opts) {
14356
- const signal = opts?.signal ?? ctx.signal;
14357
- const url = await pollUntil(checkGeneration.bind(null, ctx, id, "video"), {
14394
+ const signal = combineSignals(ctx.signal, opts?.signal);
14395
+ const jobCtx = { ...ctx, ...signal !== void 0 && { signal } };
14396
+ const url = await pollUntil(checkGeneration.bind(null, jobCtx, id, "video"), {
14358
14397
  intervalMs: opts?.pollIntervalMs ?? VIDEO_POLL_INTERVAL_MS,
14359
14398
  timeoutMs: opts?.timeoutMs ?? VIDEO_TIMEOUT_MS,
14360
14399
  ...signal !== void 0 && { signal }
14361
14400
  });
14362
- const video = await downloadBytes(ctx, url, "video/mp4");
14401
+ if (mode === "generate") {
14402
+ const result2 = {
14403
+ video: { url, expiresAt: new Date(Date.now() + OUTPUT_URL_TTL_MS) },
14404
+ usage: videoUsage4(jobCtx, req, mode, GENERATE_SECONDS),
14405
+ raw: { id }
14406
+ };
14407
+ return result2;
14408
+ }
14409
+ const video = await downloadBytes(jobCtx, url, "video/mp4");
14363
14410
  const result = {
14364
14411
  video,
14365
- usage: videoUsage4(ctx, req, mode, video.bytes),
14412
+ usage: videoUsage4(jobCtx, req, mode, editedSeconds(req, video.bytes)),
14366
14413
  raw: { id }
14367
14414
  };
14368
14415
  return result;
@@ -14372,7 +14419,7 @@ function lumaVideoJob(ctx, req, mode, id) {
14372
14419
  async function generateLumaVideo(req, ctx) {
14373
14420
  throwIfAborted(ctx.signal);
14374
14421
  lumaKey(ctx);
14375
- if (ctx.model.providerModelId.toLowerCase() !== LUMA_VIDEO_MODEL_ID) {
14422
+ if (lumaModel(ctx) !== LUMA_VIDEO_MODEL_ID) {
14376
14423
  throw new ModelKitError({
14377
14424
  code: "unsupported_option",
14378
14425
  message: `luma has no video mapping for "${ctx.model.providerModelId}"`,