@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.cjs CHANGED
@@ -47,18 +47,18 @@ __export(src_exports, {
47
47
  buildFalToolRequest: () => buildFalToolRequest,
48
48
  buildGenerateBody: () => buildGenerateBody,
49
49
  enrichPrompt: () => enrichPrompt,
50
- err: () => import_neverthrow2.err,
50
+ err: () => import_neverthrow3.err,
51
51
  estimateCost: () => estimateCost,
52
52
  estimateVideoCost: () => estimateVideoCost,
53
53
  getFalKeyFromEnv: () => getFalKeyFromEnv,
54
54
  isFalToolId: () => isFalToolId,
55
55
  motifEnvSchema: () => motifEnvSchema,
56
- ok: () => import_neverthrow2.ok,
56
+ ok: () => import_neverthrow3.ok,
57
57
  parseMotifEnv: () => parseMotifEnv,
58
58
  sanitizePrompt: () => sanitizePrompt
59
59
  });
60
60
  module.exports = __toCommonJS(src_exports);
61
- var import_neverthrow2 = require("neverthrow");
61
+ var import_neverthrow3 = require("neverthrow");
62
62
 
63
63
  // src/aspects.ts
64
64
  var ASPECT_RATIOS = [
@@ -158,22 +158,23 @@ var FORMAT_PRESETS = {
158
158
  };
159
159
 
160
160
  // src/models.ts
161
- var AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-05-12";
161
+ var AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-08-23";
162
162
  var AA_IMAGE_SOURCES = [
163
163
  "https://artificialanalysis.ai/image/leaderboard/text-to-image",
164
164
  "https://artificialanalysis.ai/image/leaderboard/editing"
165
165
  ];
166
166
  var FAL_PRICING_CHECKED_AT = "2026-05-12";
167
167
  var FAL_PRICING_CHECKED_JUL_2026 = "2026-07-11";
168
+ var FAL_PRICING_CHECKED_AUG_2026 = "2026-08-23";
168
169
  var MODELS = {
169
170
  // ─── Generation Models ────────────────────────────────────────
170
171
  gpt2: {
171
172
  benchmark: {
172
173
  artificialAnalysis: {
173
- editing: { elo: 1249, pricePer1k: 211, rank: 3, winRate: 0.66 },
174
+ editing: { elo: 1256, pricePer1k: 211, rank: 3 },
174
175
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
175
176
  sourceUrls: AA_IMAGE_SOURCES,
176
- textToImage: { elo: 1337, pricePer1k: 211, rank: 1, winRate: 0.8 }
177
+ textToImage: { elo: 1369, pricePer1k: 211, rank: 1 }
177
178
  },
178
179
  speed: {
179
180
  medianSeconds: 200.7,
@@ -213,10 +214,10 @@ var MODELS = {
213
214
  gpt: {
214
215
  benchmark: {
215
216
  artificialAnalysis: {
216
- editing: { elo: 1262, pricePer1k: 133, rank: 2, winRate: 0.69 },
217
+ editing: { elo: 1250, pricePer1k: 133, rank: 5 },
217
218
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
218
219
  sourceUrls: AA_IMAGE_SOURCES,
219
- textToImage: { elo: 1268, pricePer1k: 133, rank: 2, winRate: 0.73 }
220
+ textToImage: { elo: 1310, pricePer1k: 133, rank: 4 }
220
221
  },
221
222
  speed: {
222
223
  medianSeconds: 34.6,
@@ -256,10 +257,10 @@ var MODELS = {
256
257
  banana2: {
257
258
  benchmark: {
258
259
  artificialAnalysis: {
259
- editing: { elo: 1231, pricePer1k: 67, rank: 5, winRate: 0.63 },
260
+ editing: { elo: 1249, pricePer1k: 67, rank: 7 },
260
261
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
261
262
  sourceUrls: AA_IMAGE_SOURCES,
262
- textToImage: { elo: 1263, pricePer1k: 67, rank: 3, winRate: 0.71 }
263
+ textToImage: { elo: 1320, pricePer1k: 67, rank: 3 }
263
264
  },
264
265
  tiers: { price: "premium", quality: "frontier", speed: "unknown" },
265
266
  useCase: "Best balance of quality, edit support, web search, and cost"
@@ -297,10 +298,10 @@ var MODELS = {
297
298
  banana: {
298
299
  benchmark: {
299
300
  artificialAnalysis: {
300
- editing: { elo: 1241, pricePer1k: 134, rank: 4, winRate: 0.65 },
301
+ editing: { elo: 1245, pricePer1k: 134, rank: 9 },
301
302
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
302
303
  sourceUrls: AA_IMAGE_SOURCES,
303
- textToImage: { elo: 1220, pricePer1k: 134, rank: 5, winRate: 0.66 }
304
+ textToImage: { elo: 1297, pricePer1k: 134, rank: 6 }
304
305
  },
305
306
  speed: {
306
307
  medianSeconds: 19.2,
@@ -340,6 +341,13 @@ var MODELS = {
340
341
  type: "generation"
341
342
  },
342
343
  gemini: {
344
+ benchmark: {
345
+ artificialAnalysis: {
346
+ editing: { elo: 1180, pricePer1k: 39, rank: 29 },
347
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
348
+ sourceUrls: AA_IMAGE_SOURCES
349
+ }
350
+ },
343
351
  editEndpoint: "fal-ai/gemini-25-flash-image/edit",
344
352
  endpoint: "fal-ai/gemini-25-flash-image",
345
353
  falPricing: {
@@ -367,6 +375,14 @@ var MODELS = {
367
375
  type: "generation"
368
376
  },
369
377
  gemini3: {
378
+ benchmark: {
379
+ artificialAnalysis: {
380
+ editing: { elo: 1245, pricePer1k: 134, rank: 9 },
381
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
382
+ sourceUrls: AA_IMAGE_SOURCES,
383
+ textToImage: { elo: 1297, pricePer1k: 134, rank: 6 }
384
+ }
385
+ },
370
386
  editEndpoint: "fal-ai/gemini-3-pro-image-preview/edit",
371
387
  endpoint: "fal-ai/gemini-3-pro-image-preview",
372
388
  falPricing: {
@@ -397,10 +413,10 @@ var MODELS = {
397
413
  seedream4: {
398
414
  benchmark: {
399
415
  artificialAnalysis: {
400
- editing: { elo: 1184, pricePer1k: 30, rank: 16, winRate: 0.61 },
416
+ editing: { elo: 1185, pricePer1k: 30, rank: 28 },
401
417
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
402
418
  sourceUrls: AA_IMAGE_SOURCES,
403
- textToImage: { elo: 1198, pricePer1k: 30, rank: 6, winRate: 0.59 }
419
+ textToImage: { elo: 1226, pricePer1k: 30, rank: 16 }
404
420
  },
405
421
  speed: {
406
422
  medianSeconds: 14.3,
@@ -437,10 +453,10 @@ var MODELS = {
437
453
  seedream45: {
438
454
  benchmark: {
439
455
  artificialAnalysis: {
440
- editing: { elo: 1184, pricePer1k: 40, rank: 17, winRate: 0.58 },
456
+ editing: { elo: 1186, pricePer1k: 40, rank: 26 },
441
457
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
442
458
  sourceUrls: AA_IMAGE_SOURCES,
443
- textToImage: { elo: 1167, pricePer1k: 40, rank: 17, winRate: 0.63 }
459
+ textToImage: { elo: 1203, pricePer1k: 40, rank: 32 }
444
460
  },
445
461
  speed: {
446
462
  medianSeconds: 18.2,
@@ -475,6 +491,14 @@ var MODELS = {
475
491
  type: "generation"
476
492
  },
477
493
  seedream5: {
494
+ benchmark: {
495
+ artificialAnalysis: {
496
+ editing: { elo: 1248, pricePer1k: 90, rank: 8 },
497
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
498
+ sourceUrls: AA_IMAGE_SOURCES,
499
+ textToImage: { elo: 1279, pricePer1k: 90, rank: 10 }
500
+ }
501
+ },
478
502
  name: "Seedream 5.0 Pro",
479
503
  endpoint: "bytedance/seedream/v5/pro/text-to-image",
480
504
  editEndpoint: "bytedance/seedream/v5/pro/edit",
@@ -501,6 +525,13 @@ var MODELS = {
501
525
  maxReferenceImages: 10
502
526
  },
503
527
  "seedream5-lite": {
528
+ benchmark: {
529
+ artificialAnalysis: {
530
+ editing: { elo: 1169, pricePer1k: 35, rank: 32 },
531
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
532
+ sourceUrls: AA_IMAGE_SOURCES
533
+ }
534
+ },
504
535
  name: "Seedream 5.0 Lite",
505
536
  endpoint: "fal-ai/bytedance/seedream/v5/lite/text-to-image",
506
537
  editEndpoint: "fal-ai/bytedance/seedream/v5/lite/edit",
@@ -529,10 +560,10 @@ var MODELS = {
529
560
  "flux2-max": {
530
561
  benchmark: {
531
562
  artificialAnalysis: {
532
- editing: { elo: 1206, pricePer1k: 140, rank: 10, winRate: 0.6 },
563
+ editing: { elo: 1201, pricePer1k: 140, rank: 19 },
533
564
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
534
565
  sourceUrls: AA_IMAGE_SOURCES,
535
- textToImage: { elo: 1197, pricePer1k: 70, rank: 8, winRate: 0.67 }
566
+ textToImage: { elo: 1225, pricePer1k: 70, rank: 17 }
536
567
  },
537
568
  speed: {
538
569
  medianSeconds: 31.6,
@@ -572,10 +603,10 @@ var MODELS = {
572
603
  "flux2-pro": {
573
604
  benchmark: {
574
605
  artificialAnalysis: {
575
- editing: { elo: 1170, pricePer1k: 30, rank: 21 },
606
+ editing: { elo: 1170, pricePer1k: 45, rank: 31 },
576
607
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
577
608
  sourceUrls: AA_IMAGE_SOURCES,
578
- textToImage: { elo: 1186, pricePer1k: 30, rank: 10, winRate: 0.62 }
609
+ textToImage: { elo: 1207, pricePer1k: 30, rank: 30 }
579
610
  },
580
611
  speed: {
581
612
  medianSeconds: 16.1,
@@ -615,10 +646,9 @@ var MODELS = {
615
646
  "flux2-flex": {
616
647
  benchmark: {
617
648
  artificialAnalysis: {
618
- editing: { elo: 1161, pricePer1k: 60, rank: 22 },
619
649
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
620
650
  sourceUrls: AA_IMAGE_SOURCES,
621
- textToImage: { elo: 1182, pricePer1k: 60, rank: 13, winRate: 0.61 }
651
+ textToImage: { elo: 1223, pricePer1k: 60, rank: 18 }
622
652
  },
623
653
  speed: {
624
654
  medianSeconds: 15.3,
@@ -662,8 +692,7 @@ var MODELS = {
662
692
  benchmark: {
663
693
  artificialAnalysis: {
664
694
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
665
- sourceUrls: AA_IMAGE_SOURCES,
666
- textToImage: { elo: 1160, pricePer1k: 12, rank: 19, winRate: 0.57 }
695
+ sourceUrls: AA_IMAGE_SOURCES
667
696
  },
668
697
  speed: {
669
698
  medianSeconds: 4.6,
@@ -806,7 +835,55 @@ var MODELS = {
806
835
  supportsSyncMode: true,
807
836
  type: "generation"
808
837
  },
838
+ reve: {
839
+ benchmark: {
840
+ artificialAnalysis: {
841
+ // #2 on BOTH boards, which almost nothing else manages - the top text-to-image model
842
+ // (GPT Image 2) drops to 3rd for editing, and the top editor (MAI-Image-2.5-Pro) is not
843
+ // on fal at all. Reve is the best editor motif can actually reach.
844
+ editing: { elo: 1263, pricePer1k: 200, rank: 2 },
845
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
846
+ sourceUrls: AA_IMAGE_SOURCES,
847
+ textToImage: { elo: 1322, pricePer1k: 200, rank: 2 }
848
+ },
849
+ tiers: { price: "premium", quality: "frontier", speed: "unknown" },
850
+ useCase: "Highest-ranked editor reachable on fal; strong prompt adherence and text rendering"
851
+ },
852
+ editEndpoint: "fal-ai/reve/edit",
853
+ endpoint: "fal-ai/reve/text-to-image",
854
+ falPricing: {
855
+ checkedAt: FAL_PRICING_CHECKED_AUG_2026,
856
+ currency: "USD",
857
+ endpointId: "fal-ai/reve/text-to-image",
858
+ estimatedCostPerImageUsd: 0.04,
859
+ source: "fal-pricing-api",
860
+ unit: "images",
861
+ unitPrice: 0.04
862
+ },
863
+ maxReferenceImages: 1,
864
+ name: "Reve 2.1",
865
+ pricePerImageUsd: 0.04,
866
+ pricing: "$0.04",
867
+ // fal's schema takes aspect_ratio from a fixed set and has no width/height or resolution
868
+ // parameter, so aspect is supported and resolution is not.
869
+ sizeMode: "aspect_ratio",
870
+ supportsAspect: true,
871
+ supportsEdit: true,
872
+ supportsNumImages: true,
873
+ supportsOutputFormat: true,
874
+ supportsResolution: false,
875
+ supportsSeed: false,
876
+ supportsSyncMode: true,
877
+ type: "generation"
878
+ },
809
879
  recraft4: {
880
+ benchmark: {
881
+ artificialAnalysis: {
882
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
883
+ sourceUrls: AA_IMAGE_SOURCES,
884
+ textToImage: { elo: 1215, pricePer1k: 35, rank: 25 }
885
+ }
886
+ },
810
887
  endpoint: "fal-ai/recraft/v4/text-to-image",
811
888
  falPricing: {
812
889
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
@@ -857,6 +934,13 @@ var MODELS = {
857
934
  type: "generation"
858
935
  },
859
936
  ideogram4: {
937
+ benchmark: {
938
+ artificialAnalysis: {
939
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
940
+ sourceUrls: AA_IMAGE_SOURCES,
941
+ textToImage: { elo: 1220, pricePer1k: 60, rank: 19 }
942
+ }
943
+ },
860
944
  name: "Ideogram V4",
861
945
  endpoint: "ideogram/v4",
862
946
  type: "generation",
@@ -889,10 +973,10 @@ var MODELS = {
889
973
  "grok-image": {
890
974
  benchmark: {
891
975
  artificialAnalysis: {
892
- editing: { elo: 1213, pricePer1k: 20, rank: 7, winRate: 0.6 },
976
+ editing: { elo: 1214, pricePer1k: 20, rank: 16 },
893
977
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
894
978
  sourceUrls: AA_IMAGE_SOURCES,
895
- textToImage: { elo: 1182, pricePer1k: 20, rank: 12, winRate: 0.6 }
979
+ textToImage: { elo: 1216, pricePer1k: 20, rank: 23 }
896
980
  },
897
981
  speed: {
898
982
  medianSeconds: 5.1,
@@ -930,8 +1014,7 @@ var MODELS = {
930
1014
  benchmark: {
931
1015
  artificialAnalysis: {
932
1016
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
933
- sourceUrls: AA_IMAGE_SOURCES,
934
- textToImage: { elo: 1158, pricePer1k: 20, rank: 20, winRate: 0.6 }
1017
+ sourceUrls: AA_IMAGE_SOURCES
935
1018
  },
936
1019
  speed: {
937
1020
  medianSeconds: 23.3,
@@ -966,8 +1049,14 @@ var MODELS = {
966
1049
  },
967
1050
  qwen3: {
968
1051
  benchmark: {
1052
+ artificialAnalysis: {
1053
+ editing: { elo: 1218, pricePer1k: 33, rank: 15 },
1054
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
1055
+ sourceUrls: AA_IMAGE_SOURCES,
1056
+ textToImage: { elo: 1272, pricePer1k: 30, rank: 11 }
1057
+ },
969
1058
  tiers: { price: "budget", quality: "better", speed: "unknown" },
970
- useCase: "Qwen Image 3 - successor to qwen; no leaderboard data yet"
1059
+ useCase: "Strong quality per dollar - 11th on text-to-image at $30/1k"
971
1060
  },
972
1061
  endpoint: "fal-ai/qwen-image-3/text-to-image",
973
1062
  falPricing: {
@@ -1392,7 +1481,7 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
1392
1481
  if (options.background !== void 0 && config.supportsBackground !== true) {
1393
1482
  unsupported(config, "background");
1394
1483
  }
1395
- if (options.transparent === true && !(config.supportsBackground === true || config.supportsOutputFormat === true)) {
1484
+ if (options.transparent === true && config.supportsBackground !== true) {
1396
1485
  unsupported(config, "transparent output");
1397
1486
  }
1398
1487
  if (options.inputFidelity !== void 0) {
@@ -1674,353 +1763,351 @@ function buildGenerateBody(options) {
1674
1763
 
1675
1764
  // src/leaderboards.ts
1676
1765
  var IMAGE_TEXT_TO_IMAGE_TOP_20 = {
1677
- capturedAt: "2026-05-12",
1766
+ capturedAt: "2026-08-23",
1678
1767
  entries: [
1679
1768
  {
1680
1769
  apiPricing: "$211/1k images",
1681
1770
  creator: "OpenAI",
1682
- elo: 1337,
1771
+ elo: 1369,
1683
1772
  isCurrent: true,
1684
1773
  model: "GPT Image 2 (high)",
1685
1774
  rank: 1,
1686
1775
  routableModelId: "gpt2"
1687
1776
  },
1688
1777
  {
1689
- apiPricing: "$133/1k images",
1690
- creator: "OpenAI",
1691
- elo: 1268,
1778
+ apiPricing: "$200/1k images",
1779
+ creator: "Reve",
1780
+ elo: 1322,
1692
1781
  isCurrent: true,
1693
- model: "GPT Image 1.5 (high)",
1782
+ model: "Reve 2.1",
1694
1783
  rank: 2,
1695
- routableModelId: "gpt"
1784
+ routableModelId: "reve"
1696
1785
  },
1697
1786
  {
1698
1787
  apiPricing: "$67/1k images",
1699
1788
  creator: "Google",
1700
- elo: 1263,
1789
+ elo: 1320,
1701
1790
  isCurrent: true,
1702
1791
  model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1703
1792
  rank: 3,
1704
1793
  routableModelId: "banana2"
1705
1794
  },
1706
1795
  {
1707
- apiPricing: "$150/1k images",
1708
- creator: "Sourceful",
1709
- elo: 1256,
1796
+ apiPricing: "$133/1k images",
1797
+ creator: "OpenAI",
1798
+ elo: 1310,
1710
1799
  isCurrent: true,
1711
- model: "Riverflow 2.0",
1712
- rank: 4
1800
+ model: "GPT Image 1.5 (high)",
1801
+ rank: 4,
1802
+ routableModelId: "gpt"
1713
1803
  },
1714
1804
  {
1715
- apiPricing: "$134/1k images",
1716
- creator: "Google",
1717
- elo: 1220,
1805
+ apiPricing: "$48/1k images",
1806
+ creator: "Microsoft AI",
1807
+ elo: 1305,
1718
1808
  isCurrent: true,
1719
- model: "Nano Banana Pro (Gemini 3 Pro Image)",
1720
- rank: 5,
1721
- routableModelId: "banana"
1809
+ model: "MAI-Image-2.5",
1810
+ rank: 5
1722
1811
  },
1723
1812
  {
1724
- apiPricing: "$30/1k images",
1725
- creator: "ByteDance Seed",
1726
- elo: 1198,
1813
+ apiPricing: "$134/1k images",
1814
+ creator: "Google",
1815
+ elo: 1297,
1727
1816
  isCurrent: true,
1728
- model: "Seedream 4.0",
1817
+ model: "Nano Banana Pro (Gemini 3 Pro Image)",
1729
1818
  rank: 6,
1730
- routableModelId: "seedream4"
1819
+ routableModelId: "banana"
1731
1820
  },
1732
1821
  {
1733
- apiPricing: "$50/1k images",
1734
- creator: "Microsoft",
1735
- elo: 1198,
1822
+ apiPricing: "$109/1k images",
1823
+ creator: "Microsoft AI",
1824
+ elo: 1293,
1736
1825
  isCurrent: true,
1737
- model: "MAI-Image-2",
1826
+ model: "MAI-Image-2.5-Pro",
1738
1827
  rank: 7
1739
1828
  },
1740
1829
  {
1741
- apiPricing: "$70/1k images",
1742
- creator: "Black Forest Labs",
1743
- elo: 1197,
1830
+ apiPricing: "$34/1k images",
1831
+ creator: "Google",
1832
+ elo: 1289,
1744
1833
  isCurrent: true,
1745
- model: "FLUX.2 [max]",
1746
- rank: 8,
1747
- routableModelId: "flux2-max"
1834
+ model: "Nano Banana 2 Lite (Gemini 3.1 Flash Lite Image)",
1835
+ rank: 8
1748
1836
  },
1749
1837
  {
1750
- apiPricing: "Unavailable",
1751
- creator: "Peanut",
1752
- elo: 1187,
1838
+ apiPricing: "$43/1k images",
1839
+ creator: "Alibaba",
1840
+ elo: 1282,
1753
1841
  isCurrent: true,
1754
- model: "Peanut (Open Weights Coming Soon)",
1842
+ model: "Qwen-Image-3.0-Pro",
1755
1843
  rank: 9
1756
1844
  },
1757
1845
  {
1758
- apiPricing: "$30/1k images",
1759
- creator: "Black Forest Labs",
1760
- elo: 1186,
1846
+ apiPricing: "$90/1k images",
1847
+ creator: "Bytedance",
1848
+ elo: 1279,
1761
1849
  isCurrent: true,
1762
- model: "FLUX.2 [pro]",
1850
+ model: "Seedream 5.0 Pro",
1763
1851
  rank: 10,
1764
- routableModelId: "flux2-pro"
1852
+ routableModelId: "seedream5"
1765
1853
  },
1766
1854
  {
1767
- apiPricing: "$60/1k images",
1768
- creator: "Google",
1769
- elo: 1184,
1770
- isCurrent: false,
1771
- model: "Imagen 4 Ultra Preview 0606",
1772
- rank: 11
1855
+ apiPricing: "$30/1k images",
1856
+ creator: "Alibaba",
1857
+ elo: 1272,
1858
+ isCurrent: true,
1859
+ model: "Qwen-Image-3.0",
1860
+ rank: 11,
1861
+ routableModelId: "qwen3"
1773
1862
  },
1774
1863
  {
1775
- apiPricing: "$20/1k images",
1776
- creator: "xAI",
1777
- elo: 1182,
1864
+ apiPricing: "$75/1k images",
1865
+ creator: "Alibaba",
1866
+ elo: 1237,
1778
1867
  isCurrent: true,
1779
- model: "grok-imagine-image",
1780
- rank: 12,
1781
- routableModelId: "grok-image"
1868
+ model: "Qwen Image 2.0 Pro",
1869
+ rank: 12
1782
1870
  },
1783
1871
  {
1784
- apiPricing: "$60/1k images",
1785
- creator: "Black Forest Labs",
1786
- elo: 1182,
1872
+ apiPricing: "$50/1k images",
1873
+ creator: "SpaceXAI",
1874
+ elo: 1234,
1787
1875
  isCurrent: true,
1788
- model: "FLUX.2 [flex]",
1789
- rank: 13,
1790
- routableModelId: "flux2-flex"
1876
+ model: "grok-imagine-image-quality",
1877
+ rank: 13
1791
1878
  },
1792
1879
  {
1793
- apiPricing: "$30/1k images",
1794
- creator: "ImagineArt",
1795
- elo: 1181,
1880
+ apiPricing: "$20/1k images",
1881
+ creator: "Microsoft AI",
1882
+ elo: 1228,
1796
1883
  isCurrent: true,
1797
- model: "ImagineArt 2.0",
1884
+ model: "MAI-Image-2.5-Flash",
1798
1885
  rank: 14
1799
1886
  },
1800
1887
  {
1801
- apiPricing: "$60/1k images",
1802
- creator: "Google",
1803
- elo: 1171,
1888
+ apiPricing: "$80/1k images",
1889
+ creator: "HiDream",
1890
+ elo: 1228,
1804
1891
  isCurrent: true,
1805
- model: "Imagen 4 Ultra",
1892
+ model: "HiDream-O1-Image-1.5",
1806
1893
  rank: 15
1807
1894
  },
1808
1895
  {
1809
- apiPricing: "$40/1k images",
1810
- creator: "Google",
1811
- elo: 1169,
1812
- isCurrent: false,
1813
- model: "Imagen 4 Preview 0606",
1814
- rank: 16
1896
+ apiPricing: "$30/1k images",
1897
+ creator: "ByteDance Seed",
1898
+ elo: 1226,
1899
+ isCurrent: true,
1900
+ model: "Seedream 4.0",
1901
+ rank: 16,
1902
+ routableModelId: "seedream4"
1815
1903
  },
1816
1904
  {
1817
- apiPricing: "$40/1k images",
1818
- creator: "ByteDance Seed",
1819
- elo: 1167,
1905
+ apiPricing: "$70/1k images",
1906
+ creator: "Black Forest Labs",
1907
+ elo: 1225,
1820
1908
  isCurrent: true,
1821
- model: "Seedream 4.5",
1909
+ model: "FLUX.2 [max]",
1822
1910
  rank: 17,
1823
- routableModelId: "seedream45"
1911
+ routableModelId: "flux2-max"
1824
1912
  },
1825
1913
  {
1826
- apiPricing: "$8/1k images",
1827
- creator: "Fal",
1828
- elo: 1161,
1914
+ apiPricing: "$60/1k images",
1915
+ creator: "Black Forest Labs",
1916
+ elo: 1223,
1829
1917
  isCurrent: true,
1830
- model: "FLUX.2 [dev] Turbo",
1831
- rank: 18
1918
+ model: "FLUX.2 [flex]",
1919
+ rank: 18,
1920
+ routableModelId: "flux2-flex"
1832
1921
  },
1833
1922
  {
1834
- apiPricing: "$12/1k images",
1835
- creator: "Black Forest Labs",
1836
- elo: 1160,
1923
+ apiPricing: "$60/1k images",
1924
+ creator: "Ideogram",
1925
+ elo: 1220,
1837
1926
  isCurrent: true,
1838
- model: "FLUX.2 [dev]",
1927
+ model: "Ideogram 4.0",
1839
1928
  rank: 19,
1840
- routableModelId: "flux2-dev"
1929
+ routableModelId: "ideogram4"
1841
1930
  },
1842
1931
  {
1843
- apiPricing: "$20/1k images",
1844
- creator: "Alibaba",
1845
- elo: 1158,
1932
+ apiPricing: "$60/1k images",
1933
+ creator: "Krea",
1934
+ elo: 1220,
1846
1935
  isCurrent: true,
1847
- model: "Qwen Image Max 2512",
1848
- rank: 20,
1849
- routableModelId: "qwen"
1936
+ model: "Krea 2 Large",
1937
+ rank: 20
1850
1938
  }
1851
1939
  ],
1852
1940
  source: "https://artificialanalysis.ai/image/leaderboard/text-to-image"
1853
1941
  };
1854
1942
  var IMAGE_EDITING_TOP_20 = {
1855
- capturedAt: "2026-05-12",
1943
+ capturedAt: "2026-08-23",
1856
1944
  entries: [
1857
1945
  {
1858
- apiPricing: "$150/1k images",
1859
- creator: "Sourceful",
1860
- elo: 1286,
1946
+ apiPricing: "$109/1k images",
1947
+ creator: "Microsoft AI",
1948
+ elo: 1272,
1861
1949
  isCurrent: true,
1862
- model: "Riverflow 2.0",
1950
+ model: "MAI-Image-2.5-Pro",
1863
1951
  rank: 1
1864
1952
  },
1865
1953
  {
1866
- apiPricing: "$133/1k images",
1867
- creator: "OpenAI",
1868
- elo: 1262,
1954
+ apiPricing: "$200/1k images",
1955
+ creator: "Reve",
1956
+ elo: 1263,
1869
1957
  isCurrent: true,
1870
- model: "GPT Image 1.5 (high)",
1958
+ model: "Reve 2.1",
1871
1959
  rank: 2,
1872
- routableModelId: "gpt"
1960
+ routableModelId: "reve"
1873
1961
  },
1874
1962
  {
1875
1963
  apiPricing: "$211/1k images",
1876
1964
  creator: "OpenAI",
1877
- elo: 1249,
1965
+ elo: 1256,
1878
1966
  isCurrent: true,
1879
1967
  model: "GPT Image 2 (high)",
1880
1968
  rank: 3,
1881
1969
  routableModelId: "gpt2"
1882
1970
  },
1883
1971
  {
1884
- apiPricing: "$134/1k images",
1885
- creator: "Google",
1886
- elo: 1241,
1972
+ apiPricing: "$48/1k images",
1973
+ creator: "Microsoft AI",
1974
+ elo: 1256,
1887
1975
  isCurrent: true,
1888
- model: "Nano Banana Pro (Gemini 3 Pro Image)",
1889
- rank: 4,
1890
- routableModelId: "banana"
1976
+ model: "MAI-Image-2.5",
1977
+ rank: 4
1891
1978
  },
1892
1979
  {
1893
- apiPricing: "$67/1k images",
1894
- creator: "Google",
1895
- elo: 1231,
1980
+ apiPricing: "$133/1k images",
1981
+ creator: "OpenAI",
1982
+ elo: 1250,
1896
1983
  isCurrent: true,
1897
- model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1984
+ model: "GPT Image 1.5 (high)",
1898
1985
  rank: 5,
1899
- routableModelId: "banana2"
1986
+ routableModelId: "gpt"
1900
1987
  },
1901
1988
  {
1902
- apiPricing: "$90/1k images",
1903
- creator: "Tencent",
1904
- elo: 1222,
1989
+ apiPricing: "$43/1k images",
1990
+ creator: "Alibaba",
1991
+ elo: 1250,
1905
1992
  isCurrent: true,
1906
- model: "HunyuanImage 3.0 Instruct (Fal)",
1993
+ model: "Qwen-Image-3.0-Pro",
1907
1994
  rank: 6
1908
1995
  },
1909
1996
  {
1910
- apiPricing: "$20/1k images",
1911
- creator: "xAI",
1912
- elo: 1213,
1997
+ apiPricing: "$67/1k images",
1998
+ creator: "Google",
1999
+ elo: 1249,
1913
2000
  isCurrent: true,
1914
- model: "grok-imagine-image",
2001
+ model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1915
2002
  rank: 7,
1916
- routableModelId: "grok-image"
2003
+ routableModelId: "banana2"
1917
2004
  },
1918
2005
  {
1919
- apiPricing: "$70/1k images",
1920
- creator: "xAI",
1921
- elo: 1212,
2006
+ apiPricing: "$90/1k images",
2007
+ creator: "Bytedance",
2008
+ elo: 1248,
1922
2009
  isCurrent: true,
1923
- model: "grok-imagine-image-pro",
1924
- rank: 8
2010
+ model: "Seedream 5.0 Pro",
2011
+ rank: 8,
2012
+ routableModelId: "seedream5"
1925
2013
  },
1926
2014
  {
1927
- apiPricing: "Unavailable",
1928
- creator: "KlingAI",
1929
- elo: 1207,
2015
+ apiPricing: "$134/1k images",
2016
+ creator: "Google",
2017
+ elo: 1245,
1930
2018
  isCurrent: true,
1931
- model: "Kling Image 3.0 Omni",
1932
- rank: 9
2019
+ model: "Nano Banana Pro (Gemini 3 Pro Image)",
2020
+ rank: 9,
2021
+ routableModelId: "banana"
1933
2022
  },
1934
2023
  {
1935
- apiPricing: "$140/1k images",
1936
- creator: "Black Forest Labs",
1937
- elo: 1206,
2024
+ apiPricing: "$20/1k images",
2025
+ creator: "Microsoft AI",
2026
+ elo: 1236,
1938
2027
  isCurrent: true,
1939
- model: "FLUX.2 [max]",
1940
- rank: 10,
1941
- routableModelId: "flux2-max"
2028
+ model: "MAI-Image-2.5-Flash",
2029
+ rank: 10
1942
2030
  },
1943
2031
  {
1944
- apiPricing: "Unavailable",
1945
- creator: "Alibaba",
1946
- elo: 1200,
2032
+ apiPricing: "$50/1k images",
2033
+ creator: "SpaceXAI",
2034
+ elo: 1226,
1947
2035
  isCurrent: true,
1948
- model: "Wan 2.7 Pro",
2036
+ model: "grok-imagine-image-quality",
1949
2037
  rank: 11
1950
2038
  },
1951
2039
  {
1952
- apiPricing: "$28/1k images",
1953
- creator: "KlingAI",
1954
- elo: 1196,
2040
+ apiPricing: "$90/1k images",
2041
+ creator: "Tencent",
2042
+ elo: 1222,
1955
2043
  isCurrent: true,
1956
- model: "Kling Image 3.0",
2044
+ model: "HunyuanImage 3.0 Instruct",
1957
2045
  rank: 12
1958
2046
  },
1959
2047
  {
1960
- apiPricing: "$28/1k images",
1961
- creator: "KlingAI",
1962
- elo: 1193,
2048
+ apiPricing: "$80/1k images",
2049
+ creator: "Newport AI",
2050
+ elo: 1220,
1963
2051
  isCurrent: true,
1964
- model: "Kling Image O1",
2052
+ model: "DreamImage 2.0",
1965
2053
  rank: 13
1966
2054
  },
1967
2055
  {
1968
- apiPricing: "$30/1k images",
1969
- creator: "Alibaba",
1970
- elo: 1188,
2056
+ apiPricing: "$100/1k images",
2057
+ creator: "Luma Labs",
2058
+ elo: 1219,
1971
2059
  isCurrent: true,
1972
- model: "Wan 2.6 Image",
2060
+ model: "Luma UNI 1 Max",
1973
2061
  rank: 14
1974
2062
  },
1975
2063
  {
1976
- apiPricing: "$66/1k images",
1977
- creator: "Sourceful",
1978
- elo: 1184,
2064
+ apiPricing: "$33/1k images",
2065
+ creator: "Alibaba",
2066
+ elo: 1218,
1979
2067
  isCurrent: true,
1980
- model: "Riverflow 1",
1981
- rank: 15
2068
+ model: "Qwen-Image-3.0",
2069
+ rank: 15,
2070
+ routableModelId: "qwen3"
1982
2071
  },
1983
2072
  {
1984
- apiPricing: "$30/1k images",
1985
- creator: "ByteDance Seed",
1986
- elo: 1184,
2073
+ apiPricing: "$20/1k images",
2074
+ creator: "SpaceXAI",
2075
+ elo: 1214,
1987
2076
  isCurrent: true,
1988
- model: "Seedream 4.0",
2077
+ model: "grok-imagine-image",
1989
2078
  rank: 16,
1990
- routableModelId: "seedream4"
2079
+ routableModelId: "grok-image"
1991
2080
  },
1992
2081
  {
1993
- apiPricing: "$40/1k images",
1994
- creator: "ByteDance Seed",
1995
- elo: 1184,
2082
+ apiPricing: "$28/1k images",
2083
+ creator: "KlingAI",
2084
+ elo: 1208,
1996
2085
  isCurrent: true,
1997
- model: "Seedream 4.5",
1998
- rank: 17,
1999
- routableModelId: "seedream45"
2086
+ model: "Kling Image 3.0 Omni",
2087
+ rank: 17
2000
2088
  },
2001
2089
  {
2002
- apiPricing: "$26/1k images",
2003
- creator: "Alibaba",
2004
- elo: 1181,
2090
+ apiPricing: "$34/1k images",
2091
+ creator: "Google",
2092
+ elo: 1202,
2005
2093
  isCurrent: true,
2006
- model: "Wan 2.7",
2094
+ model: "Nano Banana 2 Lite",
2007
2095
  rank: 18
2008
2096
  },
2009
2097
  {
2010
- apiPricing: "$39/1k images",
2011
- creator: "Google",
2012
- elo: 1173,
2098
+ apiPricing: "$140/1k images",
2099
+ creator: "Black Forest Labs",
2100
+ elo: 1201,
2013
2101
  isCurrent: true,
2014
- model: "Nano Banana (Gemini 2.5 Flash Image)",
2102
+ model: "FLUX.2 [max]",
2015
2103
  rank: 19,
2016
- routableModelId: "gemini"
2104
+ routableModelId: "flux2-max"
2017
2105
  },
2018
2106
  {
2019
- apiPricing: "$40/1k images",
2020
- creator: "Reve",
2021
- elo: 1172,
2107
+ creator: "Alibaba",
2108
+ elo: 1200,
2022
2109
  isCurrent: true,
2023
- model: "Reve V1 (December)",
2110
+ model: "Wan 2.7 Pro",
2024
2111
  rank: 20
2025
2112
  }
2026
2113
  ],
@@ -2250,7 +2337,31 @@ var VIDEO_IMAGE_TO_VIDEO_TOP_15 = {
2250
2337
  };
2251
2338
 
2252
2339
  // src/server.ts
2253
- var import_neverthrow = require("neverthrow");
2340
+ var import_neverthrow2 = require("neverthrow");
2341
+
2342
+ // src/errors.ts
2343
+ var MotifError = class extends Error {
2344
+ status;
2345
+ code;
2346
+ /** fal's request-correlation id (from the `x-fal-request-id` header or the
2347
+ * error body). Ties a failure back to fal's dashboard/support. */
2348
+ requestId;
2349
+ constructor(message, status, code, requestId) {
2350
+ super(message);
2351
+ this.name = "MotifError";
2352
+ this.status = status;
2353
+ this.code = code;
2354
+ this.requestId = requestId;
2355
+ }
2356
+ };
2357
+ function toMotifError(error) {
2358
+ if (error instanceof MotifError) {
2359
+ return error;
2360
+ }
2361
+ const message = error instanceof Error ? error.message : String(error);
2362
+ const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
2363
+ return new MotifError(message, 0, code);
2364
+ }
2254
2365
 
2255
2366
  // src/fal-parse.ts
2256
2367
  function isRecord(value) {
@@ -2348,9 +2459,362 @@ function requestIdFromBody(text) {
2348
2459
  return asString(parsed.request_id) ?? asString(parsed.requestId) ?? asString(parsed.trace_id);
2349
2460
  }
2350
2461
 
2351
- // src/tools.ts
2352
- var FAL_EXPLORE_CHECKED_AT = "2026-05-12";
2353
- var FAL_TOOLS = {
2462
+ // src/server-tools.ts
2463
+ var import_neverthrow = require("neverthrow");
2464
+
2465
+ // src/tool-registry/analysis.ts
2466
+ var ANALYSIS_TOOLS = {
2467
+ "got-ocr": {
2468
+ category: "analysis",
2469
+ description: "Transcribe text from one or more images, optionally as formatted multi-page output.",
2470
+ endpoint: "fal-ai/got-ocr/v2",
2471
+ inputField: "input_image_urls",
2472
+ inputKind: "images",
2473
+ name: "GOT-OCR 2.0",
2474
+ outputKeys: ["outputs"],
2475
+ price: { kind: "metered" },
2476
+ pricing: "$0.05/image",
2477
+ queued: true,
2478
+ sourceUrl: "https://fal.ai/models/fal-ai/got-ocr/v2",
2479
+ task: "optical character recognition"
2480
+ },
2481
+ "moondream-caption": {
2482
+ category: "analysis",
2483
+ description: "Write a short, normal, or long caption for an image.",
2484
+ endpoint: "fal-ai/moondream3-preview/caption",
2485
+ inputField: "image_url",
2486
+ inputKind: "image",
2487
+ name: "Moondream 3 Caption",
2488
+ outputKeys: ["output"],
2489
+ price: { kind: "metered" },
2490
+ pricing: "$0.40/M input tokens, $3.50/M output tokens",
2491
+ sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/caption",
2492
+ task: "image captioning"
2493
+ },
2494
+ "moondream-detect": {
2495
+ category: "analysis",
2496
+ description: "Detect prompted objects and return their bounding boxes, with an optional preview.",
2497
+ endpoint: "fal-ai/moondream3-preview/detect",
2498
+ inputField: "image_url",
2499
+ inputKind: "image",
2500
+ name: "Moondream 3 Detect",
2501
+ outputKeys: ["objects", "image"],
2502
+ price: { kind: "metered" },
2503
+ pricing: "$0.40/M input tokens, $3.50/M output tokens",
2504
+ sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/detect",
2505
+ task: "object detection"
2506
+ },
2507
+ "moondream-point": {
2508
+ category: "analysis",
2509
+ description: "Return point coordinates for every instance of a prompted subject.",
2510
+ endpoint: "fal-ai/moondream3-preview/point",
2511
+ inputField: "image_url",
2512
+ inputKind: "image",
2513
+ name: "Moondream 3 Point",
2514
+ outputKeys: ["points", "image"],
2515
+ price: { kind: "metered" },
2516
+ pricing: "$0.40/M input tokens, $3.50/M output tokens",
2517
+ sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/point",
2518
+ task: "object pointing"
2519
+ },
2520
+ "moondream-query": {
2521
+ category: "analysis",
2522
+ description: "Answer a free-form question about an image, with reasoning.",
2523
+ endpoint: "fal-ai/moondream3-preview/query",
2524
+ inputField: "image_url",
2525
+ inputKind: "image",
2526
+ name: "Moondream 3 Query",
2527
+ outputKeys: ["output", "reasoning"],
2528
+ price: { kind: "metered" },
2529
+ pricing: "$0.40/M input tokens, $3.50/M output tokens",
2530
+ sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/query",
2531
+ task: "visual question answering"
2532
+ },
2533
+ nsfw: {
2534
+ category: "moderation",
2535
+ description: "Predict whether one or more images contain NSFW concepts.",
2536
+ endpoint: "fal-ai/x-ailab/nsfw",
2537
+ inputField: "image_urls",
2538
+ inputKind: "images",
2539
+ name: "NSFW Checker",
2540
+ outputKeys: ["has_nsfw_concepts"],
2541
+ price: { kind: "metered" },
2542
+ pricing: "$0.001/image",
2543
+ sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw",
2544
+ task: "vision moderation"
2545
+ }
2546
+ };
2547
+
2548
+ // src/tool-registry/assets.ts
2549
+ var ASSET_TOOLS = {
2550
+ "hunyuan3d-v3": {
2551
+ category: "3d",
2552
+ description: "Reconstruct a textured 3D mesh from a single image, with optional multi-view inputs.",
2553
+ endpoint: "fal-ai/hunyuan3d-v3/image-to-3d",
2554
+ inputField: "input_image_url",
2555
+ inputKind: "image",
2556
+ name: "Hunyuan3D v3 Image to 3D",
2557
+ outputKeys: ["model_glb", "model_urls", "thumbnail"],
2558
+ price: { kind: "call", usd: 0.375 },
2559
+ 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",
2560
+ queued: true,
2561
+ sourceUrl: "https://fal.ai/models/fal-ai/hunyuan3d-v3/image-to-3d",
2562
+ task: "single-image 3D reconstruction"
2563
+ },
2564
+ "ideogram-layerize-text": {
2565
+ category: "layers",
2566
+ description: "Split an image into background image layers plus editable HTML text containers.",
2567
+ endpoint: "fal-ai/ideogram/v3/layerize-text",
2568
+ inputField: "image_url",
2569
+ inputKind: "image",
2570
+ name: "Ideogram v3 Text Layerize",
2571
+ outputKeys: ["image_layers", "image", "text_html", "text_containers"],
2572
+ price: { kind: "call", usd: 0.09 },
2573
+ pricing: "$0.09/image",
2574
+ queued: true,
2575
+ sourceUrl: "https://fal.ai/models/fal-ai/ideogram/v3/layerize-text",
2576
+ task: "text and image layer extraction"
2577
+ },
2578
+ "ideogram-tiling": {
2579
+ category: "material",
2580
+ description: "Generate a seamlessly tiling texture, optionally conditioned on a source image.",
2581
+ endpoint: "ideogram/v4/tiling",
2582
+ inputField: "image_url",
2583
+ inputKind: "image",
2584
+ name: "Ideogram v4 Tiling",
2585
+ outputKeys: ["images"],
2586
+ price: { kind: "megapixel", usd: 0.06 },
2587
+ pricing: "$0.06/megapixel at the default BALANCED speed; $0.03 turbo, $0.10 quality",
2588
+ sourceUrl: "https://fal.ai/models/ideogram/v4/tiling",
2589
+ task: "seamless texture generation"
2590
+ },
2591
+ image2svg: {
2592
+ category: "vector",
2593
+ description: "Trace a raster image into layered SVG paths.",
2594
+ endpoint: "fal-ai/image2svg",
2595
+ inputField: "image_url",
2596
+ inputKind: "image",
2597
+ name: "Image to SVG",
2598
+ outputKeys: ["images"],
2599
+ price: { kind: "call", usd: 5e-3 },
2600
+ pricing: "$0.005/image",
2601
+ sourceUrl: "https://fal.ai/models/fal-ai/image2svg",
2602
+ task: "raster to vector tracing"
2603
+ },
2604
+ patina: {
2605
+ category: "material",
2606
+ description: "Decompose a surface photograph into PBR maps: basecolor, normal, roughness, metalness, height.",
2607
+ endpoint: "fal-ai/patina",
2608
+ inputField: "image_url",
2609
+ inputKind: "image",
2610
+ name: "Patina PBR Maps",
2611
+ outputKeys: ["images"],
2612
+ // Output order follows the request's `maps` array, so read that first;
2613
+ // the fallback is the endpoint's own schema default.
2614
+ outputLabels: {
2615
+ images: {
2616
+ fallback: ["basecolor", "normal", "roughness", "metalness", "height"],
2617
+ fromOption: "maps"
2618
+ }
2619
+ },
2620
+ price: { kind: "metered" },
2621
+ 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",
2622
+ queued: true,
2623
+ sourceUrl: "https://fal.ai/models/fal-ai/patina",
2624
+ task: "PBR material decomposition"
2625
+ },
2626
+ "patina-extract": {
2627
+ category: "material",
2628
+ description: "Extract a tiling PBR material from a prompted region of a photograph.",
2629
+ endpoint: "fal-ai/patina/material/extract",
2630
+ inputField: "image_url",
2631
+ inputKind: "image",
2632
+ name: "Patina Material Extract",
2633
+ outputKeys: ["images"],
2634
+ // Same `maps` option and schema default as fal-ai/patina, both verified.
2635
+ // `num_images` above 1 multiplies the array and breaks the mapping, which
2636
+ // the count check catches.
2637
+ outputLabels: {
2638
+ images: {
2639
+ fallback: ["basecolor", "normal", "roughness", "metalness", "height"],
2640
+ fromOption: "maps"
2641
+ }
2642
+ },
2643
+ price: { kind: "metered" },
2644
+ pricing: "$0.10 base only; add $0.02/megapixel plus $0.01/megapixel per map, so 1MP with all 5 maps is $0.17",
2645
+ queued: true,
2646
+ sourceUrl: "https://fal.ai/models/fal-ai/patina/material/extract",
2647
+ task: "tiling material extraction"
2648
+ },
2649
+ "qwen-layered": {
2650
+ category: "layers",
2651
+ description: "Split an image into a configurable number of stacked RGBA layers.",
2652
+ endpoint: "fal-ai/qwen-image-layered",
2653
+ inputField: "image_url",
2654
+ inputKind: "image",
2655
+ name: "Qwen Image Layered",
2656
+ outputKeys: ["images"],
2657
+ // fal publishes a figure but not what it counts. Encoding it as `call`
2658
+ // would assert a per-request price we cannot support, and this endpoint
2659
+ // returns several outputs — the same shape that made `seedream-layerize`
2660
+ // under-report by a factor of its layer count. `metered` reports null
2661
+ // rather than a number that is right only on one reading.
2662
+ price: { kind: "metered" },
2663
+ 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",
2664
+ queued: true,
2665
+ sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-layered",
2666
+ task: "image layer decomposition"
2667
+ },
2668
+ "recraft-vectorize": {
2669
+ category: "vector",
2670
+ description: "Convert a raster image into a clean SVG.",
2671
+ endpoint: "fal-ai/recraft/vectorize",
2672
+ inputField: "image_url",
2673
+ inputKind: "image",
2674
+ name: "Recraft Vectorize",
2675
+ outputKeys: ["image"],
2676
+ price: { kind: "call", usd: 0.04 },
2677
+ pricing: "$0.04/image, $0.08 with a vector style",
2678
+ sourceUrl: "https://fal.ai/models/fal-ai/recraft/vectorize",
2679
+ task: "raster to vector conversion"
2680
+ },
2681
+ "sam3-3d-align": {
2682
+ category: "3d",
2683
+ description: "Align SAM 3D objects and bodies into a shared scene.",
2684
+ endpoint: "fal-ai/sam-3/3d-align",
2685
+ inputField: "image_url",
2686
+ inputKind: "image",
2687
+ name: "SAM 3D Align",
2688
+ outputKeys: [
2689
+ "scene_glb",
2690
+ "model_glb",
2691
+ "visualization",
2692
+ "body_mesh_ply",
2693
+ "metadata"
2694
+ ],
2695
+ // fal publishes a figure but not what it counts. Encoding it as `call`
2696
+ // would assert a per-request price we cannot support, and this endpoint
2697
+ // returns several outputs — the same shape that made `seedream-layerize`
2698
+ // under-report by a factor of its layer count. `metered` reports null
2699
+ // rather than a number that is right only on one reading.
2700
+ price: { kind: "metered" },
2701
+ pricing: "$0.02 per unit; fal does not define a unit, so no estimate is reported",
2702
+ queued: true,
2703
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-align",
2704
+ task: "3D scene alignment"
2705
+ },
2706
+ "sam3-3d-body": {
2707
+ category: "3d",
2708
+ defaultOptions: {
2709
+ export_meshes: true,
2710
+ include_3d_keypoints: true,
2711
+ include_mhr_params: true
2712
+ },
2713
+ description: "Reconstruct human body meshes and keypoints from a single image.",
2714
+ endpoint: "fal-ai/sam-3/3d-body",
2715
+ inputField: "image_url",
2716
+ inputKind: "image",
2717
+ name: "SAM 3D Body",
2718
+ outputKeys: ["model_glb", "visualization", "meshes", "metadata"],
2719
+ // fal publishes a figure but not what it counts. Encoding it as `call`
2720
+ // would assert a per-request price we cannot support, and this endpoint
2721
+ // returns several outputs — the same shape that made `seedream-layerize`
2722
+ // under-report by a factor of its layer count. `metered` reports null
2723
+ // rather than a number that is right only on one reading.
2724
+ price: { kind: "metered" },
2725
+ pricing: "$0.02 per unit; fal does not define a unit, so no estimate is reported",
2726
+ queued: true,
2727
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-body",
2728
+ task: "single-image 3D body reconstruction"
2729
+ },
2730
+ "sam3-3d-objects": {
2731
+ category: "3d",
2732
+ defaultOptions: {
2733
+ prompt: "car"
2734
+ },
2735
+ description: "Reconstruct one or more 3D objects from an image and prompts.",
2736
+ endpoint: "fal-ai/sam-3/3d-objects",
2737
+ inputField: "image_url",
2738
+ inputKind: "image",
2739
+ name: "SAM 3D Objects",
2740
+ outputKeys: [
2741
+ "gaussian_splat",
2742
+ "model_glb",
2743
+ "metadata",
2744
+ "individual_splats",
2745
+ "individual_glbs",
2746
+ "artifacts_zip"
2747
+ ],
2748
+ // fal publishes a figure but not what it counts. Encoding it as `call`
2749
+ // would assert a per-request price we cannot support, and this endpoint
2750
+ // returns several outputs — the same shape that made `seedream-layerize`
2751
+ // under-report by a factor of its layer count. `metered` reports null
2752
+ // rather than a number that is right only on one reading.
2753
+ price: { kind: "metered" },
2754
+ 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",
2755
+ queued: true,
2756
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-objects",
2757
+ task: "single-image 3D object reconstruction"
2758
+ },
2759
+ "seedream-layerize": {
2760
+ category: "layers",
2761
+ description: "Split an image into editable Seedream layers plus flattened previews.",
2762
+ endpoint: "bytedance/seedream/v5/pro/layerize",
2763
+ inputField: "image_url",
2764
+ inputKind: "image",
2765
+ name: "Seedream v5 Pro Layerize",
2766
+ outputKeys: ["layers", "images"],
2767
+ price: { kind: "metered" },
2768
+ pricing: "$0.03375 per generated layer below 1536x1536 total area, $0.0675 per layer above; the listed rate is per layer, not per call",
2769
+ queued: true,
2770
+ sourceUrl: "https://fal.ai/models/bytedance/seedream/v5/pro/layerize",
2771
+ task: "image layer decomposition"
2772
+ },
2773
+ "seedvr-seamless": {
2774
+ category: "material",
2775
+ description: "Upscale a tiling texture while keeping its edges seamless. ",
2776
+ endpoint: "fal-ai/seedvr/upscale/image/seamless",
2777
+ inputField: "image_url",
2778
+ inputKind: "image",
2779
+ name: "SeedVR Seamless Upscale",
2780
+ outputKeys: ["image"],
2781
+ price: { kind: "megapixel", usd: 25e-4 },
2782
+ pricing: "$0.0025/megapixel",
2783
+ queued: true,
2784
+ sourceUrl: "https://fal.ai/models/fal-ai/seedvr/upscale/image/seamless",
2785
+ task: "seamless texture upscaling"
2786
+ },
2787
+ "trellis-2": {
2788
+ category: "3d",
2789
+ description: "Reconstruct a textured 3D mesh from a single image, with UV unwrapping.",
2790
+ endpoint: "fal-ai/trellis-2",
2791
+ inputField: "image_url",
2792
+ inputKind: "image",
2793
+ name: "TRELLIS 2",
2794
+ outputKeys: ["model_glb"],
2795
+ price: { kind: "call", usd: 0.3 },
2796
+ pricing: "$0.30 at the default 1024p; $0.25 at 512p, $0.35 at 1536p",
2797
+ queued: true,
2798
+ sourceUrl: "https://fal.ai/models/fal-ai/trellis-2",
2799
+ task: "single-image 3D reconstruction"
2800
+ }
2801
+ };
2802
+
2803
+ // src/tool-registry/background.ts
2804
+ var BACKGROUND_TOOLS = {
2805
+ "ben-v2": {
2806
+ category: "background",
2807
+ description: "Fast, high-quality background removal for images.",
2808
+ endpoint: "fal-ai/ben/v2/image",
2809
+ inputField: "image_url",
2810
+ inputKind: "image",
2811
+ name: "BEN v2 Background Removal",
2812
+ outputKeys: ["image"],
2813
+ price: { kind: "megapixel", usd: 0.025 },
2814
+ pricing: "$0.025/megapixel",
2815
+ sourceUrl: "https://fal.ai/models/fal-ai/ben/v2/image",
2816
+ task: "image background removal"
2817
+ },
2354
2818
  birefnet: {
2355
2819
  category: "background",
2356
2820
  defaultOptions: {
@@ -2363,41 +2827,455 @@ var FAL_TOOLS = {
2363
2827
  endpoint: "fal-ai/birefnet/v2",
2364
2828
  inputField: "image_url",
2365
2829
  inputKind: "image",
2366
- name: "BirefNet Background Removal",
2367
- outputKeys: ["image", "mask_image"],
2368
- pricing: "$0/compute-second listed by fal",
2369
- sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
2370
- task: "image background removal"
2830
+ name: "BirefNet Background Removal",
2831
+ outputKeys: ["image", "mask_image"],
2832
+ price: { kind: "metered" },
2833
+ pricing: "$0/compute-second listed by fal",
2834
+ sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
2835
+ task: "image background removal"
2836
+ },
2837
+ "bria-rmbg": {
2838
+ category: "background",
2839
+ description: "Commercial-safe background removal for images.",
2840
+ endpoint: "fal-ai/bria/background/remove",
2841
+ inputField: "image_url",
2842
+ inputKind: "image",
2843
+ name: "Bria RMBG 2.0",
2844
+ outputKeys: ["image"],
2845
+ price: { kind: "call", usd: 0.018 },
2846
+ pricing: "$0.018/generation",
2847
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/background/remove",
2848
+ task: "image background removal"
2849
+ },
2850
+ "bria-video-rmbg": {
2851
+ category: "background",
2852
+ defaultOptions: {
2853
+ background_color: "Black",
2854
+ output_container_and_codec: "webm_vp9",
2855
+ preserve_audio: true
2856
+ },
2857
+ description: "Remove video backgrounds with configurable output container.",
2858
+ endpoint: "bria/video/background-removal",
2859
+ inputField: "video_url",
2860
+ inputKind: "video",
2861
+ name: "Bria Video Background Removal",
2862
+ outputKeys: ["video"],
2863
+ price: { kind: "second", usd: 425e-5 },
2864
+ pricing: "$0.00425/sec",
2865
+ queued: true,
2866
+ sourceUrl: "https://fal.ai/models/bria/video/background-removal",
2867
+ task: "video background removal"
2868
+ },
2869
+ ddcolor: {
2870
+ category: "restoration",
2871
+ description: "Colourise black-and-white photographs.",
2872
+ endpoint: "fal-ai/ddcolor",
2873
+ inputField: "image_url",
2874
+ inputKind: "image",
2875
+ name: "DDColor Colourisation",
2876
+ outputKeys: ["image"],
2877
+ price: { kind: "megapixel", usd: 1e-3 },
2878
+ pricing: "$0.001/megapixel",
2879
+ sourceUrl: "https://fal.ai/models/fal-ai/ddcolor",
2880
+ task: "photo colourisation"
2881
+ },
2882
+ rembg: {
2883
+ category: "background",
2884
+ defaultOptions: {
2885
+ crop_to_bbox: false
2886
+ },
2887
+ description: "Generic image background removal utility.",
2888
+ endpoint: "fal-ai/imageutils/rembg",
2889
+ inputField: "image_url",
2890
+ inputKind: "image",
2891
+ name: "Remove Background",
2892
+ outputKeys: ["image"],
2893
+ price: { kind: "metered" },
2894
+ pricing: "$0/compute-second listed by fal",
2895
+ sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
2896
+ task: "image background removal"
2897
+ },
2898
+ "seedvr-upscale": {
2899
+ category: "restoration",
2900
+ defaultOptions: {
2901
+ output_format: "png"
2902
+ },
2903
+ description: "Diffusion image restoration and upscaling with SeedVR.",
2904
+ endpoint: "fal-ai/seedvr/upscale/image",
2905
+ inputField: "image_url",
2906
+ inputKind: "image",
2907
+ name: "SeedVR Image Upscale",
2908
+ outputKeys: ["image"],
2909
+ price: { kind: "megapixel", usd: 1e-3 },
2910
+ pricing: "$0.001/megapixel",
2911
+ queued: true,
2912
+ sourceUrl: "https://fal.ai/models/fal-ai/seedvr/upscale/image",
2913
+ task: "image restoration and upscaling"
2914
+ },
2915
+ "topaz-adjust": {
2916
+ category: "restoration",
2917
+ defaultOptions: {
2918
+ output_format: "png"
2919
+ },
2920
+ description: "Topaz exposure, white balance, and colour adjustment.",
2921
+ endpoint: "topaz/adjust/image",
2922
+ inputField: "image_url",
2923
+ inputKind: "image",
2924
+ name: "Topaz Adjust",
2925
+ outputKeys: ["image"],
2926
+ price: { kind: "megapixel", usd: 0.08 / 24 },
2927
+ pricing: "$0.08 per 24 output megapixels",
2928
+ queued: true,
2929
+ sourceUrl: "https://fal.ai/models/topaz/adjust/image",
2930
+ task: "tonal adjustment"
2931
+ },
2932
+ "topaz-creative": {
2933
+ category: "restoration",
2934
+ defaultOptions: {
2935
+ output_format: "png"
2936
+ },
2937
+ description: "Topaz Bloom upscaling, which reimagines detail rather than preserving it.",
2938
+ endpoint: "topaz/upscale/image/creative",
2939
+ inputField: "image_url",
2940
+ inputKind: "image",
2941
+ name: "Topaz Creative Upscale",
2942
+ outputKeys: ["image"],
2943
+ price: { kind: "megapixel", usd: 0.96 / 24 },
2944
+ pricing: "$0.96 per 24 output megapixels",
2945
+ queued: true,
2946
+ sourceUrl: "https://fal.ai/models/topaz/upscale/image/creative",
2947
+ task: "creative image upscaling"
2948
+ },
2949
+ "topaz-denoise": {
2950
+ category: "restoration",
2951
+ defaultOptions: {
2952
+ output_format: "png"
2953
+ },
2954
+ description: "Topaz noise reduction at source resolution.",
2955
+ endpoint: "topaz/denoise/image",
2956
+ inputField: "image_url",
2957
+ inputKind: "image",
2958
+ name: "Topaz Denoise",
2959
+ outputKeys: ["image"],
2960
+ price: { kind: "megapixel", usd: 0.08 / 24 },
2961
+ pricing: "$0.08 per 24 output megapixels at the default Normal model; $0.16 with Denoise Max",
2962
+ queued: true,
2963
+ sourceUrl: "https://fal.ai/models/topaz/denoise/image",
2964
+ task: "image denoising"
2965
+ },
2966
+ "topaz-generative": {
2967
+ category: "restoration",
2968
+ defaultOptions: {
2969
+ output_format: "png"
2970
+ },
2971
+ description: "Topaz Wonder upscaling, which synthesises plausible detail as it enlarges.",
2972
+ endpoint: "topaz/upscale/image/generative",
2973
+ inputField: "image_url",
2974
+ inputKind: "image",
2975
+ name: "Topaz Generative Upscale",
2976
+ outputKeys: ["image"],
2977
+ price: { kind: "megapixel", usd: 0.24 / 24 },
2978
+ pricing: "$0.24 per 24 output megapixels at the default Wonder 3; $0.48 with Wonder, Wonder 2, Standard MAX, Redefine or Recover 3",
2979
+ queued: true,
2980
+ sourceUrl: "https://fal.ai/models/topaz/upscale/image/generative",
2981
+ task: "generative image upscaling"
2982
+ },
2983
+ "topaz-image": {
2984
+ category: "upscale",
2985
+ defaultOptions: {
2986
+ model: "Standard V2",
2987
+ output_format: "jpeg",
2988
+ upscale_factor: 2
2989
+ },
2990
+ description: "Professional Topaz image enhancement and upscaling.",
2991
+ endpoint: "fal-ai/topaz/upscale/image",
2992
+ inputField: "image_url",
2993
+ inputKind: "image",
2994
+ name: "Topaz Image Upscale",
2995
+ outputKeys: ["image"],
2996
+ price: { kind: "megapixel", usd: 0.08 / 24 },
2997
+ pricing: "$0.08 for output up to 24MP; $0.16 to 48MP, $0.32 to 96MP, up to $1.36 at 512MP",
2998
+ queued: true,
2999
+ sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/image",
3000
+ task: "image enhancement"
3001
+ },
3002
+ "topaz-precision": {
3003
+ category: "restoration",
3004
+ defaultOptions: {
3005
+ output_format: "png"
3006
+ },
3007
+ description: "Topaz upscaling that preserves the original detail rather than inventing it.",
3008
+ endpoint: "topaz/upscale/image/precision",
3009
+ inputField: "image_url",
3010
+ inputKind: "image",
3011
+ name: "Topaz Precision Upscale",
3012
+ outputKeys: ["image"],
3013
+ price: { kind: "megapixel", usd: 0.08 / 24 },
3014
+ pricing: "$0.08 per 24 output megapixels, any precision model",
3015
+ queued: true,
3016
+ sourceUrl: "https://fal.ai/models/topaz/upscale/image/precision",
3017
+ task: "faithful image upscaling"
3018
+ },
3019
+ "topaz-restore": {
3020
+ category: "restoration",
3021
+ defaultOptions: {
3022
+ output_format: "png"
3023
+ },
3024
+ description: "Topaz restoration of damaged or degraded photographs at source resolution.",
3025
+ endpoint: "topaz/restore/image",
3026
+ inputField: "image_url",
3027
+ inputKind: "image",
3028
+ name: "Topaz Restore",
3029
+ outputKeys: ["image"],
3030
+ price: { kind: "megapixel", usd: 0.48 / 24 },
3031
+ pricing: "$0.48 per 24 output megapixels at the default Recover 3; $0.08 with Dust-Scratch V2",
3032
+ queued: true,
3033
+ sourceUrl: "https://fal.ai/models/topaz/restore/image",
3034
+ task: "photo restoration"
3035
+ },
3036
+ "topaz-sharpen": {
3037
+ category: "restoration",
3038
+ defaultOptions: {
3039
+ output_format: "png"
3040
+ },
3041
+ description: "Topaz deblurring and sharpening at source resolution.",
3042
+ endpoint: "topaz/sharpen/image",
3043
+ inputField: "image_url",
3044
+ inputKind: "image",
3045
+ name: "Topaz Sharpen",
3046
+ outputKeys: ["image"],
3047
+ price: { kind: "megapixel", usd: 0.08 / 24 },
3048
+ pricing: "$0.08 per 24 output megapixels at the default Standard model; $0.16 with Super Focus",
3049
+ queued: true,
3050
+ sourceUrl: "https://fal.ai/models/topaz/sharpen/image",
3051
+ task: "image sharpening"
3052
+ },
3053
+ "topaz-transparent": {
3054
+ category: "restoration",
3055
+ description: "Topaz upscaling that preserves the alpha channel end to end.",
3056
+ endpoint: "topaz/upscale/image/transparent",
3057
+ inputField: "image_url",
3058
+ inputKind: "image",
3059
+ name: "Topaz Transparent Upscale",
3060
+ outputKeys: ["image"],
3061
+ price: { kind: "megapixel", usd: 0.08 / 24 },
3062
+ pricing: "$0.08 per 24 output megapixels",
3063
+ queued: true,
3064
+ sourceUrl: "https://fal.ai/models/topaz/upscale/image/transparent",
3065
+ task: "transparent image upscaling"
3066
+ },
3067
+ "topaz-video": {
3068
+ category: "upscale",
3069
+ defaultOptions: {
3070
+ model: "Proteus",
3071
+ upscale_factor: 2
3072
+ },
3073
+ description: "Professional Topaz video enhancement and upscaling.",
3074
+ endpoint: "fal-ai/topaz/upscale/video",
3075
+ inputField: "video_url",
3076
+ inputKind: "video",
3077
+ name: "Topaz Video Upscale",
3078
+ outputKeys: ["video"],
3079
+ price: { kind: "second", usd: 0.01 },
3080
+ pricing: "$0.01/sec up to 720p; $0.02 to 1080p, $0.08 above, doubled at 60fps, halved with Gaia 2",
3081
+ queued: true,
3082
+ sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
3083
+ task: "video enhancement"
3084
+ }
3085
+ };
3086
+
3087
+ // src/tool-registry/editing.ts
3088
+ var EDITING_TOOLS = {
3089
+ "bria-eraser": {
3090
+ category: "erase",
3091
+ description: "Commercial-safe removal of a masked region, inpainted from surrounding context.",
3092
+ endpoint: "fal-ai/bria/eraser",
3093
+ inputField: "image_url",
3094
+ inputKind: "image",
3095
+ name: "Bria Eraser",
3096
+ outputKeys: ["image"],
3097
+ price: { kind: "call", usd: 0.04 },
3098
+ pricing: "$0.04/generation",
3099
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/eraser",
3100
+ task: "masked object erasure"
3101
+ },
3102
+ "bria-expand": {
3103
+ category: "reframe",
3104
+ description: "Expand an image onto a larger canvas, generating the new border area.",
3105
+ endpoint: "fal-ai/bria/expand",
3106
+ inputField: "image_url",
3107
+ inputKind: "image",
3108
+ name: "Bria Image Expansion",
3109
+ outputKeys: ["image"],
3110
+ price: { kind: "call", usd: 0.04 },
3111
+ pricing: "$0.04/generation",
3112
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/expand",
3113
+ task: "image outpainting"
3114
+ },
3115
+ "bria-genfill": {
3116
+ category: "erase",
3117
+ description: "Generative fill of a masked region from a prompt, commercial-safe.",
3118
+ endpoint: "fal-ai/bria/genfill",
3119
+ inputField: "image_url",
3120
+ inputKind: "image",
3121
+ name: "Bria GenFill",
3122
+ outputKeys: ["images"],
3123
+ price: { kind: "call", usd: 0.04 },
3124
+ pricing: "$0.04/generation",
3125
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/genfill",
3126
+ task: "masked generative fill"
3127
+ },
3128
+ "finegrain-eraser": {
3129
+ category: "erase",
3130
+ description: "Remove a prompted object and its shadows and reflections, filling the gap.",
3131
+ endpoint: "fal-ai/finegrain-eraser",
3132
+ inputField: "image_url",
3133
+ inputKind: "image",
3134
+ name: "Finegrain Object Eraser",
3135
+ outputKeys: ["image"],
3136
+ price: { kind: "call", usd: 0.27 },
3137
+ pricing: "$0.27/image at the default standard mode; $0.18 express, $0.36 premium",
3138
+ sourceUrl: "https://fal.ai/models/fal-ai/finegrain-eraser",
3139
+ task: "prompted object erasure"
3140
+ },
3141
+ "flux-outpaint": {
3142
+ category: "reframe",
3143
+ description: "Outpaint an image by a per-edge pixel expansion with FLUX.2 pro.",
3144
+ endpoint: "fal-ai/flux-2-pro/outpaint",
3145
+ inputField: "image_url",
3146
+ inputKind: "image",
3147
+ name: "FLUX.2 Pro Outpaint",
3148
+ outputKeys: ["images"],
3149
+ price: { kind: "metered" },
3150
+ pricing: "$0.03 for the first output megapixel, then $0.015 per extra megapixel of input and output, rounded up",
3151
+ sourceUrl: "https://fal.ai/models/fal-ai/flux-2-pro/outpaint",
3152
+ task: "image outpainting"
3153
+ },
3154
+ "iclight-v2": {
3155
+ category: "relight",
3156
+ description: "Relight a subject from a prompt, harmonising it with a new light direction.",
3157
+ endpoint: "fal-ai/iclight-v2",
3158
+ inputField: "image_url",
3159
+ inputKind: "image",
3160
+ name: "IC-Light v2",
3161
+ outputKeys: ["images"],
3162
+ price: { kind: "megapixel", usd: 0.1 },
3163
+ pricing: "$0.10/megapixel",
3164
+ sourceUrl: "https://fal.ai/models/fal-ai/iclight-v2",
3165
+ task: "prompted relighting"
3166
+ },
3167
+ "ideogram-reframe": {
3168
+ category: "reframe",
3169
+ description: "Reframe an image to a new aspect ratio, generating the fill.",
3170
+ endpoint: "fal-ai/ideogram/v3/reframe",
3171
+ inputField: "image_url",
3172
+ inputKind: "image",
3173
+ name: "Ideogram v3 Reframe",
3174
+ outputKeys: ["images"],
3175
+ price: { kind: "call", usd: 0.06 },
3176
+ pricing: "$0.06/image at the default BALANCED speed; $0.03 turbo, $0.09 quality",
3177
+ sourceUrl: "https://fal.ai/models/fal-ai/ideogram/v3/reframe",
3178
+ task: "aspect ratio reframing"
3179
+ },
3180
+ "lighting-restoration": {
3181
+ category: "relight",
3182
+ description: "Restore natural, even lighting across one or more input images.",
3183
+ endpoint: "fal-ai/qwen-image-edit-plus-lora-gallery/lighting-restoration",
3184
+ inputField: "image_urls",
3185
+ inputKind: "images",
3186
+ name: "Qwen Lighting Restoration",
3187
+ outputKeys: ["images"],
3188
+ price: { kind: "megapixel", usd: 0.035 },
3189
+ pricing: "$0.035/megapixel",
3190
+ sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-edit-plus-lora-gallery/lighting-restoration",
3191
+ task: "lighting restoration"
3192
+ },
3193
+ "object-removal": {
3194
+ category: "erase",
3195
+ description: "Remove a prompted object from an image and fill the gap.",
3196
+ endpoint: "fal-ai/object-removal",
3197
+ inputField: "image_url",
3198
+ inputKind: "image",
3199
+ name: "Object Removal",
3200
+ outputKeys: ["images"],
3201
+ price: { kind: "call", usd: 0.024 },
3202
+ pricing: "$0.024/image at the default best quality; $0.006 low, $0.012 medium, $0.018 high",
3203
+ sourceUrl: "https://fal.ai/models/fal-ai/object-removal",
3204
+ task: "prompted object removal"
2371
3205
  },
2372
- "bria-rmbg": {
2373
- category: "background",
2374
- description: "Commercial-safe background removal for images.",
2375
- endpoint: "fal-ai/bria/background/remove",
3206
+ "object-removal-bbox": {
3207
+ category: "erase",
3208
+ description: "Remove whatever falls inside supplied bounding boxes.",
3209
+ endpoint: "fal-ai/object-removal/bbox",
2376
3210
  inputField: "image_url",
2377
3211
  inputKind: "image",
2378
- name: "Bria RMBG 2.0",
2379
- outputKeys: ["image"],
2380
- pricing: "$0.02/image",
2381
- sourceUrl: "https://fal.ai/models/fal-ai/bria/background/remove",
2382
- task: "image background removal"
3212
+ name: "Object Removal by Box",
3213
+ outputKeys: ["images"],
3214
+ price: { kind: "call", usd: 0.024 },
3215
+ pricing: "$0.024/image at the default best quality; $0.006 low, $0.012 medium, $0.018 high",
3216
+ sourceUrl: "https://fal.ai/models/fal-ai/object-removal/bbox",
3217
+ task: "bounding box object removal"
2383
3218
  },
2384
- "bria-video-rmbg": {
2385
- category: "background",
3219
+ "object-removal-mask": {
3220
+ category: "erase",
3221
+ description: "Remove the masked region of an image and fill the gap.",
3222
+ endpoint: "fal-ai/object-removal/mask",
3223
+ inputField: "image_url",
3224
+ inputKind: "image",
3225
+ name: "Object Removal by Mask",
3226
+ outputKeys: ["images"],
3227
+ price: { kind: "call", usd: 0.024 },
3228
+ pricing: "$0.024/image at the default best quality; $0.006 low, $0.012 medium, $0.018 high",
3229
+ sourceUrl: "https://fal.ai/models/fal-ai/object-removal/mask",
3230
+ task: "masked object removal"
3231
+ },
3232
+ "remove-lighting": {
3233
+ category: "relight",
3234
+ description: "Strip baked-in lighting and shadows to leave a flat, neutral surface.",
3235
+ endpoint: "fal-ai/qwen-image-edit-plus-lora-gallery/remove-lighting",
3236
+ inputField: "image_urls",
3237
+ inputKind: "images",
3238
+ name: "Qwen Remove Lighting",
3239
+ outputKeys: ["images"],
3240
+ price: { kind: "megapixel", usd: 0.035 },
3241
+ pricing: "$0.035/megapixel",
3242
+ sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-edit-plus-lora-gallery/remove-lighting",
3243
+ task: "lighting removal"
3244
+ },
3245
+ "smart-resize": {
3246
+ category: "reframe",
3247
+ description: "Resize one image to several target sizes, recomposing rather than cropping.",
3248
+ endpoint: "fal-ai/smart-resize",
3249
+ inputField: "image_url",
3250
+ inputKind: "image",
3251
+ name: "Smart Resize",
3252
+ outputKeys: ["images", "results"],
3253
+ price: { kind: "metered" },
3254
+ pricing: "$0.15 per output image, doubled at 4K, plus a $0.05 vision analysis fee per request",
3255
+ queued: true,
3256
+ sourceUrl: "https://fal.ai/models/fal-ai/smart-resize",
3257
+ task: "multi-size recomposition"
3258
+ },
3259
+ "text-removal": {
3260
+ category: "erase",
2386
3261
  defaultOptions: {
2387
- background_color: "Black",
2388
- output_container_and_codec: "webm_vp9",
2389
- preserve_audio: true
3262
+ output_format: "png"
2390
3263
  },
2391
- description: "Remove video backgrounds with configurable output container.",
2392
- endpoint: "bria/video/background-removal",
2393
- inputField: "video_url",
2394
- inputKind: "video",
2395
- name: "Bria Video Background Removal",
2396
- outputKeys: ["video"],
2397
- pricing: "$0.14/sec",
2398
- sourceUrl: "https://fal.ai/models/bria/video/background-removal",
2399
- task: "video background removal"
2400
- },
3264
+ description: "Remove all rendered text from an image, rebuilding what sat behind it.",
3265
+ endpoint: "fal-ai/image-editing/text-removal",
3266
+ inputField: "image_url",
3267
+ inputKind: "image",
3268
+ name: "Text Removal",
3269
+ outputKeys: ["images"],
3270
+ price: { kind: "call", usd: 0.04 },
3271
+ pricing: "$0.04/image",
3272
+ sourceUrl: "https://fal.ai/models/fal-ai/image-editing/text-removal",
3273
+ task: "text removal"
3274
+ }
3275
+ };
3276
+
3277
+ // src/tool-registry/structure.ts
3278
+ var STRUCTURE_TOOLS = {
2401
3279
  "depth-anything": {
2402
3280
  category: "preprocess",
2403
3281
  description: "Generate Depth Anything v2 depth maps from input images.",
@@ -2406,10 +3284,37 @@ var FAL_TOOLS = {
2406
3284
  inputKind: "image",
2407
3285
  name: "Depth Anything v2 Preprocessor",
2408
3286
  outputKeys: ["image"],
3287
+ price: { kind: "metered" },
2409
3288
  pricing: "$0/compute-second listed by fal",
2410
3289
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/depth-anything/v2",
2411
3290
  task: "depth preprocessing"
2412
3291
  },
3292
+ dwpose: {
3293
+ category: "preprocess",
3294
+ description: "Extract body, hand, and face pose skeletons from an image.",
3295
+ endpoint: "fal-ai/dwpose",
3296
+ inputField: "image_url",
3297
+ inputKind: "image",
3298
+ name: "DWPose Estimation",
3299
+ outputKeys: ["image"],
3300
+ price: { kind: "second", usd: 6e-4 },
3301
+ pricing: "$0.0006/compute-second",
3302
+ sourceUrl: "https://fal.ai/models/fal-ai/dwpose",
3303
+ task: "pose preprocessing"
3304
+ },
3305
+ hed: {
3306
+ category: "preprocess",
3307
+ description: "Generate HED soft edge maps for ControlNet-style workflows.",
3308
+ endpoint: "fal-ai/image-preprocessors/hed",
3309
+ inputField: "image_url",
3310
+ inputKind: "image",
3311
+ name: "HED Edge Preprocessor",
3312
+ outputKeys: ["image"],
3313
+ price: { kind: "metered" },
3314
+ pricing: "$0/compute-second listed by fal",
3315
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/hed",
3316
+ task: "edge preprocessing"
3317
+ },
2413
3318
  lineart: {
2414
3319
  category: "preprocess",
2415
3320
  defaultOptions: {
@@ -2421,6 +3326,7 @@ var FAL_TOOLS = {
2421
3326
  inputKind: "image",
2422
3327
  name: "Line Art Preprocessor",
2423
3328
  outputKeys: ["image"],
3329
+ price: { kind: "metered" },
2424
3330
  pricing: "$0/compute-second listed by fal",
2425
3331
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/lineart",
2426
3332
  task: "image preprocessing"
@@ -2437,6 +3343,7 @@ var FAL_TOOLS = {
2437
3343
  inputKind: "image",
2438
3344
  name: "Marigold Depth Estimation",
2439
3345
  outputKeys: ["image"],
3346
+ price: { kind: "metered" },
2440
3347
  pricing: "$0/compute-second listed by fal",
2441
3348
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/marigold-depth",
2442
3349
  task: "depth map"
@@ -2453,6 +3360,7 @@ var FAL_TOOLS = {
2453
3360
  inputKind: "image",
2454
3361
  name: "MiDaS Depth Estimation",
2455
3362
  outputKeys: ["image"],
3363
+ price: { kind: "metered" },
2456
3364
  pricing: "$0/compute-second listed by fal",
2457
3365
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/depth",
2458
3366
  task: "depth map"
@@ -2465,36 +3373,36 @@ var FAL_TOOLS = {
2465
3373
  inputKind: "image",
2466
3374
  name: "MiDaS Preprocessor",
2467
3375
  outputKeys: ["depth_map", "normal_map"],
3376
+ price: { kind: "metered" },
2468
3377
  pricing: "$0/compute-second listed by fal",
2469
3378
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/midas",
2470
3379
  task: "depth and normal preprocessing"
2471
3380
  },
2472
- nsfw: {
2473
- category: "moderation",
2474
- description: "Predict whether one or more images contain NSFW concepts.",
2475
- endpoint: "fal-ai/x-ailab/nsfw",
2476
- inputField: "image_urls",
2477
- inputKind: "images",
2478
- name: "NSFW Checker",
2479
- outputKeys: ["has_nsfw_concepts"],
2480
- pricing: "$0.001/image",
2481
- sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw",
2482
- task: "vision moderation"
3381
+ mlsd: {
3382
+ category: "preprocess",
3383
+ description: "Detect straight line segments for architectural and interior control maps.",
3384
+ endpoint: "fal-ai/image-preprocessors/mlsd",
3385
+ inputField: "image_url",
3386
+ inputKind: "image",
3387
+ name: "M-LSD Line Preprocessor",
3388
+ outputKeys: ["image"],
3389
+ price: { kind: "metered" },
3390
+ pricing: "$0/compute-second listed by fal",
3391
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/mlsd",
3392
+ task: "line segment preprocessing"
2483
3393
  },
2484
- rembg: {
2485
- category: "background",
2486
- defaultOptions: {
2487
- crop_to_bbox: false
2488
- },
2489
- description: "Generic image background removal utility.",
2490
- endpoint: "fal-ai/imageutils/rembg",
3394
+ pidi: {
3395
+ category: "preprocess",
3396
+ description: "Generate PiDiNet soft edge maps from an input image.",
3397
+ endpoint: "fal-ai/image-preprocessors/pidi",
2491
3398
  inputField: "image_url",
2492
3399
  inputKind: "image",
2493
- name: "Remove Background",
3400
+ name: "PiDiNet Edge Preprocessor",
2494
3401
  outputKeys: ["image"],
3402
+ price: { kind: "metered" },
2495
3403
  pricing: "$0/compute-second listed by fal",
2496
- sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
2497
- task: "image background removal"
3404
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/pidi",
3405
+ task: "edge preprocessing"
2498
3406
  },
2499
3407
  "sam-preprocessor": {
2500
3408
  category: "preprocess",
@@ -2504,6 +3412,7 @@ var FAL_TOOLS = {
2504
3412
  inputKind: "image",
2505
3413
  name: "SAM Preprocessor",
2506
3414
  outputKeys: ["image"],
3415
+ price: { kind: "metered" },
2507
3416
  pricing: "$0/compute-second listed by fal",
2508
3417
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/sam",
2509
3418
  task: "segmentation preprocessing"
@@ -2523,7 +3432,9 @@ var FAL_TOOLS = {
2523
3432
  inputKind: "image",
2524
3433
  name: "SAM 2 Auto Segment",
2525
3434
  outputKeys: ["combined_mask", "individual_masks"],
3435
+ price: { kind: "metered" },
2526
3436
  pricing: "$0/compute-second listed by fal",
3437
+ queued: true,
2527
3438
  sourceUrl: "https://fal.ai/models/fal-ai/sam2/auto-segment",
2528
3439
  task: "automatic image segmentation"
2529
3440
  },
@@ -2540,7 +3451,8 @@ var FAL_TOOLS = {
2540
3451
  inputKind: "image",
2541
3452
  name: "SAM 3.1 Image",
2542
3453
  outputKeys: ["image", "masks", "metadata", "scores", "boxes"],
2543
- pricing: "$0.005/request",
3454
+ price: { kind: "call", usd: 0.01 },
3455
+ pricing: "$0.01/request",
2544
3456
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/image",
2545
3457
  task: "promptable image segmentation"
2546
3458
  },
@@ -2556,61 +3468,12 @@ var FAL_TOOLS = {
2556
3468
  inputKind: "video",
2557
3469
  name: "SAM 3.1 Video",
2558
3470
  outputKeys: ["video", "boundingbox_frames_zip"],
2559
- pricing: "fal pricing varies by frame count",
3471
+ price: { kind: "metered" },
3472
+ pricing: "$0.01/16 frames of video input",
3473
+ queued: true,
2560
3474
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/video",
2561
3475
  task: "multi-object video segmentation"
2562
3476
  },
2563
- "sam3-3d-align": {
2564
- category: "3d",
2565
- description: "Align SAM 3D objects and bodies into a shared scene.",
2566
- endpoint: "fal-ai/sam-3/3d-align",
2567
- inputField: "image_url",
2568
- inputKind: "image",
2569
- name: "SAM 3D Align",
2570
- outputKeys: ["scene_glb", "metadata", "artifacts_zip"],
2571
- pricing: "fal pricing varies by scene",
2572
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-align",
2573
- task: "3D scene alignment"
2574
- },
2575
- "sam3-3d-body": {
2576
- category: "3d",
2577
- defaultOptions: {
2578
- export_meshes: true,
2579
- include_3d_keypoints: true,
2580
- include_mhr_params: true
2581
- },
2582
- description: "Reconstruct human body meshes and keypoints from a single image.",
2583
- endpoint: "fal-ai/sam-3/3d-body",
2584
- inputField: "image_url",
2585
- inputKind: "image",
2586
- name: "SAM 3D Body",
2587
- outputKeys: ["model_glb", "visualization", "meshes", "metadata"],
2588
- pricing: "$0.015/inference",
2589
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-body",
2590
- task: "single-image 3D body reconstruction"
2591
- },
2592
- "sam3-3d-objects": {
2593
- category: "3d",
2594
- defaultOptions: {
2595
- prompt: "car"
2596
- },
2597
- description: "Reconstruct one or more 3D objects from an image and prompts.",
2598
- endpoint: "fal-ai/sam-3/3d-objects",
2599
- inputField: "image_url",
2600
- inputKind: "image",
2601
- name: "SAM 3D Objects",
2602
- outputKeys: [
2603
- "gaussian_splat",
2604
- "model_glb",
2605
- "metadata",
2606
- "individual_splats",
2607
- "individual_glbs",
2608
- "artifacts_zip"
2609
- ],
2610
- pricing: "$0.02/generation",
2611
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-objects",
2612
- task: "single-image 3D object reconstruction"
2613
- },
2614
3477
  "sam3-image": {
2615
3478
  category: "segmentation",
2616
3479
  defaultOptions: {
@@ -2624,6 +3487,7 @@ var FAL_TOOLS = {
2624
3487
  inputKind: "image",
2625
3488
  name: "SAM 3 Image",
2626
3489
  outputKeys: ["image", "masks", "metadata", "scores", "boxes"],
3490
+ price: { kind: "call", usd: 5e-3 },
2627
3491
  pricing: "$0.005/request",
2628
3492
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/image",
2629
3493
  task: "promptable image segmentation"
@@ -2639,7 +3503,8 @@ var FAL_TOOLS = {
2639
3503
  inputField: "image_url",
2640
3504
  inputKind: "image",
2641
3505
  name: "SAM 3 Image RLE",
2642
- outputKeys: ["rle_masks", "metadata", "scores", "boxes"],
3506
+ outputKeys: ["rle", "scores", "boxes", "metadata"],
3507
+ price: { kind: "call", usd: 5e-3 },
2643
3508
  pricing: "$0.005/request",
2644
3509
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/image-rle",
2645
3510
  task: "promptable image segmentation to RLE"
@@ -2658,7 +3523,9 @@ var FAL_TOOLS = {
2658
3523
  inputKind: "video",
2659
3524
  name: "SAM 3 Video",
2660
3525
  outputKeys: ["video", "boundingbox_frames_zip"],
2661
- pricing: "$0.005/16 frames",
3526
+ price: { kind: "metered" },
3527
+ pricing: "$0.005/16 frames of video input",
3528
+ queued: true,
2662
3529
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video",
2663
3530
  task: "promptable video segmentation"
2664
3531
  },
@@ -2674,45 +3541,65 @@ var FAL_TOOLS = {
2674
3541
  inputField: "video_url",
2675
3542
  inputKind: "video",
2676
3543
  name: "SAM 3 Video RLE",
2677
- outputKeys: ["rle_masks", "metadata"],
2678
- pricing: "$0.005/16 frames",
3544
+ // fal's video-rle endpoint no longer returns RLE data: its Output schema is
3545
+ // now identical to sam3-video's, so this entry differs only by endpoint.
3546
+ outputKeys: ["video", "boundingbox_frames_zip"],
3547
+ price: { kind: "metered" },
3548
+ pricing: "$0.005/16 frames of video",
3549
+ queued: true,
2679
3550
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video-rle",
2680
3551
  task: "promptable video segmentation to RLE"
2681
3552
  },
2682
- "topaz-image": {
2683
- category: "upscale",
2684
- defaultOptions: {
2685
- model: "Standard V2",
2686
- output_format: "jpeg",
2687
- upscale_factor: 2
2688
- },
2689
- description: "Professional Topaz image enhancement and upscaling.",
2690
- endpoint: "fal-ai/topaz/upscale/image",
3553
+ scribble: {
3554
+ category: "preprocess",
3555
+ description: "Turn an image into a scribble-style control map.",
3556
+ endpoint: "fal-ai/image-preprocessors/scribble",
2691
3557
  inputField: "image_url",
2692
3558
  inputKind: "image",
2693
- name: "Topaz Image Upscale",
3559
+ name: "Scribble Preprocessor",
2694
3560
  outputKeys: ["image"],
2695
- pricing: "$0.08+ by output megapixels",
2696
- sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/image",
2697
- task: "image enhancement"
3561
+ price: { kind: "metered" },
3562
+ pricing: "$0/compute-second listed by fal",
3563
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/scribble",
3564
+ task: "scribble preprocessing"
2698
3565
  },
2699
- "topaz-video": {
2700
- category: "upscale",
2701
- defaultOptions: {
2702
- model: "Proteus",
2703
- upscale_factor: 2
2704
- },
2705
- description: "Professional Topaz video enhancement and upscaling.",
2706
- endpoint: "fal-ai/topaz/upscale/video",
2707
- inputField: "video_url",
2708
- inputKind: "video",
2709
- name: "Topaz Video Upscale",
2710
- outputKeys: ["video"],
2711
- pricing: "$0.01-$0.08/sec by output resolution",
2712
- sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
2713
- task: "video enhancement"
3566
+ teed: {
3567
+ category: "preprocess",
3568
+ description: "Generate TEED edge maps from an input image.",
3569
+ endpoint: "fal-ai/image-preprocessors/teed",
3570
+ inputField: "image_url",
3571
+ inputKind: "image",
3572
+ name: "TEED Edge Preprocessor",
3573
+ outputKeys: ["image"],
3574
+ price: { kind: "metered" },
3575
+ pricing: "$0/compute-second listed by fal",
3576
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/teed",
3577
+ task: "edge preprocessing"
3578
+ },
3579
+ "zoe-depth": {
3580
+ category: "preprocess",
3581
+ description: "Generate ZoeDepth metric depth maps from an input image.",
3582
+ endpoint: "fal-ai/image-preprocessors/zoe",
3583
+ inputField: "image_url",
3584
+ inputKind: "image",
3585
+ name: "ZoeDepth Preprocessor",
3586
+ outputKeys: ["image"],
3587
+ price: { kind: "metered" },
3588
+ pricing: "$0/compute-second listed by fal",
3589
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/zoe",
3590
+ task: "depth preprocessing"
2714
3591
  }
2715
3592
  };
3593
+
3594
+ // src/tools.ts
3595
+ var FAL_EXPLORE_CHECKED_AT = "2026-08-23";
3596
+ var FAL_TOOLS = {
3597
+ ...ANALYSIS_TOOLS,
3598
+ ...ASSET_TOOLS,
3599
+ ...BACKGROUND_TOOLS,
3600
+ ...EDITING_TOOLS,
3601
+ ...STRUCTURE_TOOLS
3602
+ };
2716
3603
  var FAL_TOOLS_CHECKED_AT = FAL_EXPLORE_CHECKED_AT;
2717
3604
  var FAL_TOOL_IDS = Object.keys(
2718
3605
  FAL_TOOLS
@@ -2739,9 +3626,110 @@ function buildFalToolRequest(options) {
2739
3626
  return { body, endpoint: tool.endpoint, tool };
2740
3627
  }
2741
3628
 
2742
- // src/server.ts
3629
+ // src/server-tools.ts
2743
3630
  var FAL_BASE_URL = "https://fal.run";
2744
3631
  var FAL_QUEUE_URL = "https://queue.fal.run";
3632
+ var POLL_INTERVAL_MS = 3e3;
3633
+ var MAX_POLL_ATTEMPTS = 160;
3634
+ function buildToolRequest(options) {
3635
+ try {
3636
+ return (0, import_neverthrow.ok)(buildFalToolRequest(options));
3637
+ } catch (error) {
3638
+ return (0, import_neverthrow.err)(
3639
+ new MotifError(error instanceof Error ? error.message : String(error), 0)
3640
+ );
3641
+ }
3642
+ }
3643
+ async function runTool(exec, options) {
3644
+ const request = buildToolRequest(options);
3645
+ if (request.isErr()) {
3646
+ return (0, import_neverthrow.err)(request.error);
3647
+ }
3648
+ const response = await exec.request(
3649
+ `${FAL_BASE_URL}/${request.value.endpoint}`,
3650
+ {
3651
+ body: JSON.stringify(request.value.body),
3652
+ method: "POST"
3653
+ }
3654
+ );
3655
+ if (response.isErr()) {
3656
+ return (0, import_neverthrow.err)(response.error);
3657
+ }
3658
+ const data = await response.value.json();
3659
+ return (0, import_neverthrow.ok)(isRecord(data) ? data : {});
3660
+ }
3661
+ async function submitTool(exec, options) {
3662
+ const request = buildToolRequest(options);
3663
+ if (request.isErr()) {
3664
+ return (0, import_neverthrow.err)(request.error);
3665
+ }
3666
+ const response = await exec.request(
3667
+ `${FAL_QUEUE_URL}/${request.value.endpoint}`,
3668
+ {
3669
+ body: JSON.stringify(request.value.body),
3670
+ method: "POST"
3671
+ }
3672
+ );
3673
+ if (response.isErr()) {
3674
+ return (0, import_neverthrow.err)(response.error);
3675
+ }
3676
+ const data = await response.value.json();
3677
+ const submission = parseQueueSubmission(data);
3678
+ return (0, import_neverthrow.ok)({
3679
+ endpoint: endpointFromQueueUrl(
3680
+ submission.responseUrl,
3681
+ request.value.endpoint
3682
+ ),
3683
+ requestId: submission.requestId
3684
+ });
3685
+ }
3686
+ async function checkToolStatus(exec, job) {
3687
+ return await exec.getJobStatus(job.endpoint, job.requestId);
3688
+ }
3689
+ async function getToolResult(exec, job) {
3690
+ const url = `${FAL_QUEUE_URL}/${job.endpoint}/requests/${job.requestId}`;
3691
+ const response = await exec.request(url);
3692
+ if (response.isErr()) {
3693
+ return (0, import_neverthrow.err)(response.error);
3694
+ }
3695
+ const data = await response.value.json();
3696
+ return (0, import_neverthrow.ok)(isRecord(data) ? data : {});
3697
+ }
3698
+ async function runToolQueued(exec, options, onProgress) {
3699
+ const job = await submitTool(exec, options);
3700
+ if (job.isErr()) {
3701
+ return (0, import_neverthrow.err)(job.error);
3702
+ }
3703
+ for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
3704
+ const status = await checkToolStatus(exec, job.value);
3705
+ if (status.isErr()) {
3706
+ return (0, import_neverthrow.err)(status.error);
3707
+ }
3708
+ onProgress?.(status.value.status, status.value.queuePosition);
3709
+ if (status.value.status === "completed") {
3710
+ return await getToolResult(exec, job.value);
3711
+ }
3712
+ if (status.value.status === "failed") {
3713
+ const message = status.value.error ?? "Queued tool run failed";
3714
+ return (0, import_neverthrow.err)(new MotifError(message, 0, void 0, job.value.requestId));
3715
+ }
3716
+ await new Promise((resolve) => {
3717
+ setTimeout(resolve, POLL_INTERVAL_MS);
3718
+ });
3719
+ }
3720
+ return (0, import_neverthrow.err)(
3721
+ new MotifError(
3722
+ "Queued tool run timed out",
3723
+ 0,
3724
+ void 0,
3725
+ job.value.requestId
3726
+ )
3727
+ );
3728
+ }
3729
+
3730
+ // src/server.ts
3731
+ var FAL_BASE_URL2 = "https://fal.run";
3732
+ var FAL_QUEUE_URL2 = "https://queue.fal.run";
2745
3733
  var FAL_API_URL = "https://api.fal.ai";
2746
3734
  var FAL_REST_URL = "https://rest.alpha.fal.ai";
2747
3735
  var FalClient = class {
@@ -2773,16 +3761,16 @@ var FalClient = class {
2773
3761
  try {
2774
3762
  built = buildGenerateBody(options);
2775
3763
  } catch (error) {
2776
- return (0, import_neverthrow.err)(toMotifError(error));
3764
+ return (0, import_neverthrow2.err)(toMotifError(error));
2777
3765
  }
2778
3766
  const { endpoint, body } = built;
2779
- const response = await this.request(`${FAL_BASE_URL}/${endpoint}`, {
3767
+ const response = await this.request(`${FAL_BASE_URL2}/${endpoint}`, {
2780
3768
  body: JSON.stringify(body),
2781
3769
  headers: this.ephemeralHeaders(options),
2782
3770
  method: "POST"
2783
3771
  });
2784
3772
  if (response.isErr()) {
2785
- return (0, import_neverthrow.err)(response.error);
3773
+ return (0, import_neverthrow2.err)(response.error);
2786
3774
  }
2787
3775
  const data = await response.value.json();
2788
3776
  return this.normalizeResponse(data);
@@ -2790,7 +3778,7 @@ var FalClient = class {
2790
3778
  async generateQueued(options) {
2791
3779
  const job = await this.submitGeneration(options);
2792
3780
  if (job.isErr()) {
2793
- return (0, import_neverthrow.err)(job.error);
3781
+ return (0, import_neverthrow2.err)(job.error);
2794
3782
  }
2795
3783
  const pollIntervalMs = 3e3;
2796
3784
  const maxAttempts = 160;
@@ -2800,20 +3788,20 @@ var FalClient = class {
2800
3788
  job.value.requestId
2801
3789
  );
2802
3790
  if (status.isErr()) {
2803
- return (0, import_neverthrow.err)(status.error);
3791
+ return (0, import_neverthrow2.err)(status.error);
2804
3792
  }
2805
3793
  if (status.value.status === "completed") {
2806
3794
  return await this.getJobResult(job.value.endpoint, job.value.requestId);
2807
3795
  }
2808
3796
  if (status.value.status === "failed") {
2809
3797
  const message = status.value.error ?? "Queued generation failed";
2810
- return (0, import_neverthrow.err)(new MotifError(message, 0, void 0, job.value.requestId));
3798
+ return (0, import_neverthrow2.err)(new MotifError(message, 0, void 0, job.value.requestId));
2811
3799
  }
2812
3800
  await new Promise((resolve) => {
2813
3801
  setTimeout(resolve, pollIntervalMs);
2814
3802
  });
2815
3803
  }
2816
- return (0, import_neverthrow.err)(new MotifError("Queued generation timed out", 0));
3804
+ return (0, import_neverthrow2.err)(new MotifError("Queued generation timed out", 0));
2817
3805
  }
2818
3806
  /** ─── Queue-Based Generation ──────────────────────────────── */
2819
3807
  /** Submit a generation to the fal.ai queue (returns immediately). */
@@ -2822,20 +3810,20 @@ var FalClient = class {
2822
3810
  try {
2823
3811
  built = buildGenerateBody(options);
2824
3812
  } catch (error) {
2825
- return (0, import_neverthrow.err)(toMotifError(error));
3813
+ return (0, import_neverthrow2.err)(toMotifError(error));
2826
3814
  }
2827
3815
  const { endpoint, body } = built;
2828
- const response = await this.request(`${FAL_QUEUE_URL}/${endpoint}`, {
3816
+ const response = await this.request(`${FAL_QUEUE_URL2}/${endpoint}`, {
2829
3817
  body: JSON.stringify(body),
2830
3818
  headers: this.ephemeralHeaders(options),
2831
3819
  method: "POST"
2832
3820
  });
2833
3821
  if (response.isErr()) {
2834
- return (0, import_neverthrow.err)(response.error);
3822
+ return (0, import_neverthrow2.err)(response.error);
2835
3823
  }
2836
3824
  const data = await response.value.json();
2837
3825
  const submission = parseQueueSubmission(data);
2838
- return (0, import_neverthrow.ok)({
3826
+ return (0, import_neverthrow2.ok)({
2839
3827
  endpoint: endpointFromQueueUrl(submission.responseUrl, endpoint),
2840
3828
  estimatedCost: estimateCost(
2841
3829
  options.model,
@@ -2847,10 +3835,10 @@ var FalClient = class {
2847
3835
  }
2848
3836
  /** Check the status of a queued generation. */
2849
3837
  async getJobStatus(endpoint, requestId) {
2850
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}/status?logs=1`;
3838
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}/status?logs=1`;
2851
3839
  const response = await this.request(url);
2852
3840
  if (response.isErr()) {
2853
- return (0, import_neverthrow.err)(response.error);
3841
+ return (0, import_neverthrow2.err)(response.error);
2854
3842
  }
2855
3843
  const data = await response.value.json();
2856
3844
  const record = isRecord(data) ? data : {};
@@ -2865,9 +3853,9 @@ var FalClient = class {
2865
3853
  } else if (rawStatus === "FAILED" || rawStatus === "ERROR" || rawStatus === "CANCELED") {
2866
3854
  status = "failed";
2867
3855
  } else {
2868
- return (0, import_neverthrow.err)(new MotifError(`Unknown job status: ${rawStatus}`, 0));
3856
+ return (0, import_neverthrow2.err)(new MotifError(`Unknown job status: ${rawStatus}`, 0));
2869
3857
  }
2870
- return (0, import_neverthrow.ok)({
3858
+ return (0, import_neverthrow2.ok)({
2871
3859
  error: asString(record.error) ?? asString(record.detail),
2872
3860
  logs: parseLogs(record.logs),
2873
3861
  queuePosition: asNumber(record.queue_position),
@@ -2876,10 +3864,10 @@ var FalClient = class {
2876
3864
  }
2877
3865
  /** Fetch the completed result from the queue. */
2878
3866
  async getJobResult(endpoint, requestId) {
2879
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
3867
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}`;
2880
3868
  const response = await this.request(url);
2881
3869
  if (response.isErr()) {
2882
- return (0, import_neverthrow.err)(response.error);
3870
+ return (0, import_neverthrow2.err)(response.error);
2883
3871
  }
2884
3872
  const data = await response.value.json();
2885
3873
  return this.normalizeResponse(data, requestId);
@@ -2900,7 +3888,7 @@ var FalClient = class {
2900
3888
  } = options;
2901
3889
  const config = MODELS[model];
2902
3890
  if (!config || config.type !== "utility") {
2903
- return (0, import_neverthrow.err)(new MotifError(`Invalid upscale model: ${model}`, 0));
3891
+ return (0, import_neverthrow2.err)(new MotifError(`Invalid upscale model: ${model}`, 0));
2904
3892
  }
2905
3893
  const body = { image_url: imageUrl };
2906
3894
  if (model === "crystal") {
@@ -2933,12 +3921,12 @@ var FalClient = class {
2933
3921
  body.guidance_scale = guidanceScale;
2934
3922
  }
2935
3923
  }
2936
- const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
3924
+ const response = await this.request(`${FAL_BASE_URL2}/${config.endpoint}`, {
2937
3925
  body: JSON.stringify(body),
2938
3926
  method: "POST"
2939
3927
  });
2940
3928
  if (response.isErr()) {
2941
- return (0, import_neverthrow.err)(response.error);
3929
+ return (0, import_neverthrow2.err)(response.error);
2942
3930
  }
2943
3931
  const data = await response.value.json();
2944
3932
  return this.normalizeResponse(data);
@@ -2956,7 +3944,7 @@ var FalClient = class {
2956
3944
  } = options;
2957
3945
  const config = MODELS[model];
2958
3946
  if (!config) {
2959
- return (0, import_neverthrow.err)(
3947
+ return (0, import_neverthrow2.err)(
2960
3948
  new MotifError(`Invalid background removal model: ${model}`, 0)
2961
3949
  );
2962
3950
  }
@@ -2978,12 +3966,12 @@ var FalClient = class {
2978
3966
  rbBody.output_mask = outputMask;
2979
3967
  }
2980
3968
  }
2981
- const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
3969
+ const response = await this.request(`${FAL_BASE_URL2}/${config.endpoint}`, {
2982
3970
  body: JSON.stringify(rbBody),
2983
3971
  method: "POST"
2984
3972
  });
2985
3973
  if (response.isErr()) {
2986
- return (0, import_neverthrow.err)(response.error);
3974
+ return (0, import_neverthrow2.err)(response.error);
2987
3975
  }
2988
3976
  const data = await response.value.json();
2989
3977
  return this.normalizeResponse(data);
@@ -3006,7 +3994,7 @@ var FalClient = class {
3006
3994
  } = options;
3007
3995
  const config = MODELS.kling;
3008
3996
  if (!config) {
3009
- return (0, import_neverthrow.err)(new MotifError("Kling video model not found", 0));
3997
+ return (0, import_neverthrow2.err)(new MotifError("Kling video model not found", 0));
3010
3998
  }
3011
3999
  const body = {
3012
4000
  duration: String(duration),
@@ -3023,16 +4011,16 @@ var FalClient = class {
3023
4011
  if (cfgScale !== void 0) {
3024
4012
  body.cfg_scale = cfgScale;
3025
4013
  }
3026
- const response = await this.request(`${FAL_QUEUE_URL}/${config.endpoint}`, {
4014
+ const response = await this.request(`${FAL_QUEUE_URL2}/${config.endpoint}`, {
3027
4015
  body: JSON.stringify(body),
3028
4016
  method: "POST"
3029
4017
  });
3030
4018
  if (response.isErr()) {
3031
- return (0, import_neverthrow.err)(response.error);
4019
+ return (0, import_neverthrow2.err)(response.error);
3032
4020
  }
3033
4021
  const data = await response.value.json();
3034
4022
  const submission = parseQueueSubmission(data);
3035
- return (0, import_neverthrow.ok)({
4023
+ return (0, import_neverthrow2.ok)({
3036
4024
  endpoint: endpointFromQueueUrl(submission.responseUrl, config.endpoint),
3037
4025
  estimatedCost: estimateVideoCost(duration, generateAudio),
3038
4026
  requestId: submission.requestId
@@ -3040,17 +4028,17 @@ var FalClient = class {
3040
4028
  }
3041
4029
  /** Fetch the completed video result from the queue. */
3042
4030
  async getVideoResult(endpoint, requestId) {
3043
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
4031
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}`;
3044
4032
  const response = await this.request(url);
3045
4033
  if (response.isErr()) {
3046
- return (0, import_neverthrow.err)(response.error);
4034
+ return (0, import_neverthrow2.err)(response.error);
3047
4035
  }
3048
4036
  const data = await response.value.json();
3049
4037
  const video = isRecord(data) && isRecord(data.video) ? data.video : void 0;
3050
4038
  if (video === void 0) {
3051
- return (0, import_neverthrow.err)(new MotifError("No video in response", 0));
4039
+ return (0, import_neverthrow2.err)(new MotifError("No video in response", 0));
3052
4040
  }
3053
- return (0, import_neverthrow.ok)({
4041
+ return (0, import_neverthrow2.ok)({
3054
4042
  contentType: asString(video.content_type) ?? "",
3055
4043
  fileName: asString(video.file_name) ?? "",
3056
4044
  fileSize: asNumber(video.file_size) ?? 0,
@@ -3074,13 +4062,13 @@ var FalClient = class {
3074
4062
  }
3075
4063
  );
3076
4064
  if (initiateResponse.isErr()) {
3077
- return (0, import_neverthrow.err)(initiateResponse.error);
4065
+ return (0, import_neverthrow2.err)(initiateResponse.error);
3078
4066
  }
3079
4067
  const initiateData = await initiateResponse.value.json();
3080
4068
  const fileUrl = isRecord(initiateData) ? asString(initiateData.file_url) ?? "" : "";
3081
4069
  const uploadUrl = isRecord(initiateData) ? asString(initiateData.upload_url) : void 0;
3082
4070
  if (uploadUrl === void 0 || uploadUrl === "") {
3083
- return (0, import_neverthrow.err)(
4071
+ return (0, import_neverthrow2.err)(
3084
4072
  new MotifError("Upload initiate response missing upload_url", 0)
3085
4073
  );
3086
4074
  }
@@ -3095,7 +4083,7 @@ var FalClient = class {
3095
4083
  method: "PUT"
3096
4084
  });
3097
4085
  } catch (error) {
3098
- return (0, import_neverthrow.err)(
4086
+ return (0, import_neverthrow2.err)(
3099
4087
  new MotifError(
3100
4088
  `Upload PUT failed: ${error instanceof Error ? error.message : String(error)}`,
3101
4089
  0
@@ -3103,38 +4091,45 @@ var FalClient = class {
3103
4091
  );
3104
4092
  }
3105
4093
  if (!putResponse.ok) {
3106
- return (0, import_neverthrow.err)(
4094
+ return (0, import_neverthrow2.err)(
3107
4095
  new MotifError(
3108
4096
  `Upload PUT failed: ${putResponse.status}`,
3109
4097
  putResponse.status
3110
4098
  )
3111
4099
  );
3112
4100
  }
3113
- return (0, import_neverthrow.ok)(fileUrl);
4101
+ return (0, import_neverthrow2.ok)(fileUrl);
3114
4102
  }
3115
4103
  /** ─── Utilities ───────────────────────────────────────────── */
3116
4104
  /** Run a registered fal utility/tool endpoint. */
3117
4105
  async runTool(options) {
3118
- let request;
3119
- try {
3120
- request = buildFalToolRequest(options);
3121
- } catch (error) {
3122
- return (0, import_neverthrow.err)(
3123
- new MotifError(
3124
- error instanceof Error ? error.message : String(error),
3125
- 0
3126
- )
3127
- );
3128
- }
3129
- const response = await this.request(`${FAL_BASE_URL}/${request.endpoint}`, {
3130
- body: JSON.stringify(request.body),
3131
- method: "POST"
3132
- });
3133
- if (response.isErr()) {
3134
- return (0, import_neverthrow.err)(response.error);
3135
- }
3136
- const data = await response.value.json();
3137
- return (0, import_neverthrow.ok)(isRecord(data) ? data : {});
4106
+ return await runTool(this.toolExecutor, options);
4107
+ }
4108
+ /** ─── Queue-Based Tools ───────────────────────────────────── */
4109
+ /**
4110
+ * Submit a tool run to the fal queue. Returns as soon as fal accepts it.
4111
+ *
4112
+ * The queued counterpart to `runTool`, for endpoints that outrun the
4113
+ * synchronous request timeout. Callers choose the path — neither one falls
4114
+ * back to the other.
4115
+ */
4116
+ async submitTool(options) {
4117
+ return await submitTool(this.toolExecutor, options);
4118
+ }
4119
+ /** Poll one queued tool run. */
4120
+ async checkToolStatus(job) {
4121
+ return await checkToolStatus(this.toolExecutor, job);
4122
+ }
4123
+ /** Fetch the finished payload for a queued tool run. */
4124
+ async getToolResult(job) {
4125
+ return await getToolResult(this.toolExecutor, job);
4126
+ }
4127
+ /**
4128
+ * Submit, poll to completion, and return the result. The convenience path
4129
+ * for tools whose registry entry sets `queued`.
4130
+ */
4131
+ async runToolQueued(options, onProgress) {
4132
+ return await runToolQueued(this.toolExecutor, options, onProgress);
3138
4133
  }
3139
4134
  /**
3140
4135
  * Delete fal's stored IO payloads for a completed request.
@@ -3149,9 +4144,9 @@ var FalClient = class {
3149
4144
  { method: "DELETE" }
3150
4145
  );
3151
4146
  if (response.isErr()) {
3152
- return (0, import_neverthrow.err)(response.error);
4147
+ return (0, import_neverthrow2.err)(response.error);
3153
4148
  }
3154
- return (0, import_neverthrow.ok)();
4149
+ return (0, import_neverthrow2.ok)();
3155
4150
  }
3156
4151
  /** Estimate cost for a generation (no API call). */
3157
4152
  estimateCost(model, resolution, numImages) {
@@ -3177,6 +4172,16 @@ var FalClient = class {
3177
4172
  get tools() {
3178
4173
  return FAL_TOOLS;
3179
4174
  }
4175
+ /**
4176
+ * The seam `server-tools.ts` runs against — the client's authenticated fetch
4177
+ * and its queue status parser, exposed without widening the public surface.
4178
+ */
4179
+ get toolExecutor() {
4180
+ return {
4181
+ getJobStatus: async (endpoint, requestId) => await this.getJobStatus(endpoint, requestId),
4182
+ request: async (url, init) => await this.request(url, init)
4183
+ };
4184
+ }
3180
4185
  /** ─── Private ─────────────────────────────────────────────── */
3181
4186
  /** Authenticated fetch to fal.ai APIs with retry logic. */
3182
4187
  async request(url, options = {}) {
@@ -3208,15 +4213,15 @@ var FalClient = class {
3208
4213
  const text = await response.text();
3209
4214
  const message = `Request failed: ${response.status} ${text}`;
3210
4215
  const requestId = response.headers.get("x-fal-request-id") ?? requestIdFromBody(text);
3211
- return (0, import_neverthrow.err)(
4216
+ return (0, import_neverthrow2.err)(
3212
4217
  new MotifError(message, response.status, void 0, requestId)
3213
4218
  );
3214
4219
  }
3215
- return (0, import_neverthrow.ok)(response);
4220
+ return (0, import_neverthrow2.ok)(response);
3216
4221
  } catch (error) {
3217
4222
  clearTimeout(timeoutId);
3218
4223
  if (error instanceof MotifError) {
3219
- return (0, import_neverthrow.err)(error);
4224
+ return (0, import_neverthrow2.err)(error);
3220
4225
  }
3221
4226
  lastError = new MotifError(
3222
4227
  error instanceof Error ? error.message : String(error),
@@ -3230,7 +4235,7 @@ var FalClient = class {
3230
4235
  }
3231
4236
  }
3232
4237
  }
3233
- return (0, import_neverthrow.err)(lastError ?? new MotifError("Request failed after retries", 0));
4238
+ return (0, import_neverthrow2.err)(lastError ?? new MotifError("Request failed after retries", 0));
3234
4239
  }
3235
4240
  ephemeralHeaders(options) {
3236
4241
  return options.ephemeral === true ? { "X-Fal-Store-IO": "0" } : {};
@@ -3241,24 +4246,24 @@ var FalClient = class {
3241
4246
  */
3242
4247
  normalizeResponse(data, fallbackRequestId) {
3243
4248
  if (!isRecord(data)) {
3244
- return (0, import_neverthrow.err)(
4249
+ return (0, import_neverthrow2.err)(
3245
4250
  new MotifError("Unexpected fal response shape", 0, "FAL_ERROR")
3246
4251
  );
3247
4252
  }
3248
4253
  const obj = data;
3249
4254
  const requestId = asString(obj.request_id) ?? asString(obj.requestId) ?? fallbackRequestId;
3250
4255
  if ("detail" in obj) {
3251
- return (0, import_neverthrow.err)(new MotifError(asString(obj.detail) ?? "", 0, "FAL_ERROR"));
4256
+ return (0, import_neverthrow2.err)(new MotifError(asString(obj.detail) ?? "", 0, "FAL_ERROR"));
3252
4257
  }
3253
4258
  if ("image" in obj && !("images" in obj)) {
3254
- return (0, import_neverthrow.ok)({
4259
+ return (0, import_neverthrow2.ok)({
3255
4260
  images: [toMotifImage(obj.image)],
3256
4261
  prompt: asString(obj.prompt),
3257
4262
  requestId,
3258
4263
  seed: asNumber(obj.seed)
3259
4264
  });
3260
4265
  }
3261
- return (0, import_neverthrow.ok)({
4266
+ return (0, import_neverthrow2.ok)({
3262
4267
  images: parseImages(obj.images),
3263
4268
  prompt: asString(obj.prompt),
3264
4269
  requestId,
@@ -3266,28 +4271,6 @@ var FalClient = class {
3266
4271
  });
3267
4272
  }
3268
4273
  };
3269
- var MotifError = class extends Error {
3270
- status;
3271
- code;
3272
- /** fal's request-correlation id (from the `x-fal-request-id` header or the
3273
- * error body). Ties a failure back to fal's dashboard/support. */
3274
- requestId;
3275
- constructor(message, status, code, requestId) {
3276
- super(message);
3277
- this.name = "MotifError";
3278
- this.status = status;
3279
- this.code = code;
3280
- this.requestId = requestId;
3281
- }
3282
- };
3283
- function toMotifError(error) {
3284
- if (error instanceof MotifError) {
3285
- return error;
3286
- }
3287
- const message = error instanceof Error ? error.message : String(error);
3288
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
3289
- return new MotifError(message, 0, code);
3290
- }
3291
4274
  // Annotate the CommonJS export names for ESM import in node:
3292
4275
  0 && (module.exports = {
3293
4276
  ASPECT_RATIOS,