@howells/motif-sdk 1.0.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,
@@ -964,6 +1047,41 @@ var MODELS = {
964
1047
  supportsSyncMode: true,
965
1048
  type: "generation"
966
1049
  },
1050
+ qwen3: {
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
+ },
1058
+ tiers: { price: "budget", quality: "better", speed: "unknown" },
1059
+ useCase: "Strong quality per dollar - 11th on text-to-image at $30/1k"
1060
+ },
1061
+ endpoint: "fal-ai/qwen-image-3/text-to-image",
1062
+ falPricing: {
1063
+ checkedAt: "2026-08-05",
1064
+ currency: "USD",
1065
+ endpointId: "fal-ai/qwen-image-3/text-to-image",
1066
+ estimatedCostPerImageUsd: 0.02,
1067
+ source: "fal-pricing-api",
1068
+ unit: "megapixels",
1069
+ unitPrice: 0.02
1070
+ },
1071
+ name: "Qwen Image 3",
1072
+ pricePerImageUsd: 0.02,
1073
+ pricing: "$0.02/MP (assumed from qwen v1; fal pricing page bot-gated at check time)",
1074
+ sizeMode: "image_size_enum",
1075
+ supportsAspect: true,
1076
+ supportsEdit: false,
1077
+ supportsNegativePrompt: true,
1078
+ supportsNumImages: true,
1079
+ supportsOutputFormat: true,
1080
+ supportsResolution: false,
1081
+ supportsSafetyChecker: true,
1082
+ supportsSeed: true,
1083
+ type: "generation"
1084
+ },
967
1085
  // ─── Video Models ─────────────────────────────────────────────
968
1086
  kling: {
969
1087
  endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
@@ -1041,7 +1159,8 @@ var GENERATION_MODELS = [
1041
1159
  "ideogram",
1042
1160
  "ideogram4",
1043
1161
  "grok-image",
1044
- "qwen"
1162
+ "qwen",
1163
+ "qwen3"
1045
1164
  ];
1046
1165
  var EDIT_CAPABLE_MODELS = GENERATION_MODELS.filter(
1047
1166
  (id) => MODELS[id]?.supportsEdit === true
@@ -1362,7 +1481,7 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
1362
1481
  if (options.background !== void 0 && config.supportsBackground !== true) {
1363
1482
  unsupported(config, "background");
1364
1483
  }
1365
- if (options.transparent === true && !(config.supportsBackground === true || config.supportsOutputFormat === true)) {
1484
+ if (options.transparent === true && config.supportsBackground !== true) {
1366
1485
  unsupported(config, "transparent output");
1367
1486
  }
1368
1487
  if (options.inputFidelity !== void 0) {
@@ -1644,353 +1763,351 @@ function buildGenerateBody(options) {
1644
1763
 
1645
1764
  // src/leaderboards.ts
1646
1765
  var IMAGE_TEXT_TO_IMAGE_TOP_20 = {
1647
- capturedAt: "2026-05-12",
1766
+ capturedAt: "2026-08-23",
1648
1767
  entries: [
1649
1768
  {
1650
1769
  apiPricing: "$211/1k images",
1651
1770
  creator: "OpenAI",
1652
- elo: 1337,
1771
+ elo: 1369,
1653
1772
  isCurrent: true,
1654
1773
  model: "GPT Image 2 (high)",
1655
1774
  rank: 1,
1656
1775
  routableModelId: "gpt2"
1657
1776
  },
1658
1777
  {
1659
- apiPricing: "$133/1k images",
1660
- creator: "OpenAI",
1661
- elo: 1268,
1778
+ apiPricing: "$200/1k images",
1779
+ creator: "Reve",
1780
+ elo: 1322,
1662
1781
  isCurrent: true,
1663
- model: "GPT Image 1.5 (high)",
1782
+ model: "Reve 2.1",
1664
1783
  rank: 2,
1665
- routableModelId: "gpt"
1784
+ routableModelId: "reve"
1666
1785
  },
1667
1786
  {
1668
1787
  apiPricing: "$67/1k images",
1669
1788
  creator: "Google",
1670
- elo: 1263,
1789
+ elo: 1320,
1671
1790
  isCurrent: true,
1672
1791
  model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1673
1792
  rank: 3,
1674
1793
  routableModelId: "banana2"
1675
1794
  },
1676
1795
  {
1677
- apiPricing: "$150/1k images",
1678
- creator: "Sourceful",
1679
- elo: 1256,
1796
+ apiPricing: "$133/1k images",
1797
+ creator: "OpenAI",
1798
+ elo: 1310,
1680
1799
  isCurrent: true,
1681
- model: "Riverflow 2.0",
1682
- rank: 4
1800
+ model: "GPT Image 1.5 (high)",
1801
+ rank: 4,
1802
+ routableModelId: "gpt"
1683
1803
  },
1684
1804
  {
1685
- apiPricing: "$134/1k images",
1686
- creator: "Google",
1687
- elo: 1220,
1805
+ apiPricing: "$48/1k images",
1806
+ creator: "Microsoft AI",
1807
+ elo: 1305,
1688
1808
  isCurrent: true,
1689
- model: "Nano Banana Pro (Gemini 3 Pro Image)",
1690
- rank: 5,
1691
- routableModelId: "banana"
1809
+ model: "MAI-Image-2.5",
1810
+ rank: 5
1692
1811
  },
1693
1812
  {
1694
- apiPricing: "$30/1k images",
1695
- creator: "ByteDance Seed",
1696
- elo: 1198,
1813
+ apiPricing: "$134/1k images",
1814
+ creator: "Google",
1815
+ elo: 1297,
1697
1816
  isCurrent: true,
1698
- model: "Seedream 4.0",
1817
+ model: "Nano Banana Pro (Gemini 3 Pro Image)",
1699
1818
  rank: 6,
1700
- routableModelId: "seedream4"
1819
+ routableModelId: "banana"
1701
1820
  },
1702
1821
  {
1703
- apiPricing: "$50/1k images",
1704
- creator: "Microsoft",
1705
- elo: 1198,
1822
+ apiPricing: "$109/1k images",
1823
+ creator: "Microsoft AI",
1824
+ elo: 1293,
1706
1825
  isCurrent: true,
1707
- model: "MAI-Image-2",
1826
+ model: "MAI-Image-2.5-Pro",
1708
1827
  rank: 7
1709
1828
  },
1710
1829
  {
1711
- apiPricing: "$70/1k images",
1712
- creator: "Black Forest Labs",
1713
- elo: 1197,
1830
+ apiPricing: "$34/1k images",
1831
+ creator: "Google",
1832
+ elo: 1289,
1714
1833
  isCurrent: true,
1715
- model: "FLUX.2 [max]",
1716
- rank: 8,
1717
- routableModelId: "flux2-max"
1834
+ model: "Nano Banana 2 Lite (Gemini 3.1 Flash Lite Image)",
1835
+ rank: 8
1718
1836
  },
1719
1837
  {
1720
- apiPricing: "Unavailable",
1721
- creator: "Peanut",
1722
- elo: 1187,
1838
+ apiPricing: "$43/1k images",
1839
+ creator: "Alibaba",
1840
+ elo: 1282,
1723
1841
  isCurrent: true,
1724
- model: "Peanut (Open Weights Coming Soon)",
1842
+ model: "Qwen-Image-3.0-Pro",
1725
1843
  rank: 9
1726
1844
  },
1727
1845
  {
1728
- apiPricing: "$30/1k images",
1729
- creator: "Black Forest Labs",
1730
- elo: 1186,
1846
+ apiPricing: "$90/1k images",
1847
+ creator: "Bytedance",
1848
+ elo: 1279,
1731
1849
  isCurrent: true,
1732
- model: "FLUX.2 [pro]",
1850
+ model: "Seedream 5.0 Pro",
1733
1851
  rank: 10,
1734
- routableModelId: "flux2-pro"
1852
+ routableModelId: "seedream5"
1735
1853
  },
1736
1854
  {
1737
- apiPricing: "$60/1k images",
1738
- creator: "Google",
1739
- elo: 1184,
1740
- isCurrent: false,
1741
- model: "Imagen 4 Ultra Preview 0606",
1742
- 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"
1743
1862
  },
1744
1863
  {
1745
- apiPricing: "$20/1k images",
1746
- creator: "xAI",
1747
- elo: 1182,
1864
+ apiPricing: "$75/1k images",
1865
+ creator: "Alibaba",
1866
+ elo: 1237,
1748
1867
  isCurrent: true,
1749
- model: "grok-imagine-image",
1750
- rank: 12,
1751
- routableModelId: "grok-image"
1868
+ model: "Qwen Image 2.0 Pro",
1869
+ rank: 12
1752
1870
  },
1753
1871
  {
1754
- apiPricing: "$60/1k images",
1755
- creator: "Black Forest Labs",
1756
- elo: 1182,
1872
+ apiPricing: "$50/1k images",
1873
+ creator: "SpaceXAI",
1874
+ elo: 1234,
1757
1875
  isCurrent: true,
1758
- model: "FLUX.2 [flex]",
1759
- rank: 13,
1760
- routableModelId: "flux2-flex"
1876
+ model: "grok-imagine-image-quality",
1877
+ rank: 13
1761
1878
  },
1762
1879
  {
1763
- apiPricing: "$30/1k images",
1764
- creator: "ImagineArt",
1765
- elo: 1181,
1880
+ apiPricing: "$20/1k images",
1881
+ creator: "Microsoft AI",
1882
+ elo: 1228,
1766
1883
  isCurrent: true,
1767
- model: "ImagineArt 2.0",
1884
+ model: "MAI-Image-2.5-Flash",
1768
1885
  rank: 14
1769
1886
  },
1770
1887
  {
1771
- apiPricing: "$60/1k images",
1772
- creator: "Google",
1773
- elo: 1171,
1888
+ apiPricing: "$80/1k images",
1889
+ creator: "HiDream",
1890
+ elo: 1228,
1774
1891
  isCurrent: true,
1775
- model: "Imagen 4 Ultra",
1892
+ model: "HiDream-O1-Image-1.5",
1776
1893
  rank: 15
1777
1894
  },
1778
1895
  {
1779
- apiPricing: "$40/1k images",
1780
- creator: "Google",
1781
- elo: 1169,
1782
- isCurrent: false,
1783
- model: "Imagen 4 Preview 0606",
1784
- 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"
1785
1903
  },
1786
1904
  {
1787
- apiPricing: "$40/1k images",
1788
- creator: "ByteDance Seed",
1789
- elo: 1167,
1905
+ apiPricing: "$70/1k images",
1906
+ creator: "Black Forest Labs",
1907
+ elo: 1225,
1790
1908
  isCurrent: true,
1791
- model: "Seedream 4.5",
1909
+ model: "FLUX.2 [max]",
1792
1910
  rank: 17,
1793
- routableModelId: "seedream45"
1911
+ routableModelId: "flux2-max"
1794
1912
  },
1795
1913
  {
1796
- apiPricing: "$8/1k images",
1797
- creator: "Fal",
1798
- elo: 1161,
1914
+ apiPricing: "$60/1k images",
1915
+ creator: "Black Forest Labs",
1916
+ elo: 1223,
1799
1917
  isCurrent: true,
1800
- model: "FLUX.2 [dev] Turbo",
1801
- rank: 18
1918
+ model: "FLUX.2 [flex]",
1919
+ rank: 18,
1920
+ routableModelId: "flux2-flex"
1802
1921
  },
1803
1922
  {
1804
- apiPricing: "$12/1k images",
1805
- creator: "Black Forest Labs",
1806
- elo: 1160,
1923
+ apiPricing: "$60/1k images",
1924
+ creator: "Ideogram",
1925
+ elo: 1220,
1807
1926
  isCurrent: true,
1808
- model: "FLUX.2 [dev]",
1927
+ model: "Ideogram 4.0",
1809
1928
  rank: 19,
1810
- routableModelId: "flux2-dev"
1929
+ routableModelId: "ideogram4"
1811
1930
  },
1812
1931
  {
1813
- apiPricing: "$20/1k images",
1814
- creator: "Alibaba",
1815
- elo: 1158,
1932
+ apiPricing: "$60/1k images",
1933
+ creator: "Krea",
1934
+ elo: 1220,
1816
1935
  isCurrent: true,
1817
- model: "Qwen Image Max 2512",
1818
- rank: 20,
1819
- routableModelId: "qwen"
1936
+ model: "Krea 2 Large",
1937
+ rank: 20
1820
1938
  }
1821
1939
  ],
1822
1940
  source: "https://artificialanalysis.ai/image/leaderboard/text-to-image"
1823
1941
  };
1824
1942
  var IMAGE_EDITING_TOP_20 = {
1825
- capturedAt: "2026-05-12",
1943
+ capturedAt: "2026-08-23",
1826
1944
  entries: [
1827
1945
  {
1828
- apiPricing: "$150/1k images",
1829
- creator: "Sourceful",
1830
- elo: 1286,
1946
+ apiPricing: "$109/1k images",
1947
+ creator: "Microsoft AI",
1948
+ elo: 1272,
1831
1949
  isCurrent: true,
1832
- model: "Riverflow 2.0",
1950
+ model: "MAI-Image-2.5-Pro",
1833
1951
  rank: 1
1834
1952
  },
1835
1953
  {
1836
- apiPricing: "$133/1k images",
1837
- creator: "OpenAI",
1838
- elo: 1262,
1954
+ apiPricing: "$200/1k images",
1955
+ creator: "Reve",
1956
+ elo: 1263,
1839
1957
  isCurrent: true,
1840
- model: "GPT Image 1.5 (high)",
1958
+ model: "Reve 2.1",
1841
1959
  rank: 2,
1842
- routableModelId: "gpt"
1960
+ routableModelId: "reve"
1843
1961
  },
1844
1962
  {
1845
1963
  apiPricing: "$211/1k images",
1846
1964
  creator: "OpenAI",
1847
- elo: 1249,
1965
+ elo: 1256,
1848
1966
  isCurrent: true,
1849
1967
  model: "GPT Image 2 (high)",
1850
1968
  rank: 3,
1851
1969
  routableModelId: "gpt2"
1852
1970
  },
1853
1971
  {
1854
- apiPricing: "$134/1k images",
1855
- creator: "Google",
1856
- elo: 1241,
1972
+ apiPricing: "$48/1k images",
1973
+ creator: "Microsoft AI",
1974
+ elo: 1256,
1857
1975
  isCurrent: true,
1858
- model: "Nano Banana Pro (Gemini 3 Pro Image)",
1859
- rank: 4,
1860
- routableModelId: "banana"
1976
+ model: "MAI-Image-2.5",
1977
+ rank: 4
1861
1978
  },
1862
1979
  {
1863
- apiPricing: "$67/1k images",
1864
- creator: "Google",
1865
- elo: 1231,
1980
+ apiPricing: "$133/1k images",
1981
+ creator: "OpenAI",
1982
+ elo: 1250,
1866
1983
  isCurrent: true,
1867
- model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1984
+ model: "GPT Image 1.5 (high)",
1868
1985
  rank: 5,
1869
- routableModelId: "banana2"
1986
+ routableModelId: "gpt"
1870
1987
  },
1871
1988
  {
1872
- apiPricing: "$90/1k images",
1873
- creator: "Tencent",
1874
- elo: 1222,
1989
+ apiPricing: "$43/1k images",
1990
+ creator: "Alibaba",
1991
+ elo: 1250,
1875
1992
  isCurrent: true,
1876
- model: "HunyuanImage 3.0 Instruct (Fal)",
1993
+ model: "Qwen-Image-3.0-Pro",
1877
1994
  rank: 6
1878
1995
  },
1879
1996
  {
1880
- apiPricing: "$20/1k images",
1881
- creator: "xAI",
1882
- elo: 1213,
1997
+ apiPricing: "$67/1k images",
1998
+ creator: "Google",
1999
+ elo: 1249,
1883
2000
  isCurrent: true,
1884
- model: "grok-imagine-image",
2001
+ model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1885
2002
  rank: 7,
1886
- routableModelId: "grok-image"
2003
+ routableModelId: "banana2"
1887
2004
  },
1888
2005
  {
1889
- apiPricing: "$70/1k images",
1890
- creator: "xAI",
1891
- elo: 1212,
2006
+ apiPricing: "$90/1k images",
2007
+ creator: "Bytedance",
2008
+ elo: 1248,
1892
2009
  isCurrent: true,
1893
- model: "grok-imagine-image-pro",
1894
- rank: 8
2010
+ model: "Seedream 5.0 Pro",
2011
+ rank: 8,
2012
+ routableModelId: "seedream5"
1895
2013
  },
1896
2014
  {
1897
- apiPricing: "Unavailable",
1898
- creator: "KlingAI",
1899
- elo: 1207,
2015
+ apiPricing: "$134/1k images",
2016
+ creator: "Google",
2017
+ elo: 1245,
1900
2018
  isCurrent: true,
1901
- model: "Kling Image 3.0 Omni",
1902
- rank: 9
2019
+ model: "Nano Banana Pro (Gemini 3 Pro Image)",
2020
+ rank: 9,
2021
+ routableModelId: "banana"
1903
2022
  },
1904
2023
  {
1905
- apiPricing: "$140/1k images",
1906
- creator: "Black Forest Labs",
1907
- elo: 1206,
2024
+ apiPricing: "$20/1k images",
2025
+ creator: "Microsoft AI",
2026
+ elo: 1236,
1908
2027
  isCurrent: true,
1909
- model: "FLUX.2 [max]",
1910
- rank: 10,
1911
- routableModelId: "flux2-max"
2028
+ model: "MAI-Image-2.5-Flash",
2029
+ rank: 10
1912
2030
  },
1913
2031
  {
1914
- apiPricing: "Unavailable",
1915
- creator: "Alibaba",
1916
- elo: 1200,
2032
+ apiPricing: "$50/1k images",
2033
+ creator: "SpaceXAI",
2034
+ elo: 1226,
1917
2035
  isCurrent: true,
1918
- model: "Wan 2.7 Pro",
2036
+ model: "grok-imagine-image-quality",
1919
2037
  rank: 11
1920
2038
  },
1921
2039
  {
1922
- apiPricing: "$28/1k images",
1923
- creator: "KlingAI",
1924
- elo: 1196,
2040
+ apiPricing: "$90/1k images",
2041
+ creator: "Tencent",
2042
+ elo: 1222,
1925
2043
  isCurrent: true,
1926
- model: "Kling Image 3.0",
2044
+ model: "HunyuanImage 3.0 Instruct",
1927
2045
  rank: 12
1928
2046
  },
1929
2047
  {
1930
- apiPricing: "$28/1k images",
1931
- creator: "KlingAI",
1932
- elo: 1193,
2048
+ apiPricing: "$80/1k images",
2049
+ creator: "Newport AI",
2050
+ elo: 1220,
1933
2051
  isCurrent: true,
1934
- model: "Kling Image O1",
2052
+ model: "DreamImage 2.0",
1935
2053
  rank: 13
1936
2054
  },
1937
2055
  {
1938
- apiPricing: "$30/1k images",
1939
- creator: "Alibaba",
1940
- elo: 1188,
2056
+ apiPricing: "$100/1k images",
2057
+ creator: "Luma Labs",
2058
+ elo: 1219,
1941
2059
  isCurrent: true,
1942
- model: "Wan 2.6 Image",
2060
+ model: "Luma UNI 1 Max",
1943
2061
  rank: 14
1944
2062
  },
1945
2063
  {
1946
- apiPricing: "$66/1k images",
1947
- creator: "Sourceful",
1948
- elo: 1184,
2064
+ apiPricing: "$33/1k images",
2065
+ creator: "Alibaba",
2066
+ elo: 1218,
1949
2067
  isCurrent: true,
1950
- model: "Riverflow 1",
1951
- rank: 15
2068
+ model: "Qwen-Image-3.0",
2069
+ rank: 15,
2070
+ routableModelId: "qwen3"
1952
2071
  },
1953
2072
  {
1954
- apiPricing: "$30/1k images",
1955
- creator: "ByteDance Seed",
1956
- elo: 1184,
1957
- isCurrent: true,
1958
- model: "Seedream 4.0",
2073
+ apiPricing: "$20/1k images",
2074
+ creator: "SpaceXAI",
2075
+ elo: 1214,
2076
+ isCurrent: true,
2077
+ model: "grok-imagine-image",
1959
2078
  rank: 16,
1960
- routableModelId: "seedream4"
2079
+ routableModelId: "grok-image"
1961
2080
  },
1962
2081
  {
1963
- apiPricing: "$40/1k images",
1964
- creator: "ByteDance Seed",
1965
- elo: 1184,
2082
+ apiPricing: "$28/1k images",
2083
+ creator: "KlingAI",
2084
+ elo: 1208,
1966
2085
  isCurrent: true,
1967
- model: "Seedream 4.5",
1968
- rank: 17,
1969
- routableModelId: "seedream45"
2086
+ model: "Kling Image 3.0 Omni",
2087
+ rank: 17
1970
2088
  },
1971
2089
  {
1972
- apiPricing: "$26/1k images",
1973
- creator: "Alibaba",
1974
- elo: 1181,
2090
+ apiPricing: "$34/1k images",
2091
+ creator: "Google",
2092
+ elo: 1202,
1975
2093
  isCurrent: true,
1976
- model: "Wan 2.7",
2094
+ model: "Nano Banana 2 Lite",
1977
2095
  rank: 18
1978
2096
  },
1979
2097
  {
1980
- apiPricing: "$39/1k images",
1981
- creator: "Google",
1982
- elo: 1173,
2098
+ apiPricing: "$140/1k images",
2099
+ creator: "Black Forest Labs",
2100
+ elo: 1201,
1983
2101
  isCurrent: true,
1984
- model: "Nano Banana (Gemini 2.5 Flash Image)",
2102
+ model: "FLUX.2 [max]",
1985
2103
  rank: 19,
1986
- routableModelId: "gemini"
2104
+ routableModelId: "flux2-max"
1987
2105
  },
1988
2106
  {
1989
- apiPricing: "$40/1k images",
1990
- creator: "Reve",
1991
- elo: 1172,
2107
+ creator: "Alibaba",
2108
+ elo: 1200,
1992
2109
  isCurrent: true,
1993
- model: "Reve V1 (December)",
2110
+ model: "Wan 2.7 Pro",
1994
2111
  rank: 20
1995
2112
  }
1996
2113
  ],
@@ -2220,7 +2337,31 @@ var VIDEO_IMAGE_TO_VIDEO_TOP_15 = {
2220
2337
  };
2221
2338
 
2222
2339
  // src/server.ts
2223
- 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
+ }
2224
2365
 
2225
2366
  // src/fal-parse.ts
2226
2367
  function isRecord(value) {
@@ -2318,9 +2459,362 @@ function requestIdFromBody(text) {
2318
2459
  return asString(parsed.request_id) ?? asString(parsed.requestId) ?? asString(parsed.trace_id);
2319
2460
  }
2320
2461
 
2321
- // src/tools.ts
2322
- var FAL_EXPLORE_CHECKED_AT = "2026-05-12";
2323
- 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
+ },
2324
2818
  birefnet: {
2325
2819
  category: "background",
2326
2820
  defaultOptions: {
@@ -2333,41 +2827,455 @@ var FAL_TOOLS = {
2333
2827
  endpoint: "fal-ai/birefnet/v2",
2334
2828
  inputField: "image_url",
2335
2829
  inputKind: "image",
2336
- name: "BirefNet Background Removal",
2337
- outputKeys: ["image", "mask_image"],
2338
- pricing: "$0/compute-second listed by fal",
2339
- sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
2340
- 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"
2341
3205
  },
2342
- "bria-rmbg": {
2343
- category: "background",
2344
- description: "Commercial-safe background removal for images.",
2345
- 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",
2346
3210
  inputField: "image_url",
2347
3211
  inputKind: "image",
2348
- name: "Bria RMBG 2.0",
2349
- outputKeys: ["image"],
2350
- pricing: "$0.02/image",
2351
- sourceUrl: "https://fal.ai/models/fal-ai/bria/background/remove",
2352
- 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"
2353
3218
  },
2354
- "bria-video-rmbg": {
2355
- 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",
2356
3261
  defaultOptions: {
2357
- background_color: "Black",
2358
- output_container_and_codec: "webm_vp9",
2359
- preserve_audio: true
3262
+ output_format: "png"
2360
3263
  },
2361
- description: "Remove video backgrounds with configurable output container.",
2362
- endpoint: "bria/video/background-removal",
2363
- inputField: "video_url",
2364
- inputKind: "video",
2365
- name: "Bria Video Background Removal",
2366
- outputKeys: ["video"],
2367
- pricing: "$0.14/sec",
2368
- sourceUrl: "https://fal.ai/models/bria/video/background-removal",
2369
- task: "video background removal"
2370
- },
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 = {
2371
3279
  "depth-anything": {
2372
3280
  category: "preprocess",
2373
3281
  description: "Generate Depth Anything v2 depth maps from input images.",
@@ -2376,10 +3284,37 @@ var FAL_TOOLS = {
2376
3284
  inputKind: "image",
2377
3285
  name: "Depth Anything v2 Preprocessor",
2378
3286
  outputKeys: ["image"],
3287
+ price: { kind: "metered" },
2379
3288
  pricing: "$0/compute-second listed by fal",
2380
3289
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/depth-anything/v2",
2381
3290
  task: "depth preprocessing"
2382
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
+ },
2383
3318
  lineart: {
2384
3319
  category: "preprocess",
2385
3320
  defaultOptions: {
@@ -2391,6 +3326,7 @@ var FAL_TOOLS = {
2391
3326
  inputKind: "image",
2392
3327
  name: "Line Art Preprocessor",
2393
3328
  outputKeys: ["image"],
3329
+ price: { kind: "metered" },
2394
3330
  pricing: "$0/compute-second listed by fal",
2395
3331
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/lineart",
2396
3332
  task: "image preprocessing"
@@ -2407,6 +3343,7 @@ var FAL_TOOLS = {
2407
3343
  inputKind: "image",
2408
3344
  name: "Marigold Depth Estimation",
2409
3345
  outputKeys: ["image"],
3346
+ price: { kind: "metered" },
2410
3347
  pricing: "$0/compute-second listed by fal",
2411
3348
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/marigold-depth",
2412
3349
  task: "depth map"
@@ -2423,6 +3360,7 @@ var FAL_TOOLS = {
2423
3360
  inputKind: "image",
2424
3361
  name: "MiDaS Depth Estimation",
2425
3362
  outputKeys: ["image"],
3363
+ price: { kind: "metered" },
2426
3364
  pricing: "$0/compute-second listed by fal",
2427
3365
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/depth",
2428
3366
  task: "depth map"
@@ -2435,36 +3373,36 @@ var FAL_TOOLS = {
2435
3373
  inputKind: "image",
2436
3374
  name: "MiDaS Preprocessor",
2437
3375
  outputKeys: ["depth_map", "normal_map"],
3376
+ price: { kind: "metered" },
2438
3377
  pricing: "$0/compute-second listed by fal",
2439
3378
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/midas",
2440
3379
  task: "depth and normal preprocessing"
2441
3380
  },
2442
- nsfw: {
2443
- category: "moderation",
2444
- description: "Predict whether one or more images contain NSFW concepts.",
2445
- endpoint: "fal-ai/x-ailab/nsfw",
2446
- inputField: "image_urls",
2447
- inputKind: "images",
2448
- name: "NSFW Checker",
2449
- outputKeys: ["has_nsfw_concepts"],
2450
- pricing: "$0.001/image",
2451
- sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw",
2452
- 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"
2453
3393
  },
2454
- rembg: {
2455
- category: "background",
2456
- defaultOptions: {
2457
- crop_to_bbox: false
2458
- },
2459
- description: "Generic image background removal utility.",
2460
- 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",
2461
3398
  inputField: "image_url",
2462
3399
  inputKind: "image",
2463
- name: "Remove Background",
3400
+ name: "PiDiNet Edge Preprocessor",
2464
3401
  outputKeys: ["image"],
3402
+ price: { kind: "metered" },
2465
3403
  pricing: "$0/compute-second listed by fal",
2466
- sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
2467
- task: "image background removal"
3404
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/pidi",
3405
+ task: "edge preprocessing"
2468
3406
  },
2469
3407
  "sam-preprocessor": {
2470
3408
  category: "preprocess",
@@ -2474,6 +3412,7 @@ var FAL_TOOLS = {
2474
3412
  inputKind: "image",
2475
3413
  name: "SAM Preprocessor",
2476
3414
  outputKeys: ["image"],
3415
+ price: { kind: "metered" },
2477
3416
  pricing: "$0/compute-second listed by fal",
2478
3417
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/sam",
2479
3418
  task: "segmentation preprocessing"
@@ -2493,7 +3432,9 @@ var FAL_TOOLS = {
2493
3432
  inputKind: "image",
2494
3433
  name: "SAM 2 Auto Segment",
2495
3434
  outputKeys: ["combined_mask", "individual_masks"],
3435
+ price: { kind: "metered" },
2496
3436
  pricing: "$0/compute-second listed by fal",
3437
+ queued: true,
2497
3438
  sourceUrl: "https://fal.ai/models/fal-ai/sam2/auto-segment",
2498
3439
  task: "automatic image segmentation"
2499
3440
  },
@@ -2510,7 +3451,8 @@ var FAL_TOOLS = {
2510
3451
  inputKind: "image",
2511
3452
  name: "SAM 3.1 Image",
2512
3453
  outputKeys: ["image", "masks", "metadata", "scores", "boxes"],
2513
- pricing: "$0.005/request",
3454
+ price: { kind: "call", usd: 0.01 },
3455
+ pricing: "$0.01/request",
2514
3456
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/image",
2515
3457
  task: "promptable image segmentation"
2516
3458
  },
@@ -2526,61 +3468,12 @@ var FAL_TOOLS = {
2526
3468
  inputKind: "video",
2527
3469
  name: "SAM 3.1 Video",
2528
3470
  outputKeys: ["video", "boundingbox_frames_zip"],
2529
- pricing: "fal pricing varies by frame count",
3471
+ price: { kind: "metered" },
3472
+ pricing: "$0.01/16 frames of video input",
3473
+ queued: true,
2530
3474
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/video",
2531
3475
  task: "multi-object video segmentation"
2532
3476
  },
2533
- "sam3-3d-align": {
2534
- category: "3d",
2535
- description: "Align SAM 3D objects and bodies into a shared scene.",
2536
- endpoint: "fal-ai/sam-3/3d-align",
2537
- inputField: "image_url",
2538
- inputKind: "image",
2539
- name: "SAM 3D Align",
2540
- outputKeys: ["scene_glb", "metadata", "artifacts_zip"],
2541
- pricing: "fal pricing varies by scene",
2542
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-align",
2543
- task: "3D scene alignment"
2544
- },
2545
- "sam3-3d-body": {
2546
- category: "3d",
2547
- defaultOptions: {
2548
- export_meshes: true,
2549
- include_3d_keypoints: true,
2550
- include_mhr_params: true
2551
- },
2552
- description: "Reconstruct human body meshes and keypoints from a single image.",
2553
- endpoint: "fal-ai/sam-3/3d-body",
2554
- inputField: "image_url",
2555
- inputKind: "image",
2556
- name: "SAM 3D Body",
2557
- outputKeys: ["model_glb", "visualization", "meshes", "metadata"],
2558
- pricing: "$0.015/inference",
2559
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-body",
2560
- task: "single-image 3D body reconstruction"
2561
- },
2562
- "sam3-3d-objects": {
2563
- category: "3d",
2564
- defaultOptions: {
2565
- prompt: "car"
2566
- },
2567
- description: "Reconstruct one or more 3D objects from an image and prompts.",
2568
- endpoint: "fal-ai/sam-3/3d-objects",
2569
- inputField: "image_url",
2570
- inputKind: "image",
2571
- name: "SAM 3D Objects",
2572
- outputKeys: [
2573
- "gaussian_splat",
2574
- "model_glb",
2575
- "metadata",
2576
- "individual_splats",
2577
- "individual_glbs",
2578
- "artifacts_zip"
2579
- ],
2580
- pricing: "$0.02/generation",
2581
- sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-objects",
2582
- task: "single-image 3D object reconstruction"
2583
- },
2584
3477
  "sam3-image": {
2585
3478
  category: "segmentation",
2586
3479
  defaultOptions: {
@@ -2594,6 +3487,7 @@ var FAL_TOOLS = {
2594
3487
  inputKind: "image",
2595
3488
  name: "SAM 3 Image",
2596
3489
  outputKeys: ["image", "masks", "metadata", "scores", "boxes"],
3490
+ price: { kind: "call", usd: 5e-3 },
2597
3491
  pricing: "$0.005/request",
2598
3492
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/image",
2599
3493
  task: "promptable image segmentation"
@@ -2609,7 +3503,8 @@ var FAL_TOOLS = {
2609
3503
  inputField: "image_url",
2610
3504
  inputKind: "image",
2611
3505
  name: "SAM 3 Image RLE",
2612
- outputKeys: ["rle_masks", "metadata", "scores", "boxes"],
3506
+ outputKeys: ["rle", "scores", "boxes", "metadata"],
3507
+ price: { kind: "call", usd: 5e-3 },
2613
3508
  pricing: "$0.005/request",
2614
3509
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/image-rle",
2615
3510
  task: "promptable image segmentation to RLE"
@@ -2628,7 +3523,9 @@ var FAL_TOOLS = {
2628
3523
  inputKind: "video",
2629
3524
  name: "SAM 3 Video",
2630
3525
  outputKeys: ["video", "boundingbox_frames_zip"],
2631
- pricing: "$0.005/16 frames",
3526
+ price: { kind: "metered" },
3527
+ pricing: "$0.005/16 frames of video input",
3528
+ queued: true,
2632
3529
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video",
2633
3530
  task: "promptable video segmentation"
2634
3531
  },
@@ -2644,45 +3541,65 @@ var FAL_TOOLS = {
2644
3541
  inputField: "video_url",
2645
3542
  inputKind: "video",
2646
3543
  name: "SAM 3 Video RLE",
2647
- outputKeys: ["rle_masks", "metadata"],
2648
- 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,
2649
3550
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video-rle",
2650
3551
  task: "promptable video segmentation to RLE"
2651
3552
  },
2652
- "topaz-image": {
2653
- category: "upscale",
2654
- defaultOptions: {
2655
- model: "Standard V2",
2656
- output_format: "jpeg",
2657
- upscale_factor: 2
2658
- },
2659
- description: "Professional Topaz image enhancement and upscaling.",
2660
- 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",
2661
3557
  inputField: "image_url",
2662
3558
  inputKind: "image",
2663
- name: "Topaz Image Upscale",
3559
+ name: "Scribble Preprocessor",
2664
3560
  outputKeys: ["image"],
2665
- pricing: "$0.08+ by output megapixels",
2666
- sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/image",
2667
- 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"
2668
3565
  },
2669
- "topaz-video": {
2670
- category: "upscale",
2671
- defaultOptions: {
2672
- model: "Proteus",
2673
- upscale_factor: 2
2674
- },
2675
- description: "Professional Topaz video enhancement and upscaling.",
2676
- endpoint: "fal-ai/topaz/upscale/video",
2677
- inputField: "video_url",
2678
- inputKind: "video",
2679
- name: "Topaz Video Upscale",
2680
- outputKeys: ["video"],
2681
- pricing: "$0.01-$0.08/sec by output resolution",
2682
- sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
2683
- 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"
2684
3591
  }
2685
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
+ };
2686
3603
  var FAL_TOOLS_CHECKED_AT = FAL_EXPLORE_CHECKED_AT;
2687
3604
  var FAL_TOOL_IDS = Object.keys(
2688
3605
  FAL_TOOLS
@@ -2709,9 +3626,110 @@ function buildFalToolRequest(options) {
2709
3626
  return { body, endpoint: tool.endpoint, tool };
2710
3627
  }
2711
3628
 
2712
- // src/server.ts
3629
+ // src/server-tools.ts
2713
3630
  var FAL_BASE_URL = "https://fal.run";
2714
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";
2715
3733
  var FAL_API_URL = "https://api.fal.ai";
2716
3734
  var FAL_REST_URL = "https://rest.alpha.fal.ai";
2717
3735
  var FalClient = class {
@@ -2743,16 +3761,16 @@ var FalClient = class {
2743
3761
  try {
2744
3762
  built = buildGenerateBody(options);
2745
3763
  } catch (error) {
2746
- return (0, import_neverthrow.err)(toMotifError(error));
3764
+ return (0, import_neverthrow2.err)(toMotifError(error));
2747
3765
  }
2748
3766
  const { endpoint, body } = built;
2749
- const response = await this.request(`${FAL_BASE_URL}/${endpoint}`, {
3767
+ const response = await this.request(`${FAL_BASE_URL2}/${endpoint}`, {
2750
3768
  body: JSON.stringify(body),
2751
3769
  headers: this.ephemeralHeaders(options),
2752
3770
  method: "POST"
2753
3771
  });
2754
3772
  if (response.isErr()) {
2755
- return (0, import_neverthrow.err)(response.error);
3773
+ return (0, import_neverthrow2.err)(response.error);
2756
3774
  }
2757
3775
  const data = await response.value.json();
2758
3776
  return this.normalizeResponse(data);
@@ -2760,7 +3778,7 @@ var FalClient = class {
2760
3778
  async generateQueued(options) {
2761
3779
  const job = await this.submitGeneration(options);
2762
3780
  if (job.isErr()) {
2763
- return (0, import_neverthrow.err)(job.error);
3781
+ return (0, import_neverthrow2.err)(job.error);
2764
3782
  }
2765
3783
  const pollIntervalMs = 3e3;
2766
3784
  const maxAttempts = 160;
@@ -2770,20 +3788,20 @@ var FalClient = class {
2770
3788
  job.value.requestId
2771
3789
  );
2772
3790
  if (status.isErr()) {
2773
- return (0, import_neverthrow.err)(status.error);
3791
+ return (0, import_neverthrow2.err)(status.error);
2774
3792
  }
2775
3793
  if (status.value.status === "completed") {
2776
3794
  return await this.getJobResult(job.value.endpoint, job.value.requestId);
2777
3795
  }
2778
3796
  if (status.value.status === "failed") {
2779
3797
  const message = status.value.error ?? "Queued generation failed";
2780
- 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));
2781
3799
  }
2782
3800
  await new Promise((resolve) => {
2783
3801
  setTimeout(resolve, pollIntervalMs);
2784
3802
  });
2785
3803
  }
2786
- 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));
2787
3805
  }
2788
3806
  /** ─── Queue-Based Generation ──────────────────────────────── */
2789
3807
  /** Submit a generation to the fal.ai queue (returns immediately). */
@@ -2792,20 +3810,20 @@ var FalClient = class {
2792
3810
  try {
2793
3811
  built = buildGenerateBody(options);
2794
3812
  } catch (error) {
2795
- return (0, import_neverthrow.err)(toMotifError(error));
3813
+ return (0, import_neverthrow2.err)(toMotifError(error));
2796
3814
  }
2797
3815
  const { endpoint, body } = built;
2798
- const response = await this.request(`${FAL_QUEUE_URL}/${endpoint}`, {
3816
+ const response = await this.request(`${FAL_QUEUE_URL2}/${endpoint}`, {
2799
3817
  body: JSON.stringify(body),
2800
3818
  headers: this.ephemeralHeaders(options),
2801
3819
  method: "POST"
2802
3820
  });
2803
3821
  if (response.isErr()) {
2804
- return (0, import_neverthrow.err)(response.error);
3822
+ return (0, import_neverthrow2.err)(response.error);
2805
3823
  }
2806
3824
  const data = await response.value.json();
2807
3825
  const submission = parseQueueSubmission(data);
2808
- return (0, import_neverthrow.ok)({
3826
+ return (0, import_neverthrow2.ok)({
2809
3827
  endpoint: endpointFromQueueUrl(submission.responseUrl, endpoint),
2810
3828
  estimatedCost: estimateCost(
2811
3829
  options.model,
@@ -2817,10 +3835,10 @@ var FalClient = class {
2817
3835
  }
2818
3836
  /** Check the status of a queued generation. */
2819
3837
  async getJobStatus(endpoint, requestId) {
2820
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}/status?logs=1`;
3838
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}/status?logs=1`;
2821
3839
  const response = await this.request(url);
2822
3840
  if (response.isErr()) {
2823
- return (0, import_neverthrow.err)(response.error);
3841
+ return (0, import_neverthrow2.err)(response.error);
2824
3842
  }
2825
3843
  const data = await response.value.json();
2826
3844
  const record = isRecord(data) ? data : {};
@@ -2835,9 +3853,9 @@ var FalClient = class {
2835
3853
  } else if (rawStatus === "FAILED" || rawStatus === "ERROR" || rawStatus === "CANCELED") {
2836
3854
  status = "failed";
2837
3855
  } else {
2838
- 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));
2839
3857
  }
2840
- return (0, import_neverthrow.ok)({
3858
+ return (0, import_neverthrow2.ok)({
2841
3859
  error: asString(record.error) ?? asString(record.detail),
2842
3860
  logs: parseLogs(record.logs),
2843
3861
  queuePosition: asNumber(record.queue_position),
@@ -2846,10 +3864,10 @@ var FalClient = class {
2846
3864
  }
2847
3865
  /** Fetch the completed result from the queue. */
2848
3866
  async getJobResult(endpoint, requestId) {
2849
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
3867
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}`;
2850
3868
  const response = await this.request(url);
2851
3869
  if (response.isErr()) {
2852
- return (0, import_neverthrow.err)(response.error);
3870
+ return (0, import_neverthrow2.err)(response.error);
2853
3871
  }
2854
3872
  const data = await response.value.json();
2855
3873
  return this.normalizeResponse(data, requestId);
@@ -2870,7 +3888,7 @@ var FalClient = class {
2870
3888
  } = options;
2871
3889
  const config = MODELS[model];
2872
3890
  if (!config || config.type !== "utility") {
2873
- 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));
2874
3892
  }
2875
3893
  const body = { image_url: imageUrl };
2876
3894
  if (model === "crystal") {
@@ -2903,12 +3921,12 @@ var FalClient = class {
2903
3921
  body.guidance_scale = guidanceScale;
2904
3922
  }
2905
3923
  }
2906
- const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
3924
+ const response = await this.request(`${FAL_BASE_URL2}/${config.endpoint}`, {
2907
3925
  body: JSON.stringify(body),
2908
3926
  method: "POST"
2909
3927
  });
2910
3928
  if (response.isErr()) {
2911
- return (0, import_neverthrow.err)(response.error);
3929
+ return (0, import_neverthrow2.err)(response.error);
2912
3930
  }
2913
3931
  const data = await response.value.json();
2914
3932
  return this.normalizeResponse(data);
@@ -2926,7 +3944,7 @@ var FalClient = class {
2926
3944
  } = options;
2927
3945
  const config = MODELS[model];
2928
3946
  if (!config) {
2929
- return (0, import_neverthrow.err)(
3947
+ return (0, import_neverthrow2.err)(
2930
3948
  new MotifError(`Invalid background removal model: ${model}`, 0)
2931
3949
  );
2932
3950
  }
@@ -2948,12 +3966,12 @@ var FalClient = class {
2948
3966
  rbBody.output_mask = outputMask;
2949
3967
  }
2950
3968
  }
2951
- const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
3969
+ const response = await this.request(`${FAL_BASE_URL2}/${config.endpoint}`, {
2952
3970
  body: JSON.stringify(rbBody),
2953
3971
  method: "POST"
2954
3972
  });
2955
3973
  if (response.isErr()) {
2956
- return (0, import_neverthrow.err)(response.error);
3974
+ return (0, import_neverthrow2.err)(response.error);
2957
3975
  }
2958
3976
  const data = await response.value.json();
2959
3977
  return this.normalizeResponse(data);
@@ -2976,7 +3994,7 @@ var FalClient = class {
2976
3994
  } = options;
2977
3995
  const config = MODELS.kling;
2978
3996
  if (!config) {
2979
- 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));
2980
3998
  }
2981
3999
  const body = {
2982
4000
  duration: String(duration),
@@ -2993,16 +4011,16 @@ var FalClient = class {
2993
4011
  if (cfgScale !== void 0) {
2994
4012
  body.cfg_scale = cfgScale;
2995
4013
  }
2996
- const response = await this.request(`${FAL_QUEUE_URL}/${config.endpoint}`, {
4014
+ const response = await this.request(`${FAL_QUEUE_URL2}/${config.endpoint}`, {
2997
4015
  body: JSON.stringify(body),
2998
4016
  method: "POST"
2999
4017
  });
3000
4018
  if (response.isErr()) {
3001
- return (0, import_neverthrow.err)(response.error);
4019
+ return (0, import_neverthrow2.err)(response.error);
3002
4020
  }
3003
4021
  const data = await response.value.json();
3004
4022
  const submission = parseQueueSubmission(data);
3005
- return (0, import_neverthrow.ok)({
4023
+ return (0, import_neverthrow2.ok)({
3006
4024
  endpoint: endpointFromQueueUrl(submission.responseUrl, config.endpoint),
3007
4025
  estimatedCost: estimateVideoCost(duration, generateAudio),
3008
4026
  requestId: submission.requestId
@@ -3010,17 +4028,17 @@ var FalClient = class {
3010
4028
  }
3011
4029
  /** Fetch the completed video result from the queue. */
3012
4030
  async getVideoResult(endpoint, requestId) {
3013
- const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
4031
+ const url = `${FAL_QUEUE_URL2}/${endpoint}/requests/${requestId}`;
3014
4032
  const response = await this.request(url);
3015
4033
  if (response.isErr()) {
3016
- return (0, import_neverthrow.err)(response.error);
4034
+ return (0, import_neverthrow2.err)(response.error);
3017
4035
  }
3018
4036
  const data = await response.value.json();
3019
4037
  const video = isRecord(data) && isRecord(data.video) ? data.video : void 0;
3020
4038
  if (video === void 0) {
3021
- 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));
3022
4040
  }
3023
- return (0, import_neverthrow.ok)({
4041
+ return (0, import_neverthrow2.ok)({
3024
4042
  contentType: asString(video.content_type) ?? "",
3025
4043
  fileName: asString(video.file_name) ?? "",
3026
4044
  fileSize: asNumber(video.file_size) ?? 0,
@@ -3044,13 +4062,13 @@ var FalClient = class {
3044
4062
  }
3045
4063
  );
3046
4064
  if (initiateResponse.isErr()) {
3047
- return (0, import_neverthrow.err)(initiateResponse.error);
4065
+ return (0, import_neverthrow2.err)(initiateResponse.error);
3048
4066
  }
3049
4067
  const initiateData = await initiateResponse.value.json();
3050
4068
  const fileUrl = isRecord(initiateData) ? asString(initiateData.file_url) ?? "" : "";
3051
4069
  const uploadUrl = isRecord(initiateData) ? asString(initiateData.upload_url) : void 0;
3052
4070
  if (uploadUrl === void 0 || uploadUrl === "") {
3053
- return (0, import_neverthrow.err)(
4071
+ return (0, import_neverthrow2.err)(
3054
4072
  new MotifError("Upload initiate response missing upload_url", 0)
3055
4073
  );
3056
4074
  }
@@ -3065,7 +4083,7 @@ var FalClient = class {
3065
4083
  method: "PUT"
3066
4084
  });
3067
4085
  } catch (error) {
3068
- return (0, import_neverthrow.err)(
4086
+ return (0, import_neverthrow2.err)(
3069
4087
  new MotifError(
3070
4088
  `Upload PUT failed: ${error instanceof Error ? error.message : String(error)}`,
3071
4089
  0
@@ -3073,38 +4091,45 @@ var FalClient = class {
3073
4091
  );
3074
4092
  }
3075
4093
  if (!putResponse.ok) {
3076
- return (0, import_neverthrow.err)(
4094
+ return (0, import_neverthrow2.err)(
3077
4095
  new MotifError(
3078
4096
  `Upload PUT failed: ${putResponse.status}`,
3079
4097
  putResponse.status
3080
4098
  )
3081
4099
  );
3082
4100
  }
3083
- return (0, import_neverthrow.ok)(fileUrl);
4101
+ return (0, import_neverthrow2.ok)(fileUrl);
3084
4102
  }
3085
4103
  /** ─── Utilities ───────────────────────────────────────────── */
3086
4104
  /** Run a registered fal utility/tool endpoint. */
3087
4105
  async runTool(options) {
3088
- let request;
3089
- try {
3090
- request = buildFalToolRequest(options);
3091
- } catch (error) {
3092
- return (0, import_neverthrow.err)(
3093
- new MotifError(
3094
- error instanceof Error ? error.message : String(error),
3095
- 0
3096
- )
3097
- );
3098
- }
3099
- const response = await this.request(`${FAL_BASE_URL}/${request.endpoint}`, {
3100
- body: JSON.stringify(request.body),
3101
- method: "POST"
3102
- });
3103
- if (response.isErr()) {
3104
- return (0, import_neverthrow.err)(response.error);
3105
- }
3106
- const data = await response.value.json();
3107
- 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);
3108
4133
  }
3109
4134
  /**
3110
4135
  * Delete fal's stored IO payloads for a completed request.
@@ -3119,9 +4144,9 @@ var FalClient = class {
3119
4144
  { method: "DELETE" }
3120
4145
  );
3121
4146
  if (response.isErr()) {
3122
- return (0, import_neverthrow.err)(response.error);
4147
+ return (0, import_neverthrow2.err)(response.error);
3123
4148
  }
3124
- return (0, import_neverthrow.ok)();
4149
+ return (0, import_neverthrow2.ok)();
3125
4150
  }
3126
4151
  /** Estimate cost for a generation (no API call). */
3127
4152
  estimateCost(model, resolution, numImages) {
@@ -3147,6 +4172,16 @@ var FalClient = class {
3147
4172
  get tools() {
3148
4173
  return FAL_TOOLS;
3149
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
+ }
3150
4185
  /** ─── Private ─────────────────────────────────────────────── */
3151
4186
  /** Authenticated fetch to fal.ai APIs with retry logic. */
3152
4187
  async request(url, options = {}) {
@@ -3178,15 +4213,15 @@ var FalClient = class {
3178
4213
  const text = await response.text();
3179
4214
  const message = `Request failed: ${response.status} ${text}`;
3180
4215
  const requestId = response.headers.get("x-fal-request-id") ?? requestIdFromBody(text);
3181
- return (0, import_neverthrow.err)(
4216
+ return (0, import_neverthrow2.err)(
3182
4217
  new MotifError(message, response.status, void 0, requestId)
3183
4218
  );
3184
4219
  }
3185
- return (0, import_neverthrow.ok)(response);
4220
+ return (0, import_neverthrow2.ok)(response);
3186
4221
  } catch (error) {
3187
4222
  clearTimeout(timeoutId);
3188
4223
  if (error instanceof MotifError) {
3189
- return (0, import_neverthrow.err)(error);
4224
+ return (0, import_neverthrow2.err)(error);
3190
4225
  }
3191
4226
  lastError = new MotifError(
3192
4227
  error instanceof Error ? error.message : String(error),
@@ -3200,7 +4235,7 @@ var FalClient = class {
3200
4235
  }
3201
4236
  }
3202
4237
  }
3203
- 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));
3204
4239
  }
3205
4240
  ephemeralHeaders(options) {
3206
4241
  return options.ephemeral === true ? { "X-Fal-Store-IO": "0" } : {};
@@ -3211,24 +4246,24 @@ var FalClient = class {
3211
4246
  */
3212
4247
  normalizeResponse(data, fallbackRequestId) {
3213
4248
  if (!isRecord(data)) {
3214
- return (0, import_neverthrow.err)(
4249
+ return (0, import_neverthrow2.err)(
3215
4250
  new MotifError("Unexpected fal response shape", 0, "FAL_ERROR")
3216
4251
  );
3217
4252
  }
3218
4253
  const obj = data;
3219
4254
  const requestId = asString(obj.request_id) ?? asString(obj.requestId) ?? fallbackRequestId;
3220
4255
  if ("detail" in obj) {
3221
- 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"));
3222
4257
  }
3223
4258
  if ("image" in obj && !("images" in obj)) {
3224
- return (0, import_neverthrow.ok)({
4259
+ return (0, import_neverthrow2.ok)({
3225
4260
  images: [toMotifImage(obj.image)],
3226
4261
  prompt: asString(obj.prompt),
3227
4262
  requestId,
3228
4263
  seed: asNumber(obj.seed)
3229
4264
  });
3230
4265
  }
3231
- return (0, import_neverthrow.ok)({
4266
+ return (0, import_neverthrow2.ok)({
3232
4267
  images: parseImages(obj.images),
3233
4268
  prompt: asString(obj.prompt),
3234
4269
  requestId,
@@ -3236,28 +4271,6 @@ var FalClient = class {
3236
4271
  });
3237
4272
  }
3238
4273
  };
3239
- var MotifError = class extends Error {
3240
- status;
3241
- code;
3242
- /** fal's request-correlation id (from the `x-fal-request-id` header or the
3243
- * error body). Ties a failure back to fal's dashboard/support. */
3244
- requestId;
3245
- constructor(message, status, code, requestId) {
3246
- super(message);
3247
- this.name = "MotifError";
3248
- this.status = status;
3249
- this.code = code;
3250
- this.requestId = requestId;
3251
- }
3252
- };
3253
- function toMotifError(error) {
3254
- if (error instanceof MotifError) {
3255
- return error;
3256
- }
3257
- const message = error instanceof Error ? error.message : String(error);
3258
- const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
3259
- return new MotifError(message, 0, code);
3260
- }
3261
4274
  // Annotate the CommonJS export names for ESM import in node:
3262
4275
  0 && (module.exports = {
3263
4276
  ASPECT_RATIOS,