@howells/motif-sdk 5.2.0 → 5.4.0

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.js CHANGED
@@ -555,6 +555,7 @@ const CARRIED_FIELDS = [
555
555
  "video",
556
556
  "prompt",
557
557
  "mask",
558
+ "markup",
558
559
  "references",
559
560
  "boxes",
560
561
  "margin",
@@ -712,6 +713,7 @@ const FAL_PRICING_CHECKED_AT = "2026-05-12";
712
713
  const FAL_PRICING_CHECKED_JUL_2026 = "2026-07-11";
713
714
  const FAL_PRICING_CHECKED_AUG_2026 = "2026-08-23";
714
715
  const FAL_PRICING_CHECKED_SEP_2026 = "2026-09-16";
716
+ const FAL_PRICING_CHECKED_OCT_2026 = "2026-10-29";
715
717
  const MODELS = {
716
718
  flare: {
717
719
  benchmark: {
@@ -727,7 +729,7 @@ const MODELS = {
727
729
  rank: 2
728
730
  }
729
731
  },
730
- useCase: "Frontier generation and editing, evaluated at max quality"
732
+ useCase: "Frontier generation and editing at max reasoning effort; scored as GPT Image 2.5 until 2.6 is benchmarked"
731
733
  },
732
734
  defaultQuality: "max",
733
735
  customImageSize: {
@@ -737,11 +739,11 @@ const MODELS = {
737
739
  minPixels: 655360,
738
740
  multipleOf: 16
739
741
  },
740
- editEndpoint: "openai/gpt-image-2.5/flare/edit",
741
- endpoint: "openai/gpt-image-2.5/flare/text-to-image",
742
+ editEndpoint: "openai/gpt-image-2.6/flare/edit",
743
+ endpoint: "openai/gpt-image-2.6/flare/text-to-image",
742
744
  maxReferenceImages: 16,
743
- name: "GPT Image 2.5 Flare",
744
- pricing: "Token-based: text $5/M input, $1.25/M cached, $10/M output; image $8/M input, $2/M cached, $30/M output",
745
+ name: "GPT Image 2.6 Flare",
746
+ pricing: "Token-based: image $8/M input, $2/M cached, $30/M output; text $5/M input, $1.25/M cached",
745
747
  pricePerImageUsd: null,
746
748
  sizeMode: "image_size_enum",
747
749
  supportsAspect: true,
@@ -749,8 +751,23 @@ const MODELS = {
749
751
  supportsEdit: true,
750
752
  supportsMaskImage: true,
751
753
  maskImageField: "mask_url",
754
+ supportsMarkupImage: true,
755
+ autoEditSize: true,
752
756
  supportsNumImages: true,
753
757
  supportsOutputFormat: true,
758
+ supportedOutputFormats: [
759
+ "jpeg",
760
+ "png",
761
+ "webp",
762
+ "gif",
763
+ "svg"
764
+ ],
765
+ supportedEditOutputFormats: [
766
+ "jpeg",
767
+ "png",
768
+ "webp",
769
+ "gif"
770
+ ],
754
771
  supportsQuality: true,
755
772
  supportedQualities: [
756
773
  "auto",
@@ -779,7 +796,7 @@ const MODELS = {
779
796
  rank: 1
780
797
  }
781
798
  },
782
- useCase: "Frontier generation and editing, evaluated at max quality"
799
+ useCase: "Frontier generation and editing at max reasoning effort; scored as GPT Image 2.5 until 2.6 is benchmarked"
783
800
  },
784
801
  defaultQuality: "max",
785
802
  customImageSize: {
@@ -789,11 +806,11 @@ const MODELS = {
789
806
  minPixels: 655360,
790
807
  multipleOf: 16
791
808
  },
792
- editEndpoint: "openai/gpt-image-2.5/sunburst/edit",
793
- endpoint: "openai/gpt-image-2.5/sunburst/text-to-image",
809
+ editEndpoint: "openai/gpt-image-2.6/sunburst/edit",
810
+ endpoint: "openai/gpt-image-2.6/sunburst/text-to-image",
794
811
  maxReferenceImages: 16,
795
- name: "GPT Image 2.5 Sunburst",
796
- pricing: "Token-based: text $5/M input, $1.25/M cached, $10/M output; image $8/M input, $2/M cached, $30/M output",
812
+ name: "GPT Image 2.6 Sunburst",
813
+ pricing: "Token-based: image $8/M input, $2/M cached, $30/M output; text $5/M input, $1.25/M cached",
797
814
  pricePerImageUsd: null,
798
815
  sizeMode: "image_size_enum",
799
816
  supportsAspect: true,
@@ -801,8 +818,23 @@ const MODELS = {
801
818
  supportsEdit: true,
802
819
  supportsMaskImage: true,
803
820
  maskImageField: "mask_url",
821
+ supportsMarkupImage: true,
822
+ autoEditSize: true,
804
823
  supportsNumImages: true,
805
824
  supportsOutputFormat: true,
825
+ supportedOutputFormats: [
826
+ "jpeg",
827
+ "png",
828
+ "webp",
829
+ "gif",
830
+ "svg"
831
+ ],
832
+ supportedEditOutputFormats: [
833
+ "jpeg",
834
+ "png",
835
+ "webp",
836
+ "gif"
837
+ ],
806
838
  supportsQuality: true,
807
839
  supportedQualities: [
808
840
  "auto",
@@ -2068,6 +2100,7 @@ const MODELS = {
2068
2100
  maxPixels: 4194304,
2069
2101
  minPixels: 262144
2070
2102
  },
2103
+ editEndpoint: "alibaba/qwen-image-3/edit",
2071
2104
  endpoint: "alibaba/qwen-image-3/text-to-image",
2072
2105
  falPricing: {
2073
2106
  checkedAt: FAL_PRICING_CHECKED_SEP_2026,
@@ -2078,12 +2111,13 @@ const MODELS = {
2078
2111
  unit: "images",
2079
2112
  unitPrice: .04
2080
2113
  },
2114
+ maxReferenceImages: 3,
2081
2115
  name: "Qwen Image 3",
2082
2116
  pricePerImageUsd: .04,
2083
2117
  pricing: "$0.04 (1K) / $0.075 (2K)",
2084
2118
  sizeMode: "image_size_enum",
2085
2119
  supportsAspect: true,
2086
- supportsEdit: false,
2120
+ supportsEdit: true,
2087
2121
  supportsNegativePrompt: true,
2088
2122
  supportsNumImages: true,
2089
2123
  supportsOutputFormat: true,
@@ -2286,6 +2320,120 @@ const MODELS = {
2286
2320
  supportsSyncMode: true,
2287
2321
  type: "generation"
2288
2322
  },
2323
+ "muse-image": {
2324
+ benchmark: { useCase: "Low-cost generation and multi-reference editing; Arena #8 generation / #10 editing, 2026-10-29" },
2325
+ editEndpoint: "meta/muse-image/edit",
2326
+ endpoint: "meta/muse-image/text-to-image",
2327
+ falPricing: {
2328
+ checkedAt: FAL_PRICING_CHECKED_OCT_2026,
2329
+ currency: "USD",
2330
+ endpointId: "meta/muse-image/text-to-image",
2331
+ estimatedCostPerImageUsd: .01,
2332
+ source: "fal-pricing-api",
2333
+ unit: "images",
2334
+ unitPrice: .01
2335
+ },
2336
+ maxReferenceImages: 10,
2337
+ name: "Meta Muse Image",
2338
+ pricePerImageUsd: .01,
2339
+ pricing: "$0.01",
2340
+ sizeMode: "aspect_ratio",
2341
+ supportedAspects: [
2342
+ "1:1",
2343
+ "4:3",
2344
+ "3:4",
2345
+ "16:9",
2346
+ "9:16",
2347
+ "3:2",
2348
+ "2:3",
2349
+ "4:5",
2350
+ "5:4",
2351
+ "21:9",
2352
+ "4:1",
2353
+ "1:4",
2354
+ "8:1",
2355
+ "1:8"
2356
+ ],
2357
+ supportsAspect: true,
2358
+ supportsEdit: true,
2359
+ supportsNumImages: true,
2360
+ supportsOutputFormat: true,
2361
+ supportsResolution: false,
2362
+ supportsSyncMode: true,
2363
+ type: "generation"
2364
+ },
2365
+ krea3: {
2366
+ benchmark: { useCase: "Reference editing with prompt planning. Motif sends fal's defaults (berry, medium reasoning), which Arena has not scored; Arena's 1411 editing score (2026-10-29) is for Ultra, available with --param reasoning_level=ultra" },
2367
+ customImageSize: {
2368
+ maxEdge: 3200,
2369
+ maxPixels: 4220160,
2370
+ minEdge: 256,
2371
+ multipleOf: 16
2372
+ },
2373
+ editEndpoint: "krea/v3/edit",
2374
+ endpoint: "krea/v3/text-to-image",
2375
+ falPricing: {
2376
+ checkedAt: FAL_PRICING_CHECKED_OCT_2026,
2377
+ currency: "USD",
2378
+ endpointId: "krea/v3/text-to-image",
2379
+ estimatedCostPerImageUsd: .025,
2380
+ source: "fal-pricing-api",
2381
+ unit: "megapixels",
2382
+ unitPrice: .025
2383
+ },
2384
+ maxReferenceImages: 8,
2385
+ name: "Krea 3",
2386
+ pricePerImageUsd: .025,
2387
+ pricing: "$0.025/MP (edits also bill each reference)",
2388
+ sizeMode: "image_size_enum",
2389
+ supportedOutputFormats: ["png", "jpeg"],
2390
+ supportsAspect: true,
2391
+ supportsEdit: true,
2392
+ supportsNegativePrompt: true,
2393
+ supportsNumImages: false,
2394
+ supportsOutputFormat: true,
2395
+ supportsResolution: false,
2396
+ supportsSafetyChecker: true,
2397
+ supportsSeed: true,
2398
+ supportsSyncMode: true,
2399
+ type: "generation"
2400
+ },
2401
+ "grok-image-quality": {
2402
+ benchmark: { useCase: "Detailed generation and editing with stronger text rendering; Arena #11 editing, 2026-10-29" },
2403
+ editEndpoint: "xai/grok-imagine-image/quality/edit",
2404
+ endpoint: "xai/grok-imagine-image/quality/text-to-image",
2405
+ falPricing: {
2406
+ checkedAt: FAL_PRICING_CHECKED_OCT_2026,
2407
+ currency: "USD",
2408
+ endpointId: "xai/grok-imagine-image/quality/text-to-image",
2409
+ estimatedCostPerImageUsd: .05,
2410
+ source: "fal-pricing-api",
2411
+ unit: "images",
2412
+ unitPrice: .05
2413
+ },
2414
+ maxReferenceImages: 3,
2415
+ name: "Grok Imagine Image Quality",
2416
+ pricePerImageUsd: .05,
2417
+ pricing: "$0.05 (1K) / $0.07 (2K)",
2418
+ sizeMode: "aspect_ratio",
2419
+ supportedAspects: [
2420
+ "16:9",
2421
+ "4:3",
2422
+ "3:2",
2423
+ "1:1",
2424
+ "2:3",
2425
+ "3:4",
2426
+ "9:16"
2427
+ ],
2428
+ supportedResolutions: ["1K", "2K"],
2429
+ supportsAspect: true,
2430
+ supportsEdit: true,
2431
+ supportsNumImages: true,
2432
+ supportsOutputFormat: true,
2433
+ supportsResolution: true,
2434
+ supportsSyncMode: true,
2435
+ type: "generation"
2436
+ },
2289
2437
  kling: {
2290
2438
  endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
2291
2439
  name: "Kling v3 Pro",
@@ -2364,7 +2512,10 @@ const GENERATION_MODELS = [
2364
2512
  "qwen3",
2365
2513
  "banana2-lite",
2366
2514
  "grok-image-2",
2515
+ "grok-image-quality",
2367
2516
  "grok-image",
2517
+ "muse-image",
2518
+ "krea3",
2368
2519
  "flux2-flex",
2369
2520
  "flux2-pro",
2370
2521
  "flux2-turbo",
@@ -2406,6 +2557,7 @@ function estimateCost(model, resolution, numImages = 1, editImages = 0) {
2406
2557
  if (configuredPrice === null) return null;
2407
2558
  if (model === "mai-image-2.5-pro" && editImages > 0) return .27 * numImages;
2408
2559
  if (model === "grok-image-2" && editImages > 0) return (resolution === "2K" || resolution === "4K" ? .08 : .06) * numImages + .01 * editImages;
2560
+ if (model === "grok-image-quality") return (resolution === "2K" || resolution === "4K" ? .07 : .05) * numImages + .01 * editImages;
2409
2561
  if (configuredPrice !== void 0) {
2410
2562
  if ((model === "banana" || model === "gemini3") && resolution === "4K") return configuredPrice * 2 * numImages;
2411
2563
  if (model === "seedream5" && (resolution === "2K" || resolution === "4K")) return configuredPrice * 2 * numImages;
@@ -2551,6 +2703,18 @@ function requestIdFromBody(text) {
2551
2703
  * error can name both what a model does support and which models support the
2552
2704
  * option that was refused.
2553
2705
  */
2706
+ const DEFAULT_OUTPUT_FORMATS = [
2707
+ "jpeg",
2708
+ "png",
2709
+ "webp"
2710
+ ];
2711
+ /** The output formats a Model returns, for an edit when `editing` is set.
2712
+ * A Model that takes no format returns none it can be asked for. */
2713
+ function outputFormatsOf(config, editing) {
2714
+ if (config.supportsOutputFormat !== true) return [];
2715
+ const formats = config.supportedOutputFormats ?? DEFAULT_OUTPUT_FORMATS;
2716
+ return editing ? config.supportedEditOutputFormats ?? formats : formats;
2717
+ }
2554
2718
  /** Generation options a model can refuse, with the registry test for each. */
2555
2719
  const OPTION_CAPABILITIES = {
2556
2720
  aspect: (config) => (config.sizeMode ?? "aspect_ratio") !== "none",
@@ -2566,6 +2730,7 @@ const OPTION_CAPABILITIES = {
2566
2730
  imageSize: (config) => config.sizeMode === "gpt_size" || config.sizeMode === "image_size_enum",
2567
2731
  inputFidelity: (config) => config.sizeMode === "gpt_size",
2568
2732
  limitGenerations: (config) => config.supportsLimitGenerations === true,
2733
+ markupImageUrl: (config) => config.supportsMarkupImage === true,
2569
2734
  maskImageUrl: (config) => config.supportsMaskImage === true,
2570
2735
  negativePrompt: (config) => config.supportsNegativePrompt === true,
2571
2736
  numImages: (config) => config.supportsNumImages,
@@ -2699,6 +2864,9 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
2699
2864
  if (options.imagePromptStrength !== void 0 && !hasEditImages) throw new Error("imagePromptStrength requires editImageUrls");
2700
2865
  if (options.maskImageUrl !== void 0 && config.supportsMaskImage !== true) unsupported(config, "maskImageUrl");
2701
2866
  if (options.maskImageUrl !== void 0 && !hasEditImages) throw new Error("maskImageUrl requires editImageUrls");
2867
+ if (options.markupImageUrl !== void 0 && config.supportsMarkupImage !== true) unsupported(config, "markupImageUrl");
2868
+ if (options.markupImageUrl !== void 0 && !hasEditImages) throw new Error("markupImageUrl requires editImageUrls");
2869
+ if (options.markupImageUrl !== void 0 && options.maskImageUrl !== void 0) throw new Error("A markup image can't be combined with a mask.");
2702
2870
  if (options.seed !== void 0 && config.supportsSeed !== true) unsupported(config, "seed");
2703
2871
  if (options.outputFormat !== void 0 && config.supportsOutputFormat !== true) unsupported(config, "outputFormat");
2704
2872
  if (options.quality !== void 0 && config.supportsQuality !== true) unsupported(config, "quality");
@@ -2738,7 +2906,7 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
2738
2906
  * - `none`: no size control (video models)
2739
2907
  */
2740
2908
  function buildGenerateBody(options) {
2741
- const { prompt, model, creative, aspect = "1:1", resolution = "2K", numImages = 1, background, editImageUrls, enableGoogleSearch, enableSafetyChecker, transparent, inputFidelity, imageSize, imagePromptStrength, limitGenerations, maskImageUrl, seed, outputFormat, quality, negativePrompt, style, syncMode, thinkingLevel, renderingSpeed, guidanceScale, numInferenceSteps, raw, enhancePrompt, safetyTolerance, enableWebSearch, expandPrompt } = options;
2909
+ const { prompt, model, creative, aspect = "1:1", resolution = "2K", numImages = 1, background, editImageUrls, enableGoogleSearch, enableSafetyChecker, transparent, inputFidelity, imageSize, imagePromptStrength, limitGenerations, maskImageUrl, markupImageUrl, seed, outputFormat, quality, negativePrompt, style, syncMode, thinkingLevel, renderingSpeed, guidanceScale, numInferenceSteps, raw, enhancePrompt, safetyTolerance, enableWebSearch, expandPrompt } = options;
2742
2910
  const config = MODELS[model];
2743
2911
  if (!config) throw new Error(`Unknown model: ${model}`);
2744
2912
  let { endpoint } = config;
@@ -2757,11 +2925,11 @@ function buildGenerateBody(options) {
2757
2925
  body.image_size = normalizedImageSize ?? (hasEditImages && !explicitAspect ? "auto" : aspectToGptSize(aspect));
2758
2926
  break;
2759
2927
  case "image_size_enum":
2760
- body.image_size = normalizedImageSize ?? falImageSizeFor(config, aspect, resolution);
2928
+ body.image_size = normalizedImageSize ?? (config.autoEditSize === true && hasEditImages && !explicitAspect ? "auto" : falImageSizeFor(config, aspect, resolution));
2761
2929
  break;
2762
2930
  case "aspect_ratio":
2763
2931
  if (config.supportsAspect) body.aspect_ratio = aspect;
2764
- if (config.supportsResolution) body.resolution = model === "grok-image" || model === "grok-image-2" ? resolution.toLowerCase() : resolution;
2932
+ if (config.supportsResolution) body.resolution = model === "grok-image" || model === "grok-image-2" || model === "grok-image-quality" ? resolution.toLowerCase() : resolution;
2765
2933
  }
2766
2934
  if (config.supportsQuality === true) body.quality = quality ?? config.defaultQuality ?? "high";
2767
2935
  if (config.supportsBackground === true && background) body.background = background;
@@ -2770,7 +2938,8 @@ function buildGenerateBody(options) {
2770
2938
  if (syncMode !== void 0 && config.supportsSyncMode === true) body.sync_mode = syncMode;
2771
2939
  if (seed !== void 0 && config.supportsSeed === true) body.seed = seed;
2772
2940
  if (outputFormat && config.supportsOutputFormat === true && transparent !== true) {
2773
- if (config.supportedOutputFormats && !config.supportedOutputFormats.includes(outputFormat)) throw new Error(`${config.name} supports output formats: ${config.supportedOutputFormats.join(", ")}`);
2941
+ const formats = outputFormatsOf(config, hasEditImages);
2942
+ if (!formats.includes(outputFormat)) throw new Error(`${config.name} ${hasEditImages ? "edits support" : "supports"} output formats: ${formats.join(", ")}`);
2774
2943
  body.output_format = outputFormat;
2775
2944
  }
2776
2945
  if (config.supportsRaw === true && raw !== void 0) body.raw = raw;
@@ -2829,6 +2998,7 @@ function buildGenerateBody(options) {
2829
2998
  if (inputFidelity) body.input_fidelity = inputFidelity;
2830
2999
  }
2831
3000
  if (maskImageUrl !== void 0 && maskImageUrl !== "" && config.supportsMaskImage === true) body[config.maskImageField ?? "mask_image_url"] = maskImageUrl;
3001
+ if (markupImageUrl !== void 0 && config.supportsMarkupImage === true) body.markup_image_url = markupImageUrl;
2832
3002
  return {
2833
3003
  body,
2834
3004
  endpoint
@@ -4417,10 +4587,15 @@ async function getToolResult(exec, job) {
4417
4587
  }
4418
4588
  /** Poll a submitted job to completion and fetch its result. Submits nothing. */
4419
4589
  async function awaitQueuedRequest(exec, job, onProgress) {
4590
+ let logsSeen = 0;
4420
4591
  for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
4421
4592
  const status = await checkToolStatus(exec, job);
4422
4593
  if (status.isErr()) return err$1(status.error);
4423
- onProgress?.(status.value.status, status.value.queuePosition);
4594
+ const logs = status.value.logs ?? [];
4595
+ const fresh = logs.slice(logs.length < logsSeen ? 0 : logsSeen);
4596
+ logsSeen = logs.length;
4597
+ if (fresh.length > 0) onProgress?.(status.value.status, status.value.queuePosition, fresh);
4598
+ else onProgress?.(status.value.status, status.value.queuePosition);
4424
4599
  if (status.value.status === "completed") return (await getToolResult(exec, job)).map((data) => ({
4425
4600
  data,
4426
4601
  requestId: job.requestId
@@ -5165,6 +5340,201 @@ function sumCosts(costs) {
5165
5340
  };
5166
5341
  }
5167
5342
  //#endregion
5343
+ //#region src/image-benchmarks.ts
5344
+ const BOARDS = {
5345
+ "text-to-image": {
5346
+ sunburst: {
5347
+ elo: 1427,
5348
+ rank: 1,
5349
+ uncertainty: 5,
5350
+ evaluatedModel: "GPT-Image-2.5 Sunburst",
5351
+ preliminary: true,
5352
+ votes: 40323
5353
+ },
5354
+ flare: {
5355
+ elo: 1401,
5356
+ rank: 2,
5357
+ uncertainty: 5,
5358
+ evaluatedModel: "GPT-Image-2.5 Flare",
5359
+ preliminary: true,
5360
+ votes: 39218
5361
+ },
5362
+ gpt2: {
5363
+ elo: 1381,
5364
+ rank: 3,
5365
+ uncertainty: 4,
5366
+ evaluatedModel: "GPT-Image-2 (Medium)",
5367
+ votes: 103385
5368
+ },
5369
+ banana21: {
5370
+ elo: 1325,
5371
+ rank: 5,
5372
+ uncertainty: 9,
5373
+ evaluatedModel: "Nano Banana 2.1",
5374
+ preliminary: true,
5375
+ votes: 5290
5376
+ },
5377
+ "grok-image-2": {
5378
+ elo: 1317,
5379
+ rank: 6,
5380
+ uncertainty: 7,
5381
+ evaluatedModel: "Grok Imagine Image 2.0",
5382
+ preliminary: true,
5383
+ votes: 10902
5384
+ },
5385
+ "muse-image": {
5386
+ elo: 1269,
5387
+ rank: 8,
5388
+ uncertainty: 5,
5389
+ evaluatedModel: "Muse Image (Meta)",
5390
+ votes: 53444
5391
+ },
5392
+ qwen3: {
5393
+ elo: 1254,
5394
+ rank: 9,
5395
+ uncertainty: 6,
5396
+ evaluatedModel: "Qwen-Image-3.0-Pro",
5397
+ votes: 11444
5398
+ },
5399
+ "mai-image-2.5": {
5400
+ elo: 1253,
5401
+ rank: 10,
5402
+ uncertainty: 4,
5403
+ evaluatedModel: "MAI-Image-2.5",
5404
+ votes: 61286
5405
+ },
5406
+ seedream5: {
5407
+ elo: 1250,
5408
+ rank: 11,
5409
+ uncertainty: 4,
5410
+ evaluatedModel: "Seedream 5.0 Pro",
5411
+ votes: 105824
5412
+ },
5413
+ "banana2-lite": {
5414
+ elo: 1249,
5415
+ rank: 12,
5416
+ uncertainty: 6,
5417
+ evaluatedModel: "Nano Banana 2 Lite",
5418
+ votes: 17813
5419
+ },
5420
+ banana: {
5421
+ elo: 1246,
5422
+ rank: 13,
5423
+ uncertainty: 3,
5424
+ evaluatedModel: "Nano Banana Pro (2K)",
5425
+ votes: 188288
5426
+ },
5427
+ gpt: {
5428
+ elo: 1236,
5429
+ rank: 14,
5430
+ uncertainty: 3,
5431
+ evaluatedModel: "GPT-Image-1.5 (High Fidelity)",
5432
+ votes: 174012
5433
+ },
5434
+ ideogram4: {
5435
+ elo: 1200,
5436
+ rank: 15,
5437
+ uncertainty: 4,
5438
+ evaluatedModel: "Ideogram 4.0 (Quality)",
5439
+ votes: 72865
5440
+ },
5441
+ "flux2-flex": {
5442
+ elo: 1156,
5443
+ rank: 17,
5444
+ uncertainty: 3,
5445
+ evaluatedModel: "FLUX.2 [flex]",
5446
+ votes: 156876
5447
+ }
5448
+ },
5449
+ "image-edit-single-image": {
5450
+ sunburst: {
5451
+ elo: 1517,
5452
+ rank: 1,
5453
+ uncertainty: 5,
5454
+ evaluatedModel: "GPT-Image-2.5 Sunburst",
5455
+ preliminary: true
5456
+ },
5457
+ flare: {
5458
+ elo: 1482,
5459
+ rank: 2,
5460
+ uncertainty: 5,
5461
+ evaluatedModel: "GPT-Image-2.5 Flare",
5462
+ preliminary: true
5463
+ },
5464
+ gpt2: {
5465
+ elo: 1460,
5466
+ rank: 4,
5467
+ uncertainty: 3,
5468
+ evaluatedModel: "GPT-Image-2 (Medium)"
5469
+ },
5470
+ "grok-image-2": {
5471
+ elo: 1439,
5472
+ rank: 5,
5473
+ uncertainty: 6,
5474
+ evaluatedModel: "Grok Imagine Image 2.0",
5475
+ preliminary: true
5476
+ },
5477
+ banana21: {
5478
+ elo: 1427,
5479
+ rank: 7,
5480
+ uncertainty: 6,
5481
+ evaluatedModel: "Nano Banana 2.1",
5482
+ preliminary: true
5483
+ },
5484
+ "mai-image-2.5": {
5485
+ elo: 1400,
5486
+ rank: 9,
5487
+ uncertainty: 4,
5488
+ evaluatedModel: "MAI-Image-2.5"
5489
+ },
5490
+ "muse-image": {
5491
+ elo: 1400,
5492
+ rank: 10,
5493
+ uncertainty: 4,
5494
+ evaluatedModel: "Muse Image (Meta)"
5495
+ },
5496
+ "grok-image-quality": {
5497
+ elo: 1391,
5498
+ rank: 11,
5499
+ uncertainty: 6,
5500
+ evaluatedModel: "Grok Imagine Image (Quality, 20260519)"
5501
+ },
5502
+ seedream5: {
5503
+ elo: 1390,
5504
+ rank: 12,
5505
+ uncertainty: 3,
5506
+ evaluatedModel: "Seedream 5.0 Pro"
5507
+ },
5508
+ banana: {
5509
+ elo: 1390,
5510
+ rank: 13,
5511
+ uncertainty: 3,
5512
+ evaluatedModel: "Nano Banana Pro (2K)"
5513
+ },
5514
+ gpt: {
5515
+ elo: 1368,
5516
+ rank: 14,
5517
+ uncertainty: 3,
5518
+ evaluatedModel: "GPT-Image-1.5 (High Fidelity)"
5519
+ }
5520
+ }
5521
+ };
5522
+ const SOURCES = {
5523
+ "text-to-image": "https://arena.ai/leaderboard/image/text-to-image",
5524
+ "image-edit-single-image": "https://arena.ai/leaderboard/image/image-edit/single-image"
5525
+ };
5526
+ /** A published score for this route, or null when no matching score was saved. */
5527
+ function imageModelBenchmark(model, task) {
5528
+ const score = BOARDS[task][model];
5529
+ if (score === void 0) return null;
5530
+ return {
5531
+ ...score,
5532
+ source: SOURCES[task],
5533
+ snapshotDate: "2026-10-29",
5534
+ task
5535
+ };
5536
+ }
5537
+ //#endregion
5168
5538
  //#region src/tasks.ts
5169
5539
  /**
5170
5540
  * The Task registry: every job Motif does, with the Models that can do it
@@ -5191,7 +5561,195 @@ const TIERS = [
5191
5561
  ];
5192
5562
  const DEFAULT_TIER = "balanced";
5193
5563
  const HAND_RANKED_AT = "2026-09-16";
5194
- const IMAGE_RANKED_AT = "2026-10-07";
5564
+ const IMAGE_RANKED_AT = "2026-10-29";
5565
+ function withImageBenchmarks(models, task) {
5566
+ return models.map((entry) => ({
5567
+ ...entry,
5568
+ benchmark: imageModelBenchmark(entry.model, task)
5569
+ }));
5570
+ }
5571
+ const GENERATION_MODELS_RANKED = withImageBenchmarks([
5572
+ {
5573
+ model: "sunburst",
5574
+ tier: "quality"
5575
+ },
5576
+ {
5577
+ model: "flare",
5578
+ tier: "quality"
5579
+ },
5580
+ {
5581
+ model: "gpt2",
5582
+ tier: "quality"
5583
+ },
5584
+ {
5585
+ model: "seedream5",
5586
+ tier: "quality"
5587
+ },
5588
+ {
5589
+ model: "gpt",
5590
+ tier: "quality"
5591
+ },
5592
+ {
5593
+ model: "mai-image-2.5-pro",
5594
+ tier: "quality"
5595
+ },
5596
+ {
5597
+ model: "banana21",
5598
+ tier: "balanced"
5599
+ },
5600
+ {
5601
+ model: "grok-image-2",
5602
+ tier: "balanced"
5603
+ },
5604
+ {
5605
+ model: "muse-image",
5606
+ tier: "balanced"
5607
+ },
5608
+ {
5609
+ model: "banana2",
5610
+ tier: "balanced"
5611
+ },
5612
+ {
5613
+ model: "qwen3",
5614
+ tier: "balanced"
5615
+ },
5616
+ {
5617
+ model: "mai-image-2.5",
5618
+ tier: "balanced"
5619
+ },
5620
+ {
5621
+ model: "banana",
5622
+ tier: "balanced"
5623
+ },
5624
+ {
5625
+ model: "ideogram4",
5626
+ tier: "balanced"
5627
+ },
5628
+ {
5629
+ model: "flux2-flex",
5630
+ tier: "balanced"
5631
+ },
5632
+ {
5633
+ model: "recraft41",
5634
+ tier: "balanced"
5635
+ },
5636
+ {
5637
+ model: "flux2-pro",
5638
+ tier: "balanced"
5639
+ },
5640
+ {
5641
+ model: "krea3",
5642
+ tier: "balanced"
5643
+ },
5644
+ {
5645
+ model: "grok-image-quality",
5646
+ tier: "balanced"
5647
+ },
5648
+ {
5649
+ model: "ideogram3-transparent",
5650
+ requires: ["transparency"],
5651
+ supports: ["transparency"],
5652
+ tier: "balanced"
5653
+ },
5654
+ {
5655
+ model: "banana2-lite",
5656
+ tier: "fast"
5657
+ },
5658
+ {
5659
+ model: "grok-image",
5660
+ tier: "fast"
5661
+ },
5662
+ {
5663
+ model: "seedream5-lite",
5664
+ tier: "fast"
5665
+ },
5666
+ {
5667
+ model: "flux2-turbo",
5668
+ tier: "fast"
5669
+ }
5670
+ ], "text-to-image");
5671
+ const EDITING_MODELS_RANKED = withImageBenchmarks([
5672
+ {
5673
+ model: "sunburst",
5674
+ tier: "quality"
5675
+ },
5676
+ {
5677
+ model: "flare",
5678
+ tier: "quality"
5679
+ },
5680
+ {
5681
+ model: "gpt2",
5682
+ tier: "quality"
5683
+ },
5684
+ {
5685
+ model: "seedream5",
5686
+ tier: "quality"
5687
+ },
5688
+ {
5689
+ model: "gpt",
5690
+ tier: "quality"
5691
+ },
5692
+ {
5693
+ model: "mai-image-2.5-pro",
5694
+ tier: "quality"
5695
+ },
5696
+ {
5697
+ model: "grok-image-2",
5698
+ tier: "balanced"
5699
+ },
5700
+ {
5701
+ model: "banana21",
5702
+ tier: "balanced"
5703
+ },
5704
+ {
5705
+ model: "mai-image-2.5",
5706
+ tier: "balanced"
5707
+ },
5708
+ {
5709
+ model: "muse-image",
5710
+ tier: "balanced"
5711
+ },
5712
+ {
5713
+ model: "grok-image-quality",
5714
+ tier: "balanced"
5715
+ },
5716
+ {
5717
+ model: "banana",
5718
+ tier: "balanced"
5719
+ },
5720
+ {
5721
+ model: "banana2",
5722
+ tier: "balanced"
5723
+ },
5724
+ {
5725
+ model: "krea3",
5726
+ tier: "balanced"
5727
+ },
5728
+ {
5729
+ model: "qwen3",
5730
+ tier: "balanced"
5731
+ },
5732
+ {
5733
+ model: "flux2-pro",
5734
+ tier: "balanced"
5735
+ },
5736
+ {
5737
+ model: "flux2-flex",
5738
+ tier: "balanced"
5739
+ },
5740
+ {
5741
+ model: "grok-image",
5742
+ tier: "fast"
5743
+ },
5744
+ {
5745
+ model: "banana2-lite",
5746
+ tier: "fast"
5747
+ },
5748
+ {
5749
+ model: "seedream5-lite",
5750
+ tier: "fast"
5751
+ }
5752
+ ], "image-edit-single-image");
5195
5753
  /** Every Task with a Model. A Task is absent here until it has at least one Model. */
5196
5754
  const TASKS = {
5197
5755
  animate: {
@@ -5362,95 +5920,8 @@ const TASKS = {
5362
5920
  summary: "Remove an object, person, text or clutter from an image and fill the gap."
5363
5921
  },
5364
5922
  generate: {
5365
- basis: "Curated from Artificial Analysis v2, captured 2026-10-07, and Arena 2026-10-06. Sunburst and Flare lead quality at the benchmarked max setting. Nano Banana 2.1 leads balanced on preliminary Arena evidence and lower representative cost; Grok Image 2 and MAI 2.5 are established alternatives. Fast keeps Nano Banana 2 Lite, budget Grok and Seedream 5 Lite, and FLUX.2 Turbo. Design, transparency and tuned Look routes are capability exceptions. Generation with References uses the separate editing ranking. Older routes are explicit/pin/Look-only.",
5366
- models: [
5367
- {
5368
- model: "sunburst",
5369
- tier: "quality"
5370
- },
5371
- {
5372
- model: "flare",
5373
- tier: "quality"
5374
- },
5375
- {
5376
- model: "gpt2",
5377
- tier: "quality"
5378
- },
5379
- {
5380
- model: "gpt",
5381
- tier: "quality"
5382
- },
5383
- {
5384
- model: "mai-image-2.5-pro",
5385
- tier: "quality"
5386
- },
5387
- {
5388
- model: "seedream5",
5389
- tier: "quality"
5390
- },
5391
- {
5392
- model: "banana21",
5393
- tier: "balanced"
5394
- },
5395
- {
5396
- model: "grok-image-2",
5397
- tier: "balanced"
5398
- },
5399
- {
5400
- model: "banana2",
5401
- tier: "balanced"
5402
- },
5403
- {
5404
- model: "mai-image-2.5",
5405
- tier: "balanced"
5406
- },
5407
- {
5408
- model: "banana",
5409
- tier: "balanced"
5410
- },
5411
- {
5412
- model: "qwen3",
5413
- tier: "balanced"
5414
- },
5415
- {
5416
- model: "flux2-flex",
5417
- tier: "balanced"
5418
- },
5419
- {
5420
- model: "flux2-pro",
5421
- tier: "balanced"
5422
- },
5423
- {
5424
- model: "ideogram4",
5425
- tier: "balanced"
5426
- },
5427
- {
5428
- model: "recraft41",
5429
- tier: "balanced"
5430
- },
5431
- {
5432
- model: "ideogram3-transparent",
5433
- requires: ["transparency"],
5434
- supports: ["transparency"],
5435
- tier: "balanced"
5436
- },
5437
- {
5438
- model: "banana2-lite",
5439
- tier: "fast"
5440
- },
5441
- {
5442
- model: "grok-image",
5443
- tier: "fast"
5444
- },
5445
- {
5446
- model: "seedream5-lite",
5447
- tier: "fast"
5448
- },
5449
- {
5450
- model: "flux2-turbo",
5451
- tier: "fast"
5452
- }
5453
- ],
5923
+ basis: "Order follows Arena text-to-image (https://arena.ai/leaderboard/image/text-to-image), read 2026-10-29 (MOT-72). Sunburst and Flare lead quality at the benchmarked max setting. Balanced starts with Nano Banana 2.1 on preliminary Arena evidence, then Grok Image 2 and Muse Image. Models not yet on the board follow the ranked ones in their Tier, in their previous order. The Artificial Analysis v2 snapshot of 2026-10-07 stays as recorded benchmark data. Fast keeps Nano Banana 2 Lite, budget Grok and Seedream 5 Lite, and FLUX.2 Turbo. Design, transparency and tuned Look routes are capability exceptions. Generation with References uses the separate editing ranking. Older routes are explicit/pin/Look-only.",
5924
+ models: GENERATION_MODELS_RANKED,
5454
5925
  notFor: "Variations of an image you already have (vary), or a consistent set of images (series run).",
5455
5926
  rankedAt: IMAGE_RANKED_AT,
5456
5927
  rankedFrom: "hand",
@@ -5937,73 +6408,8 @@ const TASKS = {
5937
6408
  summary: "Trace a raster image to a clean SVG."
5938
6409
  },
5939
6410
  vary: {
5940
- basis: "Curated from Artificial Analysis v2 editing, captured 2026-10-07, and preliminary Arena 2026-10-06 evidence for Nano Banana 2.1. Editing order differs from generation: MAI 2.5 outranks Grok Image 2, and Seedream 5 Pro outranks GPT Image 1.5. Sunburst and Flare use max quality. vary reuses a previously chosen Model when explicitly supplied, including retained legacy routes.",
5941
- models: [
5942
- {
5943
- model: "sunburst",
5944
- tier: "quality"
5945
- },
5946
- {
5947
- model: "flare",
5948
- tier: "quality"
5949
- },
5950
- {
5951
- model: "gpt2",
5952
- tier: "quality"
5953
- },
5954
- {
5955
- model: "mai-image-2.5-pro",
5956
- tier: "quality"
5957
- },
5958
- {
5959
- model: "seedream5",
5960
- tier: "quality"
5961
- },
5962
- {
5963
- model: "gpt",
5964
- tier: "quality"
5965
- },
5966
- {
5967
- model: "banana21",
5968
- tier: "balanced"
5969
- },
5970
- {
5971
- model: "mai-image-2.5",
5972
- tier: "balanced"
5973
- },
5974
- {
5975
- model: "grok-image-2",
5976
- tier: "balanced"
5977
- },
5978
- {
5979
- model: "banana2",
5980
- tier: "balanced"
5981
- },
5982
- {
5983
- model: "banana",
5984
- tier: "balanced"
5985
- },
5986
- {
5987
- model: "flux2-pro",
5988
- tier: "balanced"
5989
- },
5990
- {
5991
- model: "flux2-flex",
5992
- tier: "balanced"
5993
- },
5994
- {
5995
- model: "grok-image",
5996
- tier: "fast"
5997
- },
5998
- {
5999
- model: "seedream5-lite",
6000
- tier: "fast"
6001
- },
6002
- {
6003
- model: "banana2-lite",
6004
- tier: "fast"
6005
- }
6006
- ],
6411
+ basis: "Order follows Arena image edit, single image (https://arena.ai/leaderboard/image/image-edit/single-image), read 2026-10-29 (MOT-72). Editing order differs from generation: Grok Image 2 leads balanced, ahead of Nano Banana 2.1, and Seedream 5 Pro outranks GPT Image 1.5. Arena scores Krea 3 only at its Ultra setting, which costs $0.27 to $0.35 an edit and ignores the requested size, so krea3, which runs fal's default setting, is ranked as unscored. Models not yet on the board follow the ranked ones in their Tier, in their previous order. The Artificial Analysis v2 snapshot of 2026-10-07 stays as recorded benchmark data. Sunburst and Flare use max quality. vary reuses a previously chosen Model when explicitly supplied, including retained legacy routes.",
6412
+ models: EDITING_MODELS_RANKED,
6007
6413
  notFor: "A specific change to an image described in words (generate with a reference), or a set of different scenes in one style (series run).",
6008
6414
  rankedAt: IMAGE_RANKED_AT,
6009
6415
  rankedFrom: "hand",
@@ -8202,6 +8608,7 @@ function generationProfile(config) {
8202
8608
  else if (config.transparencyRoute !== void 0) keyed.set("transparency", config.transparencyRoute.apiKeyEnv);
8203
8609
  if (config.supportsEdit) capabilities.add("references");
8204
8610
  if (config.supportsMaskImage === true) capabilities.add("mask");
8611
+ if (config.supportsMarkupImage === true) capabilities.add("markup");
8205
8612
  if ((config.sizeMode ?? "aspect_ratio") !== "none") capabilities.add("aspect");
8206
8613
  if (config.supportsResolution) capabilities.add("resolution");
8207
8614
  if (config.supportsNumImages) capabilities.add("count");
@@ -8214,6 +8621,10 @@ function generationProfile(config) {
8214
8621
  keyedCapabilities: keyed,
8215
8622
  keys: [FAL_KEY],
8216
8623
  maxReferences: config.supportsEdit ? config.maxReferenceImages ?? 1 : 0,
8624
+ outputFormats: {
8625
+ edit: outputFormatsOf(config, true),
8626
+ text: outputFormatsOf(config, false)
8627
+ },
8217
8628
  resolutions: config.supportedResolutions
8218
8629
  };
8219
8630
  }
@@ -8264,12 +8675,13 @@ function requirements(request) {
8264
8675
  if (request.transparent === true) needed.push("transparency");
8265
8676
  if ((request.references ?? 0) > 0) needed.push("references");
8266
8677
  if (request.mask === true) needed.push("mask");
8678
+ if (request.markup === true) needed.push("markup");
8267
8679
  if (request.aspect !== void 0) needed.push("aspect");
8268
8680
  if (request.resolution !== void 0) needed.push("resolution");
8269
8681
  if ((request.count ?? 1) > 1) needed.push("count");
8270
8682
  if (request.seed === true) needed.push("seed");
8271
8683
  if (request.negativePrompt === true) needed.push("negativePrompt");
8272
- if (request.outputFormat === true) needed.push("outputFormat");
8684
+ if (request.outputFormat !== void 0) needed.push("outputFormat");
8273
8685
  if (request.rig === true) needed.push("rig");
8274
8686
  return needed;
8275
8687
  }
@@ -8278,6 +8690,7 @@ function withinLimits(profile, capability, request) {
8278
8690
  if (capability === "references") return (request.references ?? 0) <= profile.maxReferences;
8279
8691
  if (capability === "aspect") return accepts(profile.aspects, request.aspect);
8280
8692
  if (capability === "resolution") return accepts(profile.resolutions, request.resolution);
8693
+ if (capability === "outputFormat") return accepts((request.references ?? 0) > 0 || request.source !== void 0 ? profile.outputFormats?.edit : profile.outputFormats?.text, request.outputFormat);
8281
8694
  return true;
8282
8695
  }
8283
8696
  function accepts(allowed, value) {
@@ -8332,7 +8745,11 @@ const TIER_ORDER = {
8332
8745
  "fast"
8333
8746
  ]
8334
8747
  };
8335
- function describeBlock(blockedBy, missingKey, missingInput) {
8748
+ function describeBlock(blockedBy, request, missingKey, missingInput) {
8749
+ if (blockedBy === "outputFormat" && request.outputFormat !== void 0) {
8750
+ const editing = (request.references ?? 0) > 0 || request.source !== void 0;
8751
+ return `cannot return ${request.outputFormat}${editing ? " from an edit" : ""}`;
8752
+ }
8336
8753
  if (missingInput === true) return `needs a ${blockedBy} input`;
8337
8754
  if (blockedBy === "key") return `needs ${missingKey ?? "a provider key"}`;
8338
8755
  if (blockedBy === "unknown-model") return "is not a Model for this Task";
@@ -8401,7 +8818,7 @@ function resolveFixed(task, fixed, request, needed, keys) {
8401
8818
  if (result.ok) return resolved(task, model, tier, chosenBy);
8402
8819
  const unblockers = chosenBy === "model" ? [] : ["model"];
8403
8820
  unblockers.push(unblockerFor(result));
8404
- return unresolved(task, result.blockedBy, unblockers, `${model} (${chosenBy}) ${describeBlock(result.blockedBy, result.missingKey, result.missingInput)}.`, result.missingKey);
8821
+ return unresolved(task, result.blockedBy, unblockers, `${model} (${chosenBy}) ${describeBlock(result.blockedBy, request, result.missingKey, result.missingInput)}.`, result.missingKey);
8405
8822
  }
8406
8823
  /** Old pins and tuned Looks remain usable without recommending predecessors. */
8407
8824
  function legacyEntry(task, model, mode, tier) {
@@ -8576,6 +8993,8 @@ const FAL_IMAGE_PRICE_USD = {
8576
8993
  "ideogram/v4": MODELS.ideogram4?.pricePerImageUsd ?? .03,
8577
8994
  "xai/grok-imagine-image": MODELS["grok-image"]?.pricePerImageUsd ?? .02,
8578
8995
  "xai/grok-imagine-image/v2.0/text-to-image": MODELS["grok-image-2"]?.pricePerImageUsd ?? .06,
8996
+ "xai/grok-imagine-image/quality/text-to-image": MODELS["grok-image-quality"]?.pricePerImageUsd ?? .05,
8997
+ "meta/muse-image/text-to-image": MODELS["muse-image"]?.pricePerImageUsd ?? .01,
8579
8998
  "microsoft/mai-image-2.5-pro": MODELS["mai-image-2.5-pro"]?.pricePerImageUsd ?? .17,
8580
8999
  "fal-ai/ideogram/v3/generate-transparent": MODELS["ideogram3-transparent"]?.pricePerImageUsd ?? .06,
8581
9000
  "alibaba/qwen-image-3/text-to-image": MODELS.qwen3?.pricePerImageUsd ?? .04,
@@ -8633,10 +9052,10 @@ const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
8633
9052
  * Sanity anchor: the Phase 0 benchmark measured gpt-image direct at $0.042
8634
9053
  * (vs $0.133 via fal — full data and methodology on Linear MOT-23).
8635
9054
  *
8636
- * GPT Image 2.5 is token-priced, with no published per-image estimate. Leave
9055
+ * GPT Image 2.6 is token-priced, with no published per-image estimate. Leave
8637
9056
  * these models absent so cost remains unknown unless supplied by the provider.
8638
- * https://developers.openai.com/api/docs/models/gpt-image-2.5-flare
8639
- * https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst
9057
+ * https://developers.openai.com/api/docs/models/gpt-image-2.6-flare
9058
+ * https://developers.openai.com/api/docs/models/gpt-image-2.6-sunburst
8640
9059
  */
8641
9060
  const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
8642
9061
  /**
@@ -9888,11 +10307,12 @@ function taskRequest(task, input) {
9888
10307
  aspect: input.aspect,
9889
10308
  count: input.count,
9890
10309
  look: task === "generate" || task === "vary" ? input.look : void 0,
10310
+ markup: input.markup !== void 0,
9891
10311
  mask: input.mask !== void 0,
9892
10312
  mode: input.mode,
9893
10313
  model: input.model,
9894
10314
  negativePrompt: input.negativePrompt !== void 0,
9895
- outputFormat: input.outputFormat !== void 0,
10315
+ outputFormat: input.outputFormat,
9896
10316
  references,
9897
10317
  resolution: (task === "generate" || task === "vary") && input.resolution === "2K" ? void 0 : input.resolution,
9898
10318
  rig: input.rig,
@@ -10003,6 +10423,7 @@ const GENERATION_FIELDS = /* @__PURE__ */ new Set([
10003
10423
  "aspect",
10004
10424
  "count",
10005
10425
  "look",
10426
+ "markup",
10006
10427
  "mask",
10007
10428
  "mood",
10008
10429
  "negativePrompt",
@@ -10013,11 +10434,23 @@ const GENERATION_FIELDS = /* @__PURE__ */ new Set([
10013
10434
  "seed",
10014
10435
  "transparent"
10015
10436
  ]);
10437
+ const IMAGE_QUALITIES = [
10438
+ "auto",
10439
+ "low",
10440
+ "medium",
10441
+ "high",
10442
+ "xhigh",
10443
+ "max"
10444
+ ];
10445
+ function isImageQuality(value) {
10446
+ return IMAGE_QUALITIES.some((level) => level === value);
10447
+ }
10016
10448
  /** The TaskInput field behind each option `buildGenerateBody` can refuse. */
10017
10449
  const FIELD_FOR_OPTION = {
10018
10450
  aspect: "aspect",
10019
10451
  background: "transparent",
10020
10452
  "image editing": "references",
10453
+ markupImageUrl: "markup",
10021
10454
  maskImageUrl: "mask",
10022
10455
  negativePrompt: "negativePrompt",
10023
10456
  numImages: "count",
@@ -10057,23 +10490,35 @@ function generationPlan(task, model, config, input) {
10057
10490
  task
10058
10491
  }));
10059
10492
  const editImageUrls = [...task === "vary" && input.image !== void 0 ? [input.image] : [], ...input.references ?? []];
10493
+ if (input.markup !== void 0 && editImageUrls.length === 0) return err$1(invalidOption("Markup applies to the image being edited; pass references.", {
10494
+ field: "markup",
10495
+ requires: "references"
10496
+ }));
10060
10497
  if (input.mask !== void 0 && editImageUrls.length === 0) return err$1(invalidOption("A mask applies to a reference image; pass references.", {
10061
10498
  field: "mask",
10062
10499
  requires: "references"
10063
10500
  }));
10064
10501
  if (input.transparent === true && config.supportsBackground !== true && config.transparencyRoute !== void 0) return openAiPlan(task, model, config.transparencyRoute, input, editImageUrls);
10065
- if (input.outputFormat !== void 0 && config.supportedOutputFormats !== void 0 && !config.supportedOutputFormats.includes(input.outputFormat)) return err$1(cannotCarry("outputFormat", model, task));
10502
+ if (input.outputFormat !== void 0 && !outputFormatsOf(config, editImageUrls.length > 0).includes(input.outputFormat)) return err$1(cannotCarry("outputFormat", model, task));
10503
+ const { quality, ...params } = input.params ?? {};
10504
+ const liftQuality = config.supportsQuality === true && quality !== void 0;
10505
+ if (liftQuality && !isImageQuality(quality)) return err$1(invalidOption(`params.quality must be one of ${IMAGE_QUALITIES.join(", ")}.`, {
10506
+ field: "params",
10507
+ key: "quality"
10508
+ }));
10066
10509
  const options = {
10067
10510
  aspect: input.aspect,
10068
10511
  background: input.transparent === true ? "transparent" : void 0,
10069
10512
  creative: creativeOf(input),
10070
10513
  editImageUrls: editImageUrls.length > 0 ? editImageUrls : void 0,
10514
+ markupImageUrl: input.markup,
10071
10515
  maskImageUrl: input.mask,
10072
10516
  model,
10073
10517
  negativePrompt: input.negativePrompt,
10074
10518
  numImages: input.count,
10075
10519
  outputFormat: input.outputFormat,
10076
10520
  prompt: task === "vary" && (input.prompt ?? "") === "" ? VARY_PROMPT : input.prompt ?? "",
10521
+ quality: liftQuality && isImageQuality(quality) ? quality : void 0,
10077
10522
  resolution: input.resolution,
10078
10523
  seed: input.seed
10079
10524
  };
@@ -10083,7 +10528,7 @@ function generationPlan(task, model, config, input) {
10083
10528
  } catch (error) {
10084
10529
  return err$1(generationError(error, model, task));
10085
10530
  }
10086
- const merged = mergeParams(built.body, input.params, ownedGenerationKeys(input));
10531
+ const merged = mergeParams(built.body, liftQuality ? params : input.params, ownedGenerationKeys(input));
10087
10532
  if (merged.isErr()) return err$1(merged.error);
10088
10533
  const badSize = checkParamsImageSize(config, model, task, input.params);
10089
10534
  if (badSize !== void 0) return err$1(badSize);
@@ -10701,7 +11146,7 @@ function createMotif(config = {}) {
10701
11146
  headers
10702
11147
  };
10703
11148
  const executor = falRequestExecutor(client.value);
10704
- return (chosen.queued ? await runRequestQueued(executor, prepared, input.onProgress, input.onSubmitted) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
11149
+ return (chosen.queued || input.onSubmitted !== void 0 ? await runRequestQueued(executor, prepared, input.onProgress, input.onSubmitted) : await runRequest(executor, prepared)).map((value) => falOutput(chosen, value));
10705
11150
  }
10706
11151
  async function resume(task, input, requestId) {
10707
11152
  const planned = plan(task, input);