@shop-os/modelkit 0.1.2 → 0.1.4

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
@@ -165,7 +165,18 @@ var Catalog = class _Catalog {
165
165
 
166
166
  // src/catalog/image.ts
167
167
  var IMAGE_USAGE = { unit: "images", modality: "IMAGE" };
168
- var GEMINI_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "4:5", "5:4"];
168
+ var GEMINI_RATIOS = [
169
+ "1:1",
170
+ "2:3",
171
+ "3:2",
172
+ "16:9",
173
+ "9:16",
174
+ "4:3",
175
+ "3:4",
176
+ "21:9",
177
+ "4:5",
178
+ "5:4"
179
+ ];
169
180
  function geminiImage(id, label, aliases, sizes) {
170
181
  const model = {
171
182
  id,
@@ -188,7 +199,8 @@ var IMAGE_MODELS = [
188
199
  "gemini-3.1-flash-image-preview",
189
200
  "Nano Banana Flash",
190
201
  ["nb-2"],
191
- ["1K", "2K", "4K"]
202
+ // Flash 3.1 is the only Gemini image model with the 512 tier.
203
+ ["512", "1K", "2K", "4K"]
192
204
  ),
193
205
  geminiImage("gemini-3.1-flash-lite-image", "Nano Banana Lite", ["nb-lite"], ["1K"]),
194
206
  // trigger-workflows validates 1K only; pricing seeds 2K/4K rows so allow all three.
@@ -292,7 +304,10 @@ var IMAGE_MODELS = [
292
304
  constraints: {
293
305
  aspectRatios: ["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9", "4:5", "5:4"],
294
306
  sizes: ["1K", "2K"],
295
- references: { images: 10 }
307
+ references: { images: 10 },
308
+ // Ark's Images API has no output-format, quality or count field; only
309
+ // the seed is forwarded, so anything else must warn, not vanish.
310
+ supports: ["seed"]
296
311
  },
297
312
  defaults: { size: "1K", aspectRatio: "1:1" },
298
313
  usage: { ...IMAGE_USAGE },
@@ -307,21 +322,12 @@ var IMAGE_MODELS = [
307
322
  providerModelId: "seedream-4-5-251128",
308
323
  lifecycle: "active",
309
324
  constraints: {
310
- aspectRatios: [
311
- "1:1",
312
- "2:3",
313
- "3:2",
314
- "4:3",
315
- "3:4",
316
- "4:5",
317
- "5:4",
318
- "9:16",
319
- "16:9",
320
- "21:9",
321
- "97:30"
322
- ],
325
+ // 97:30 has no Ark size spelling; the A+ flow pads inputs to 97:30 and
326
+ // omits the ratio instead. Advertise only sizes the adapter can send.
327
+ aspectRatios: ["1:1", "2:3", "3:2", "4:3", "3:4", "4:5", "5:4", "9:16", "16:9", "21:9"],
323
328
  sizes: ["1K", "2K"],
324
- references: { images: 10 }
329
+ references: { images: 10 },
330
+ supports: ["seed"]
325
331
  },
326
332
  defaults: { size: "1K", aspectRatio: "1:1" },
327
333
  usage: { ...IMAGE_USAGE },
@@ -345,6 +351,9 @@ var IMAGE_MODELS = [
345
351
  pricingKind: "image_size"
346
352
  },
347
353
  {
354
+ // Prodia's gemini-3-pro jobs take aspect_ratio + resolution (1K/2K/4K), not
355
+ // width/height; 97:30 is outside its aspect_ratio enum.
356
+ // https://docs.prodia.com/models/gemini-3/
348
357
  id: "prodia-nano-banana-pro",
349
358
  aliases: [],
350
359
  label: "Nano Banana Pro via Prodia",
@@ -353,19 +362,7 @@ var IMAGE_MODELS = [
353
362
  providerModelId: "inference.gemini-3-pro.img2img.v1",
354
363
  lifecycle: "active",
355
364
  constraints: {
356
- aspectRatios: [
357
- "1:1",
358
- "2:3",
359
- "3:2",
360
- "4:3",
361
- "3:4",
362
- "4:5",
363
- "5:4",
364
- "9:16",
365
- "16:9",
366
- "21:9",
367
- "97:30"
368
- ],
365
+ aspectRatios: ["1:1", "2:3", "3:2", "4:3", "3:4", "4:5", "5:4", "9:16", "16:9", "21:9"],
369
366
  sizes: ["1K", "2K", "4K"],
370
367
  requires: ["references"]
371
368
  },
@@ -374,6 +371,8 @@ var IMAGE_MODELS = [
374
371
  pricingKind: "image_size"
375
372
  },
376
373
  {
374
+ // Nano Banana (img2img.v2) is flat-rate with aspect_ratio only; 3:2, 4:5,
375
+ // 5:4 and 97:30 are not in its enum. https://docs.prodia.com/models/nano-banana/
377
376
  id: "prodia-nano-banana",
378
377
  aliases: [],
379
378
  label: "Nano Banana via Prodia",
@@ -382,19 +381,7 @@ var IMAGE_MODELS = [
382
381
  providerModelId: "inference.nano-banana.img2img.v2",
383
382
  lifecycle: "active",
384
383
  constraints: {
385
- aspectRatios: [
386
- "1:1",
387
- "2:3",
388
- "3:2",
389
- "4:3",
390
- "3:4",
391
- "4:5",
392
- "5:4",
393
- "9:16",
394
- "16:9",
395
- "21:9",
396
- "97:30"
397
- ],
384
+ aspectRatios: ["1:1", "2:3", "3:4", "4:3", "9:16", "16:9", "21:9"],
398
385
  sizes: ["1K"],
399
386
  requires: ["references"]
400
387
  },
@@ -403,6 +390,9 @@ var IMAGE_MODELS = [
403
390
  pricingKind: "image_size"
404
391
  },
405
392
  {
393
+ // WAN 2.7 takes `images` (up to 9) and a `size` enum whose exact values
394
+ // cover 1:1/16:9/9:16/4:3/3:4 only; other ratios have no spelling.
395
+ // https://replicate.com/wan-video/wan-2.7-image-pro (input schema)
406
396
  id: "wan-2.7-image-pro",
407
397
  aliases: [],
408
398
  label: "WAN 2.7 Image Pro",
@@ -411,19 +401,7 @@ var IMAGE_MODELS = [
411
401
  providerModelId: "wan-video/wan-2.7-image-pro",
412
402
  lifecycle: "active",
413
403
  constraints: {
414
- aspectRatios: [
415
- "1:1",
416
- "2:3",
417
- "3:2",
418
- "4:3",
419
- "3:4",
420
- "4:5",
421
- "5:4",
422
- "9:16",
423
- "16:9",
424
- "21:9",
425
- "97:30"
426
- ],
404
+ aspectRatios: ["1:1", "16:9", "9:16", "4:3", "3:4"],
427
405
  sizes: ["1K", "2K", "4K"],
428
406
  references: { images: 9 }
429
407
  },
@@ -432,6 +410,9 @@ var IMAGE_MODELS = [
432
410
  pricingKind: "image_size"
433
411
  },
434
412
  {
413
+ // Qwen Image 2 Pro has aspect_ratio but no size input; resolution follows
414
+ // the reference image (match_input_image) or the provider default.
415
+ // https://replicate.com/qwen/qwen-image-2-pro (input schema)
435
416
  id: "qwen-image-2-pro",
436
417
  aliases: [],
437
418
  label: "Qwen Image 2 Pro",
@@ -440,28 +421,19 @@ var IMAGE_MODELS = [
440
421
  providerModelId: "qwen/qwen-image-2-pro",
441
422
  lifecycle: "active",
442
423
  constraints: {
443
- aspectRatios: [
444
- "1:1",
445
- "2:3",
446
- "3:2",
447
- "4:3",
448
- "3:4",
449
- "4:5",
450
- "5:4",
451
- "9:16",
452
- "16:9",
453
- "21:9",
454
- "97:30"
455
- ],
456
- sizes: ["1K", "2K", "4K"],
424
+ aspectRatios: ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"],
425
+ sizes: [],
457
426
  references: { images: 1 },
458
427
  requires: ["references"]
459
428
  },
460
- defaults: { size: "1K", aspectRatio: "1:1" },
429
+ defaults: { aspectRatio: "1:1" },
461
430
  usage: { unit: "compute_seconds", modality: "IMAGE" },
462
431
  pricingKind: "image_size"
463
432
  },
464
433
  {
434
+ // Qwen Image Edit Plus takes aspect_ratio (or match_input_image) and
435
+ // output_format, but no size; no default ratio so edits keep following
436
+ // the input image. https://replicate.com/qwen-edit-apps/qwen-image-edit-plus-lora-skin
465
437
  id: "qwen-image-edit-plus-lora-skin",
466
438
  aliases: [],
467
439
  label: "Qwen Image Edit Plus",
@@ -470,25 +442,13 @@ var IMAGE_MODELS = [
470
442
  providerModelId: "qwen-edit-apps/qwen-image-edit-plus-lora-skin",
471
443
  lifecycle: "active",
472
444
  constraints: {
473
- aspectRatios: [
474
- "1:1",
475
- "2:3",
476
- "3:2",
477
- "4:3",
478
- "3:4",
479
- "4:5",
480
- "5:4",
481
- "9:16",
482
- "16:9",
483
- "21:9",
484
- "97:30"
485
- ],
486
- sizes: ["1K", "2K", "4K"],
445
+ aspectRatios: ["1:1", "16:9", "9:16", "4:3", "3:4"],
446
+ sizes: [],
487
447
  references: { images: 1 },
488
448
  requires: ["references"],
489
449
  supports: ["outputFormat"]
490
450
  },
491
- defaults: { size: "1K", aspectRatio: "1:1", outputFormat: "png" },
451
+ defaults: { outputFormat: "png" },
492
452
  usage: { unit: "compute_seconds", modality: "IMAGE" },
493
453
  pricingKind: "image_size"
494
454
  }
@@ -585,8 +545,12 @@ var OTHER_MODELS = [
585
545
  adapter: "replicate",
586
546
  providerModelId: "topazlabs/image-upscale",
587
547
  lifecycle: "active",
588
- constraints: {},
589
- defaults: {},
548
+ constraints: {
549
+ references: { images: 1 },
550
+ requires: ["references"],
551
+ supports: ["scaleFactor", "outputFormat"]
552
+ },
553
+ defaults: { scaleFactor: 2, outputFormat: "png" },
590
554
  usage: { unit: "compute_seconds", modality: "IMAGE" },
591
555
  pricingKind: "flat"
592
556
  },
@@ -642,7 +606,7 @@ var TEXT_MODELS = [
642
606
  text("gpt-5.6-sol", "GPT-5.6 Sol", "gpt-5.6-sol"),
643
607
  // Library key "claude-sonnet-4.5" maps to the SDK id "claude-sonnet-4-5".
644
608
  text("claude-sonnet-4-5", "Claude Sonnet 4.5", "claude-sonnet-4-5", {
645
- aliases: ["claude-sonnet-4.5"]
609
+ aliases: ["claude-sonnet-4.5", "claude-sonnet-4-5-20250929"]
646
610
  }),
647
611
  // Anthropic retired the dated 4.0 ids; callers that still send them get 4.6.
648
612
  text("claude-sonnet-4-6", "Claude Sonnet 4.6", "claude-sonnet-4-6", {
@@ -658,7 +622,7 @@ var TEXT_MODELS = [
658
622
  aliases: ["claude-opus-4.8", "claude-opus-4", "claude-opus-4.0", "claude-opus-4-20250514"]
659
623
  }),
660
624
  text("claude-haiku-4-5", "Claude Haiku 4.5", "claude-haiku-4-5", {
661
- aliases: ["claude-haiku-4.5", "claude-haiku-4-5-20251001"]
625
+ aliases: ["claude-haiku-4.5", "claude-haiku-4-5-20251001", "claude-haiku-4"]
662
626
  }),
663
627
  text("claude-opus-5", "Claude Opus 5", "claude-opus-5", {
664
628
  aliases: ["claude-opus-5.0"]
@@ -1141,7 +1105,7 @@ var MODELS = [
1141
1105
  ];
1142
1106
 
1143
1107
  // src/resolve-options.ts
1144
- var SIZE_ORDER = ["1K", "2K", "4K"];
1108
+ var SIZE_ORDER = ["512", "1K", "2K", "4K"];
1145
1109
  var RESOLUTION_ORDER = ["360p", "480p", "720p", "768p", "1080p", "2k", "4k"];
1146
1110
  function ratioValue(r) {
1147
1111
  if (r === "adaptive") return 1;
@@ -1273,7 +1237,13 @@ function resolveImageOptions(model, request, onUnsupported = "error") {
1273
1237
  model: model.id
1274
1238
  });
1275
1239
  }
1276
- dropUnsupported(merged, model, warnings, ["seed", "quality", "count", "outputFormat"]);
1240
+ dropUnsupported(merged, model, warnings, [
1241
+ "seed",
1242
+ "quality",
1243
+ "count",
1244
+ "outputFormat",
1245
+ "scaleFactor"
1246
+ ]);
1277
1247
  return { request: merged, warnings };
1278
1248
  }
1279
1249
  function resolveVideoOptions(model, request, onUnsupported = "error") {
@@ -1898,7 +1868,11 @@ var aiSdkTextAdapter = {
1898
1868
  function requiredKeys(model, config) {
1899
1869
  if (model.adapter === "fal" && config.routing?.fal === "cloudflare-passthrough")
1900
1870
  return ["cloudflare"];
1901
- if (model.adapter === "ai-sdk-text") return [textKeyName(model, config.routing ?? {})];
1871
+ if (model.adapter === "ai-sdk-text") {
1872
+ const names = [textKeyName(model, config.routing ?? {})];
1873
+ if (config.routing?.text === "cloudflare") names.push("cloudflare");
1874
+ return names;
1875
+ }
1902
1876
  return ADAPTER_KEYS[model.adapter];
1903
1877
  }
1904
1878
  var IMAGE_OUTPUT_TYPES = /* @__PURE__ */ new Set(["image", "upscale", "bg-remove"]);
@@ -1956,6 +1930,20 @@ function sdkAspectRatio(ratio) {
1956
1930
  const parsed = sdkRatio.safeParse(ratio);
1957
1931
  return parsed.success ? parsed.data : void 0;
1958
1932
  }
1933
+ function sdkDataContent(ref) {
1934
+ return "url" in ref ? ref.url : ref.bytes;
1935
+ }
1936
+ function sdkReference(ref) {
1937
+ return "url" in ref ? ref.url : { data: ref.bytes, mediaType: ref.mime };
1938
+ }
1939
+ function pushIgnored(warnings, field, from, modelId) {
1940
+ warnings.push({
1941
+ code: "option_ignored",
1942
+ field,
1943
+ from,
1944
+ message: `${modelId} provider instance does not support ${field}; ignored`
1945
+ });
1946
+ }
1959
1947
  function createModelKit(config, adapters) {
1960
1948
  const catalog = new Catalog(MODELS).withOverrides(config.catalogOverride ?? []);
1961
1949
  const logger = config.logger ?? console;
@@ -1978,6 +1966,7 @@ function createModelKit(config, adapters) {
1978
1966
  call,
1979
1967
  warnings
1980
1968
  };
1969
+ if (config.endpoints) ctx.endpoints = config.endpoints;
1981
1970
  if (call.signal) ctx.signal = call.signal;
1982
1971
  return { adapter, ctx };
1983
1972
  }
@@ -2017,16 +2006,45 @@ function createModelKit(config, adapters) {
2017
2006
  };
2018
2007
  }
2019
2008
  async function passthroughImage(req, call) {
2020
- const { model, prompt, n, count, size, aspectRatio, seed, providerOptions } = req;
2009
+ const {
2010
+ model,
2011
+ prompt,
2012
+ n,
2013
+ count,
2014
+ size,
2015
+ aspectRatio,
2016
+ seed,
2017
+ providerOptions,
2018
+ references,
2019
+ exactSize: exactSizeField,
2020
+ scaleFactor,
2021
+ quality: _quality,
2022
+ outputFormat: _outputFormat
2023
+ } = req;
2021
2024
  const options = { model, prompt };
2025
+ const warnings = [];
2022
2026
  const quantity = n ?? count;
2023
2027
  if (quantity !== void 0) options.n = quantity;
2024
- const exact = exactSize.safeParse(size);
2025
- if (exact.success) options.size = exact.data;
2028
+ if (references !== void 0 && references.length > 0)
2029
+ options.prompt = { images: references.map(sdkDataContent), text: prompt };
2030
+ const exact = exactSizeField === void 0 ? void 0 : exactSize.safeParse(exactSizeField);
2031
+ if (exact?.success) options.size = exact.data;
2032
+ else if (exactSizeField !== void 0)
2033
+ pushIgnored(warnings, "exactSize", exactSizeField, model.modelId);
2034
+ const requested = size === void 0 ? void 0 : exactSize.safeParse(size);
2035
+ if (requested?.success) options.size ??= requested.data;
2036
+ else if (size !== void 0) pushIgnored(warnings, "size", size, model.modelId);
2026
2037
  const ratio = sdkAspectRatio(aspectRatio);
2027
2038
  if (ratio) options.aspectRatio = ratio;
2039
+ else if (aspectRatio !== void 0)
2040
+ pushIgnored(warnings, "aspectRatio", aspectRatio, model.modelId);
2028
2041
  if (seed !== void 0) options.seed = seed;
2029
2042
  if (providerOptions) options.providerOptions = providerOptions;
2043
+ if (_quality !== void 0) pushIgnored(warnings, "quality", _quality, model.modelId);
2044
+ if (_outputFormat !== void 0)
2045
+ pushIgnored(warnings, "outputFormat", _outputFormat, model.modelId);
2046
+ if (scaleFactor !== void 0)
2047
+ pushIgnored(warnings, "scaleFactor", scaleFactor, model.modelId);
2030
2048
  const abortSignal = combineSignals(req.abortSignal, call.signal);
2031
2049
  if (abortSignal) options.abortSignal = abortSignal;
2032
2050
  const result = await (0, import_ai2.generateImage)(options);
@@ -2041,17 +2059,73 @@ function createModelKit(config, adapters) {
2041
2059
  quantity: images.length
2042
2060
  };
2043
2061
  await emit(usage2);
2044
- return { image: images[0], images, usage: usage2, raw: result, warnings: [] };
2062
+ return { image: images[0], images, usage: usage2, raw: result, warnings };
2045
2063
  }
2046
2064
  function passthroughVideo(req, call) {
2047
- const { model, prompt, aspectRatio, duration, durationSeconds, seed, providerOptions } = req;
2065
+ const {
2066
+ model,
2067
+ prompt,
2068
+ aspectRatio,
2069
+ duration,
2070
+ durationSeconds,
2071
+ seed,
2072
+ providerOptions,
2073
+ firstFrame,
2074
+ lastFrame,
2075
+ sourceVideo,
2076
+ referenceImages,
2077
+ referenceVideos,
2078
+ referenceAudios,
2079
+ resolution,
2080
+ generateAudio: generateAudio2,
2081
+ negativePrompt: _negativePrompt,
2082
+ cfgScale: _cfgScale,
2083
+ promptExpansion: _promptExpansion,
2084
+ mode: _mode
2085
+ } = req;
2048
2086
  const seconds = duration ?? durationSeconds;
2049
2087
  const abortSignal = combineSignals(req.abortSignal, call.signal);
2050
2088
  const options = { model, prompt };
2051
- const ratio = sdkAspectRatio(aspectRatio);
2089
+ const warnings = [];
2090
+ const ratio = sdkAspectRatio(aspectRatio) ?? (aspectRatio === "adaptive" ? "adaptive" : void 0);
2052
2091
  if (ratio) options.aspectRatio = ratio;
2092
+ else if (aspectRatio !== void 0)
2093
+ pushIgnored(warnings, "aspectRatio", aspectRatio, model.modelId);
2053
2094
  if (seconds !== void 0) options.duration = seconds;
2054
2095
  if (seed !== void 0) options.seed = seed;
2096
+ if (generateAudio2 !== void 0) options.generateAudio = generateAudio2;
2097
+ const frames = [
2098
+ ...firstFrame ? [{ image: sdkDataContent(firstFrame), frameType: "first_frame" }] : [],
2099
+ ...lastFrame ? [{ image: sdkDataContent(lastFrame), frameType: "last_frame" }] : []
2100
+ ];
2101
+ if (frames.length > 0) options.frameImages = frames;
2102
+ const references = [
2103
+ ...(referenceImages ?? []).map(sdkDataContent),
2104
+ ...(referenceVideos ?? []).map(sdkReference)
2105
+ ];
2106
+ if (references.length > 0) {
2107
+ if (frames.length > 0) {
2108
+ warnings.push({
2109
+ code: "option_ignored",
2110
+ field: "inputReferences",
2111
+ from: references.length,
2112
+ message: `${model.modelId} provider instance ignores references when frames are set`
2113
+ });
2114
+ } else {
2115
+ options.inputReferences = references;
2116
+ }
2117
+ }
2118
+ if (sourceVideo) pushIgnored(warnings, "sourceVideo", void 0, model.modelId);
2119
+ if (referenceAudios?.length)
2120
+ pushIgnored(warnings, "referenceAudios", referenceAudios.length, model.modelId);
2121
+ if (resolution !== void 0)
2122
+ pushIgnored(warnings, "resolution", resolution, model.modelId);
2123
+ if (_negativePrompt !== void 0)
2124
+ pushIgnored(warnings, "negativePrompt", void 0, model.modelId);
2125
+ if (_cfgScale !== void 0) pushIgnored(warnings, "cfgScale", _cfgScale, model.modelId);
2126
+ if (_promptExpansion !== void 0)
2127
+ pushIgnored(warnings, "promptExpansion", _promptExpansion, model.modelId);
2128
+ if (_mode !== void 0) pushIgnored(warnings, "mode", _mode, model.modelId);
2055
2129
  if (providerOptions) options.providerOptions = providerOptions;
2056
2130
  if (abortSignal) options.abortSignal = abortSignal;
2057
2131
  const pending = (0, import_ai2.experimental_generateVideo)(options);
@@ -2079,7 +2153,7 @@ function createModelKit(config, adapters) {
2079
2153
  });
2080
2154
  return {
2081
2155
  jobId: `ai-sdk:${model.provider}:${model.modelId}`,
2082
- warnings: [],
2156
+ warnings,
2083
2157
  async poll() {
2084
2158
  if (settled) return { status: "succeeded", raw: settled.raw };
2085
2159
  if (failed) return { status: "failed" };
@@ -2303,6 +2377,9 @@ var SUBMIT_PATH = "/contents/generations/tasks";
2303
2377
  var SEEDANCE_POLL_INTERVAL_MS = 15e3;
2304
2378
  var RESULT_TIMEOUT_MS = 15 * 60 * 1e3;
2305
2379
  var VIDEO_URL_TTL_MS = 24 * 60 * 60 * 1e3;
2380
+ function arkBaseUrl(ctx) {
2381
+ return ctx.endpoints?.ark ?? ARK_BASE_URL;
2382
+ }
2306
2383
  var SEEDREAM_SIZES = {
2307
2384
  "1K": {
2308
2385
  "1:1": "1024x1024",
@@ -2313,7 +2390,8 @@ var SEEDREAM_SIZES = {
2313
2390
  "3:2": "1248x832",
2314
2391
  "2:3": "832x1248",
2315
2392
  "21:9": "1568x672",
2316
- "4:5": "864x1080"
2393
+ "4:5": "864x1080",
2394
+ "5:4": "1080x864"
2317
2395
  },
2318
2396
  "2K": {
2319
2397
  "1:1": "2048x2048",
@@ -2324,7 +2402,8 @@ var SEEDREAM_SIZES = {
2324
2402
  "3:2": "2496x1664",
2325
2403
  "2:3": "1664x2496",
2326
2404
  "21:9": "3136x1344",
2327
- "4:5": "1728x2160"
2405
+ "4:5": "1728x2160",
2406
+ "5:4": "2160x1728"
2328
2407
  }
2329
2408
  };
2330
2409
  var arkAdapter = {
@@ -2371,9 +2450,11 @@ function seedreamSize(aspect, size) {
2371
2450
  const table = SEEDREAM_SIZES[tier];
2372
2451
  return table[aspect ?? ""] ?? table["1:1"] ?? "2048x2048";
2373
2452
  }
2374
- function outputMime(format) {
2375
- if (format === "png") return "image/png";
2376
- if (format === "webp") return "image/webp";
2453
+ function sniffedImageMime(bytes) {
2454
+ if (bytes.length >= 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71)
2455
+ return "image/png";
2456
+ if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80)
2457
+ return "image/webp";
2377
2458
  return "image/jpeg";
2378
2459
  }
2379
2460
  function arkUrl(ref) {
@@ -2415,7 +2496,7 @@ async function generateSeedreamImage(req, ctx) {
2415
2496
  else if (refs.length > 1) body.image = refs;
2416
2497
  const data = await withRetry(async () => {
2417
2498
  const res = await ctx.fetch(
2418
- `${ARK_BASE_URL}/images/generations`,
2499
+ `${arkBaseUrl(ctx)}/images/generations`,
2419
2500
  fetchInitFor(ctx, {
2420
2501
  method: "POST",
2421
2502
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
@@ -2454,7 +2535,7 @@ async function generateSeedreamImage(req, ctx) {
2454
2535
  };
2455
2536
  if (req.size !== void 0) usage2.imageSize = req.size;
2456
2537
  if (req.quality !== void 0) usage2.imageQuality = req.quality;
2457
- return { images: [{ bytes, mime: outputMime(req.outputFormat) }], usage: usage2, raw: data };
2538
+ return { images: [{ bytes, mime: sniffedImageMime(bytes) }], usage: usage2, raw: data };
2458
2539
  }
2459
2540
  var SeedanceResponseSchema = import_zod4.z.object({
2460
2541
  id: import_zod4.z.string().optional(),
@@ -2542,7 +2623,7 @@ async function generateSeedanceVideo(req, ctx) {
2542
2623
  if (req.seed !== void 0) body.seed = req.seed;
2543
2624
  const submit = await withRetry(async () => {
2544
2625
  const res = await ctx.fetch(
2545
- `${ARK_BASE_URL}${SUBMIT_PATH}`,
2626
+ `${arkBaseUrl(ctx)}${SUBMIT_PATH}`,
2546
2627
  fetchInitFor(ctx, {
2547
2628
  method: "POST",
2548
2629
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
@@ -2562,7 +2643,7 @@ async function generateSeedanceVideo(req, ctx) {
2562
2643
  const usage2 = seedanceUsage(ctx, req, duration);
2563
2644
  const fetchStatus = async () => {
2564
2645
  const res = await ctx.fetch(
2565
- `${ARK_BASE_URL}${SUBMIT_PATH}/${jobId}`,
2646
+ `${arkBaseUrl(ctx)}${SUBMIT_PATH}/${jobId}`,
2566
2647
  fetchInitFor(ctx, {
2567
2648
  headers: { Authorization: `Bearer ${key}` }
2568
2649
  })
@@ -3621,60 +3702,6 @@ async function generateOmniVideo(req, ctx, createGenAIClient) {
3621
3702
  uploadedNames.push(file.fileName);
3622
3703
  return file.fileUri;
3623
3704
  };
3624
- const parts = [];
3625
- if (req.sourceVideo) {
3626
- const media = await resolveMedia(
3627
- req.sourceVideo,
3628
- ctx.fetch,
3629
- ctx.signal,
3630
- "video/mp4",
3631
- modelId
3632
- );
3633
- parts.push({
3634
- type: "video",
3635
- uri: await uploadOne("video", media.bytes, media.mime),
3636
- mime_type: media.mime
3637
- });
3638
- }
3639
- for (const ref of videos) {
3640
- const media = await resolveMedia(ref, ctx.fetch, ctx.signal, "video/mp4", modelId);
3641
- parts.push({
3642
- type: "video",
3643
- uri: await uploadOne("video", media.bytes, media.mime),
3644
- mime_type: media.mime
3645
- });
3646
- }
3647
- if (req.firstFrame) {
3648
- const media = await resolveMedia(
3649
- req.firstFrame,
3650
- ctx.fetch,
3651
- ctx.signal,
3652
- "image/png",
3653
- modelId
3654
- );
3655
- if (media.bytes.length > OMNI_INLINE_LIMIT_BYTES) {
3656
- parts.push({
3657
- type: "image",
3658
- uri: await uploadOne("image", media.bytes, media.mime),
3659
- mime_type: media.mime
3660
- });
3661
- } else {
3662
- parts.push({ type: "image", data: toBase64(media.bytes), mime_type: media.mime });
3663
- }
3664
- }
3665
- for (const ref of images) {
3666
- const media = await resolveMedia(ref, ctx.fetch, ctx.signal, "image/png", modelId);
3667
- if (media.bytes.length > OMNI_INLINE_LIMIT_BYTES) {
3668
- parts.push({
3669
- type: "image",
3670
- uri: await uploadOne("image", media.bytes, media.mime),
3671
- mime_type: media.mime
3672
- });
3673
- } else {
3674
- parts.push({ type: "image", data: toBase64(media.bytes), mime_type: media.mime });
3675
- }
3676
- }
3677
- parts.push({ type: "text", text: req.prompt });
3678
3705
  const cleanupUploads = async () => {
3679
3706
  for (const fileName of uploadedNames) await deleteGoogleFile(fileName, apiKey, ctx.fetch);
3680
3707
  };
@@ -3688,6 +3715,60 @@ async function generateOmniVideo(req, ctx, createGenAIClient) {
3688
3715
  if (ctx.signal !== void 0) createRetryOptions.signal = ctx.signal;
3689
3716
  let interaction;
3690
3717
  try {
3718
+ const parts = [];
3719
+ if (req.sourceVideo) {
3720
+ const media = await resolveMedia(
3721
+ req.sourceVideo,
3722
+ ctx.fetch,
3723
+ ctx.signal,
3724
+ "video/mp4",
3725
+ modelId
3726
+ );
3727
+ parts.push({
3728
+ type: "video",
3729
+ uri: await uploadOne("video", media.bytes, media.mime),
3730
+ mime_type: media.mime
3731
+ });
3732
+ }
3733
+ for (const ref of videos) {
3734
+ const media = await resolveMedia(ref, ctx.fetch, ctx.signal, "video/mp4", modelId);
3735
+ parts.push({
3736
+ type: "video",
3737
+ uri: await uploadOne("video", media.bytes, media.mime),
3738
+ mime_type: media.mime
3739
+ });
3740
+ }
3741
+ if (req.firstFrame) {
3742
+ const media = await resolveMedia(
3743
+ req.firstFrame,
3744
+ ctx.fetch,
3745
+ ctx.signal,
3746
+ "image/png",
3747
+ modelId
3748
+ );
3749
+ if (media.bytes.length > OMNI_INLINE_LIMIT_BYTES) {
3750
+ parts.push({
3751
+ type: "image",
3752
+ uri: await uploadOne("image", media.bytes, media.mime),
3753
+ mime_type: media.mime
3754
+ });
3755
+ } else {
3756
+ parts.push({ type: "image", data: toBase64(media.bytes), mime_type: media.mime });
3757
+ }
3758
+ }
3759
+ for (const ref of images) {
3760
+ const media = await resolveMedia(ref, ctx.fetch, ctx.signal, "image/png", modelId);
3761
+ if (media.bytes.length > OMNI_INLINE_LIMIT_BYTES) {
3762
+ parts.push({
3763
+ type: "image",
3764
+ uri: await uploadOne("image", media.bytes, media.mime),
3765
+ mime_type: media.mime
3766
+ });
3767
+ } else {
3768
+ parts.push({ type: "image", data: toBase64(media.bytes), mime_type: media.mime });
3769
+ }
3770
+ }
3771
+ parts.push({ type: "text", text: req.prompt });
3691
3772
  try {
3692
3773
  interaction = await withRetry(async () => {
3693
3774
  try {
@@ -4021,6 +4102,10 @@ function createGoogleImageAdapter(deps = defaultGoogleImageDeps) {
4021
4102
  imageQuality: "auto"
4022
4103
  };
4023
4104
  if (req.size !== void 0) usage2.imageSize = req.size;
4105
+ if (result.usage?.inputTokens !== void 0)
4106
+ usage2.inputTokens = result.usage.inputTokens;
4107
+ if (result.usage?.outputTokens !== void 0)
4108
+ usage2.outputTokens = result.usage.outputTokens;
4024
4109
  return { images, usage: usage2, raw: result };
4025
4110
  }
4026
4111
  };
@@ -4552,7 +4637,7 @@ function aspectRatioToSize(ratio, tier) {
4552
4637
  };
4553
4638
  const MIN_PIXELS = 655360;
4554
4639
  const MAX_PIXELS = 8294400;
4555
- const maxLong = TIER_LONG_EDGE[tier];
4640
+ const maxLong = TIER_LONG_EDGE[tier] ?? 1024;
4556
4641
  const RATIO_MAP = /* @__PURE__ */ new Map([
4557
4642
  ["1:1", [1, 1]],
4558
4643
  ["16:9", [16, 9]],
@@ -4753,7 +4838,8 @@ var RATIO_TO_DIMS = {
4753
4838
  "9:16": { width: 768, height: 1360 },
4754
4839
  "3:4": { width: 768, height: 1024 },
4755
4840
  "2:3": { width: 768, height: 1152 },
4756
- "4:5": { width: 896, height: 1120 }
4841
+ "4:5": { width: 896, height: 1120 },
4842
+ "5:4": { width: 1120, height: 896 }
4757
4843
  };
4758
4844
  function dimsFor(aspectRatio) {
4759
4845
  if (aspectRatio !== void 0) {
@@ -4762,6 +4848,27 @@ function dimsFor(aspectRatio) {
4762
4848
  }
4763
4849
  return { width: 1024, height: 1024 };
4764
4850
  }
4851
+ var GEMINI_JOB_RATIOS = /* @__PURE__ */ new Set([
4852
+ "1:1",
4853
+ "2:3",
4854
+ "3:2",
4855
+ "3:4",
4856
+ "4:3",
4857
+ "4:5",
4858
+ "5:4",
4859
+ "9:16",
4860
+ "16:9",
4861
+ "21:9"
4862
+ ]);
4863
+ var NANO_BANANA_RATIOS = /* @__PURE__ */ new Set([
4864
+ "1:1",
4865
+ "2:3",
4866
+ "3:4",
4867
+ "4:3",
4868
+ "9:16",
4869
+ "16:9",
4870
+ "21:9"
4871
+ ]);
4765
4872
  async function refToBytes(ctx, ref) {
4766
4873
  if ("bytes" in ref) return { bytes: ref.bytes, mime: ref.mime };
4767
4874
  const res = await ctx.fetch(ref.url);
@@ -4861,7 +4968,7 @@ async function generateProdiaImage(req, ctx) {
4861
4968
  inputs = refs.length > 0 && firstRef !== void 0 ? [firstRef] : [];
4862
4969
  accept = req.outputFormat === "png" ? "image/png" : "image/jpeg";
4863
4970
  mime = accept;
4864
- } else if (providerModelId === "flux-2-pro" || providerModelId === FLUX_2_PRO_JOB || providerModelId === "inference.gemini-3-pro.img2img.v1" || providerModelId === "inference.nano-banana.img2img.v2") {
4971
+ } else if (providerModelId === "flux-2-pro" || providerModelId === FLUX_2_PRO_JOB) {
4865
4972
  const dims = dimsFor(req.aspectRatio);
4866
4973
  const config = {
4867
4974
  prompt: req.prompt,
@@ -4870,8 +4977,30 @@ async function generateProdiaImage(req, ctx) {
4870
4977
  safety_tolerance: 5
4871
4978
  };
4872
4979
  if (req.seed !== void 0) config.seed = req.seed;
4873
- const jobType = providerModelId === "flux-2-pro" ? FLUX_2_PRO_JOB : providerModelId;
4874
- job = { type: jobType, config };
4980
+ job = { type: FLUX_2_PRO_JOB, config };
4981
+ inputs = refs;
4982
+ accept = "image/png";
4983
+ mime = "image/png";
4984
+ } else if (providerModelId === "inference.gemini-3-pro.img2img.v1" || providerModelId === "inference.nano-banana.img2img.v2") {
4985
+ const allowed = providerModelId === "inference.nano-banana.img2img.v2" ? NANO_BANANA_RATIOS : GEMINI_JOB_RATIOS;
4986
+ const config = {
4987
+ prompt: req.prompt,
4988
+ safety_tolerance: 5
4989
+ };
4990
+ if (req.aspectRatio !== void 0 && req.aspectRatio !== "adaptive") {
4991
+ if (allowed.has(req.aspectRatio)) config.aspect_ratio = req.aspectRatio;
4992
+ else
4993
+ ctx.warnings.push({
4994
+ code: "option_ignored",
4995
+ field: "aspectRatio",
4996
+ from: req.aspectRatio,
4997
+ message: `${providerModelId} has no ${req.aspectRatio} aspect_ratio; using the provider default`
4998
+ });
4999
+ }
5000
+ if (providerModelId === "inference.gemini-3-pro.img2img.v1")
5001
+ config.resolution = req.size ?? "1K";
5002
+ if (req.seed !== void 0) config.seed = req.seed;
5003
+ job = { type: providerModelId, config };
4875
5004
  inputs = refs;
4876
5005
  accept = "image/png";
4877
5006
  mime = "image/png";
@@ -5142,13 +5271,88 @@ function isUpscaler(providerModelId) {
5142
5271
  const id = isLower(providerModelId);
5143
5272
  return id.includes("upscal") || id.includes("crystal") || id.includes("topaz");
5144
5273
  }
5274
+ function isTopazUpscaler(providerModelId) {
5275
+ return isLower(providerModelId).includes("topazlabs/image-upscale");
5276
+ }
5145
5277
  function isQwen(providerModelId) {
5146
5278
  return isLower(providerModelId).includes("qwen");
5147
5279
  }
5280
+ function isWanImage(providerModelId) {
5281
+ return isLower(providerModelId).includes("wan-2.7");
5282
+ }
5283
+ function isQwenEditPlus(providerModelId) {
5284
+ return isLower(providerModelId).includes("edit");
5285
+ }
5286
+ var QWEN_IMAGE_RATIOS = /* @__PURE__ */ new Set([
5287
+ "1:1",
5288
+ "16:9",
5289
+ "9:16",
5290
+ "4:3",
5291
+ "3:4",
5292
+ "3:2",
5293
+ "2:3"
5294
+ ]);
5295
+ var QWEN_EDIT_RATIOS = /* @__PURE__ */ new Set(["1:1", "16:9", "9:16", "4:3", "3:4"]);
5296
+ var WAN_IMAGE_SIZES = {
5297
+ "1K": {
5298
+ "1:1": "1024*1024",
5299
+ "16:9": "1280*720",
5300
+ "9:16": "720*1280",
5301
+ "4:3": "1024*768",
5302
+ "3:4": "768*1024"
5303
+ },
5304
+ "2K": {
5305
+ "1:1": "2048*2048",
5306
+ "16:9": "2048*1152",
5307
+ "9:16": "1152*2048",
5308
+ "4:3": "2048*1536",
5309
+ "3:4": "1536*2048"
5310
+ },
5311
+ "4K": {
5312
+ "1:1": "4096*4096",
5313
+ "16:9": "4096*2304",
5314
+ "9:16": "2304*4096",
5315
+ "4:3": "4096*3072",
5316
+ "3:4": "3072*4096"
5317
+ }
5318
+ };
5319
+ function wanImageSize(aspectRatio, size) {
5320
+ const tier = size === "2K" || size === "4K" ? size : "1K";
5321
+ const table = WAN_IMAGE_SIZES[tier];
5322
+ return table[aspectRatio ?? ""] ?? table["1:1"] ?? "1024*1024";
5323
+ }
5148
5324
  async function generateReplicateImage(req, ctx, deps) {
5149
5325
  const id = ctx.model.providerModelId;
5150
- const input = { prompt: req.prompt };
5151
- if (isUpscaler(id)) {
5326
+ let input;
5327
+ if (isTopazUpscaler(id)) {
5328
+ const first = (req.references ?? [])[0];
5329
+ if (!first) {
5330
+ throw new ModelKitError({
5331
+ code: "missing_input",
5332
+ message: `${ctx.model.id} requires a reference image to upscale`,
5333
+ provider: "replicate",
5334
+ model: ctx.model.id,
5335
+ retryable: false
5336
+ });
5337
+ }
5338
+ let format = req.outputFormat === "jpeg" ? "jpg" : req.outputFormat ?? "png";
5339
+ if (format !== "png" && format !== "jpg") {
5340
+ ctx.warnings.push({
5341
+ code: "option_ignored",
5342
+ field: "outputFormat",
5343
+ from: format,
5344
+ message: "topazlabs/image-upscale supports only png and jpg; using png"
5345
+ });
5346
+ format = "png";
5347
+ }
5348
+ input = {
5349
+ image: refToUrl3(first),
5350
+ enhance_model: "Standard V2",
5351
+ output_format: format,
5352
+ upscale_factor: `${req.scaleFactor ?? 2}x`,
5353
+ face_enhancement: true
5354
+ };
5355
+ } else if (isUpscaler(id)) {
5152
5356
  const refs = req.references ?? [];
5153
5357
  const first = refs[0];
5154
5358
  if (!first) {
@@ -5160,28 +5364,53 @@ async function generateReplicateImage(req, ctx, deps) {
5160
5364
  retryable: false
5161
5365
  });
5162
5366
  }
5163
- input.image = refToUrl3(first);
5164
- input.creativity = 0;
5165
- input.scale_factor = 2;
5166
- input.output_format = "png";
5367
+ const imageInput = { prompt: req.prompt };
5368
+ imageInput.image = refToUrl3(first);
5369
+ imageInput.creativity = 0;
5370
+ imageInput.scale_factor = 2;
5371
+ imageInput.output_format = "png";
5372
+ input = imageInput;
5373
+ } else if (isWanImage(id)) {
5374
+ const refs = (req.references ?? []).map(refToUrl3);
5375
+ const imageInput = { prompt: req.prompt };
5376
+ if (refs.length > 0) imageInput.images = refs;
5377
+ imageInput.size = wanImageSize(req.aspectRatio, req.size);
5378
+ input = imageInput;
5167
5379
  } else if (isQwen(id)) {
5168
5380
  const refs = (req.references ?? []).map(refToUrl3);
5169
5381
  const firstUrl = refs[0];
5382
+ const imageInput = { prompt: req.prompt };
5170
5383
  if (firstUrl !== void 0) {
5171
- input.image = firstUrl;
5172
- input.match_input_image = true;
5384
+ imageInput.image = firstUrl;
5385
+ if (!isQwenEditPlus(id)) imageInput.match_input_image = true;
5386
+ }
5387
+ const ratios = isQwenEditPlus(id) ? QWEN_EDIT_RATIOS : QWEN_IMAGE_RATIOS;
5388
+ if (req.aspectRatio !== void 0) {
5389
+ if (ratios.has(req.aspectRatio)) imageInput.aspect_ratio = req.aspectRatio;
5390
+ else
5391
+ ctx.warnings.push({
5392
+ code: "option_ignored",
5393
+ field: "aspectRatio",
5394
+ from: req.aspectRatio,
5395
+ message: `${id} has no ${req.aspectRatio} aspect_ratio; using the provider default`
5396
+ });
5397
+ }
5398
+ if (isQwenEditPlus(id) && req.outputFormat !== void 0) {
5399
+ imageInput.output_format = req.outputFormat === "jpeg" ? "jpg" : req.outputFormat;
5173
5400
  }
5174
- if (req.outputFormat !== void 0) input.output_format = req.outputFormat;
5401
+ input = imageInput;
5175
5402
  } else {
5176
5403
  const refs = req.references ?? [];
5177
5404
  const first = refs[0];
5178
- if (first) input.image = refToUrl3(first);
5405
+ const imageInput = { prompt: req.prompt };
5406
+ if (first) imageInput.image = refToUrl3(first);
5407
+ input = imageInput;
5179
5408
  }
5180
5409
  const { url, predictTime, raw } = await runPrediction(ctx, input, deps);
5181
5410
  const file = await download(ctx, url);
5182
5411
  const usage2 = imageUsage2(ctx, req);
5183
5412
  usage2.quantity = predictTime;
5184
- return { images: [{ bytes: file.bytes, mime: file.mime }], usage: usage2, raw };
5413
+ return { images: [{ bytes: file.bytes, mime: file.mime }], usage: usage2, raw, outputUrl: url };
5185
5414
  }
5186
5415
  function wanSize(aspectRatio) {
5187
5416
  if (!aspectRatio || aspectRatio === "adaptive" || aspectRatio === "1:1") return "2K";