@howells/motif-sdk 5.3.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",
@@ -729,7 +730,7 @@ const MODELS = {
729
730
  rank: 2
730
731
  }
731
732
  },
732
- 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"
733
734
  },
734
735
  defaultQuality: "max",
735
736
  customImageSize: {
@@ -739,11 +740,11 @@ const MODELS = {
739
740
  minPixels: 655360,
740
741
  multipleOf: 16
741
742
  },
742
- editEndpoint: "openai/gpt-image-2.5/flare/edit",
743
- 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",
744
745
  maxReferenceImages: 16,
745
- name: "GPT Image 2.5 Flare",
746
- 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",
747
748
  pricePerImageUsd: null,
748
749
  sizeMode: "image_size_enum",
749
750
  supportsAspect: true,
@@ -751,8 +752,23 @@ const MODELS = {
751
752
  supportsEdit: true,
752
753
  supportsMaskImage: true,
753
754
  maskImageField: "mask_url",
755
+ supportsMarkupImage: true,
756
+ autoEditSize: true,
754
757
  supportsNumImages: true,
755
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
+ ],
756
772
  supportsQuality: true,
757
773
  supportedQualities: [
758
774
  "auto",
@@ -781,7 +797,7 @@ const MODELS = {
781
797
  rank: 1
782
798
  }
783
799
  },
784
- 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"
785
801
  },
786
802
  defaultQuality: "max",
787
803
  customImageSize: {
@@ -791,11 +807,11 @@ const MODELS = {
791
807
  minPixels: 655360,
792
808
  multipleOf: 16
793
809
  },
794
- editEndpoint: "openai/gpt-image-2.5/sunburst/edit",
795
- 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",
796
812
  maxReferenceImages: 16,
797
- name: "GPT Image 2.5 Sunburst",
798
- 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",
799
815
  pricePerImageUsd: null,
800
816
  sizeMode: "image_size_enum",
801
817
  supportsAspect: true,
@@ -803,8 +819,23 @@ const MODELS = {
803
819
  supportsEdit: true,
804
820
  supportsMaskImage: true,
805
821
  maskImageField: "mask_url",
822
+ supportsMarkupImage: true,
823
+ autoEditSize: true,
806
824
  supportsNumImages: true,
807
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
+ ],
808
839
  supportsQuality: true,
809
840
  supportedQualities: [
810
841
  "auto",
@@ -2673,6 +2704,18 @@ function requestIdFromBody(text) {
2673
2704
  * error can name both what a model does support and which models support the
2674
2705
  * option that was refused.
2675
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
+ }
2676
2719
  /** Generation options a model can refuse, with the registry test for each. */
2677
2720
  const OPTION_CAPABILITIES = {
2678
2721
  aspect: (config) => (config.sizeMode ?? "aspect_ratio") !== "none",
@@ -2688,6 +2731,7 @@ const OPTION_CAPABILITIES = {
2688
2731
  imageSize: (config) => config.sizeMode === "gpt_size" || config.sizeMode === "image_size_enum",
2689
2732
  inputFidelity: (config) => config.sizeMode === "gpt_size",
2690
2733
  limitGenerations: (config) => config.supportsLimitGenerations === true,
2734
+ markupImageUrl: (config) => config.supportsMarkupImage === true,
2691
2735
  maskImageUrl: (config) => config.supportsMaskImage === true,
2692
2736
  negativePrompt: (config) => config.supportsNegativePrompt === true,
2693
2737
  numImages: (config) => config.supportsNumImages,
@@ -2821,6 +2865,9 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
2821
2865
  if (options.imagePromptStrength !== void 0 && !hasEditImages) throw new Error("imagePromptStrength requires editImageUrls");
2822
2866
  if (options.maskImageUrl !== void 0 && config.supportsMaskImage !== true) unsupported(config, "maskImageUrl");
2823
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.");
2824
2871
  if (options.seed !== void 0 && config.supportsSeed !== true) unsupported(config, "seed");
2825
2872
  if (options.outputFormat !== void 0 && config.supportsOutputFormat !== true) unsupported(config, "outputFormat");
2826
2873
  if (options.quality !== void 0 && config.supportsQuality !== true) unsupported(config, "quality");
@@ -2860,7 +2907,7 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
2860
2907
  * - `none`: no size control (video models)
2861
2908
  */
2862
2909
  function buildGenerateBody(options) {
2863
- 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;
2864
2911
  const config = MODELS[model];
2865
2912
  if (!config) throw new Error(`Unknown model: ${model}`);
2866
2913
  let { endpoint } = config;
@@ -2879,7 +2926,7 @@ function buildGenerateBody(options) {
2879
2926
  body.image_size = normalizedImageSize ?? (hasEditImages && !explicitAspect ? "auto" : aspectToGptSize(aspect));
2880
2927
  break;
2881
2928
  case "image_size_enum":
2882
- body.image_size = normalizedImageSize ?? falImageSizeFor(config, aspect, resolution);
2929
+ body.image_size = normalizedImageSize ?? (config.autoEditSize === true && hasEditImages && !explicitAspect ? "auto" : falImageSizeFor(config, aspect, resolution));
2883
2930
  break;
2884
2931
  case "aspect_ratio":
2885
2932
  if (config.supportsAspect) body.aspect_ratio = aspect;
@@ -2892,7 +2939,8 @@ function buildGenerateBody(options) {
2892
2939
  if (syncMode !== void 0 && config.supportsSyncMode === true) body.sync_mode = syncMode;
2893
2940
  if (seed !== void 0 && config.supportsSeed === true) body.seed = seed;
2894
2941
  if (outputFormat && config.supportsOutputFormat === true && transparent !== true) {
2895
- 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(", ")}`);
2896
2944
  body.output_format = outputFormat;
2897
2945
  }
2898
2946
  if (config.supportsRaw === true && raw !== void 0) body.raw = raw;
@@ -2951,6 +2999,7 @@ function buildGenerateBody(options) {
2951
2999
  if (inputFidelity) body.input_fidelity = inputFidelity;
2952
3000
  }
2953
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;
2954
3003
  return {
2955
3004
  body,
2956
3005
  endpoint
@@ -4539,10 +4588,15 @@ async function getToolResult(exec, job) {
4539
4588
  }
4540
4589
  /** Poll a submitted job to completion and fetch its result. Submits nothing. */
4541
4590
  async function awaitQueuedRequest(exec, job, onProgress) {
4591
+ let logsSeen = 0;
4542
4592
  for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
4543
4593
  const status = await checkToolStatus(exec, job);
4544
4594
  if (status.isErr()) return (0, neverthrow.err)(status.error);
4545
- 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);
4546
4600
  if (status.value.status === "completed") return (await getToolResult(exec, job)).map((data) => ({
4547
4601
  data,
4548
4602
  requestId: job.requestId
@@ -5287,6 +5341,201 @@ function sumCosts(costs) {
5287
5341
  };
5288
5342
  }
5289
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
5290
5539
  //#region src/tasks.ts
5291
5540
  /**
5292
5541
  * The Task registry: every job Motif does, with the Models that can do it
@@ -5314,6 +5563,194 @@ const TIERS = [
5314
5563
  const DEFAULT_TIER = "balanced";
5315
5564
  const HAND_RANKED_AT = "2026-09-16";
5316
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");
5317
5754
  /** Every Task with a Model. A Task is absent here until it has at least one Model. */
5318
5755
  const TASKS = {
5319
5756
  animate: {
@@ -5485,106 +5922,7 @@ const TASKS = {
5485
5922
  },
5486
5923
  generate: {
5487
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.",
5488
- models: [
5489
- {
5490
- model: "sunburst",
5491
- tier: "quality"
5492
- },
5493
- {
5494
- model: "flare",
5495
- tier: "quality"
5496
- },
5497
- {
5498
- model: "gpt2",
5499
- tier: "quality"
5500
- },
5501
- {
5502
- model: "seedream5",
5503
- tier: "quality"
5504
- },
5505
- {
5506
- model: "gpt",
5507
- tier: "quality"
5508
- },
5509
- {
5510
- model: "mai-image-2.5-pro",
5511
- tier: "quality"
5512
- },
5513
- {
5514
- model: "banana21",
5515
- tier: "balanced"
5516
- },
5517
- {
5518
- model: "grok-image-2",
5519
- tier: "balanced"
5520
- },
5521
- {
5522
- model: "muse-image",
5523
- tier: "balanced"
5524
- },
5525
- {
5526
- model: "banana2",
5527
- tier: "balanced"
5528
- },
5529
- {
5530
- model: "qwen3",
5531
- tier: "balanced"
5532
- },
5533
- {
5534
- model: "mai-image-2.5",
5535
- tier: "balanced"
5536
- },
5537
- {
5538
- model: "banana",
5539
- tier: "balanced"
5540
- },
5541
- {
5542
- model: "ideogram4",
5543
- tier: "balanced"
5544
- },
5545
- {
5546
- model: "flux2-flex",
5547
- tier: "balanced"
5548
- },
5549
- {
5550
- model: "recraft41",
5551
- tier: "balanced"
5552
- },
5553
- {
5554
- model: "flux2-pro",
5555
- tier: "balanced"
5556
- },
5557
- {
5558
- model: "krea3",
5559
- tier: "balanced"
5560
- },
5561
- {
5562
- model: "grok-image-quality",
5563
- tier: "balanced"
5564
- },
5565
- {
5566
- model: "ideogram3-transparent",
5567
- requires: ["transparency"],
5568
- supports: ["transparency"],
5569
- tier: "balanced"
5570
- },
5571
- {
5572
- model: "banana2-lite",
5573
- tier: "fast"
5574
- },
5575
- {
5576
- model: "grok-image",
5577
- tier: "fast"
5578
- },
5579
- {
5580
- model: "seedream5-lite",
5581
- tier: "fast"
5582
- },
5583
- {
5584
- model: "flux2-turbo",
5585
- tier: "fast"
5586
- }
5587
- ],
5925
+ models: GENERATION_MODELS_RANKED,
5588
5926
  notFor: "Variations of an image you already have (vary), or a consistent set of images (series run).",
5589
5927
  rankedAt: IMAGE_RANKED_AT,
5590
5928
  rankedFrom: "hand",
@@ -6072,88 +6410,7 @@ const TASKS = {
6072
6410
  },
6073
6411
  vary: {
6074
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.",
6075
- models: [
6076
- {
6077
- model: "sunburst",
6078
- tier: "quality"
6079
- },
6080
- {
6081
- model: "flare",
6082
- tier: "quality"
6083
- },
6084
- {
6085
- model: "gpt2",
6086
- tier: "quality"
6087
- },
6088
- {
6089
- model: "seedream5",
6090
- tier: "quality"
6091
- },
6092
- {
6093
- model: "gpt",
6094
- tier: "quality"
6095
- },
6096
- {
6097
- model: "mai-image-2.5-pro",
6098
- tier: "quality"
6099
- },
6100
- {
6101
- model: "grok-image-2",
6102
- tier: "balanced"
6103
- },
6104
- {
6105
- model: "banana21",
6106
- tier: "balanced"
6107
- },
6108
- {
6109
- model: "mai-image-2.5",
6110
- tier: "balanced"
6111
- },
6112
- {
6113
- model: "muse-image",
6114
- tier: "balanced"
6115
- },
6116
- {
6117
- model: "grok-image-quality",
6118
- tier: "balanced"
6119
- },
6120
- {
6121
- model: "banana",
6122
- tier: "balanced"
6123
- },
6124
- {
6125
- model: "banana2",
6126
- tier: "balanced"
6127
- },
6128
- {
6129
- model: "krea3",
6130
- tier: "balanced"
6131
- },
6132
- {
6133
- model: "qwen3",
6134
- tier: "balanced"
6135
- },
6136
- {
6137
- model: "flux2-pro",
6138
- tier: "balanced"
6139
- },
6140
- {
6141
- model: "flux2-flex",
6142
- tier: "balanced"
6143
- },
6144
- {
6145
- model: "grok-image",
6146
- tier: "fast"
6147
- },
6148
- {
6149
- model: "banana2-lite",
6150
- tier: "fast"
6151
- },
6152
- {
6153
- model: "seedream5-lite",
6154
- tier: "fast"
6155
- }
6156
- ],
6413
+ models: EDITING_MODELS_RANKED,
6157
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).",
6158
6415
  rankedAt: IMAGE_RANKED_AT,
6159
6416
  rankedFrom: "hand",
@@ -8352,6 +8609,7 @@ function generationProfile(config) {
8352
8609
  else if (config.transparencyRoute !== void 0) keyed.set("transparency", config.transparencyRoute.apiKeyEnv);
8353
8610
  if (config.supportsEdit) capabilities.add("references");
8354
8611
  if (config.supportsMaskImage === true) capabilities.add("mask");
8612
+ if (config.supportsMarkupImage === true) capabilities.add("markup");
8355
8613
  if ((config.sizeMode ?? "aspect_ratio") !== "none") capabilities.add("aspect");
8356
8614
  if (config.supportsResolution) capabilities.add("resolution");
8357
8615
  if (config.supportsNumImages) capabilities.add("count");
@@ -8364,6 +8622,10 @@ function generationProfile(config) {
8364
8622
  keyedCapabilities: keyed,
8365
8623
  keys: [FAL_KEY],
8366
8624
  maxReferences: config.supportsEdit ? config.maxReferenceImages ?? 1 : 0,
8625
+ outputFormats: {
8626
+ edit: outputFormatsOf(config, true),
8627
+ text: outputFormatsOf(config, false)
8628
+ },
8367
8629
  resolutions: config.supportedResolutions
8368
8630
  };
8369
8631
  }
@@ -8414,12 +8676,13 @@ function requirements(request) {
8414
8676
  if (request.transparent === true) needed.push("transparency");
8415
8677
  if ((request.references ?? 0) > 0) needed.push("references");
8416
8678
  if (request.mask === true) needed.push("mask");
8679
+ if (request.markup === true) needed.push("markup");
8417
8680
  if (request.aspect !== void 0) needed.push("aspect");
8418
8681
  if (request.resolution !== void 0) needed.push("resolution");
8419
8682
  if ((request.count ?? 1) > 1) needed.push("count");
8420
8683
  if (request.seed === true) needed.push("seed");
8421
8684
  if (request.negativePrompt === true) needed.push("negativePrompt");
8422
- if (request.outputFormat === true) needed.push("outputFormat");
8685
+ if (request.outputFormat !== void 0) needed.push("outputFormat");
8423
8686
  if (request.rig === true) needed.push("rig");
8424
8687
  return needed;
8425
8688
  }
@@ -8428,6 +8691,7 @@ function withinLimits(profile, capability, request) {
8428
8691
  if (capability === "references") return (request.references ?? 0) <= profile.maxReferences;
8429
8692
  if (capability === "aspect") return accepts(profile.aspects, request.aspect);
8430
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);
8431
8695
  return true;
8432
8696
  }
8433
8697
  function accepts(allowed, value) {
@@ -8482,7 +8746,11 @@ const TIER_ORDER = {
8482
8746
  "fast"
8483
8747
  ]
8484
8748
  };
8485
- 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
+ }
8486
8754
  if (missingInput === true) return `needs a ${blockedBy} input`;
8487
8755
  if (blockedBy === "key") return `needs ${missingKey ?? "a provider key"}`;
8488
8756
  if (blockedBy === "unknown-model") return "is not a Model for this Task";
@@ -8551,7 +8819,7 @@ function resolveFixed(task, fixed, request, needed, keys) {
8551
8819
  if (result.ok) return resolved(task, model, tier, chosenBy);
8552
8820
  const unblockers = chosenBy === "model" ? [] : ["model"];
8553
8821
  unblockers.push(unblockerFor(result));
8554
- 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);
8555
8823
  }
8556
8824
  /** Old pins and tuned Looks remain usable without recommending predecessors. */
8557
8825
  function legacyEntry(task, model, mode, tier) {
@@ -8785,10 +9053,10 @@ const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
8785
9053
  * Sanity anchor: the Phase 0 benchmark measured gpt-image direct at $0.042
8786
9054
  * (vs $0.133 via fal — full data and methodology on Linear MOT-23).
8787
9055
  *
8788
- * 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
8789
9057
  * these models absent so cost remains unknown unless supplied by the provider.
8790
- * https://developers.openai.com/api/docs/models/gpt-image-2.5-flare
8791
- * 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
8792
9060
  */
8793
9061
  const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
8794
9062
  /**
@@ -10040,11 +10308,12 @@ function taskRequest(task, input) {
10040
10308
  aspect: input.aspect,
10041
10309
  count: input.count,
10042
10310
  look: task === "generate" || task === "vary" ? input.look : void 0,
10311
+ markup: input.markup !== void 0,
10043
10312
  mask: input.mask !== void 0,
10044
10313
  mode: input.mode,
10045
10314
  model: input.model,
10046
10315
  negativePrompt: input.negativePrompt !== void 0,
10047
- outputFormat: input.outputFormat !== void 0,
10316
+ outputFormat: input.outputFormat,
10048
10317
  references,
10049
10318
  resolution: (task === "generate" || task === "vary") && input.resolution === "2K" ? void 0 : input.resolution,
10050
10319
  rig: input.rig,
@@ -10155,6 +10424,7 @@ const GENERATION_FIELDS = /* @__PURE__ */ new Set([
10155
10424
  "aspect",
10156
10425
  "count",
10157
10426
  "look",
10427
+ "markup",
10158
10428
  "mask",
10159
10429
  "mood",
10160
10430
  "negativePrompt",
@@ -10165,11 +10435,23 @@ const GENERATION_FIELDS = /* @__PURE__ */ new Set([
10165
10435
  "seed",
10166
10436
  "transparent"
10167
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
+ }
10168
10449
  /** The TaskInput field behind each option `buildGenerateBody` can refuse. */
10169
10450
  const FIELD_FOR_OPTION = {
10170
10451
  aspect: "aspect",
10171
10452
  background: "transparent",
10172
10453
  "image editing": "references",
10454
+ markupImageUrl: "markup",
10173
10455
  maskImageUrl: "mask",
10174
10456
  negativePrompt: "negativePrompt",
10175
10457
  numImages: "count",
@@ -10209,23 +10491,35 @@ function generationPlan(task, model, config, input) {
10209
10491
  task
10210
10492
  }));
10211
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
+ }));
10212
10498
  if (input.mask !== void 0 && editImageUrls.length === 0) return (0, neverthrow.err)(invalidOption("A mask applies to a reference image; pass references.", {
10213
10499
  field: "mask",
10214
10500
  requires: "references"
10215
10501
  }));
10216
10502
  if (input.transparent === true && config.supportsBackground !== true && config.transparencyRoute !== void 0) return openAiPlan(task, model, config.transparencyRoute, input, editImageUrls);
10217
- 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
+ }));
10218
10510
  const options = {
10219
10511
  aspect: input.aspect,
10220
10512
  background: input.transparent === true ? "transparent" : void 0,
10221
10513
  creative: creativeOf(input),
10222
10514
  editImageUrls: editImageUrls.length > 0 ? editImageUrls : void 0,
10515
+ markupImageUrl: input.markup,
10223
10516
  maskImageUrl: input.mask,
10224
10517
  model,
10225
10518
  negativePrompt: input.negativePrompt,
10226
10519
  numImages: input.count,
10227
10520
  outputFormat: input.outputFormat,
10228
10521
  prompt: task === "vary" && (input.prompt ?? "") === "" ? VARY_PROMPT : input.prompt ?? "",
10522
+ quality: liftQuality && isImageQuality(quality) ? quality : void 0,
10229
10523
  resolution: input.resolution,
10230
10524
  seed: input.seed
10231
10525
  };
@@ -10235,7 +10529,7 @@ function generationPlan(task, model, config, input) {
10235
10529
  } catch (error) {
10236
10530
  return (0, neverthrow.err)(generationError(error, model, task));
10237
10531
  }
10238
- const merged = mergeParams(built.body, input.params, ownedGenerationKeys(input));
10532
+ const merged = mergeParams(built.body, liftQuality ? params : input.params, ownedGenerationKeys(input));
10239
10533
  if (merged.isErr()) return (0, neverthrow.err)(merged.error);
10240
10534
  const badSize = checkParamsImageSize(config, model, task, input.params);
10241
10535
  if (badSize !== void 0) return (0, neverthrow.err)(badSize);
@@ -10853,7 +11147,7 @@ function createMotif(config = {}) {
10853
11147
  headers
10854
11148
  };
10855
11149
  const executor = falRequestExecutor(client.value);
10856
- 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));
10857
11151
  }
10858
11152
  async function resume(task, input, requestId) {
10859
11153
  const planned = plan(task, input);