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