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