@shop-os/modelkit 0.7.1 → 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/dist/index.cjs CHANGED
@@ -89,6 +89,7 @@ __export(index_exports, {
89
89
  liveCatalog: () => liveCatalog,
90
90
  liveState: () => liveState,
91
91
  lumaAdapter: () => lumaAdapter,
92
+ lumaReframeTooTall: () => lumaReframeTooTall,
92
93
  mergeTextProviderOptions: () => mergeTextProviderOptions,
93
94
  missingTierKeys: () => missingTierKeys,
94
95
  model3dCall: () => model3dCall,
@@ -126,6 +127,7 @@ __export(index_exports, {
126
127
  sortCatalogRows: () => sortCatalogRows,
127
128
  streamObject: () => streamObject,
128
129
  streamText: () => streamText,
130
+ takesTargetAspectRatio: () => takesTargetAspectRatio,
129
131
  textUsageEvent: () => textUsageEvent,
130
132
  toGeneratedImage: () => toGeneratedImage,
131
133
  validateInputSpec: () => validateInputSpec,
@@ -6486,6 +6488,57 @@ function applyXaiVideoRules(model, req, onUnsupported, warnings) {
6486
6488
  });
6487
6489
  req.resolution = XAI_REFERENCE_MAX_RESOLUTION;
6488
6490
  }
6491
+ function takesTargetAspectRatio(model) {
6492
+ const ratios = model.constraints.aspectRatios ?? [];
6493
+ return (model.constraints.requires ?? []).includes("sourceVideo") && ratios.length > 0 && !ratios.includes("adaptive");
6494
+ }
6495
+ var LUMA_REFRAME_VERTICAL_MAX_RESOLUTION = "720p";
6496
+ function lumaReframeTooTall(req) {
6497
+ const vertical = req.aspectRatio === "9:16" || req.aspectRatio === "3:4";
6498
+ return vertical && req.resolution === "1080p";
6499
+ }
6500
+ function applyLumaVideoRules(model, req, onUnsupported, warnings) {
6501
+ if (model.adapter !== "luma" || !takesTargetAspectRatio(model)) return;
6502
+ if (req.aspectRatio === void 0 || req.aspectRatio === "adaptive") {
6503
+ const listed2 = model.constraints.aspectRatios ?? [];
6504
+ const fallback = model.defaults.aspectRatio !== void 0 && model.defaults.aspectRatio !== "adaptive" ? model.defaults.aspectRatio : listed2[0];
6505
+ const message2 = `${model.id} needs a target aspect ratio: ${listed2.join(", ")}`;
6506
+ if (onUnsupported === "error" || fallback === void 0) {
6507
+ throw new ModelKitError({
6508
+ code: "unsupported_option",
6509
+ message: message2,
6510
+ model: model.id,
6511
+ retryable: false
6512
+ });
6513
+ }
6514
+ warnings.push({
6515
+ code: "ratio_coerced",
6516
+ field: "aspectRatio",
6517
+ from: req.aspectRatio ?? "adaptive",
6518
+ to: fallback,
6519
+ message: message2
6520
+ });
6521
+ req.aspectRatio = fallback;
6522
+ }
6523
+ if (!lumaReframeTooTall(req)) return;
6524
+ const message = `${model.id} renders 9:16 and 3:4 reframes at up to ${LUMA_REFRAME_VERTICAL_MAX_RESOLUTION}`;
6525
+ if (onUnsupported === "error") {
6526
+ throw new ModelKitError({
6527
+ code: "unsupported_option",
6528
+ message,
6529
+ model: model.id,
6530
+ retryable: false
6531
+ });
6532
+ }
6533
+ warnings.push({
6534
+ code: "resolution_coerced",
6535
+ field: "resolution",
6536
+ from: req.resolution,
6537
+ to: LUMA_REFRAME_VERTICAL_MAX_RESOLUTION,
6538
+ message
6539
+ });
6540
+ req.resolution = LUMA_REFRAME_VERTICAL_MAX_RESOLUTION;
6541
+ }
6489
6542
  var MINIMAX_H3_INSERT = "minimax/h3-max/insert-video";
6490
6543
  var INSERT_MIN = 1.625;
6491
6544
  var INSERT_MAX = 60;
@@ -13755,24 +13808,24 @@ var import_zod25 = require("zod");
13755
13808
  function mp4DurationSeconds(bytes) {
13756
13809
  const moov = findBox(bytes, 0, bytes.length, "moov");
13757
13810
  if (!moov) return void 0;
13758
- const mvhd = findBox(bytes, moov.start + 8, moov.end, "mvhd");
13811
+ const mvhd = findBox(bytes, moov.body, moov.end, "mvhd");
13759
13812
  if (!mvhd) return void 0;
13760
13813
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
13761
- const size = mvhd.end - mvhd.start;
13762
- if (size < 8 + 4) return void 0;
13763
- 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);
13764
13817
  if (version === 0) {
13765
- if (size < 8 + 4 + 4 + 4 + 4 + 4) return void 0;
13766
- const timescale = view.getUint32(mvhd.start + 8 + 4 + 4 + 4);
13767
- 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);
13768
13821
  if (timescale === 0) return void 0;
13769
13822
  return duration / timescale;
13770
13823
  }
13771
13824
  if (version === 1) {
13772
- if (size < 8 + 4 + 8 + 8 + 4 + 8) return void 0;
13773
- const timescale = view.getUint32(mvhd.start + 8 + 4 + 8 + 8);
13774
- const high = view.getUint32(mvhd.start + 8 + 4 + 8 + 8 + 4);
13775
- 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);
13776
13829
  if (timescale === 0) return void 0;
13777
13830
  return (high * 2 ** 32 + low) / timescale;
13778
13831
  }
@@ -13782,15 +13835,23 @@ function findBox(bytes, from, to, type) {
13782
13835
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
13783
13836
  let offset = from;
13784
13837
  while (offset + 8 <= to) {
13785
- const size = view.getUint32(offset);
13838
+ let size = view.getUint32(offset);
13839
+ let header = 8;
13786
13840
  const name = String.fromCharCode(
13787
13841
  view.getUint8(offset + 4),
13788
13842
  view.getUint8(offset + 5),
13789
13843
  view.getUint8(offset + 6),
13790
13844
  view.getUint8(offset + 7)
13791
13845
  );
13792
- if (size < 8 || offset + size > to) return void 0;
13793
- 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 };
13794
13855
  offset += size;
13795
13856
  }
13796
13857
  return void 0;
@@ -13803,6 +13864,7 @@ var IMAGE_TIMEOUT_MS = 5 * 6e4;
13803
13864
  var VIDEO_POLL_INTERVAL_MS = 5e3;
13804
13865
  var VIDEO_TIMEOUT_MS = 20 * 6e4;
13805
13866
  var PROMPT_MAX_CHARS = 6e3;
13867
+ var OUTPUT_URL_TTL_MS = 55 * 6e4;
13806
13868
  var LUMA_IMAGE_MODEL_IDS = /* @__PURE__ */ new Set(["uni-1", "uni-1-max"]);
13807
13869
  var LUMA_VIDEO_MODEL_ID = "ray-3.2";
13808
13870
  var LUMA_VIDEO_MODES = /* @__PURE__ */ new Map([
@@ -13821,6 +13883,9 @@ function lumaRowRefusal(row2) {
13821
13883
  }
13822
13884
  return `luma has no ${row2.type} rows`;
13823
13885
  }
13886
+ function lumaModel(ctx) {
13887
+ return ctx.model.providerModelId.toLowerCase();
13888
+ }
13824
13889
  function lumaBase(ctx) {
13825
13890
  return ctx.endpoints?.luma ?? DEFAULT_BASE_URL;
13826
13891
  }
@@ -13863,8 +13928,23 @@ function imageRef(ref) {
13863
13928
  if ("url" in ref) return { url: ref.url };
13864
13929
  return { data: mediaRefToBase64(ref.bytes), media_type: ref.mime };
13865
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
+ }
13866
13946
  function videoRef(ref) {
13867
- 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) };
13868
13948
  const mediaType = ref.mime.startsWith("video/") ? ref.mime : "video/mp4";
13869
13949
  return { data: mediaRefToBase64(ref.bytes), media_type: mediaType };
13870
13950
  }
@@ -13914,16 +13994,12 @@ function lumaVideoMode(ctx) {
13914
13994
  }
13915
13995
  return mode;
13916
13996
  }
13917
- function reframeTooTall(req) {
13918
- const vertical = req.aspectRatio === "9:16" || req.aspectRatio === "3:4";
13919
- return vertical && req.resolution === "1080p";
13920
- }
13921
13997
  function sentResolution(req, mode) {
13922
- return mode === "reframe" && reframeTooTall(req) ? "720p" : req.resolution;
13998
+ return mode === "reframe" && lumaReframeTooTall(req) ? "720p" : req.resolution;
13923
13999
  }
13924
14000
  function lumaReframeResolution(req, ctx) {
13925
14001
  const requested = req.resolution;
13926
- if (requested === void 0 || !reframeTooTall(req)) return requested;
14002
+ if (requested === void 0 || !lumaReframeTooTall(req)) return requested;
13927
14003
  const message = `${ctx.model.id} renders 9:16 and 3:4 reframes at up to 720p`;
13928
14004
  if ((ctx.call.onUnsupported ?? "error") === "error") {
13929
14005
  throw new ModelKitError({
@@ -14032,14 +14108,22 @@ async function lumaRequest(ctx, url, body) {
14032
14108
  }
14033
14109
  return res;
14034
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
+ }
14035
14119
  async function submitGeneration(ctx, body) {
14036
14120
  const retryOptions = mediaRetryOptions(ctx.call, ctx.signal);
14037
14121
  const res = await withRetry(
14038
14122
  () => lumaRequest(ctx, `${lumaBase(ctx)}/v1/generations`, JSON.stringify(body)),
14039
14123
  retryOptions
14040
14124
  );
14041
- const parsed = lumaGenerationSchema.safeParse(await res.json());
14042
- if (!parsed.success) {
14125
+ const generation = await parseGeneration(res);
14126
+ if (generation === void 0 || generation === "not_json") {
14043
14127
  throw new ModelKitError({
14044
14128
  code: "provider_rejected",
14045
14129
  message: "Luma submit returned no generation id",
@@ -14048,13 +14132,20 @@ async function submitGeneration(ctx, body) {
14048
14132
  retryable: false
14049
14133
  });
14050
14134
  }
14051
- return parsed.data;
14135
+ return generation;
14052
14136
  }
14053
14137
  async function readGeneration(ctx, id) {
14054
14138
  const res = await lumaRequest(ctx, `${lumaBase(ctx)}/v1/generations/${id}`);
14055
- const payload = JsonValueSchema.parse(await res.json());
14056
- const parsed = lumaGenerationSchema.safeParse(payload);
14057
- 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) {
14058
14149
  throw new ModelKitError({
14059
14150
  code: "provider_rejected",
14060
14151
  message: "Luma returned an unreadable generation",
@@ -14063,14 +14154,12 @@ async function readGeneration(ctx, id) {
14063
14154
  retryable: false
14064
14155
  });
14065
14156
  }
14066
- return parsed.data;
14157
+ return generation;
14067
14158
  }
14068
14159
  function outputUrl(generation, type) {
14069
14160
  const outputs = generation.output ?? [];
14070
- const match = outputs.find((item) => item.type === type && item.url);
14071
- if (match?.url) return match.url;
14072
- const fallback = outputs.find((item) => item.url);
14073
- 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;
14074
14163
  }
14075
14164
  async function readGenerationOnce(ctx, id) {
14076
14165
  try {
@@ -14109,21 +14198,22 @@ async function checkGeneration(ctx, id, type) {
14109
14198
  return void 0;
14110
14199
  }
14111
14200
  async function downloadBytes(ctx, url, fallback) {
14112
- let bytes;
14113
- let header;
14114
- try {
14115
- const out = await mediaRefToBytes({ url }, ctx.fetch, {
14116
- ...ctx.signal !== void 0 && { signal: ctx.signal },
14117
- provider: "luma",
14118
- model: ctx.model.id
14119
- });
14120
- bytes = out.bytes;
14121
- header = out.mime;
14122
- } catch (cause) {
14123
- throw classifyThrown("luma", cause, ctx.model.id);
14124
- }
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
+ );
14125
14215
  const kind = fallback.split("/")[0];
14126
- const usable = header !== void 0 && header.startsWith(`${kind}/`);
14216
+ const usable = header.startsWith(`${kind}/`);
14127
14217
  return { bytes, mime: usable ? header : fallback };
14128
14218
  }
14129
14219
  function imageUsage2(ctx) {
@@ -14138,15 +14228,12 @@ function imageUsage2(ctx) {
14138
14228
  }
14139
14229
  var GENERATE_SECONDS = 5;
14140
14230
  var MAX_PLAUSIBLE_SECONDS = 60;
14141
- function billedSeconds(req, mode, bytes) {
14142
- if (mode === "generate") return GENERATE_SECONDS;
14231
+ function editedSeconds(req, bytes) {
14143
14232
  const measured = mp4DurationSeconds(bytes);
14144
- const source2 = req.sourceVideo;
14145
- 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;
14146
14234
  return Math.max(length ?? 0, GENERATE_SECONDS);
14147
14235
  }
14148
- function videoUsage4(ctx, req, mode, bytes) {
14149
- const seconds3 = billedSeconds(req, mode, bytes);
14236
+ function videoUsage4(ctx, req, mode, seconds3) {
14150
14237
  const event = {
14151
14238
  model: ctx.model.id,
14152
14239
  provider: "luma",
@@ -14161,7 +14248,7 @@ function videoUsage4(ctx, req, mode, bytes) {
14161
14248
  }
14162
14249
  function imageBody(req, ctx, outputFormat) {
14163
14250
  const prompt = checkPrompt(req, ctx);
14164
- const model = ctx.model.providerModelId;
14251
+ const model = lumaModel(ctx);
14165
14252
  const ratio = req.aspectRatio !== "adaptive" ? req.aspectRatio : void 0;
14166
14253
  const [source2, ...rest] = req.references ?? [];
14167
14254
  if (source2 === void 0) {
@@ -14189,7 +14276,7 @@ function imageBody(req, ctx, outputFormat) {
14189
14276
  }
14190
14277
  function videoBody(req, ctx, mode) {
14191
14278
  const prompt = checkPrompt(req, ctx);
14192
- const model = ctx.model.providerModelId;
14279
+ const model = lumaModel(ctx);
14193
14280
  const hasReferences = (req.referenceImages ?? []).length + (req.referenceVideos ?? []).length + (req.referenceAudios ?? []).length > 0;
14194
14281
  if (hasReferences) {
14195
14282
  refuseOrDrop(
@@ -14271,7 +14358,7 @@ function videoBody(req, ctx, mode) {
14271
14358
  async function generateLumaImage(req, ctx) {
14272
14359
  throwIfAborted(ctx.signal);
14273
14360
  lumaKey(ctx);
14274
- if (!LUMA_IMAGE_MODEL_IDS.has(ctx.model.providerModelId.toLowerCase())) {
14361
+ if (!LUMA_IMAGE_MODEL_IDS.has(lumaModel(ctx))) {
14275
14362
  throw new ModelKitError({
14276
14363
  code: "unsupported_option",
14277
14364
  message: `luma has no image mapping for "${ctx.model.providerModelId}"`,
@@ -14304,16 +14391,25 @@ function lumaVideoJob(ctx, req, mode, id) {
14304
14391
  return { status: "pending" };
14305
14392
  },
14306
14393
  async result(opts) {
14307
- const signal = opts?.signal ?? ctx.signal;
14308
- 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"), {
14309
14397
  intervalMs: opts?.pollIntervalMs ?? VIDEO_POLL_INTERVAL_MS,
14310
14398
  timeoutMs: opts?.timeoutMs ?? VIDEO_TIMEOUT_MS,
14311
14399
  ...signal !== void 0 && { signal }
14312
14400
  });
14313
- 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");
14314
14410
  const result = {
14315
14411
  video,
14316
- usage: videoUsage4(ctx, req, mode, video.bytes),
14412
+ usage: videoUsage4(jobCtx, req, mode, editedSeconds(req, video.bytes)),
14317
14413
  raw: { id }
14318
14414
  };
14319
14415
  return result;
@@ -14323,7 +14419,7 @@ function lumaVideoJob(ctx, req, mode, id) {
14323
14419
  async function generateLumaVideo(req, ctx) {
14324
14420
  throwIfAborted(ctx.signal);
14325
14421
  lumaKey(ctx);
14326
- if (ctx.model.providerModelId.toLowerCase() !== LUMA_VIDEO_MODEL_ID) {
14422
+ if (lumaModel(ctx) !== LUMA_VIDEO_MODEL_ID) {
14327
14423
  throw new ModelKitError({
14328
14424
  code: "unsupported_option",
14329
14425
  message: `luma has no video mapping for "${ctx.model.providerModelId}"`,
@@ -16653,6 +16749,7 @@ function resolveVideoOptions(model, request, onUnsupported = "error") {
16653
16749
  delete merged.previousInteractionId;
16654
16750
  }
16655
16751
  applyXaiVideoRules(model, merged, onUnsupported, warnings);
16752
+ applyLumaVideoRules(model, merged, onUnsupported, warnings);
16656
16753
  checkVideoRequired(model, merged);
16657
16754
  applyFalVideoRules(model, merged, request.aspectRatio, warnings);
16658
16755
  return { request: merged, warnings };
@@ -18251,6 +18348,7 @@ var describe = (modelId) => createDefaultModelKit().describe(modelId);
18251
18348
  liveCatalog,
18252
18349
  liveState,
18253
18350
  lumaAdapter,
18351
+ lumaReframeTooTall,
18254
18352
  mergeTextProviderOptions,
18255
18353
  missingTierKeys,
18256
18354
  model3dCall,
@@ -18288,6 +18386,7 @@ var describe = (modelId) => createDefaultModelKit().describe(modelId);
18288
18386
  sortCatalogRows,
18289
18387
  streamObject,
18290
18388
  streamText,
18389
+ takesTargetAspectRatio,
18291
18390
  textUsageEvent,
18292
18391
  toGeneratedImage,
18293
18392
  validateInputSpec,