@howells/motif-sdk 1.1.0 → 1.2.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
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { err as err2, ok as ok2 } from "neverthrow";
2
+ import { err as err3, ok as ok3 } from "neverthrow";
3
3
 
4
4
  // src/aspects.ts
5
5
  var ASPECT_RATIOS = [
@@ -99,22 +99,23 @@ var FORMAT_PRESETS = {
99
99
  };
100
100
 
101
101
  // src/models.ts
102
- var AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-05-12";
102
+ var AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-08-23";
103
103
  var AA_IMAGE_SOURCES = [
104
104
  "https://artificialanalysis.ai/image/leaderboard/text-to-image",
105
105
  "https://artificialanalysis.ai/image/leaderboard/editing"
106
106
  ];
107
107
  var FAL_PRICING_CHECKED_AT = "2026-05-12";
108
108
  var FAL_PRICING_CHECKED_JUL_2026 = "2026-07-11";
109
+ var FAL_PRICING_CHECKED_AUG_2026 = "2026-08-23";
109
110
  var MODELS = {
110
111
  // ─── Generation Models ────────────────────────────────────────
111
112
  gpt2: {
112
113
  benchmark: {
113
114
  artificialAnalysis: {
114
- editing: { elo: 1249, pricePer1k: 211, rank: 3, winRate: 0.66 },
115
+ editing: { elo: 1256, pricePer1k: 211, rank: 3 },
115
116
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
116
117
  sourceUrls: AA_IMAGE_SOURCES,
117
- textToImage: { elo: 1337, pricePer1k: 211, rank: 1, winRate: 0.8 }
118
+ textToImage: { elo: 1369, pricePer1k: 211, rank: 1 }
118
119
  },
119
120
  speed: {
120
121
  medianSeconds: 200.7,
@@ -154,10 +155,10 @@ var MODELS = {
154
155
  gpt: {
155
156
  benchmark: {
156
157
  artificialAnalysis: {
157
- editing: { elo: 1262, pricePer1k: 133, rank: 2, winRate: 0.69 },
158
+ editing: { elo: 1250, pricePer1k: 133, rank: 5 },
158
159
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
159
160
  sourceUrls: AA_IMAGE_SOURCES,
160
- textToImage: { elo: 1268, pricePer1k: 133, rank: 2, winRate: 0.73 }
161
+ textToImage: { elo: 1310, pricePer1k: 133, rank: 4 }
161
162
  },
162
163
  speed: {
163
164
  medianSeconds: 34.6,
@@ -197,10 +198,10 @@ var MODELS = {
197
198
  banana2: {
198
199
  benchmark: {
199
200
  artificialAnalysis: {
200
- editing: { elo: 1231, pricePer1k: 67, rank: 5, winRate: 0.63 },
201
+ editing: { elo: 1249, pricePer1k: 67, rank: 7 },
201
202
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
202
203
  sourceUrls: AA_IMAGE_SOURCES,
203
- textToImage: { elo: 1263, pricePer1k: 67, rank: 3, winRate: 0.71 }
204
+ textToImage: { elo: 1320, pricePer1k: 67, rank: 3 }
204
205
  },
205
206
  tiers: { price: "premium", quality: "frontier", speed: "unknown" },
206
207
  useCase: "Best balance of quality, edit support, web search, and cost"
@@ -238,10 +239,10 @@ var MODELS = {
238
239
  banana: {
239
240
  benchmark: {
240
241
  artificialAnalysis: {
241
- editing: { elo: 1241, pricePer1k: 134, rank: 4, winRate: 0.65 },
242
+ editing: { elo: 1245, pricePer1k: 134, rank: 9 },
242
243
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
243
244
  sourceUrls: AA_IMAGE_SOURCES,
244
- textToImage: { elo: 1220, pricePer1k: 134, rank: 5, winRate: 0.66 }
245
+ textToImage: { elo: 1297, pricePer1k: 134, rank: 6 }
245
246
  },
246
247
  speed: {
247
248
  medianSeconds: 19.2,
@@ -281,6 +282,13 @@ var MODELS = {
281
282
  type: "generation"
282
283
  },
283
284
  gemini: {
285
+ benchmark: {
286
+ artificialAnalysis: {
287
+ editing: { elo: 1180, pricePer1k: 39, rank: 29 },
288
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
289
+ sourceUrls: AA_IMAGE_SOURCES
290
+ }
291
+ },
284
292
  editEndpoint: "fal-ai/gemini-25-flash-image/edit",
285
293
  endpoint: "fal-ai/gemini-25-flash-image",
286
294
  falPricing: {
@@ -308,6 +316,14 @@ var MODELS = {
308
316
  type: "generation"
309
317
  },
310
318
  gemini3: {
319
+ benchmark: {
320
+ artificialAnalysis: {
321
+ editing: { elo: 1245, pricePer1k: 134, rank: 9 },
322
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
323
+ sourceUrls: AA_IMAGE_SOURCES,
324
+ textToImage: { elo: 1297, pricePer1k: 134, rank: 6 }
325
+ }
326
+ },
311
327
  editEndpoint: "fal-ai/gemini-3-pro-image-preview/edit",
312
328
  endpoint: "fal-ai/gemini-3-pro-image-preview",
313
329
  falPricing: {
@@ -338,10 +354,10 @@ var MODELS = {
338
354
  seedream4: {
339
355
  benchmark: {
340
356
  artificialAnalysis: {
341
- editing: { elo: 1184, pricePer1k: 30, rank: 16, winRate: 0.61 },
357
+ editing: { elo: 1185, pricePer1k: 30, rank: 28 },
342
358
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
343
359
  sourceUrls: AA_IMAGE_SOURCES,
344
- textToImage: { elo: 1198, pricePer1k: 30, rank: 6, winRate: 0.59 }
360
+ textToImage: { elo: 1226, pricePer1k: 30, rank: 16 }
345
361
  },
346
362
  speed: {
347
363
  medianSeconds: 14.3,
@@ -378,10 +394,10 @@ var MODELS = {
378
394
  seedream45: {
379
395
  benchmark: {
380
396
  artificialAnalysis: {
381
- editing: { elo: 1184, pricePer1k: 40, rank: 17, winRate: 0.58 },
397
+ editing: { elo: 1186, pricePer1k: 40, rank: 26 },
382
398
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
383
399
  sourceUrls: AA_IMAGE_SOURCES,
384
- textToImage: { elo: 1167, pricePer1k: 40, rank: 17, winRate: 0.63 }
400
+ textToImage: { elo: 1203, pricePer1k: 40, rank: 32 }
385
401
  },
386
402
  speed: {
387
403
  medianSeconds: 18.2,
@@ -416,6 +432,14 @@ var MODELS = {
416
432
  type: "generation"
417
433
  },
418
434
  seedream5: {
435
+ benchmark: {
436
+ artificialAnalysis: {
437
+ editing: { elo: 1248, pricePer1k: 90, rank: 8 },
438
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
439
+ sourceUrls: AA_IMAGE_SOURCES,
440
+ textToImage: { elo: 1279, pricePer1k: 90, rank: 10 }
441
+ }
442
+ },
419
443
  name: "Seedream 5.0 Pro",
420
444
  endpoint: "bytedance/seedream/v5/pro/text-to-image",
421
445
  editEndpoint: "bytedance/seedream/v5/pro/edit",
@@ -442,6 +466,13 @@ var MODELS = {
442
466
  maxReferenceImages: 10
443
467
  },
444
468
  "seedream5-lite": {
469
+ benchmark: {
470
+ artificialAnalysis: {
471
+ editing: { elo: 1169, pricePer1k: 35, rank: 32 },
472
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
473
+ sourceUrls: AA_IMAGE_SOURCES
474
+ }
475
+ },
445
476
  name: "Seedream 5.0 Lite",
446
477
  endpoint: "fal-ai/bytedance/seedream/v5/lite/text-to-image",
447
478
  editEndpoint: "fal-ai/bytedance/seedream/v5/lite/edit",
@@ -470,10 +501,10 @@ var MODELS = {
470
501
  "flux2-max": {
471
502
  benchmark: {
472
503
  artificialAnalysis: {
473
- editing: { elo: 1206, pricePer1k: 140, rank: 10, winRate: 0.6 },
504
+ editing: { elo: 1201, pricePer1k: 140, rank: 19 },
474
505
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
475
506
  sourceUrls: AA_IMAGE_SOURCES,
476
- textToImage: { elo: 1197, pricePer1k: 70, rank: 8, winRate: 0.67 }
507
+ textToImage: { elo: 1225, pricePer1k: 70, rank: 17 }
477
508
  },
478
509
  speed: {
479
510
  medianSeconds: 31.6,
@@ -513,10 +544,10 @@ var MODELS = {
513
544
  "flux2-pro": {
514
545
  benchmark: {
515
546
  artificialAnalysis: {
516
- editing: { elo: 1170, pricePer1k: 30, rank: 21 },
547
+ editing: { elo: 1170, pricePer1k: 45, rank: 31 },
517
548
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
518
549
  sourceUrls: AA_IMAGE_SOURCES,
519
- textToImage: { elo: 1186, pricePer1k: 30, rank: 10, winRate: 0.62 }
550
+ textToImage: { elo: 1207, pricePer1k: 30, rank: 30 }
520
551
  },
521
552
  speed: {
522
553
  medianSeconds: 16.1,
@@ -556,10 +587,9 @@ var MODELS = {
556
587
  "flux2-flex": {
557
588
  benchmark: {
558
589
  artificialAnalysis: {
559
- editing: { elo: 1161, pricePer1k: 60, rank: 22 },
560
590
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
561
591
  sourceUrls: AA_IMAGE_SOURCES,
562
- textToImage: { elo: 1182, pricePer1k: 60, rank: 13, winRate: 0.61 }
592
+ textToImage: { elo: 1223, pricePer1k: 60, rank: 18 }
563
593
  },
564
594
  speed: {
565
595
  medianSeconds: 15.3,
@@ -603,8 +633,7 @@ var MODELS = {
603
633
  benchmark: {
604
634
  artificialAnalysis: {
605
635
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
606
- sourceUrls: AA_IMAGE_SOURCES,
607
- textToImage: { elo: 1160, pricePer1k: 12, rank: 19, winRate: 0.57 }
636
+ sourceUrls: AA_IMAGE_SOURCES
608
637
  },
609
638
  speed: {
610
639
  medianSeconds: 4.6,
@@ -747,7 +776,55 @@ var MODELS = {
747
776
  supportsSyncMode: true,
748
777
  type: "generation"
749
778
  },
779
+ reve: {
780
+ benchmark: {
781
+ artificialAnalysis: {
782
+ // #2 on BOTH boards, which almost nothing else manages - the top text-to-image model
783
+ // (GPT Image 2) drops to 3rd for editing, and the top editor (MAI-Image-2.5-Pro) is not
784
+ // on fal at all. Reve is the best editor motif can actually reach.
785
+ editing: { elo: 1263, pricePer1k: 200, rank: 2 },
786
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
787
+ sourceUrls: AA_IMAGE_SOURCES,
788
+ textToImage: { elo: 1322, pricePer1k: 200, rank: 2 }
789
+ },
790
+ tiers: { price: "premium", quality: "frontier", speed: "unknown" },
791
+ useCase: "Highest-ranked editor reachable on fal; strong prompt adherence and text rendering"
792
+ },
793
+ editEndpoint: "fal-ai/reve/edit",
794
+ endpoint: "fal-ai/reve/text-to-image",
795
+ falPricing: {
796
+ checkedAt: FAL_PRICING_CHECKED_AUG_2026,
797
+ currency: "USD",
798
+ endpointId: "fal-ai/reve/text-to-image",
799
+ estimatedCostPerImageUsd: 0.04,
800
+ source: "fal-pricing-api",
801
+ unit: "images",
802
+ unitPrice: 0.04
803
+ },
804
+ maxReferenceImages: 1,
805
+ name: "Reve 2.1",
806
+ pricePerImageUsd: 0.04,
807
+ pricing: "$0.04",
808
+ // fal's schema takes aspect_ratio from a fixed set and has no width/height or resolution
809
+ // parameter, so aspect is supported and resolution is not.
810
+ sizeMode: "aspect_ratio",
811
+ supportsAspect: true,
812
+ supportsEdit: true,
813
+ supportsNumImages: true,
814
+ supportsOutputFormat: true,
815
+ supportsResolution: false,
816
+ supportsSeed: false,
817
+ supportsSyncMode: true,
818
+ type: "generation"
819
+ },
750
820
  recraft4: {
821
+ benchmark: {
822
+ artificialAnalysis: {
823
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
824
+ sourceUrls: AA_IMAGE_SOURCES,
825
+ textToImage: { elo: 1215, pricePer1k: 35, rank: 25 }
826
+ }
827
+ },
751
828
  endpoint: "fal-ai/recraft/v4/text-to-image",
752
829
  falPricing: {
753
830
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
@@ -798,6 +875,13 @@ var MODELS = {
798
875
  type: "generation"
799
876
  },
800
877
  ideogram4: {
878
+ benchmark: {
879
+ artificialAnalysis: {
880
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
881
+ sourceUrls: AA_IMAGE_SOURCES,
882
+ textToImage: { elo: 1220, pricePer1k: 60, rank: 19 }
883
+ }
884
+ },
801
885
  name: "Ideogram V4",
802
886
  endpoint: "ideogram/v4",
803
887
  type: "generation",
@@ -830,10 +914,10 @@ var MODELS = {
830
914
  "grok-image": {
831
915
  benchmark: {
832
916
  artificialAnalysis: {
833
- editing: { elo: 1213, pricePer1k: 20, rank: 7, winRate: 0.6 },
917
+ editing: { elo: 1214, pricePer1k: 20, rank: 16 },
834
918
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
835
919
  sourceUrls: AA_IMAGE_SOURCES,
836
- textToImage: { elo: 1182, pricePer1k: 20, rank: 12, winRate: 0.6 }
920
+ textToImage: { elo: 1216, pricePer1k: 20, rank: 23 }
837
921
  },
838
922
  speed: {
839
923
  medianSeconds: 5.1,
@@ -871,8 +955,7 @@ var MODELS = {
871
955
  benchmark: {
872
956
  artificialAnalysis: {
873
957
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
874
- sourceUrls: AA_IMAGE_SOURCES,
875
- textToImage: { elo: 1158, pricePer1k: 20, rank: 20, winRate: 0.6 }
958
+ sourceUrls: AA_IMAGE_SOURCES
876
959
  },
877
960
  speed: {
878
961
  medianSeconds: 23.3,
@@ -907,8 +990,14 @@ var MODELS = {
907
990
  },
908
991
  qwen3: {
909
992
  benchmark: {
993
+ artificialAnalysis: {
994
+ editing: { elo: 1218, pricePer1k: 33, rank: 15 },
995
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
996
+ sourceUrls: AA_IMAGE_SOURCES,
997
+ textToImage: { elo: 1272, pricePer1k: 30, rank: 11 }
998
+ },
910
999
  tiers: { price: "budget", quality: "better", speed: "unknown" },
911
- useCase: "Qwen Image 3 - successor to qwen; no leaderboard data yet"
1000
+ useCase: "Strong quality per dollar - 11th on text-to-image at $30/1k"
912
1001
  },
913
1002
  endpoint: "fal-ai/qwen-image-3/text-to-image",
914
1003
  falPricing: {
@@ -1333,7 +1422,7 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
1333
1422
  if (options.background !== void 0 && config.supportsBackground !== true) {
1334
1423
  unsupported(config, "background");
1335
1424
  }
1336
- if (options.transparent === true && !(config.supportsBackground === true || config.supportsOutputFormat === true)) {
1425
+ if (options.transparent === true && config.supportsBackground !== true) {
1337
1426
  unsupported(config, "transparent output");
1338
1427
  }
1339
1428
  if (options.inputFidelity !== void 0) {
@@ -1615,353 +1704,351 @@ function buildGenerateBody(options) {
1615
1704
 
1616
1705
  // src/leaderboards.ts
1617
1706
  var IMAGE_TEXT_TO_IMAGE_TOP_20 = {
1618
- capturedAt: "2026-05-12",
1707
+ capturedAt: "2026-08-23",
1619
1708
  entries: [
1620
1709
  {
1621
1710
  apiPricing: "$211/1k images",
1622
1711
  creator: "OpenAI",
1623
- elo: 1337,
1712
+ elo: 1369,
1624
1713
  isCurrent: true,
1625
1714
  model: "GPT Image 2 (high)",
1626
1715
  rank: 1,
1627
1716
  routableModelId: "gpt2"
1628
1717
  },
1629
1718
  {
1630
- apiPricing: "$133/1k images",
1631
- creator: "OpenAI",
1632
- elo: 1268,
1719
+ apiPricing: "$200/1k images",
1720
+ creator: "Reve",
1721
+ elo: 1322,
1633
1722
  isCurrent: true,
1634
- model: "GPT Image 1.5 (high)",
1723
+ model: "Reve 2.1",
1635
1724
  rank: 2,
1636
- routableModelId: "gpt"
1725
+ routableModelId: "reve"
1637
1726
  },
1638
1727
  {
1639
1728
  apiPricing: "$67/1k images",
1640
1729
  creator: "Google",
1641
- elo: 1263,
1730
+ elo: 1320,
1642
1731
  isCurrent: true,
1643
1732
  model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1644
1733
  rank: 3,
1645
1734
  routableModelId: "banana2"
1646
1735
  },
1647
1736
  {
1648
- apiPricing: "$150/1k images",
1649
- creator: "Sourceful",
1650
- elo: 1256,
1737
+ apiPricing: "$133/1k images",
1738
+ creator: "OpenAI",
1739
+ elo: 1310,
1651
1740
  isCurrent: true,
1652
- model: "Riverflow 2.0",
1653
- rank: 4
1741
+ model: "GPT Image 1.5 (high)",
1742
+ rank: 4,
1743
+ routableModelId: "gpt"
1654
1744
  },
1655
1745
  {
1656
- apiPricing: "$134/1k images",
1657
- creator: "Google",
1658
- elo: 1220,
1746
+ apiPricing: "$48/1k images",
1747
+ creator: "Microsoft AI",
1748
+ elo: 1305,
1659
1749
  isCurrent: true,
1660
- model: "Nano Banana Pro (Gemini 3 Pro Image)",
1661
- rank: 5,
1662
- routableModelId: "banana"
1750
+ model: "MAI-Image-2.5",
1751
+ rank: 5
1663
1752
  },
1664
1753
  {
1665
- apiPricing: "$30/1k images",
1666
- creator: "ByteDance Seed",
1667
- elo: 1198,
1754
+ apiPricing: "$134/1k images",
1755
+ creator: "Google",
1756
+ elo: 1297,
1668
1757
  isCurrent: true,
1669
- model: "Seedream 4.0",
1758
+ model: "Nano Banana Pro (Gemini 3 Pro Image)",
1670
1759
  rank: 6,
1671
- routableModelId: "seedream4"
1760
+ routableModelId: "banana"
1672
1761
  },
1673
1762
  {
1674
- apiPricing: "$50/1k images",
1675
- creator: "Microsoft",
1676
- elo: 1198,
1763
+ apiPricing: "$109/1k images",
1764
+ creator: "Microsoft AI",
1765
+ elo: 1293,
1677
1766
  isCurrent: true,
1678
- model: "MAI-Image-2",
1767
+ model: "MAI-Image-2.5-Pro",
1679
1768
  rank: 7
1680
1769
  },
1681
1770
  {
1682
- apiPricing: "$70/1k images",
1683
- creator: "Black Forest Labs",
1684
- elo: 1197,
1771
+ apiPricing: "$34/1k images",
1772
+ creator: "Google",
1773
+ elo: 1289,
1685
1774
  isCurrent: true,
1686
- model: "FLUX.2 [max]",
1687
- rank: 8,
1688
- routableModelId: "flux2-max"
1775
+ model: "Nano Banana 2 Lite (Gemini 3.1 Flash Lite Image)",
1776
+ rank: 8
1689
1777
  },
1690
1778
  {
1691
- apiPricing: "Unavailable",
1692
- creator: "Peanut",
1693
- elo: 1187,
1779
+ apiPricing: "$43/1k images",
1780
+ creator: "Alibaba",
1781
+ elo: 1282,
1694
1782
  isCurrent: true,
1695
- model: "Peanut (Open Weights Coming Soon)",
1783
+ model: "Qwen-Image-3.0-Pro",
1696
1784
  rank: 9
1697
1785
  },
1698
1786
  {
1699
- apiPricing: "$30/1k images",
1700
- creator: "Black Forest Labs",
1701
- elo: 1186,
1787
+ apiPricing: "$90/1k images",
1788
+ creator: "Bytedance",
1789
+ elo: 1279,
1702
1790
  isCurrent: true,
1703
- model: "FLUX.2 [pro]",
1791
+ model: "Seedream 5.0 Pro",
1704
1792
  rank: 10,
1705
- routableModelId: "flux2-pro"
1793
+ routableModelId: "seedream5"
1706
1794
  },
1707
1795
  {
1708
- apiPricing: "$60/1k images",
1709
- creator: "Google",
1710
- elo: 1184,
1711
- isCurrent: false,
1712
- model: "Imagen 4 Ultra Preview 0606",
1713
- rank: 11
1796
+ apiPricing: "$30/1k images",
1797
+ creator: "Alibaba",
1798
+ elo: 1272,
1799
+ isCurrent: true,
1800
+ model: "Qwen-Image-3.0",
1801
+ rank: 11,
1802
+ routableModelId: "qwen3"
1714
1803
  },
1715
1804
  {
1716
- apiPricing: "$20/1k images",
1717
- creator: "xAI",
1718
- elo: 1182,
1805
+ apiPricing: "$75/1k images",
1806
+ creator: "Alibaba",
1807
+ elo: 1237,
1719
1808
  isCurrent: true,
1720
- model: "grok-imagine-image",
1721
- rank: 12,
1722
- routableModelId: "grok-image"
1809
+ model: "Qwen Image 2.0 Pro",
1810
+ rank: 12
1723
1811
  },
1724
1812
  {
1725
- apiPricing: "$60/1k images",
1726
- creator: "Black Forest Labs",
1727
- elo: 1182,
1813
+ apiPricing: "$50/1k images",
1814
+ creator: "SpaceXAI",
1815
+ elo: 1234,
1728
1816
  isCurrent: true,
1729
- model: "FLUX.2 [flex]",
1730
- rank: 13,
1731
- routableModelId: "flux2-flex"
1817
+ model: "grok-imagine-image-quality",
1818
+ rank: 13
1732
1819
  },
1733
1820
  {
1734
- apiPricing: "$30/1k images",
1735
- creator: "ImagineArt",
1736
- elo: 1181,
1821
+ apiPricing: "$20/1k images",
1822
+ creator: "Microsoft AI",
1823
+ elo: 1228,
1737
1824
  isCurrent: true,
1738
- model: "ImagineArt 2.0",
1825
+ model: "MAI-Image-2.5-Flash",
1739
1826
  rank: 14
1740
1827
  },
1741
1828
  {
1742
- apiPricing: "$60/1k images",
1743
- creator: "Google",
1744
- elo: 1171,
1829
+ apiPricing: "$80/1k images",
1830
+ creator: "HiDream",
1831
+ elo: 1228,
1745
1832
  isCurrent: true,
1746
- model: "Imagen 4 Ultra",
1833
+ model: "HiDream-O1-Image-1.5",
1747
1834
  rank: 15
1748
1835
  },
1749
1836
  {
1750
- apiPricing: "$40/1k images",
1751
- creator: "Google",
1752
- elo: 1169,
1753
- isCurrent: false,
1754
- model: "Imagen 4 Preview 0606",
1755
- rank: 16
1837
+ apiPricing: "$30/1k images",
1838
+ creator: "ByteDance Seed",
1839
+ elo: 1226,
1840
+ isCurrent: true,
1841
+ model: "Seedream 4.0",
1842
+ rank: 16,
1843
+ routableModelId: "seedream4"
1756
1844
  },
1757
1845
  {
1758
- apiPricing: "$40/1k images",
1759
- creator: "ByteDance Seed",
1760
- elo: 1167,
1846
+ apiPricing: "$70/1k images",
1847
+ creator: "Black Forest Labs",
1848
+ elo: 1225,
1761
1849
  isCurrent: true,
1762
- model: "Seedream 4.5",
1850
+ model: "FLUX.2 [max]",
1763
1851
  rank: 17,
1764
- routableModelId: "seedream45"
1852
+ routableModelId: "flux2-max"
1765
1853
  },
1766
1854
  {
1767
- apiPricing: "$8/1k images",
1768
- creator: "Fal",
1769
- elo: 1161,
1855
+ apiPricing: "$60/1k images",
1856
+ creator: "Black Forest Labs",
1857
+ elo: 1223,
1770
1858
  isCurrent: true,
1771
- model: "FLUX.2 [dev] Turbo",
1772
- rank: 18
1859
+ model: "FLUX.2 [flex]",
1860
+ rank: 18,
1861
+ routableModelId: "flux2-flex"
1773
1862
  },
1774
1863
  {
1775
- apiPricing: "$12/1k images",
1776
- creator: "Black Forest Labs",
1777
- elo: 1160,
1864
+ apiPricing: "$60/1k images",
1865
+ creator: "Ideogram",
1866
+ elo: 1220,
1778
1867
  isCurrent: true,
1779
- model: "FLUX.2 [dev]",
1868
+ model: "Ideogram 4.0",
1780
1869
  rank: 19,
1781
- routableModelId: "flux2-dev"
1870
+ routableModelId: "ideogram4"
1782
1871
  },
1783
1872
  {
1784
- apiPricing: "$20/1k images",
1785
- creator: "Alibaba",
1786
- elo: 1158,
1873
+ apiPricing: "$60/1k images",
1874
+ creator: "Krea",
1875
+ elo: 1220,
1787
1876
  isCurrent: true,
1788
- model: "Qwen Image Max 2512",
1789
- rank: 20,
1790
- routableModelId: "qwen"
1877
+ model: "Krea 2 Large",
1878
+ rank: 20
1791
1879
  }
1792
1880
  ],
1793
1881
  source: "https://artificialanalysis.ai/image/leaderboard/text-to-image"
1794
1882
  };
1795
1883
  var IMAGE_EDITING_TOP_20 = {
1796
- capturedAt: "2026-05-12",
1884
+ capturedAt: "2026-08-23",
1797
1885
  entries: [
1798
1886
  {
1799
- apiPricing: "$150/1k images",
1800
- creator: "Sourceful",
1801
- elo: 1286,
1887
+ apiPricing: "$109/1k images",
1888
+ creator: "Microsoft AI",
1889
+ elo: 1272,
1802
1890
  isCurrent: true,
1803
- model: "Riverflow 2.0",
1891
+ model: "MAI-Image-2.5-Pro",
1804
1892
  rank: 1
1805
1893
  },
1806
1894
  {
1807
- apiPricing: "$133/1k images",
1808
- creator: "OpenAI",
1809
- elo: 1262,
1895
+ apiPricing: "$200/1k images",
1896
+ creator: "Reve",
1897
+ elo: 1263,
1810
1898
  isCurrent: true,
1811
- model: "GPT Image 1.5 (high)",
1899
+ model: "Reve 2.1",
1812
1900
  rank: 2,
1813
- routableModelId: "gpt"
1901
+ routableModelId: "reve"
1814
1902
  },
1815
1903
  {
1816
1904
  apiPricing: "$211/1k images",
1817
1905
  creator: "OpenAI",
1818
- elo: 1249,
1906
+ elo: 1256,
1819
1907
  isCurrent: true,
1820
1908
  model: "GPT Image 2 (high)",
1821
1909
  rank: 3,
1822
1910
  routableModelId: "gpt2"
1823
1911
  },
1824
1912
  {
1825
- apiPricing: "$134/1k images",
1826
- creator: "Google",
1827
- elo: 1241,
1913
+ apiPricing: "$48/1k images",
1914
+ creator: "Microsoft AI",
1915
+ elo: 1256,
1828
1916
  isCurrent: true,
1829
- model: "Nano Banana Pro (Gemini 3 Pro Image)",
1830
- rank: 4,
1831
- routableModelId: "banana"
1917
+ model: "MAI-Image-2.5",
1918
+ rank: 4
1832
1919
  },
1833
1920
  {
1834
- apiPricing: "$67/1k images",
1835
- creator: "Google",
1836
- elo: 1231,
1921
+ apiPricing: "$133/1k images",
1922
+ creator: "OpenAI",
1923
+ elo: 1250,
1837
1924
  isCurrent: true,
1838
- model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1925
+ model: "GPT Image 1.5 (high)",
1839
1926
  rank: 5,
1840
- routableModelId: "banana2"
1927
+ routableModelId: "gpt"
1841
1928
  },
1842
1929
  {
1843
- apiPricing: "$90/1k images",
1844
- creator: "Tencent",
1845
- elo: 1222,
1930
+ apiPricing: "$43/1k images",
1931
+ creator: "Alibaba",
1932
+ elo: 1250,
1846
1933
  isCurrent: true,
1847
- model: "HunyuanImage 3.0 Instruct (Fal)",
1934
+ model: "Qwen-Image-3.0-Pro",
1848
1935
  rank: 6
1849
1936
  },
1850
1937
  {
1851
- apiPricing: "$20/1k images",
1852
- creator: "xAI",
1853
- elo: 1213,
1938
+ apiPricing: "$67/1k images",
1939
+ creator: "Google",
1940
+ elo: 1249,
1854
1941
  isCurrent: true,
1855
- model: "grok-imagine-image",
1942
+ model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1856
1943
  rank: 7,
1857
- routableModelId: "grok-image"
1944
+ routableModelId: "banana2"
1858
1945
  },
1859
1946
  {
1860
- apiPricing: "$70/1k images",
1861
- creator: "xAI",
1862
- elo: 1212,
1947
+ apiPricing: "$90/1k images",
1948
+ creator: "Bytedance",
1949
+ elo: 1248,
1863
1950
  isCurrent: true,
1864
- model: "grok-imagine-image-pro",
1865
- rank: 8
1951
+ model: "Seedream 5.0 Pro",
1952
+ rank: 8,
1953
+ routableModelId: "seedream5"
1866
1954
  },
1867
1955
  {
1868
- apiPricing: "Unavailable",
1869
- creator: "KlingAI",
1870
- elo: 1207,
1956
+ apiPricing: "$134/1k images",
1957
+ creator: "Google",
1958
+ elo: 1245,
1871
1959
  isCurrent: true,
1872
- model: "Kling Image 3.0 Omni",
1873
- rank: 9
1960
+ model: "Nano Banana Pro (Gemini 3 Pro Image)",
1961
+ rank: 9,
1962
+ routableModelId: "banana"
1874
1963
  },
1875
1964
  {
1876
- apiPricing: "$140/1k images",
1877
- creator: "Black Forest Labs",
1878
- elo: 1206,
1965
+ apiPricing: "$20/1k images",
1966
+ creator: "Microsoft AI",
1967
+ elo: 1236,
1879
1968
  isCurrent: true,
1880
- model: "FLUX.2 [max]",
1881
- rank: 10,
1882
- routableModelId: "flux2-max"
1969
+ model: "MAI-Image-2.5-Flash",
1970
+ rank: 10
1883
1971
  },
1884
1972
  {
1885
- apiPricing: "Unavailable",
1886
- creator: "Alibaba",
1887
- elo: 1200,
1973
+ apiPricing: "$50/1k images",
1974
+ creator: "SpaceXAI",
1975
+ elo: 1226,
1888
1976
  isCurrent: true,
1889
- model: "Wan 2.7 Pro",
1977
+ model: "grok-imagine-image-quality",
1890
1978
  rank: 11
1891
1979
  },
1892
1980
  {
1893
- apiPricing: "$28/1k images",
1894
- creator: "KlingAI",
1895
- elo: 1196,
1981
+ apiPricing: "$90/1k images",
1982
+ creator: "Tencent",
1983
+ elo: 1222,
1896
1984
  isCurrent: true,
1897
- model: "Kling Image 3.0",
1985
+ model: "HunyuanImage 3.0 Instruct",
1898
1986
  rank: 12
1899
1987
  },
1900
1988
  {
1901
- apiPricing: "$28/1k images",
1902
- creator: "KlingAI",
1903
- elo: 1193,
1989
+ apiPricing: "$80/1k images",
1990
+ creator: "Newport AI",
1991
+ elo: 1220,
1904
1992
  isCurrent: true,
1905
- model: "Kling Image O1",
1993
+ model: "DreamImage 2.0",
1906
1994
  rank: 13
1907
1995
  },
1908
1996
  {
1909
- apiPricing: "$30/1k images",
1910
- creator: "Alibaba",
1911
- elo: 1188,
1997
+ apiPricing: "$100/1k images",
1998
+ creator: "Luma Labs",
1999
+ elo: 1219,
1912
2000
  isCurrent: true,
1913
- model: "Wan 2.6 Image",
2001
+ model: "Luma UNI 1 Max",
1914
2002
  rank: 14
1915
2003
  },
1916
2004
  {
1917
- apiPricing: "$66/1k images",
1918
- creator: "Sourceful",
1919
- elo: 1184,
2005
+ apiPricing: "$33/1k images",
2006
+ creator: "Alibaba",
2007
+ elo: 1218,
1920
2008
  isCurrent: true,
1921
- model: "Riverflow 1",
1922
- rank: 15
2009
+ model: "Qwen-Image-3.0",
2010
+ rank: 15,
2011
+ routableModelId: "qwen3"
1923
2012
  },
1924
2013
  {
1925
- apiPricing: "$30/1k images",
1926
- creator: "ByteDance Seed",
1927
- elo: 1184,
2014
+ apiPricing: "$20/1k images",
2015
+ creator: "SpaceXAI",
2016
+ elo: 1214,
1928
2017
  isCurrent: true,
1929
- model: "Seedream 4.0",
2018
+ model: "grok-imagine-image",
1930
2019
  rank: 16,
1931
- routableModelId: "seedream4"
2020
+ routableModelId: "grok-image"
1932
2021
  },
1933
2022
  {
1934
- apiPricing: "$40/1k images",
1935
- creator: "ByteDance Seed",
1936
- elo: 1184,
2023
+ apiPricing: "$28/1k images",
2024
+ creator: "KlingAI",
2025
+ elo: 1208,
1937
2026
  isCurrent: true,
1938
- model: "Seedream 4.5",
1939
- rank: 17,
1940
- routableModelId: "seedream45"
2027
+ model: "Kling Image 3.0 Omni",
2028
+ rank: 17
1941
2029
  },
1942
2030
  {
1943
- apiPricing: "$26/1k images",
1944
- creator: "Alibaba",
1945
- elo: 1181,
2031
+ apiPricing: "$34/1k images",
2032
+ creator: "Google",
2033
+ elo: 1202,
1946
2034
  isCurrent: true,
1947
- model: "Wan 2.7",
2035
+ model: "Nano Banana 2 Lite",
1948
2036
  rank: 18
1949
2037
  },
1950
2038
  {
1951
- apiPricing: "$39/1k images",
1952
- creator: "Google",
1953
- elo: 1173,
2039
+ apiPricing: "$140/1k images",
2040
+ creator: "Black Forest Labs",
2041
+ elo: 1201,
1954
2042
  isCurrent: true,
1955
- model: "Nano Banana (Gemini 2.5 Flash Image)",
2043
+ model: "FLUX.2 [max]",
1956
2044
  rank: 19,
1957
- routableModelId: "gemini"
2045
+ routableModelId: "flux2-max"
1958
2046
  },
1959
2047
  {
1960
- apiPricing: "$40/1k images",
1961
- creator: "Reve",
1962
- elo: 1172,
2048
+ creator: "Alibaba",
2049
+ elo: 1200,
1963
2050
  isCurrent: true,
1964
- model: "Reve V1 (December)",
2051
+ model: "Wan 2.7 Pro",
1965
2052
  rank: 20
1966
2053
  }
1967
2054
  ],
@@ -2191,7 +2278,31 @@ var VIDEO_IMAGE_TO_VIDEO_TOP_15 = {
2191
2278
  };
2192
2279
 
2193
2280
  // src/server.ts
2194
- import { err, ok } from "neverthrow";
2281
+ import { err as err2, ok as ok2 } from "neverthrow";
2282
+
2283
+ // src/errors.ts
2284
+ var MotifError = class extends Error {
2285
+ status;
2286
+ code;
2287
+ /** fal's request-correlation id (from the `x-fal-request-id` header or the
2288
+ * error body). Ties a failure back to fal's dashboard/support. */
2289
+ requestId;
2290
+ constructor(message, status, code, requestId) {
2291
+ super(message);
2292
+ this.name = "MotifError";
2293
+ this.status = status;
2294
+ this.code = code;
2295
+ this.requestId = requestId;
2296
+ }
2297
+ };
2298
+ function toMotifError(error) {
2299
+ if (error instanceof MotifError) {
2300
+ return error;
2301
+ }
2302
+ const message = error instanceof Error ? error.message : String(error);
2303
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
2304
+ return new MotifError(message, 0, code);
2305
+ }
2195
2306
 
2196
2307
  // src/fal-parse.ts
2197
2308
  function isRecord(value) {
@@ -2289,9 +2400,362 @@ function requestIdFromBody(text) {
2289
2400
  return asString(parsed.request_id) ?? asString(parsed.requestId) ?? asString(parsed.trace_id);
2290
2401
  }
2291
2402
 
2292
- // src/tools.ts
2293
- var FAL_EXPLORE_CHECKED_AT = "2026-05-12";
2294
- var FAL_TOOLS = {
2403
+ // src/server-tools.ts
2404
+ import { err, ok } from "neverthrow";
2405
+
2406
+ // src/tool-registry/analysis.ts
2407
+ var ANALYSIS_TOOLS = {
2408
+ "got-ocr": {
2409
+ category: "analysis",
2410
+ description: "Transcribe text from one or more images, optionally as formatted multi-page output.",
2411
+ endpoint: "fal-ai/got-ocr/v2",
2412
+ inputField: "input_image_urls",
2413
+ inputKind: "images",
2414
+ name: "GOT-OCR 2.0",
2415
+ outputKeys: ["outputs"],
2416
+ price: { kind: "metered" },
2417
+ pricing: "$0.05/image",
2418
+ queued: true,
2419
+ sourceUrl: "https://fal.ai/models/fal-ai/got-ocr/v2",
2420
+ task: "optical character recognition"
2421
+ },
2422
+ "moondream-caption": {
2423
+ category: "analysis",
2424
+ description: "Write a short, normal, or long caption for an image.",
2425
+ endpoint: "fal-ai/moondream3-preview/caption",
2426
+ inputField: "image_url",
2427
+ inputKind: "image",
2428
+ name: "Moondream 3 Caption",
2429
+ outputKeys: ["output"],
2430
+ price: { kind: "metered" },
2431
+ pricing: "$0.40/M input tokens, $3.50/M output tokens",
2432
+ sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/caption",
2433
+ task: "image captioning"
2434
+ },
2435
+ "moondream-detect": {
2436
+ category: "analysis",
2437
+ description: "Detect prompted objects and return their bounding boxes, with an optional preview.",
2438
+ endpoint: "fal-ai/moondream3-preview/detect",
2439
+ inputField: "image_url",
2440
+ inputKind: "image",
2441
+ name: "Moondream 3 Detect",
2442
+ outputKeys: ["objects", "image"],
2443
+ price: { kind: "metered" },
2444
+ pricing: "$0.40/M input tokens, $3.50/M output tokens",
2445
+ sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/detect",
2446
+ task: "object detection"
2447
+ },
2448
+ "moondream-point": {
2449
+ category: "analysis",
2450
+ description: "Return point coordinates for every instance of a prompted subject.",
2451
+ endpoint: "fal-ai/moondream3-preview/point",
2452
+ inputField: "image_url",
2453
+ inputKind: "image",
2454
+ name: "Moondream 3 Point",
2455
+ outputKeys: ["points", "image"],
2456
+ price: { kind: "metered" },
2457
+ pricing: "$0.40/M input tokens, $3.50/M output tokens",
2458
+ sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/point",
2459
+ task: "object pointing"
2460
+ },
2461
+ "moondream-query": {
2462
+ category: "analysis",
2463
+ description: "Answer a free-form question about an image, with reasoning.",
2464
+ endpoint: "fal-ai/moondream3-preview/query",
2465
+ inputField: "image_url",
2466
+ inputKind: "image",
2467
+ name: "Moondream 3 Query",
2468
+ outputKeys: ["output", "reasoning"],
2469
+ price: { kind: "metered" },
2470
+ pricing: "$0.40/M input tokens, $3.50/M output tokens",
2471
+ sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/query",
2472
+ task: "visual question answering"
2473
+ },
2474
+ nsfw: {
2475
+ category: "moderation",
2476
+ description: "Predict whether one or more images contain NSFW concepts.",
2477
+ endpoint: "fal-ai/x-ailab/nsfw",
2478
+ inputField: "image_urls",
2479
+ inputKind: "images",
2480
+ name: "NSFW Checker",
2481
+ outputKeys: ["has_nsfw_concepts"],
2482
+ price: { kind: "metered" },
2483
+ pricing: "$0.001/image",
2484
+ sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw",
2485
+ task: "vision moderation"
2486
+ }
2487
+ };
2488
+
2489
+ // src/tool-registry/assets.ts
2490
+ var ASSET_TOOLS = {
2491
+ "hunyuan3d-v3": {
2492
+ category: "3d",
2493
+ description: "Reconstruct a textured 3D mesh from a single image, with optional multi-view inputs.",
2494
+ endpoint: "fal-ai/hunyuan3d-v3/image-to-3d",
2495
+ inputField: "input_image_url",
2496
+ inputKind: "image",
2497
+ name: "Hunyuan3D v3 Image to 3D",
2498
+ outputKeys: ["model_glb", "model_urls", "thumbnail"],
2499
+ price: { kind: "call", usd: 0.375 },
2500
+ pricing: "$0.375/generation at the default Normal type; $0.45 low poly, $0.225 geometry only, and $0.15 each for PBR, multi-view, or a custom face count",
2501
+ queued: true,
2502
+ sourceUrl: "https://fal.ai/models/fal-ai/hunyuan3d-v3/image-to-3d",
2503
+ task: "single-image 3D reconstruction"
2504
+ },
2505
+ "ideogram-layerize-text": {
2506
+ category: "layers",
2507
+ description: "Split an image into background image layers plus editable HTML text containers.",
2508
+ endpoint: "fal-ai/ideogram/v3/layerize-text",
2509
+ inputField: "image_url",
2510
+ inputKind: "image",
2511
+ name: "Ideogram v3 Text Layerize",
2512
+ outputKeys: ["image_layers", "image", "text_html", "text_containers"],
2513
+ price: { kind: "call", usd: 0.09 },
2514
+ pricing: "$0.09/image",
2515
+ queued: true,
2516
+ sourceUrl: "https://fal.ai/models/fal-ai/ideogram/v3/layerize-text",
2517
+ task: "text and image layer extraction"
2518
+ },
2519
+ "ideogram-tiling": {
2520
+ category: "material",
2521
+ description: "Generate a seamlessly tiling texture, optionally conditioned on a source image.",
2522
+ endpoint: "ideogram/v4/tiling",
2523
+ inputField: "image_url",
2524
+ inputKind: "image",
2525
+ name: "Ideogram v4 Tiling",
2526
+ outputKeys: ["images"],
2527
+ price: { kind: "megapixel", usd: 0.06 },
2528
+ pricing: "$0.06/megapixel at the default BALANCED speed; $0.03 turbo, $0.10 quality",
2529
+ sourceUrl: "https://fal.ai/models/ideogram/v4/tiling",
2530
+ task: "seamless texture generation"
2531
+ },
2532
+ image2svg: {
2533
+ category: "vector",
2534
+ description: "Trace a raster image into layered SVG paths.",
2535
+ endpoint: "fal-ai/image2svg",
2536
+ inputField: "image_url",
2537
+ inputKind: "image",
2538
+ name: "Image to SVG",
2539
+ outputKeys: ["images"],
2540
+ price: { kind: "call", usd: 5e-3 },
2541
+ pricing: "$0.005/image",
2542
+ sourceUrl: "https://fal.ai/models/fal-ai/image2svg",
2543
+ task: "raster to vector tracing"
2544
+ },
2545
+ patina: {
2546
+ category: "material",
2547
+ description: "Decompose a surface photograph into PBR maps: basecolor, normal, roughness, metalness, height.",
2548
+ endpoint: "fal-ai/patina",
2549
+ inputField: "image_url",
2550
+ inputKind: "image",
2551
+ name: "Patina PBR Maps",
2552
+ outputKeys: ["images"],
2553
+ // Output order follows the request's `maps` array, so read that first;
2554
+ // the fallback is the endpoint's own schema default.
2555
+ outputLabels: {
2556
+ images: {
2557
+ fallback: ["basecolor", "normal", "roughness", "metalness", "height"],
2558
+ fromOption: "maps"
2559
+ }
2560
+ },
2561
+ price: { kind: "metered" },
2562
+ pricing: "$0.01 base plus $0.01/megapixel per output map, so all 5 maps on a 1MP image cost $0.06; the listed rate is per map",
2563
+ queued: true,
2564
+ sourceUrl: "https://fal.ai/models/fal-ai/patina",
2565
+ task: "PBR material decomposition"
2566
+ },
2567
+ "patina-extract": {
2568
+ category: "material",
2569
+ description: "Extract a tiling PBR material from a prompted region of a photograph.",
2570
+ endpoint: "fal-ai/patina/material/extract",
2571
+ inputField: "image_url",
2572
+ inputKind: "image",
2573
+ name: "Patina Material Extract",
2574
+ outputKeys: ["images"],
2575
+ // Same `maps` option and schema default as fal-ai/patina, both verified.
2576
+ // `num_images` above 1 multiplies the array and breaks the mapping, which
2577
+ // the count check catches.
2578
+ outputLabels: {
2579
+ images: {
2580
+ fallback: ["basecolor", "normal", "roughness", "metalness", "height"],
2581
+ fromOption: "maps"
2582
+ }
2583
+ },
2584
+ price: { kind: "metered" },
2585
+ pricing: "$0.10 base only; add $0.02/megapixel plus $0.01/megapixel per map, so 1MP with all 5 maps is $0.17",
2586
+ queued: true,
2587
+ sourceUrl: "https://fal.ai/models/fal-ai/patina/material/extract",
2588
+ task: "tiling material extraction"
2589
+ },
2590
+ "qwen-layered": {
2591
+ category: "layers",
2592
+ description: "Split an image into a configurable number of stacked RGBA layers.",
2593
+ endpoint: "fal-ai/qwen-image-layered",
2594
+ inputField: "image_url",
2595
+ inputKind: "image",
2596
+ name: "Qwen Image Layered",
2597
+ outputKeys: ["images"],
2598
+ // fal publishes a figure but not what it counts. Encoding it as `call`
2599
+ // would assert a per-request price we cannot support, and this endpoint
2600
+ // returns several outputs — the same shape that made `seedream-layerize`
2601
+ // under-report by a factor of its layer count. `metered` reports null
2602
+ // rather than a number that is right only on one reading.
2603
+ price: { kind: "metered" },
2604
+ pricing: "$0.05 per image; fal does not say whether that counts the input image or each of the generated layers, so no estimate is reported",
2605
+ queued: true,
2606
+ sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-layered",
2607
+ task: "image layer decomposition"
2608
+ },
2609
+ "recraft-vectorize": {
2610
+ category: "vector",
2611
+ description: "Convert a raster image into a clean SVG.",
2612
+ endpoint: "fal-ai/recraft/vectorize",
2613
+ inputField: "image_url",
2614
+ inputKind: "image",
2615
+ name: "Recraft Vectorize",
2616
+ outputKeys: ["image"],
2617
+ price: { kind: "call", usd: 0.04 },
2618
+ pricing: "$0.04/image, $0.08 with a vector style",
2619
+ sourceUrl: "https://fal.ai/models/fal-ai/recraft/vectorize",
2620
+ task: "raster to vector conversion"
2621
+ },
2622
+ "sam3-3d-align": {
2623
+ category: "3d",
2624
+ description: "Align SAM 3D objects and bodies into a shared scene.",
2625
+ endpoint: "fal-ai/sam-3/3d-align",
2626
+ inputField: "image_url",
2627
+ inputKind: "image",
2628
+ name: "SAM 3D Align",
2629
+ outputKeys: [
2630
+ "scene_glb",
2631
+ "model_glb",
2632
+ "visualization",
2633
+ "body_mesh_ply",
2634
+ "metadata"
2635
+ ],
2636
+ // fal publishes a figure but not what it counts. Encoding it as `call`
2637
+ // would assert a per-request price we cannot support, and this endpoint
2638
+ // returns several outputs — the same shape that made `seedream-layerize`
2639
+ // under-report by a factor of its layer count. `metered` reports null
2640
+ // rather than a number that is right only on one reading.
2641
+ price: { kind: "metered" },
2642
+ pricing: "$0.02 per unit; fal does not define a unit, so no estimate is reported",
2643
+ queued: true,
2644
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-align",
2645
+ task: "3D scene alignment"
2646
+ },
2647
+ "sam3-3d-body": {
2648
+ category: "3d",
2649
+ defaultOptions: {
2650
+ export_meshes: true,
2651
+ include_3d_keypoints: true,
2652
+ include_mhr_params: true
2653
+ },
2654
+ description: "Reconstruct human body meshes and keypoints from a single image.",
2655
+ endpoint: "fal-ai/sam-3/3d-body",
2656
+ inputField: "image_url",
2657
+ inputKind: "image",
2658
+ name: "SAM 3D Body",
2659
+ outputKeys: ["model_glb", "visualization", "meshes", "metadata"],
2660
+ // fal publishes a figure but not what it counts. Encoding it as `call`
2661
+ // would assert a per-request price we cannot support, and this endpoint
2662
+ // returns several outputs — the same shape that made `seedream-layerize`
2663
+ // under-report by a factor of its layer count. `metered` reports null
2664
+ // rather than a number that is right only on one reading.
2665
+ price: { kind: "metered" },
2666
+ pricing: "$0.02 per unit; fal does not define a unit, so no estimate is reported",
2667
+ queued: true,
2668
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-body",
2669
+ task: "single-image 3D body reconstruction"
2670
+ },
2671
+ "sam3-3d-objects": {
2672
+ category: "3d",
2673
+ defaultOptions: {
2674
+ prompt: "car"
2675
+ },
2676
+ description: "Reconstruct one or more 3D objects from an image and prompts.",
2677
+ endpoint: "fal-ai/sam-3/3d-objects",
2678
+ inputField: "image_url",
2679
+ inputKind: "image",
2680
+ name: "SAM 3D Objects",
2681
+ outputKeys: [
2682
+ "gaussian_splat",
2683
+ "model_glb",
2684
+ "metadata",
2685
+ "individual_splats",
2686
+ "individual_glbs",
2687
+ "artifacts_zip"
2688
+ ],
2689
+ // fal publishes a figure but not what it counts. Encoding it as `call`
2690
+ // would assert a per-request price we cannot support, and this endpoint
2691
+ // returns several outputs — the same shape that made `seedream-layerize`
2692
+ // under-report by a factor of its layer count. `metered` reports null
2693
+ // rather than a number that is right only on one reading.
2694
+ price: { kind: "metered" },
2695
+ pricing: "$0.02 per unit; fal does not define a unit, and this endpoint returns one mesh per detected object, so no estimate is reported",
2696
+ queued: true,
2697
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-objects",
2698
+ task: "single-image 3D object reconstruction"
2699
+ },
2700
+ "seedream-layerize": {
2701
+ category: "layers",
2702
+ description: "Split an image into editable Seedream layers plus flattened previews.",
2703
+ endpoint: "bytedance/seedream/v5/pro/layerize",
2704
+ inputField: "image_url",
2705
+ inputKind: "image",
2706
+ name: "Seedream v5 Pro Layerize",
2707
+ outputKeys: ["layers", "images"],
2708
+ price: { kind: "metered" },
2709
+ pricing: "$0.03375 per generated layer below 1536x1536 total area, $0.0675 per layer above; the listed rate is per layer, not per call",
2710
+ queued: true,
2711
+ sourceUrl: "https://fal.ai/models/bytedance/seedream/v5/pro/layerize",
2712
+ task: "image layer decomposition"
2713
+ },
2714
+ "seedvr-seamless": {
2715
+ category: "material",
2716
+ description: "Upscale a tiling texture while keeping its edges seamless. ",
2717
+ endpoint: "fal-ai/seedvr/upscale/image/seamless",
2718
+ inputField: "image_url",
2719
+ inputKind: "image",
2720
+ name: "SeedVR Seamless Upscale",
2721
+ outputKeys: ["image"],
2722
+ price: { kind: "megapixel", usd: 25e-4 },
2723
+ pricing: "$0.0025/megapixel",
2724
+ queued: true,
2725
+ sourceUrl: "https://fal.ai/models/fal-ai/seedvr/upscale/image/seamless",
2726
+ task: "seamless texture upscaling"
2727
+ },
2728
+ "trellis-2": {
2729
+ category: "3d",
2730
+ description: "Reconstruct a textured 3D mesh from a single image, with UV unwrapping.",
2731
+ endpoint: "fal-ai/trellis-2",
2732
+ inputField: "image_url",
2733
+ inputKind: "image",
2734
+ name: "TRELLIS 2",
2735
+ outputKeys: ["model_glb"],
2736
+ price: { kind: "call", usd: 0.3 },
2737
+ pricing: "$0.30 at the default 1024p; $0.25 at 512p, $0.35 at 1536p",
2738
+ queued: true,
2739
+ sourceUrl: "https://fal.ai/models/fal-ai/trellis-2",
2740
+ task: "single-image 3D reconstruction"
2741
+ }
2742
+ };
2743
+
2744
+ // src/tool-registry/background.ts
2745
+ var BACKGROUND_TOOLS = {
2746
+ "ben-v2": {
2747
+ category: "background",
2748
+ description: "Fast, high-quality background removal for images.",
2749
+ endpoint: "fal-ai/ben/v2/image",
2750
+ inputField: "image_url",
2751
+ inputKind: "image",
2752
+ name: "BEN v2 Background Removal",
2753
+ outputKeys: ["image"],
2754
+ price: { kind: "megapixel", usd: 0.025 },
2755
+ pricing: "$0.025/megapixel",
2756
+ sourceUrl: "https://fal.ai/models/fal-ai/ben/v2/image",
2757
+ task: "image background removal"
2758
+ },
2295
2759
  birefnet: {
2296
2760
  category: "background",
2297
2761
  defaultOptions: {
@@ -2304,41 +2768,455 @@ var FAL_TOOLS = {
2304
2768
  endpoint: "fal-ai/birefnet/v2",
2305
2769
  inputField: "image_url",
2306
2770
  inputKind: "image",
2307
- name: "BirefNet Background Removal",
2308
- outputKeys: ["image", "mask_image"],
2309
- pricing: "$0/compute-second listed by fal",
2310
- sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
2311
- task: "image background removal"
2771
+ name: "BirefNet Background Removal",
2772
+ outputKeys: ["image", "mask_image"],
2773
+ price: { kind: "metered" },
2774
+ pricing: "$0/compute-second listed by fal",
2775
+ sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
2776
+ task: "image background removal"
2777
+ },
2778
+ "bria-rmbg": {
2779
+ category: "background",
2780
+ description: "Commercial-safe background removal for images.",
2781
+ endpoint: "fal-ai/bria/background/remove",
2782
+ inputField: "image_url",
2783
+ inputKind: "image",
2784
+ name: "Bria RMBG 2.0",
2785
+ outputKeys: ["image"],
2786
+ price: { kind: "call", usd: 0.018 },
2787
+ pricing: "$0.018/generation",
2788
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/background/remove",
2789
+ task: "image background removal"
2790
+ },
2791
+ "bria-video-rmbg": {
2792
+ category: "background",
2793
+ defaultOptions: {
2794
+ background_color: "Black",
2795
+ output_container_and_codec: "webm_vp9",
2796
+ preserve_audio: true
2797
+ },
2798
+ description: "Remove video backgrounds with configurable output container.",
2799
+ endpoint: "bria/video/background-removal",
2800
+ inputField: "video_url",
2801
+ inputKind: "video",
2802
+ name: "Bria Video Background Removal",
2803
+ outputKeys: ["video"],
2804
+ price: { kind: "second", usd: 425e-5 },
2805
+ pricing: "$0.00425/sec",
2806
+ queued: true,
2807
+ sourceUrl: "https://fal.ai/models/bria/video/background-removal",
2808
+ task: "video background removal"
2809
+ },
2810
+ ddcolor: {
2811
+ category: "restoration",
2812
+ description: "Colourise black-and-white photographs.",
2813
+ endpoint: "fal-ai/ddcolor",
2814
+ inputField: "image_url",
2815
+ inputKind: "image",
2816
+ name: "DDColor Colourisation",
2817
+ outputKeys: ["image"],
2818
+ price: { kind: "megapixel", usd: 1e-3 },
2819
+ pricing: "$0.001/megapixel",
2820
+ sourceUrl: "https://fal.ai/models/fal-ai/ddcolor",
2821
+ task: "photo colourisation"
2822
+ },
2823
+ rembg: {
2824
+ category: "background",
2825
+ defaultOptions: {
2826
+ crop_to_bbox: false
2827
+ },
2828
+ description: "Generic image background removal utility.",
2829
+ endpoint: "fal-ai/imageutils/rembg",
2830
+ inputField: "image_url",
2831
+ inputKind: "image",
2832
+ name: "Remove Background",
2833
+ outputKeys: ["image"],
2834
+ price: { kind: "metered" },
2835
+ pricing: "$0/compute-second listed by fal",
2836
+ sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
2837
+ task: "image background removal"
2838
+ },
2839
+ "seedvr-upscale": {
2840
+ category: "restoration",
2841
+ defaultOptions: {
2842
+ output_format: "png"
2843
+ },
2844
+ description: "Diffusion image restoration and upscaling with SeedVR.",
2845
+ endpoint: "fal-ai/seedvr/upscale/image",
2846
+ inputField: "image_url",
2847
+ inputKind: "image",
2848
+ name: "SeedVR Image Upscale",
2849
+ outputKeys: ["image"],
2850
+ price: { kind: "megapixel", usd: 1e-3 },
2851
+ pricing: "$0.001/megapixel",
2852
+ queued: true,
2853
+ sourceUrl: "https://fal.ai/models/fal-ai/seedvr/upscale/image",
2854
+ task: "image restoration and upscaling"
2855
+ },
2856
+ "topaz-adjust": {
2857
+ category: "restoration",
2858
+ defaultOptions: {
2859
+ output_format: "png"
2860
+ },
2861
+ description: "Topaz exposure, white balance, and colour adjustment.",
2862
+ endpoint: "topaz/adjust/image",
2863
+ inputField: "image_url",
2864
+ inputKind: "image",
2865
+ name: "Topaz Adjust",
2866
+ outputKeys: ["image"],
2867
+ price: { kind: "megapixel", usd: 0.08 / 24 },
2868
+ pricing: "$0.08 per 24 output megapixels",
2869
+ queued: true,
2870
+ sourceUrl: "https://fal.ai/models/topaz/adjust/image",
2871
+ task: "tonal adjustment"
2872
+ },
2873
+ "topaz-creative": {
2874
+ category: "restoration",
2875
+ defaultOptions: {
2876
+ output_format: "png"
2877
+ },
2878
+ description: "Topaz Bloom upscaling, which reimagines detail rather than preserving it.",
2879
+ endpoint: "topaz/upscale/image/creative",
2880
+ inputField: "image_url",
2881
+ inputKind: "image",
2882
+ name: "Topaz Creative Upscale",
2883
+ outputKeys: ["image"],
2884
+ price: { kind: "megapixel", usd: 0.96 / 24 },
2885
+ pricing: "$0.96 per 24 output megapixels",
2886
+ queued: true,
2887
+ sourceUrl: "https://fal.ai/models/topaz/upscale/image/creative",
2888
+ task: "creative image upscaling"
2889
+ },
2890
+ "topaz-denoise": {
2891
+ category: "restoration",
2892
+ defaultOptions: {
2893
+ output_format: "png"
2894
+ },
2895
+ description: "Topaz noise reduction at source resolution.",
2896
+ endpoint: "topaz/denoise/image",
2897
+ inputField: "image_url",
2898
+ inputKind: "image",
2899
+ name: "Topaz Denoise",
2900
+ outputKeys: ["image"],
2901
+ price: { kind: "megapixel", usd: 0.08 / 24 },
2902
+ pricing: "$0.08 per 24 output megapixels at the default Normal model; $0.16 with Denoise Max",
2903
+ queued: true,
2904
+ sourceUrl: "https://fal.ai/models/topaz/denoise/image",
2905
+ task: "image denoising"
2906
+ },
2907
+ "topaz-generative": {
2908
+ category: "restoration",
2909
+ defaultOptions: {
2910
+ output_format: "png"
2911
+ },
2912
+ description: "Topaz Wonder upscaling, which synthesises plausible detail as it enlarges.",
2913
+ endpoint: "topaz/upscale/image/generative",
2914
+ inputField: "image_url",
2915
+ inputKind: "image",
2916
+ name: "Topaz Generative Upscale",
2917
+ outputKeys: ["image"],
2918
+ price: { kind: "megapixel", usd: 0.24 / 24 },
2919
+ pricing: "$0.24 per 24 output megapixels at the default Wonder 3; $0.48 with Wonder, Wonder 2, Standard MAX, Redefine or Recover 3",
2920
+ queued: true,
2921
+ sourceUrl: "https://fal.ai/models/topaz/upscale/image/generative",
2922
+ task: "generative image upscaling"
2923
+ },
2924
+ "topaz-image": {
2925
+ category: "upscale",
2926
+ defaultOptions: {
2927
+ model: "Standard V2",
2928
+ output_format: "jpeg",
2929
+ upscale_factor: 2
2930
+ },
2931
+ description: "Professional Topaz image enhancement and upscaling.",
2932
+ endpoint: "fal-ai/topaz/upscale/image",
2933
+ inputField: "image_url",
2934
+ inputKind: "image",
2935
+ name: "Topaz Image Upscale",
2936
+ outputKeys: ["image"],
2937
+ price: { kind: "megapixel", usd: 0.08 / 24 },
2938
+ pricing: "$0.08 for output up to 24MP; $0.16 to 48MP, $0.32 to 96MP, up to $1.36 at 512MP",
2939
+ queued: true,
2940
+ sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/image",
2941
+ task: "image enhancement"
2942
+ },
2943
+ "topaz-precision": {
2944
+ category: "restoration",
2945
+ defaultOptions: {
2946
+ output_format: "png"
2947
+ },
2948
+ description: "Topaz upscaling that preserves the original detail rather than inventing it.",
2949
+ endpoint: "topaz/upscale/image/precision",
2950
+ inputField: "image_url",
2951
+ inputKind: "image",
2952
+ name: "Topaz Precision Upscale",
2953
+ outputKeys: ["image"],
2954
+ price: { kind: "megapixel", usd: 0.08 / 24 },
2955
+ pricing: "$0.08 per 24 output megapixels, any precision model",
2956
+ queued: true,
2957
+ sourceUrl: "https://fal.ai/models/topaz/upscale/image/precision",
2958
+ task: "faithful image upscaling"
2959
+ },
2960
+ "topaz-restore": {
2961
+ category: "restoration",
2962
+ defaultOptions: {
2963
+ output_format: "png"
2964
+ },
2965
+ description: "Topaz restoration of damaged or degraded photographs at source resolution.",
2966
+ endpoint: "topaz/restore/image",
2967
+ inputField: "image_url",
2968
+ inputKind: "image",
2969
+ name: "Topaz Restore",
2970
+ outputKeys: ["image"],
2971
+ price: { kind: "megapixel", usd: 0.48 / 24 },
2972
+ pricing: "$0.48 per 24 output megapixels at the default Recover 3; $0.08 with Dust-Scratch V2",
2973
+ queued: true,
2974
+ sourceUrl: "https://fal.ai/models/topaz/restore/image",
2975
+ task: "photo restoration"
2976
+ },
2977
+ "topaz-sharpen": {
2978
+ category: "restoration",
2979
+ defaultOptions: {
2980
+ output_format: "png"
2981
+ },
2982
+ description: "Topaz deblurring and sharpening at source resolution.",
2983
+ endpoint: "topaz/sharpen/image",
2984
+ inputField: "image_url",
2985
+ inputKind: "image",
2986
+ name: "Topaz Sharpen",
2987
+ outputKeys: ["image"],
2988
+ price: { kind: "megapixel", usd: 0.08 / 24 },
2989
+ pricing: "$0.08 per 24 output megapixels at the default Standard model; $0.16 with Super Focus",
2990
+ queued: true,
2991
+ sourceUrl: "https://fal.ai/models/topaz/sharpen/image",
2992
+ task: "image sharpening"
2993
+ },
2994
+ "topaz-transparent": {
2995
+ category: "restoration",
2996
+ description: "Topaz upscaling that preserves the alpha channel end to end.",
2997
+ endpoint: "topaz/upscale/image/transparent",
2998
+ inputField: "image_url",
2999
+ inputKind: "image",
3000
+ name: "Topaz Transparent Upscale",
3001
+ outputKeys: ["image"],
3002
+ price: { kind: "megapixel", usd: 0.08 / 24 },
3003
+ pricing: "$0.08 per 24 output megapixels",
3004
+ queued: true,
3005
+ sourceUrl: "https://fal.ai/models/topaz/upscale/image/transparent",
3006
+ task: "transparent image upscaling"
3007
+ },
3008
+ "topaz-video": {
3009
+ category: "upscale",
3010
+ defaultOptions: {
3011
+ model: "Proteus",
3012
+ upscale_factor: 2
3013
+ },
3014
+ description: "Professional Topaz video enhancement and upscaling.",
3015
+ endpoint: "fal-ai/topaz/upscale/video",
3016
+ inputField: "video_url",
3017
+ inputKind: "video",
3018
+ name: "Topaz Video Upscale",
3019
+ outputKeys: ["video"],
3020
+ price: { kind: "second", usd: 0.01 },
3021
+ pricing: "$0.01/sec up to 720p; $0.02 to 1080p, $0.08 above, doubled at 60fps, halved with Gaia 2",
3022
+ queued: true,
3023
+ sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
3024
+ task: "video enhancement"
3025
+ }
3026
+ };
3027
+
3028
+ // src/tool-registry/editing.ts
3029
+ var EDITING_TOOLS = {
3030
+ "bria-eraser": {
3031
+ category: "erase",
3032
+ description: "Commercial-safe removal of a masked region, inpainted from surrounding context.",
3033
+ endpoint: "fal-ai/bria/eraser",
3034
+ inputField: "image_url",
3035
+ inputKind: "image",
3036
+ name: "Bria Eraser",
3037
+ outputKeys: ["image"],
3038
+ price: { kind: "call", usd: 0.04 },
3039
+ pricing: "$0.04/generation",
3040
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/eraser",
3041
+ task: "masked object erasure"
3042
+ },
3043
+ "bria-expand": {
3044
+ category: "reframe",
3045
+ description: "Expand an image onto a larger canvas, generating the new border area.",
3046
+ endpoint: "fal-ai/bria/expand",
3047
+ inputField: "image_url",
3048
+ inputKind: "image",
3049
+ name: "Bria Image Expansion",
3050
+ outputKeys: ["image"],
3051
+ price: { kind: "call", usd: 0.04 },
3052
+ pricing: "$0.04/generation",
3053
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/expand",
3054
+ task: "image outpainting"
3055
+ },
3056
+ "bria-genfill": {
3057
+ category: "erase",
3058
+ description: "Generative fill of a masked region from a prompt, commercial-safe.",
3059
+ endpoint: "fal-ai/bria/genfill",
3060
+ inputField: "image_url",
3061
+ inputKind: "image",
3062
+ name: "Bria GenFill",
3063
+ outputKeys: ["images"],
3064
+ price: { kind: "call", usd: 0.04 },
3065
+ pricing: "$0.04/generation",
3066
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/genfill",
3067
+ task: "masked generative fill"
3068
+ },
3069
+ "finegrain-eraser": {
3070
+ category: "erase",
3071
+ description: "Remove a prompted object and its shadows and reflections, filling the gap.",
3072
+ endpoint: "fal-ai/finegrain-eraser",
3073
+ inputField: "image_url",
3074
+ inputKind: "image",
3075
+ name: "Finegrain Object Eraser",
3076
+ outputKeys: ["image"],
3077
+ price: { kind: "call", usd: 0.27 },
3078
+ pricing: "$0.27/image at the default standard mode; $0.18 express, $0.36 premium",
3079
+ sourceUrl: "https://fal.ai/models/fal-ai/finegrain-eraser",
3080
+ task: "prompted object erasure"
3081
+ },
3082
+ "flux-outpaint": {
3083
+ category: "reframe",
3084
+ description: "Outpaint an image by a per-edge pixel expansion with FLUX.2 pro.",
3085
+ endpoint: "fal-ai/flux-2-pro/outpaint",
3086
+ inputField: "image_url",
3087
+ inputKind: "image",
3088
+ name: "FLUX.2 Pro Outpaint",
3089
+ outputKeys: ["images"],
3090
+ price: { kind: "metered" },
3091
+ pricing: "$0.03 for the first output megapixel, then $0.015 per extra megapixel of input and output, rounded up",
3092
+ sourceUrl: "https://fal.ai/models/fal-ai/flux-2-pro/outpaint",
3093
+ task: "image outpainting"
3094
+ },
3095
+ "iclight-v2": {
3096
+ category: "relight",
3097
+ description: "Relight a subject from a prompt, harmonising it with a new light direction.",
3098
+ endpoint: "fal-ai/iclight-v2",
3099
+ inputField: "image_url",
3100
+ inputKind: "image",
3101
+ name: "IC-Light v2",
3102
+ outputKeys: ["images"],
3103
+ price: { kind: "megapixel", usd: 0.1 },
3104
+ pricing: "$0.10/megapixel",
3105
+ sourceUrl: "https://fal.ai/models/fal-ai/iclight-v2",
3106
+ task: "prompted relighting"
3107
+ },
3108
+ "ideogram-reframe": {
3109
+ category: "reframe",
3110
+ description: "Reframe an image to a new aspect ratio, generating the fill.",
3111
+ endpoint: "fal-ai/ideogram/v3/reframe",
3112
+ inputField: "image_url",
3113
+ inputKind: "image",
3114
+ name: "Ideogram v3 Reframe",
3115
+ outputKeys: ["images"],
3116
+ price: { kind: "call", usd: 0.06 },
3117
+ pricing: "$0.06/image at the default BALANCED speed; $0.03 turbo, $0.09 quality",
3118
+ sourceUrl: "https://fal.ai/models/fal-ai/ideogram/v3/reframe",
3119
+ task: "aspect ratio reframing"
3120
+ },
3121
+ "lighting-restoration": {
3122
+ category: "relight",
3123
+ description: "Restore natural, even lighting across one or more input images.",
3124
+ endpoint: "fal-ai/qwen-image-edit-plus-lora-gallery/lighting-restoration",
3125
+ inputField: "image_urls",
3126
+ inputKind: "images",
3127
+ name: "Qwen Lighting Restoration",
3128
+ outputKeys: ["images"],
3129
+ price: { kind: "megapixel", usd: 0.035 },
3130
+ pricing: "$0.035/megapixel",
3131
+ sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-edit-plus-lora-gallery/lighting-restoration",
3132
+ task: "lighting restoration"
3133
+ },
3134
+ "object-removal": {
3135
+ category: "erase",
3136
+ description: "Remove a prompted object from an image and fill the gap.",
3137
+ endpoint: "fal-ai/object-removal",
3138
+ inputField: "image_url",
3139
+ inputKind: "image",
3140
+ name: "Object Removal",
3141
+ outputKeys: ["images"],
3142
+ price: { kind: "call", usd: 0.024 },
3143
+ pricing: "$0.024/image at the default best quality; $0.006 low, $0.012 medium, $0.018 high",
3144
+ sourceUrl: "https://fal.ai/models/fal-ai/object-removal",
3145
+ task: "prompted object removal"
2312
3146
  },
2313
- "bria-rmbg": {
2314
- category: "background",
2315
- description: "Commercial-safe background removal for images.",
2316
- endpoint: "fal-ai/bria/background/remove",
3147
+ "object-removal-bbox": {
3148
+ category: "erase",
3149
+ description: "Remove whatever falls inside supplied bounding boxes.",
3150
+ endpoint: "fal-ai/object-removal/bbox",
2317
3151
  inputField: "image_url",
2318
3152
  inputKind: "image",
2319
- name: "Bria RMBG 2.0",
2320
- outputKeys: ["image"],
2321
- pricing: "$0.02/image",
2322
- sourceUrl: "https://fal.ai/models/fal-ai/bria/background/remove",
2323
- task: "image background removal"
3153
+ name: "Object Removal by Box",
3154
+ outputKeys: ["images"],
3155
+ price: { kind: "call", usd: 0.024 },
3156
+ pricing: "$0.024/image at the default best quality; $0.006 low, $0.012 medium, $0.018 high",
3157
+ sourceUrl: "https://fal.ai/models/fal-ai/object-removal/bbox",
3158
+ task: "bounding box object removal"
2324
3159
  },
2325
- "bria-video-rmbg": {
2326
- category: "background",
3160
+ "object-removal-mask": {
3161
+ category: "erase",
3162
+ description: "Remove the masked region of an image and fill the gap.",
3163
+ endpoint: "fal-ai/object-removal/mask",
3164
+ inputField: "image_url",
3165
+ inputKind: "image",
3166
+ name: "Object Removal by Mask",
3167
+ outputKeys: ["images"],
3168
+ price: { kind: "call", usd: 0.024 },
3169
+ pricing: "$0.024/image at the default best quality; $0.006 low, $0.012 medium, $0.018 high",
3170
+ sourceUrl: "https://fal.ai/models/fal-ai/object-removal/mask",
3171
+ task: "masked object removal"
3172
+ },
3173
+ "remove-lighting": {
3174
+ category: "relight",
3175
+ description: "Strip baked-in lighting and shadows to leave a flat, neutral surface.",
3176
+ endpoint: "fal-ai/qwen-image-edit-plus-lora-gallery/remove-lighting",
3177
+ inputField: "image_urls",
3178
+ inputKind: "images",
3179
+ name: "Qwen Remove Lighting",
3180
+ outputKeys: ["images"],
3181
+ price: { kind: "megapixel", usd: 0.035 },
3182
+ pricing: "$0.035/megapixel",
3183
+ sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-edit-plus-lora-gallery/remove-lighting",
3184
+ task: "lighting removal"
3185
+ },
3186
+ "smart-resize": {
3187
+ category: "reframe",
3188
+ description: "Resize one image to several target sizes, recomposing rather than cropping.",
3189
+ endpoint: "fal-ai/smart-resize",
3190
+ inputField: "image_url",
3191
+ inputKind: "image",
3192
+ name: "Smart Resize",
3193
+ outputKeys: ["images", "results"],
3194
+ price: { kind: "metered" },
3195
+ pricing: "$0.15 per output image, doubled at 4K, plus a $0.05 vision analysis fee per request",
3196
+ queued: true,
3197
+ sourceUrl: "https://fal.ai/models/fal-ai/smart-resize",
3198
+ task: "multi-size recomposition"
3199
+ },
3200
+ "text-removal": {
3201
+ category: "erase",
2327
3202
  defaultOptions: {
2328
- background_color: "Black",
2329
- output_container_and_codec: "webm_vp9",
2330
- preserve_audio: true
3203
+ output_format: "png"
2331
3204
  },
2332
- description: "Remove video backgrounds with configurable output container.",
2333
- endpoint: "bria/video/background-removal",
2334
- inputField: "video_url",
2335
- inputKind: "video",
2336
- name: "Bria Video Background Removal",
2337
- outputKeys: ["video"],
2338
- pricing: "$0.14/sec",
2339
- sourceUrl: "https://fal.ai/models/bria/video/background-removal",
2340
- task: "video background removal"
2341
- },
3205
+ description: "Remove all rendered text from an image, rebuilding what sat behind it.",
3206
+ endpoint: "fal-ai/image-editing/text-removal",
3207
+ inputField: "image_url",
3208
+ inputKind: "image",
3209
+ name: "Text Removal",
3210
+ outputKeys: ["images"],
3211
+ price: { kind: "call", usd: 0.04 },
3212
+ pricing: "$0.04/image",
3213
+ sourceUrl: "https://fal.ai/models/fal-ai/image-editing/text-removal",
3214
+ task: "text removal"
3215
+ }
3216
+ };
3217
+
3218
+ // src/tool-registry/structure.ts
3219
+ var STRUCTURE_TOOLS = {
2342
3220
  "depth-anything": {
2343
3221
  category: "preprocess",
2344
3222
  description: "Generate Depth Anything v2 depth maps from input images.",
@@ -2347,10 +3225,37 @@ var FAL_TOOLS = {
2347
3225
  inputKind: "image",
2348
3226
  name: "Depth Anything v2 Preprocessor",
2349
3227
  outputKeys: ["image"],
3228
+ price: { kind: "metered" },
2350
3229
  pricing: "$0/compute-second listed by fal",
2351
3230
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/depth-anything/v2",
2352
3231
  task: "depth preprocessing"
2353
3232
  },
3233
+ dwpose: {
3234
+ category: "preprocess",
3235
+ description: "Extract body, hand, and face pose skeletons from an image.",
3236
+ endpoint: "fal-ai/dwpose",
3237
+ inputField: "image_url",
3238
+ inputKind: "image",
3239
+ name: "DWPose Estimation",
3240
+ outputKeys: ["image"],
3241
+ price: { kind: "second", usd: 6e-4 },
3242
+ pricing: "$0.0006/compute-second",
3243
+ sourceUrl: "https://fal.ai/models/fal-ai/dwpose",
3244
+ task: "pose preprocessing"
3245
+ },
3246
+ hed: {
3247
+ category: "preprocess",
3248
+ description: "Generate HED soft edge maps for ControlNet-style workflows.",
3249
+ endpoint: "fal-ai/image-preprocessors/hed",
3250
+ inputField: "image_url",
3251
+ inputKind: "image",
3252
+ name: "HED Edge Preprocessor",
3253
+ outputKeys: ["image"],
3254
+ price: { kind: "metered" },
3255
+ pricing: "$0/compute-second listed by fal",
3256
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/hed",
3257
+ task: "edge preprocessing"
3258
+ },
2354
3259
  lineart: {
2355
3260
  category: "preprocess",
2356
3261
  defaultOptions: {
@@ -2362,6 +3267,7 @@ var FAL_TOOLS = {
2362
3267
  inputKind: "image",
2363
3268
  name: "Line Art Preprocessor",
2364
3269
  outputKeys: ["image"],
3270
+ price: { kind: "metered" },
2365
3271
  pricing: "$0/compute-second listed by fal",
2366
3272
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/lineart",
2367
3273
  task: "image preprocessing"
@@ -2378,6 +3284,7 @@ var FAL_TOOLS = {
2378
3284
  inputKind: "image",
2379
3285
  name: "Marigold Depth Estimation",
2380
3286
  outputKeys: ["image"],
3287
+ price: { kind: "metered" },
2381
3288
  pricing: "$0/compute-second listed by fal",
2382
3289
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/marigold-depth",
2383
3290
  task: "depth map"
@@ -2394,6 +3301,7 @@ var FAL_TOOLS = {
2394
3301
  inputKind: "image",
2395
3302
  name: "MiDaS Depth Estimation",
2396
3303
  outputKeys: ["image"],
3304
+ price: { kind: "metered" },
2397
3305
  pricing: "$0/compute-second listed by fal",
2398
3306
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/depth",
2399
3307
  task: "depth map"
@@ -2406,36 +3314,36 @@ var FAL_TOOLS = {
2406
3314
  inputKind: "image",
2407
3315
  name: "MiDaS Preprocessor",
2408
3316
  outputKeys: ["depth_map", "normal_map"],
3317
+ price: { kind: "metered" },
2409
3318
  pricing: "$0/compute-second listed by fal",
2410
3319
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/midas",
2411
3320
  task: "depth and normal preprocessing"
2412
3321
  },
2413
- nsfw: {
2414
- category: "moderation",
2415
- description: "Predict whether one or more images contain NSFW concepts.",
2416
- endpoint: "fal-ai/x-ailab/nsfw",
2417
- inputField: "image_urls",
2418
- inputKind: "images",
2419
- name: "NSFW Checker",
2420
- outputKeys: ["has_nsfw_concepts"],
2421
- pricing: "$0.001/image",
2422
- sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw",
2423
- task: "vision moderation"
3322
+ mlsd: {
3323
+ category: "preprocess",
3324
+ description: "Detect straight line segments for architectural and interior control maps.",
3325
+ endpoint: "fal-ai/image-preprocessors/mlsd",
3326
+ inputField: "image_url",
3327
+ inputKind: "image",
3328
+ name: "M-LSD Line Preprocessor",
3329
+ outputKeys: ["image"],
3330
+ price: { kind: "metered" },
3331
+ pricing: "$0/compute-second listed by fal",
3332
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/mlsd",
3333
+ task: "line segment preprocessing"
2424
3334
  },
2425
- rembg: {
2426
- category: "background",
2427
- defaultOptions: {
2428
- crop_to_bbox: false
2429
- },
2430
- description: "Generic image background removal utility.",
2431
- endpoint: "fal-ai/imageutils/rembg",
3335
+ pidi: {
3336
+ category: "preprocess",
3337
+ description: "Generate PiDiNet soft edge maps from an input image.",
3338
+ endpoint: "fal-ai/image-preprocessors/pidi",
2432
3339
  inputField: "image_url",
2433
3340
  inputKind: "image",
2434
- name: "Remove Background",
3341
+ name: "PiDiNet Edge Preprocessor",
2435
3342
  outputKeys: ["image"],
3343
+ price: { kind: "metered" },
2436
3344
  pricing: "$0/compute-second listed by fal",
2437
- sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
2438
- task: "image background removal"
3345
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/pidi",
3346
+ task: "edge preprocessing"
2439
3347
  },
2440
3348
  "sam-preprocessor": {
2441
3349
  category: "preprocess",
@@ -2445,6 +3353,7 @@ var FAL_TOOLS = {
2445
3353
  inputKind: "image",
2446
3354
  name: "SAM Preprocessor",
2447
3355
  outputKeys: ["image"],
3356
+ price: { kind: "metered" },
2448
3357
  pricing: "$0/compute-second listed by fal",
2449
3358
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/sam",
2450
3359
  task: "segmentation preprocessing"
@@ -2464,7 +3373,9 @@ var FAL_TOOLS = {
2464
3373
  inputKind: "image",
2465
3374
  name: "SAM 2 Auto Segment",
2466
3375
  outputKeys: ["combined_mask", "individual_masks"],
3376
+ price: { kind: "metered" },
2467
3377
  pricing: "$0/compute-second listed by fal",
3378
+ queued: true,
2468
3379
  sourceUrl: "https://fal.ai/models/fal-ai/sam2/auto-segment",
2469
3380
  task: "automatic image segmentation"
2470
3381
  },
@@ -2481,7 +3392,8 @@ var FAL_TOOLS = {
2481
3392
  inputKind: "image",
2482
3393
  name: "SAM 3.1 Image",
2483
3394
  outputKeys: ["image", "masks", "metadata", "scores", "boxes"],
2484
- pricing: "$0.005/request",
3395
+ price: { kind: "call", usd: 0.01 },
3396
+ pricing: "$0.01/request",
2485
3397
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/image",
2486
3398
  task: "promptable image segmentation"
2487
3399
  },
@@ -2497,61 +3409,12 @@ var FAL_TOOLS = {
2497
3409
  inputKind: "video",
2498
3410
  name: "SAM 3.1 Video",
2499
3411
  outputKeys: ["video", "boundingbox_frames_zip"],
2500
- pricing: "fal pricing varies by frame count",
3412
+ price: { kind: "metered" },
3413
+ pricing: "$0.01/16 frames of video input",
3414
+ queued: true,
2501
3415
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/video",
2502
3416
  task: "multi-object video segmentation"
2503
3417
  },
2504
- "sam3-3d-align": {
2505
- category: "3d",
2506
- description: "Align SAM 3D objects and bodies into a shared scene.",
2507
- endpoint: "fal-ai/sam-3/3d-align",
2508
- inputField: "image_url",
2509
- inputKind: "image",
2510
- name: "SAM 3D Align",
2511
- outputKeys: ["scene_glb", "metadata", "artifacts_zip"],
2512
- pricing: "fal pricing varies by scene",
2513
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-align",
2514
- task: "3D scene alignment"
2515
- },
2516
- "sam3-3d-body": {
2517
- category: "3d",
2518
- defaultOptions: {
2519
- export_meshes: true,
2520
- include_3d_keypoints: true,
2521
- include_mhr_params: true
2522
- },
2523
- description: "Reconstruct human body meshes and keypoints from a single image.",
2524
- endpoint: "fal-ai/sam-3/3d-body",
2525
- inputField: "image_url",
2526
- inputKind: "image",
2527
- name: "SAM 3D Body",
2528
- outputKeys: ["model_glb", "visualization", "meshes", "metadata"],
2529
- pricing: "$0.015/inference",
2530
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-body",
2531
- task: "single-image 3D body reconstruction"
2532
- },
2533
- "sam3-3d-objects": {
2534
- category: "3d",
2535
- defaultOptions: {
2536
- prompt: "car"
2537
- },
2538
- description: "Reconstruct one or more 3D objects from an image and prompts.",
2539
- endpoint: "fal-ai/sam-3/3d-objects",
2540
- inputField: "image_url",
2541
- inputKind: "image",
2542
- name: "SAM 3D Objects",
2543
- outputKeys: [
2544
- "gaussian_splat",
2545
- "model_glb",
2546
- "metadata",
2547
- "individual_splats",
2548
- "individual_glbs",
2549
- "artifacts_zip"
2550
- ],
2551
- pricing: "$0.02/generation",
2552
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-objects",
2553
- task: "single-image 3D object reconstruction"
2554
- },
2555
3418
  "sam3-image": {
2556
3419
  category: "segmentation",
2557
3420
  defaultOptions: {
@@ -2565,6 +3428,7 @@ var FAL_TOOLS = {
2565
3428
  inputKind: "image",
2566
3429
  name: "SAM 3 Image",
2567
3430
  outputKeys: ["image", "masks", "metadata", "scores", "boxes"],
3431
+ price: { kind: "call", usd: 5e-3 },
2568
3432
  pricing: "$0.005/request",
2569
3433
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/image",
2570
3434
  task: "promptable image segmentation"
@@ -2580,7 +3444,8 @@ var FAL_TOOLS = {
2580
3444
  inputField: "image_url",
2581
3445
  inputKind: "image",
2582
3446
  name: "SAM 3 Image RLE",
2583
- outputKeys: ["rle_masks", "metadata", "scores", "boxes"],
3447
+ outputKeys: ["rle", "scores", "boxes", "metadata"],
3448
+ price: { kind: "call", usd: 5e-3 },
2584
3449
  pricing: "$0.005/request",
2585
3450
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/image-rle",
2586
3451
  task: "promptable image segmentation to RLE"
@@ -2599,7 +3464,9 @@ var FAL_TOOLS = {
2599
3464
  inputKind: "video",
2600
3465
  name: "SAM 3 Video",
2601
3466
  outputKeys: ["video", "boundingbox_frames_zip"],
2602
- pricing: "$0.005/16 frames",
3467
+ price: { kind: "metered" },
3468
+ pricing: "$0.005/16 frames of video input",
3469
+ queued: true,
2603
3470
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video",
2604
3471
  task: "promptable video segmentation"
2605
3472
  },
@@ -2615,45 +3482,65 @@ var FAL_TOOLS = {
2615
3482
  inputField: "video_url",
2616
3483
  inputKind: "video",
2617
3484
  name: "SAM 3 Video RLE",
2618
- outputKeys: ["rle_masks", "metadata"],
2619
- pricing: "$0.005/16 frames",
3485
+ // fal's video-rle endpoint no longer returns RLE data: its Output schema is
3486
+ // now identical to sam3-video's, so this entry differs only by endpoint.
3487
+ outputKeys: ["video", "boundingbox_frames_zip"],
3488
+ price: { kind: "metered" },
3489
+ pricing: "$0.005/16 frames of video",
3490
+ queued: true,
2620
3491
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video-rle",
2621
3492
  task: "promptable video segmentation to RLE"
2622
3493
  },
2623
- "topaz-image": {
2624
- category: "upscale",
2625
- defaultOptions: {
2626
- model: "Standard V2",
2627
- output_format: "jpeg",
2628
- upscale_factor: 2
2629
- },
2630
- description: "Professional Topaz image enhancement and upscaling.",
2631
- endpoint: "fal-ai/topaz/upscale/image",
3494
+ scribble: {
3495
+ category: "preprocess",
3496
+ description: "Turn an image into a scribble-style control map.",
3497
+ endpoint: "fal-ai/image-preprocessors/scribble",
2632
3498
  inputField: "image_url",
2633
3499
  inputKind: "image",
2634
- name: "Topaz Image Upscale",
3500
+ name: "Scribble Preprocessor",
2635
3501
  outputKeys: ["image"],
2636
- pricing: "$0.08+ by output megapixels",
2637
- sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/image",
2638
- task: "image enhancement"
3502
+ price: { kind: "metered" },
3503
+ pricing: "$0/compute-second listed by fal",
3504
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/scribble",
3505
+ task: "scribble preprocessing"
2639
3506
  },
2640
- "topaz-video": {
2641
- category: "upscale",
2642
- defaultOptions: {
2643
- model: "Proteus",
2644
- upscale_factor: 2
2645
- },
2646
- description: "Professional Topaz video enhancement and upscaling.",
2647
- endpoint: "fal-ai/topaz/upscale/video",
2648
- inputField: "video_url",
2649
- inputKind: "video",
2650
- name: "Topaz Video Upscale",
2651
- outputKeys: ["video"],
2652
- pricing: "$0.01-$0.08/sec by output resolution",
2653
- sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
2654
- task: "video enhancement"
3507
+ teed: {
3508
+ category: "preprocess",
3509
+ description: "Generate TEED edge maps from an input image.",
3510
+ endpoint: "fal-ai/image-preprocessors/teed",
3511
+ inputField: "image_url",
3512
+ inputKind: "image",
3513
+ name: "TEED Edge Preprocessor",
3514
+ outputKeys: ["image"],
3515
+ price: { kind: "metered" },
3516
+ pricing: "$0/compute-second listed by fal",
3517
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/teed",
3518
+ task: "edge preprocessing"
3519
+ },
3520
+ "zoe-depth": {
3521
+ category: "preprocess",
3522
+ description: "Generate ZoeDepth metric depth maps from an input image.",
3523
+ endpoint: "fal-ai/image-preprocessors/zoe",
3524
+ inputField: "image_url",
3525
+ inputKind: "image",
3526
+ name: "ZoeDepth Preprocessor",
3527
+ outputKeys: ["image"],
3528
+ price: { kind: "metered" },
3529
+ pricing: "$0/compute-second listed by fal",
3530
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/zoe",
3531
+ task: "depth preprocessing"
2655
3532
  }
2656
3533
  };
3534
+
3535
+ // src/tools.ts
3536
+ var FAL_EXPLORE_CHECKED_AT = "2026-08-23";
3537
+ var FAL_TOOLS = {
3538
+ ...ANALYSIS_TOOLS,
3539
+ ...ASSET_TOOLS,
3540
+ ...BACKGROUND_TOOLS,
3541
+ ...EDITING_TOOLS,
3542
+ ...STRUCTURE_TOOLS
3543
+ };
2657
3544
  var FAL_TOOLS_CHECKED_AT = FAL_EXPLORE_CHECKED_AT;
2658
3545
  var FAL_TOOL_IDS = Object.keys(
2659
3546
  FAL_TOOLS
@@ -2680,9 +3567,110 @@ function buildFalToolRequest(options) {
2680
3567
  return { body, endpoint: tool.endpoint, tool };
2681
3568
  }
2682
3569
 
2683
- // src/server.ts
3570
+ // src/server-tools.ts
2684
3571
  var FAL_BASE_URL = "https://fal.run";
2685
3572
  var FAL_QUEUE_URL = "https://queue.fal.run";
3573
+ var POLL_INTERVAL_MS = 3e3;
3574
+ var MAX_POLL_ATTEMPTS = 160;
3575
+ function buildToolRequest(options) {
3576
+ try {
3577
+ return ok(buildFalToolRequest(options));
3578
+ } catch (error) {
3579
+ return err(
3580
+ new MotifError(error instanceof Error ? error.message : String(error), 0)
3581
+ );
3582
+ }
3583
+ }
3584
+ async function runTool(exec, options) {
3585
+ const request = buildToolRequest(options);
3586
+ if (request.isErr()) {
3587
+ return err(request.error);
3588
+ }
3589
+ const response = await exec.request(
3590
+ `${FAL_BASE_URL}/${request.value.endpoint}`,
3591
+ {
3592
+ body: JSON.stringify(request.value.body),
3593
+ method: "POST"
3594
+ }
3595
+ );
3596
+ if (response.isErr()) {
3597
+ return err(response.error);
3598
+ }
3599
+ const data = await response.value.json();
3600
+ return ok(isRecord(data) ? data : {});
3601
+ }
3602
+ async function submitTool(exec, options) {
3603
+ const request = buildToolRequest(options);
3604
+ if (request.isErr()) {
3605
+ return err(request.error);
3606
+ }
3607
+ const response = await exec.request(
3608
+ `${FAL_QUEUE_URL}/${request.value.endpoint}`,
3609
+ {
3610
+ body: JSON.stringify(request.value.body),
3611
+ method: "POST"
3612
+ }
3613
+ );
3614
+ if (response.isErr()) {
3615
+ return err(response.error);
3616
+ }
3617
+ const data = await response.value.json();
3618
+ const submission = parseQueueSubmission(data);
3619
+ return ok({
3620
+ endpoint: endpointFromQueueUrl(
3621
+ submission.responseUrl,
3622
+ request.value.endpoint
3623
+ ),
3624
+ requestId: submission.requestId
3625
+ });
3626
+ }
3627
+ async function checkToolStatus(exec, job) {
3628
+ return await exec.getJobStatus(job.endpoint, job.requestId);
3629
+ }
3630
+ async function getToolResult(exec, job) {
3631
+ const url = `${FAL_QUEUE_URL}/${job.endpoint}/requests/${job.requestId}`;
3632
+ const response = await exec.request(url);
3633
+ if (response.isErr()) {
3634
+ return err(response.error);
3635
+ }
3636
+ const data = await response.value.json();
3637
+ return ok(isRecord(data) ? data : {});
3638
+ }
3639
+ async function runToolQueued(exec, options, onProgress) {
3640
+ const job = await submitTool(exec, options);
3641
+ if (job.isErr()) {
3642
+ return err(job.error);
3643
+ }
3644
+ for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
3645
+ const status = await checkToolStatus(exec, job.value);
3646
+ if (status.isErr()) {
3647
+ return err(status.error);
3648
+ }
3649
+ onProgress?.(status.value.status, status.value.queuePosition);
3650
+ if (status.value.status === "completed") {
3651
+ return await getToolResult(exec, job.value);
3652
+ }
3653
+ if (status.value.status === "failed") {
3654
+ const message = status.value.error ?? "Queued tool run failed";
3655
+ return err(new MotifError(message, 0, void 0, job.value.requestId));
3656
+ }
3657
+ await new Promise((resolve) => {
3658
+ setTimeout(resolve, POLL_INTERVAL_MS);
3659
+ });
3660
+ }
3661
+ return err(
3662
+ new MotifError(
3663
+ "Queued tool run timed out",
3664
+ 0,
3665
+ void 0,
3666
+ job.value.requestId
3667
+ )
3668
+ );
3669
+ }
3670
+
3671
+ // src/server.ts
3672
+ var FAL_BASE_URL2 = "https://fal.run";
3673
+ var FAL_QUEUE_URL2 = "https://queue.fal.run";
2686
3674
  var FAL_API_URL = "https://api.fal.ai";
2687
3675
  var FAL_REST_URL = "https://rest.alpha.fal.ai";
2688
3676
  var FalClient = class {
@@ -2714,16 +3702,16 @@ var FalClient = class {
2714
3702
  try {
2715
3703
  built = buildGenerateBody(options);
2716
3704
  } catch (error) {
2717
- return err(toMotifError(error));
3705
+ return err2(toMotifError(error));
2718
3706
  }
2719
3707
  const { endpoint, body } = built;
2720
- const response = await this.request(`${FAL_BASE_URL}/${endpoint}`, {
3708
+ const response = await this.request(`${FAL_BASE_URL2}/${endpoint}`, {
2721
3709
  body: JSON.stringify(body),
2722
3710
  headers: this.ephemeralHeaders(options),
2723
3711
  method: "POST"
2724
3712
  });
2725
3713
  if (response.isErr()) {
2726
- return err(response.error);
3714
+ return err2(response.error);
2727
3715
  }
2728
3716
  const data = await response.value.json();
2729
3717
  return this.normalizeResponse(data);
@@ -2731,7 +3719,7 @@ var FalClient = class {
2731
3719
  async generateQueued(options) {
2732
3720
  const job = await this.submitGeneration(options);
2733
3721
  if (job.isErr()) {
2734
- return err(job.error);
3722
+ return err2(job.error);
2735
3723
  }
2736
3724
  const pollIntervalMs = 3e3;
2737
3725
  const maxAttempts = 160;
@@ -2741,20 +3729,20 @@ var FalClient = class {
2741
3729
  job.value.requestId
2742
3730
  );
2743
3731
  if (status.isErr()) {
2744
- return err(status.error);
3732
+ return err2(status.error);
2745
3733
  }
2746
3734
  if (status.value.status === "completed") {
2747
3735
  return await this.getJobResult(job.value.endpoint, job.value.requestId);
2748
3736
  }
2749
3737
  if (status.value.status === "failed") {
2750
3738
  const message = status.value.error ?? "Queued generation failed";
2751
- return err(new MotifError(message, 0, void 0, job.value.requestId));
3739
+ return err2(new MotifError(message, 0, void 0, job.value.requestId));
2752
3740
  }
2753
3741
  await new Promise((resolve) => {
2754
3742
  setTimeout(resolve, pollIntervalMs);
2755
3743
  });
2756
3744
  }
2757
- return err(new MotifError("Queued generation timed out", 0));
3745
+ return err2(new MotifError("Queued generation timed out", 0));
2758
3746
  }
2759
3747
  /** ─── Queue-Based Generation ──────────────────────────────── */
2760
3748
  /** Submit a generation to the fal.ai queue (returns immediately). */
@@ -2763,20 +3751,20 @@ var FalClient = class {
2763
3751
  try {
2764
3752
  built = buildGenerateBody(options);
2765
3753
  } catch (error) {
2766
- return err(toMotifError(error));
3754
+ return err2(toMotifError(error));
2767
3755
  }
2768
3756
  const { endpoint, body } = built;
2769
- const response = await this.request(`${FAL_QUEUE_URL}/${endpoint}`, {
3757
+ const response = await this.request(`${FAL_QUEUE_URL2}/${endpoint}`, {
2770
3758
  body: JSON.stringify(body),
2771
3759
  headers: this.ephemeralHeaders(options),
2772
3760
  method: "POST"
2773
3761
  });
2774
3762
  if (response.isErr()) {
2775
- return err(response.error);
3763
+ return err2(response.error);
2776
3764
  }
2777
3765
  const data = await response.value.json();
2778
3766
  const submission = parseQueueSubmission(data);
2779
- return ok({
3767
+ return ok2({
2780
3768
  endpoint: endpointFromQueueUrl(submission.responseUrl, endpoint),
2781
3769
  estimatedCost: estimateCost(
2782
3770
  options.model,
@@ -2788,10 +3776,10 @@ var FalClient = class {
2788
3776
  }
2789
3777
  /** Check the status of a queued generation. */
2790
3778
  async getJobStatus(endpoint, requestId) {
2791
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}/status?logs=1`;
3779
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}/status?logs=1`;
2792
3780
  const response = await this.request(url);
2793
3781
  if (response.isErr()) {
2794
- return err(response.error);
3782
+ return err2(response.error);
2795
3783
  }
2796
3784
  const data = await response.value.json();
2797
3785
  const record = isRecord(data) ? data : {};
@@ -2806,9 +3794,9 @@ var FalClient = class {
2806
3794
  } else if (rawStatus === "FAILED" || rawStatus === "ERROR" || rawStatus === "CANCELED") {
2807
3795
  status = "failed";
2808
3796
  } else {
2809
- return err(new MotifError(`Unknown job status: ${rawStatus}`, 0));
3797
+ return err2(new MotifError(`Unknown job status: ${rawStatus}`, 0));
2810
3798
  }
2811
- return ok({
3799
+ return ok2({
2812
3800
  error: asString(record.error) ?? asString(record.detail),
2813
3801
  logs: parseLogs(record.logs),
2814
3802
  queuePosition: asNumber(record.queue_position),
@@ -2817,10 +3805,10 @@ var FalClient = class {
2817
3805
  }
2818
3806
  /** Fetch the completed result from the queue. */
2819
3807
  async getJobResult(endpoint, requestId) {
2820
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
3808
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}`;
2821
3809
  const response = await this.request(url);
2822
3810
  if (response.isErr()) {
2823
- return err(response.error);
3811
+ return err2(response.error);
2824
3812
  }
2825
3813
  const data = await response.value.json();
2826
3814
  return this.normalizeResponse(data, requestId);
@@ -2841,7 +3829,7 @@ var FalClient = class {
2841
3829
  } = options;
2842
3830
  const config = MODELS[model];
2843
3831
  if (!config || config.type !== "utility") {
2844
- return err(new MotifError(`Invalid upscale model: ${model}`, 0));
3832
+ return err2(new MotifError(`Invalid upscale model: ${model}`, 0));
2845
3833
  }
2846
3834
  const body = { image_url: imageUrl };
2847
3835
  if (model === "crystal") {
@@ -2874,12 +3862,12 @@ var FalClient = class {
2874
3862
  body.guidance_scale = guidanceScale;
2875
3863
  }
2876
3864
  }
2877
- const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
3865
+ const response = await this.request(`${FAL_BASE_URL2}/${config.endpoint}`, {
2878
3866
  body: JSON.stringify(body),
2879
3867
  method: "POST"
2880
3868
  });
2881
3869
  if (response.isErr()) {
2882
- return err(response.error);
3870
+ return err2(response.error);
2883
3871
  }
2884
3872
  const data = await response.value.json();
2885
3873
  return this.normalizeResponse(data);
@@ -2897,7 +3885,7 @@ var FalClient = class {
2897
3885
  } = options;
2898
3886
  const config = MODELS[model];
2899
3887
  if (!config) {
2900
- return err(
3888
+ return err2(
2901
3889
  new MotifError(`Invalid background removal model: ${model}`, 0)
2902
3890
  );
2903
3891
  }
@@ -2919,12 +3907,12 @@ var FalClient = class {
2919
3907
  rbBody.output_mask = outputMask;
2920
3908
  }
2921
3909
  }
2922
- const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
3910
+ const response = await this.request(`${FAL_BASE_URL2}/${config.endpoint}`, {
2923
3911
  body: JSON.stringify(rbBody),
2924
3912
  method: "POST"
2925
3913
  });
2926
3914
  if (response.isErr()) {
2927
- return err(response.error);
3915
+ return err2(response.error);
2928
3916
  }
2929
3917
  const data = await response.value.json();
2930
3918
  return this.normalizeResponse(data);
@@ -2947,7 +3935,7 @@ var FalClient = class {
2947
3935
  } = options;
2948
3936
  const config = MODELS.kling;
2949
3937
  if (!config) {
2950
- return err(new MotifError("Kling video model not found", 0));
3938
+ return err2(new MotifError("Kling video model not found", 0));
2951
3939
  }
2952
3940
  const body = {
2953
3941
  duration: String(duration),
@@ -2964,16 +3952,16 @@ var FalClient = class {
2964
3952
  if (cfgScale !== void 0) {
2965
3953
  body.cfg_scale = cfgScale;
2966
3954
  }
2967
- const response = await this.request(`${FAL_QUEUE_URL}/${config.endpoint}`, {
3955
+ const response = await this.request(`${FAL_QUEUE_URL2}/${config.endpoint}`, {
2968
3956
  body: JSON.stringify(body),
2969
3957
  method: "POST"
2970
3958
  });
2971
3959
  if (response.isErr()) {
2972
- return err(response.error);
3960
+ return err2(response.error);
2973
3961
  }
2974
3962
  const data = await response.value.json();
2975
3963
  const submission = parseQueueSubmission(data);
2976
- return ok({
3964
+ return ok2({
2977
3965
  endpoint: endpointFromQueueUrl(submission.responseUrl, config.endpoint),
2978
3966
  estimatedCost: estimateVideoCost(duration, generateAudio),
2979
3967
  requestId: submission.requestId
@@ -2981,17 +3969,17 @@ var FalClient = class {
2981
3969
  }
2982
3970
  /** Fetch the completed video result from the queue. */
2983
3971
  async getVideoResult(endpoint, requestId) {
2984
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
3972
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}`;
2985
3973
  const response = await this.request(url);
2986
3974
  if (response.isErr()) {
2987
- return err(response.error);
3975
+ return err2(response.error);
2988
3976
  }
2989
3977
  const data = await response.value.json();
2990
3978
  const video = isRecord(data) && isRecord(data.video) ? data.video : void 0;
2991
3979
  if (video === void 0) {
2992
- return err(new MotifError("No video in response", 0));
3980
+ return err2(new MotifError("No video in response", 0));
2993
3981
  }
2994
- return ok({
3982
+ return ok2({
2995
3983
  contentType: asString(video.content_type) ?? "",
2996
3984
  fileName: asString(video.file_name) ?? "",
2997
3985
  fileSize: asNumber(video.file_size) ?? 0,
@@ -3015,13 +4003,13 @@ var FalClient = class {
3015
4003
  }
3016
4004
  );
3017
4005
  if (initiateResponse.isErr()) {
3018
- return err(initiateResponse.error);
4006
+ return err2(initiateResponse.error);
3019
4007
  }
3020
4008
  const initiateData = await initiateResponse.value.json();
3021
4009
  const fileUrl = isRecord(initiateData) ? asString(initiateData.file_url) ?? "" : "";
3022
4010
  const uploadUrl = isRecord(initiateData) ? asString(initiateData.upload_url) : void 0;
3023
4011
  if (uploadUrl === void 0 || uploadUrl === "") {
3024
- return err(
4012
+ return err2(
3025
4013
  new MotifError("Upload initiate response missing upload_url", 0)
3026
4014
  );
3027
4015
  }
@@ -3036,7 +4024,7 @@ var FalClient = class {
3036
4024
  method: "PUT"
3037
4025
  });
3038
4026
  } catch (error) {
3039
- return err(
4027
+ return err2(
3040
4028
  new MotifError(
3041
4029
  `Upload PUT failed: ${error instanceof Error ? error.message : String(error)}`,
3042
4030
  0
@@ -3044,38 +4032,45 @@ var FalClient = class {
3044
4032
  );
3045
4033
  }
3046
4034
  if (!putResponse.ok) {
3047
- return err(
4035
+ return err2(
3048
4036
  new MotifError(
3049
4037
  `Upload PUT failed: ${putResponse.status}`,
3050
4038
  putResponse.status
3051
4039
  )
3052
4040
  );
3053
4041
  }
3054
- return ok(fileUrl);
4042
+ return ok2(fileUrl);
3055
4043
  }
3056
4044
  /** ─── Utilities ───────────────────────────────────────────── */
3057
4045
  /** Run a registered fal utility/tool endpoint. */
3058
4046
  async runTool(options) {
3059
- let request;
3060
- try {
3061
- request = buildFalToolRequest(options);
3062
- } catch (error) {
3063
- return err(
3064
- new MotifError(
3065
- error instanceof Error ? error.message : String(error),
3066
- 0
3067
- )
3068
- );
3069
- }
3070
- const response = await this.request(`${FAL_BASE_URL}/${request.endpoint}`, {
3071
- body: JSON.stringify(request.body),
3072
- method: "POST"
3073
- });
3074
- if (response.isErr()) {
3075
- return err(response.error);
3076
- }
3077
- const data = await response.value.json();
3078
- return ok(isRecord(data) ? data : {});
4047
+ return await runTool(this.toolExecutor, options);
4048
+ }
4049
+ /** ─── Queue-Based Tools ───────────────────────────────────── */
4050
+ /**
4051
+ * Submit a tool run to the fal queue. Returns as soon as fal accepts it.
4052
+ *
4053
+ * The queued counterpart to `runTool`, for endpoints that outrun the
4054
+ * synchronous request timeout. Callers choose the path — neither one falls
4055
+ * back to the other.
4056
+ */
4057
+ async submitTool(options) {
4058
+ return await submitTool(this.toolExecutor, options);
4059
+ }
4060
+ /** Poll one queued tool run. */
4061
+ async checkToolStatus(job) {
4062
+ return await checkToolStatus(this.toolExecutor, job);
4063
+ }
4064
+ /** Fetch the finished payload for a queued tool run. */
4065
+ async getToolResult(job) {
4066
+ return await getToolResult(this.toolExecutor, job);
4067
+ }
4068
+ /**
4069
+ * Submit, poll to completion, and return the result. The convenience path
4070
+ * for tools whose registry entry sets `queued`.
4071
+ */
4072
+ async runToolQueued(options, onProgress) {
4073
+ return await runToolQueued(this.toolExecutor, options, onProgress);
3079
4074
  }
3080
4075
  /**
3081
4076
  * Delete fal's stored IO payloads for a completed request.
@@ -3090,9 +4085,9 @@ var FalClient = class {
3090
4085
  { method: "DELETE" }
3091
4086
  );
3092
4087
  if (response.isErr()) {
3093
- return err(response.error);
4088
+ return err2(response.error);
3094
4089
  }
3095
- return ok();
4090
+ return ok2();
3096
4091
  }
3097
4092
  /** Estimate cost for a generation (no API call). */
3098
4093
  estimateCost(model, resolution, numImages) {
@@ -3118,6 +4113,16 @@ var FalClient = class {
3118
4113
  get tools() {
3119
4114
  return FAL_TOOLS;
3120
4115
  }
4116
+ /**
4117
+ * The seam `server-tools.ts` runs against — the client's authenticated fetch
4118
+ * and its queue status parser, exposed without widening the public surface.
4119
+ */
4120
+ get toolExecutor() {
4121
+ return {
4122
+ getJobStatus: async (endpoint, requestId) => await this.getJobStatus(endpoint, requestId),
4123
+ request: async (url, init) => await this.request(url, init)
4124
+ };
4125
+ }
3121
4126
  /** ─── Private ─────────────────────────────────────────────── */
3122
4127
  /** Authenticated fetch to fal.ai APIs with retry logic. */
3123
4128
  async request(url, options = {}) {
@@ -3149,15 +4154,15 @@ var FalClient = class {
3149
4154
  const text = await response.text();
3150
4155
  const message = `Request failed: ${response.status} ${text}`;
3151
4156
  const requestId = response.headers.get("x-fal-request-id") ?? requestIdFromBody(text);
3152
- return err(
4157
+ return err2(
3153
4158
  new MotifError(message, response.status, void 0, requestId)
3154
4159
  );
3155
4160
  }
3156
- return ok(response);
4161
+ return ok2(response);
3157
4162
  } catch (error) {
3158
4163
  clearTimeout(timeoutId);
3159
4164
  if (error instanceof MotifError) {
3160
- return err(error);
4165
+ return err2(error);
3161
4166
  }
3162
4167
  lastError = new MotifError(
3163
4168
  error instanceof Error ? error.message : String(error),
@@ -3171,7 +4176,7 @@ var FalClient = class {
3171
4176
  }
3172
4177
  }
3173
4178
  }
3174
- return err(lastError ?? new MotifError("Request failed after retries", 0));
4179
+ return err2(lastError ?? new MotifError("Request failed after retries", 0));
3175
4180
  }
3176
4181
  ephemeralHeaders(options) {
3177
4182
  return options.ephemeral === true ? { "X-Fal-Store-IO": "0" } : {};
@@ -3182,24 +4187,24 @@ var FalClient = class {
3182
4187
  */
3183
4188
  normalizeResponse(data, fallbackRequestId) {
3184
4189
  if (!isRecord(data)) {
3185
- return err(
4190
+ return err2(
3186
4191
  new MotifError("Unexpected fal response shape", 0, "FAL_ERROR")
3187
4192
  );
3188
4193
  }
3189
4194
  const obj = data;
3190
4195
  const requestId = asString(obj.request_id) ?? asString(obj.requestId) ?? fallbackRequestId;
3191
4196
  if ("detail" in obj) {
3192
- return err(new MotifError(asString(obj.detail) ?? "", 0, "FAL_ERROR"));
4197
+ return err2(new MotifError(asString(obj.detail) ?? "", 0, "FAL_ERROR"));
3193
4198
  }
3194
4199
  if ("image" in obj && !("images" in obj)) {
3195
- return ok({
4200
+ return ok2({
3196
4201
  images: [toMotifImage(obj.image)],
3197
4202
  prompt: asString(obj.prompt),
3198
4203
  requestId,
3199
4204
  seed: asNumber(obj.seed)
3200
4205
  });
3201
4206
  }
3202
- return ok({
4207
+ return ok2({
3203
4208
  images: parseImages(obj.images),
3204
4209
  prompt: asString(obj.prompt),
3205
4210
  requestId,
@@ -3207,28 +4212,6 @@ var FalClient = class {
3207
4212
  });
3208
4213
  }
3209
4214
  };
3210
- var MotifError = class extends Error {
3211
- status;
3212
- code;
3213
- /** fal's request-correlation id (from the `x-fal-request-id` header or the
3214
- * error body). Ties a failure back to fal's dashboard/support. */
3215
- requestId;
3216
- constructor(message, status, code, requestId) {
3217
- super(message);
3218
- this.name = "MotifError";
3219
- this.status = status;
3220
- this.code = code;
3221
- this.requestId = requestId;
3222
- }
3223
- };
3224
- function toMotifError(error) {
3225
- if (error instanceof MotifError) {
3226
- return error;
3227
- }
3228
- const message = error instanceof Error ? error.message : String(error);
3229
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
3230
- return new MotifError(message, 0, code);
3231
- }
3232
4215
  export {
3233
4216
  ASPECT_RATIOS,
3234
4217
  CREATIVE_FIELDS,
@@ -3257,13 +4240,13 @@ export {
3257
4240
  buildFalToolRequest,
3258
4241
  buildGenerateBody,
3259
4242
  enrichPrompt,
3260
- err2 as err,
4243
+ err3 as err,
3261
4244
  estimateCost,
3262
4245
  estimateVideoCost,
3263
4246
  getFalKeyFromEnv,
3264
4247
  isFalToolId,
3265
4248
  motifEnvSchema,
3266
- ok2 as ok,
4249
+ ok3 as ok,
3267
4250
  parseMotifEnv,
3268
4251
  sanitizePrompt
3269
4252
  };