@howells/motif-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2693 @@
1
+ // src/index.ts
2
+ import { err as err2, ok as ok2 } from "neverthrow";
3
+
4
+ // src/aspects.ts
5
+ var ASPECT_RATIOS = [
6
+ "auto",
7
+ "1:1",
8
+ "4:3",
9
+ "3:4",
10
+ "16:9",
11
+ "9:16",
12
+ "3:2",
13
+ "2:3",
14
+ "4:5",
15
+ "5:4",
16
+ "21:9",
17
+ "4:1",
18
+ "1:4",
19
+ "8:1",
20
+ "1:8"
21
+ ];
22
+ var RESOLUTIONS = ["0.5K", "1K", "2K", "4K"];
23
+ function aspectToGptSize(aspect) {
24
+ switch (aspect) {
25
+ case "auto":
26
+ return "auto";
27
+ case "9:16":
28
+ case "1:8":
29
+ case "1:4":
30
+ case "2:3":
31
+ case "4:5":
32
+ case "3:4":
33
+ return "1024x1536";
34
+ case "16:9":
35
+ case "8:1":
36
+ case "4:1":
37
+ case "3:2":
38
+ case "5:4":
39
+ case "4:3":
40
+ case "21:9":
41
+ return "1536x1024";
42
+ default:
43
+ return "1024x1024";
44
+ }
45
+ }
46
+ function aspectToFalImageSize(aspect) {
47
+ switch (aspect) {
48
+ case "auto":
49
+ return "auto";
50
+ case "16:9":
51
+ case "21:9":
52
+ case "4:1":
53
+ case "8:1":
54
+ return "landscape_16_9";
55
+ case "3:2":
56
+ case "5:4":
57
+ return "landscape_4_3";
58
+ case "4:3":
59
+ return "landscape_4_3";
60
+ case "9:16":
61
+ case "1:4":
62
+ case "1:8":
63
+ return "portrait_16_9";
64
+ case "2:3":
65
+ case "4:5":
66
+ return "portrait_4_3";
67
+ case "3:4":
68
+ return "portrait_4_3";
69
+ default:
70
+ return "square_hd";
71
+ }
72
+ }
73
+ var FORMAT_PRESETS = {
74
+ cover: { label: "Cover", aspect: "2:3", resolution: "2K" },
75
+ square: { label: "Square", aspect: "1:1" },
76
+ landscape: { label: "Landscape", aspect: "16:9" },
77
+ portrait: { label: "Portrait", aspect: "2:3" },
78
+ story: { label: "Story", aspect: "9:16" },
79
+ og: { label: "OG Image", aspect: "16:9" },
80
+ wide: { label: "Wide", aspect: "21:9" }
81
+ };
82
+
83
+ // src/models.ts
84
+ var AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-05-12";
85
+ var AA_IMAGE_SOURCES = [
86
+ "https://artificialanalysis.ai/image/leaderboard/text-to-image",
87
+ "https://artificialanalysis.ai/image/leaderboard/editing"
88
+ ];
89
+ var FAL_PRICING_CHECKED_AT = "2026-05-12";
90
+ var MODELS = {
91
+ // ─── Generation Models ────────────────────────────────────────
92
+ gpt2: {
93
+ name: "GPT Image 2",
94
+ endpoint: "openai/gpt-image-2",
95
+ editEndpoint: "openai/gpt-image-2/image-to-image",
96
+ type: "generation",
97
+ pricing: "$0.211",
98
+ pricePerImageUsd: 0.211,
99
+ falPricing: {
100
+ endpointId: "openai/gpt-image-2",
101
+ unitPrice: 1,
102
+ unit: "units",
103
+ currency: "USD",
104
+ estimatedCostPerImageUsd: 0.211,
105
+ source: "fal-pricing-api",
106
+ checkedAt: FAL_PRICING_CHECKED_AT
107
+ },
108
+ benchmark: {
109
+ useCase: "Highest-ranked text-to-image quality and transparent PNGs",
110
+ tiers: { quality: "frontier", speed: "very_slow", price: "ultra" },
111
+ speed: {
112
+ medianSeconds: 200.7,
113
+ p95Seconds: 232.6,
114
+ source: "artificial-analysis-models"
115
+ },
116
+ artificialAnalysis: {
117
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
118
+ sourceUrls: AA_IMAGE_SOURCES,
119
+ textToImage: { rank: 1, elo: 1337, winRate: 0.8, pricePer1k: 211 },
120
+ editing: { rank: 3, elo: 1249, winRate: 0.66, pricePer1k: 211 }
121
+ }
122
+ },
123
+ sizeMode: "image_size_enum",
124
+ supportsAspect: true,
125
+ supportsResolution: false,
126
+ supportsEdit: true,
127
+ supportsNumImages: true,
128
+ supportsOutputFormat: true,
129
+ supportsQuality: true,
130
+ supportsMaskImage: true,
131
+ supportsSyncMode: true,
132
+ maxReferenceImages: 4,
133
+ useQueue: true
134
+ },
135
+ gpt: {
136
+ name: "GPT Image 1.5",
137
+ endpoint: "fal-ai/gpt-image-1.5",
138
+ editEndpoint: "fal-ai/gpt-image-1.5/edit",
139
+ type: "generation",
140
+ pricing: "$0.133",
141
+ pricePerImageUsd: 0.133,
142
+ falPricing: {
143
+ endpointId: "fal-ai/gpt-image-1.5",
144
+ unitPrice: 1,
145
+ unit: "units",
146
+ currency: "USD",
147
+ estimatedCostPerImageUsd: 0.133,
148
+ source: "fal-pricing-api",
149
+ checkedAt: FAL_PRICING_CHECKED_AT
150
+ },
151
+ benchmark: {
152
+ useCase: "Best OpenAI edit quality with transparent PNG support",
153
+ tiers: { quality: "frontier", speed: "slow", price: "ultra" },
154
+ speed: {
155
+ medianSeconds: 34.6,
156
+ p95Seconds: 38.3,
157
+ source: "artificial-analysis-models"
158
+ },
159
+ artificialAnalysis: {
160
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
161
+ sourceUrls: AA_IMAGE_SOURCES,
162
+ textToImage: { rank: 2, elo: 1268, winRate: 0.73, pricePer1k: 133 },
163
+ editing: { rank: 2, elo: 1262, winRate: 0.69, pricePer1k: 133 }
164
+ }
165
+ },
166
+ sizeMode: "gpt_size",
167
+ supportsAspect: false,
168
+ supportsBackground: true,
169
+ supportsResolution: false,
170
+ supportsEdit: true,
171
+ supportsNumImages: true,
172
+ supportsOutputFormat: true,
173
+ supportsQuality: true,
174
+ supportsMaskImage: true,
175
+ supportsSyncMode: true,
176
+ maxReferenceImages: 4
177
+ },
178
+ banana2: {
179
+ name: "Nano Banana 2",
180
+ endpoint: "fal-ai/nano-banana-2",
181
+ editEndpoint: "fal-ai/nano-banana-2/edit",
182
+ type: "generation",
183
+ pricing: "$0.08",
184
+ pricePerImageUsd: 0.08,
185
+ falPricing: {
186
+ endpointId: "fal-ai/nano-banana-2",
187
+ unitPrice: 0.08,
188
+ unit: "images",
189
+ currency: "USD",
190
+ estimatedCostPerImageUsd: 0.08,
191
+ source: "fal-pricing-api",
192
+ checkedAt: FAL_PRICING_CHECKED_AT
193
+ },
194
+ benchmark: {
195
+ useCase: "Best balance of quality, edit support, web search, and cost",
196
+ tiers: { quality: "frontier", speed: "unknown", price: "premium" },
197
+ artificialAnalysis: {
198
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
199
+ sourceUrls: AA_IMAGE_SOURCES,
200
+ textToImage: { rank: 3, elo: 1263, winRate: 0.71, pricePer1k: 67 },
201
+ editing: { rank: 5, elo: 1231, winRate: 0.63, pricePer1k: 67 }
202
+ }
203
+ },
204
+ sizeMode: "aspect_ratio",
205
+ supportsAspect: true,
206
+ supportsResolution: true,
207
+ supportsEdit: true,
208
+ supportsNumImages: true,
209
+ supportsOutputFormat: true,
210
+ supportsSafetyTolerance: true,
211
+ supportsSeed: true,
212
+ supportsWebSearch: true,
213
+ supportsGoogleSearch: true,
214
+ supportsLimitGenerations: true,
215
+ supportsSyncMode: true,
216
+ supportsThinkingLevel: true,
217
+ maxReferenceImages: 4
218
+ },
219
+ banana: {
220
+ name: "Nano Banana Pro",
221
+ endpoint: "fal-ai/nano-banana-pro",
222
+ editEndpoint: "fal-ai/nano-banana-pro/edit",
223
+ type: "generation",
224
+ pricing: "$0.15",
225
+ pricePerImageUsd: 0.15,
226
+ falPricing: {
227
+ endpointId: "fal-ai/nano-banana-pro",
228
+ unitPrice: 0.15,
229
+ unit: "images",
230
+ currency: "USD",
231
+ estimatedCostPerImageUsd: 0.15,
232
+ source: "fal-pricing-api",
233
+ checkedAt: FAL_PRICING_CHECKED_AT
234
+ },
235
+ benchmark: {
236
+ useCase: "Premium Gemini image generation and multi-reference edits",
237
+ tiers: { quality: "frontier", speed: "balanced", price: "ultra" },
238
+ speed: {
239
+ medianSeconds: 19.2,
240
+ p95Seconds: 30.3,
241
+ source: "artificial-analysis-models"
242
+ },
243
+ artificialAnalysis: {
244
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
245
+ sourceUrls: AA_IMAGE_SOURCES,
246
+ textToImage: { rank: 5, elo: 1220, winRate: 0.66, pricePer1k: 134 },
247
+ editing: { rank: 4, elo: 1241, winRate: 0.65, pricePer1k: 134 }
248
+ }
249
+ },
250
+ sizeMode: "aspect_ratio",
251
+ supportsAspect: true,
252
+ supportsResolution: true,
253
+ supportsEdit: true,
254
+ supportsNumImages: true,
255
+ supportsOutputFormat: true,
256
+ supportsSafetyTolerance: true,
257
+ supportsSeed: true,
258
+ supportsWebSearch: true,
259
+ supportsGoogleSearch: true,
260
+ supportsLimitGenerations: true,
261
+ supportsSyncMode: true,
262
+ maxReferenceImages: 14
263
+ },
264
+ gemini: {
265
+ name: "Gemini 2.5 Flash",
266
+ endpoint: "fal-ai/gemini-25-flash-image",
267
+ editEndpoint: "fal-ai/gemini-25-flash-image/edit",
268
+ type: "generation",
269
+ pricing: "$0.04",
270
+ pricePerImageUsd: 0.0398,
271
+ falPricing: {
272
+ endpointId: "fal-ai/gemini-25-flash-image",
273
+ unitPrice: 0.0398,
274
+ unit: "images",
275
+ currency: "USD",
276
+ estimatedCostPerImageUsd: 0.0398,
277
+ source: "fal-pricing-api",
278
+ checkedAt: FAL_PRICING_CHECKED_AT
279
+ },
280
+ sizeMode: "aspect_ratio",
281
+ supportsAspect: true,
282
+ supportsResolution: false,
283
+ supportsEdit: true,
284
+ supportsNumImages: true,
285
+ supportsOutputFormat: true,
286
+ supportsSafetyTolerance: true,
287
+ supportsSeed: true,
288
+ supportsSyncMode: true,
289
+ maxReferenceImages: 4
290
+ },
291
+ gemini3: {
292
+ name: "Gemini 3 Pro",
293
+ endpoint: "fal-ai/gemini-3-pro-image-preview",
294
+ editEndpoint: "fal-ai/gemini-3-pro-image-preview/edit",
295
+ type: "generation",
296
+ pricing: "$0.15",
297
+ pricePerImageUsd: 0.15,
298
+ falPricing: {
299
+ endpointId: "fal-ai/gemini-3-pro-image-preview",
300
+ unitPrice: 0.15,
301
+ unit: "images",
302
+ currency: "USD",
303
+ estimatedCostPerImageUsd: 0.15,
304
+ source: "fal-pricing-api",
305
+ checkedAt: FAL_PRICING_CHECKED_AT
306
+ },
307
+ sizeMode: "aspect_ratio",
308
+ supportsAspect: true,
309
+ supportsResolution: true,
310
+ supportsEdit: true,
311
+ supportsNumImages: true,
312
+ supportsOutputFormat: true,
313
+ supportsSafetyTolerance: true,
314
+ supportsSeed: true,
315
+ supportsWebSearch: true,
316
+ supportsSyncMode: true,
317
+ maxReferenceImages: 4
318
+ },
319
+ seedream4: {
320
+ name: "Seedream 4.0",
321
+ endpoint: "fal-ai/bytedance/seedream/v4/text-to-image",
322
+ editEndpoint: "fal-ai/bytedance/seedream/v4/edit",
323
+ type: "generation",
324
+ pricing: "$0.03",
325
+ pricePerImageUsd: 0.03,
326
+ falPricing: {
327
+ endpointId: "fal-ai/bytedance/seedream/v4/text-to-image",
328
+ unitPrice: 0.03,
329
+ unit: "images",
330
+ currency: "USD",
331
+ estimatedCostPerImageUsd: 0.03,
332
+ source: "fal-pricing-api",
333
+ checkedAt: FAL_PRICING_CHECKED_AT
334
+ },
335
+ benchmark: {
336
+ useCase: "High-ranked budget generation and large multi-reference edits",
337
+ tiers: { quality: "best", speed: "balanced", price: "budget" },
338
+ speed: {
339
+ medianSeconds: 14.3,
340
+ p95Seconds: 21,
341
+ source: "artificial-analysis-models"
342
+ },
343
+ artificialAnalysis: {
344
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
345
+ sourceUrls: AA_IMAGE_SOURCES,
346
+ textToImage: { rank: 6, elo: 1198, winRate: 0.59, pricePer1k: 30 },
347
+ editing: { rank: 16, elo: 1184, winRate: 0.61, pricePer1k: 30 }
348
+ }
349
+ },
350
+ sizeMode: "image_size_enum",
351
+ supportsAspect: true,
352
+ supportsResolution: false,
353
+ supportsEdit: true,
354
+ supportsNumImages: true,
355
+ supportsSeed: true,
356
+ supportsSyncMode: true,
357
+ maxReferenceImages: 10
358
+ },
359
+ seedream45: {
360
+ name: "Seedream 4.5",
361
+ endpoint: "fal-ai/bytedance/seedream/v4.5/text-to-image",
362
+ editEndpoint: "fal-ai/bytedance/seedream/v4.5/edit",
363
+ type: "generation",
364
+ pricing: "$0.04",
365
+ pricePerImageUsd: 0.04,
366
+ falPricing: {
367
+ endpointId: "fal-ai/bytedance/seedream/v4.5/text-to-image",
368
+ unitPrice: 0.04,
369
+ unit: "images",
370
+ currency: "USD",
371
+ estimatedCostPerImageUsd: 0.04,
372
+ source: "fal-pricing-api",
373
+ checkedAt: FAL_PRICING_CHECKED_AT
374
+ },
375
+ benchmark: {
376
+ useCase: "Cheap current Seedream model with good animation/art scores",
377
+ tiers: { quality: "better", speed: "balanced", price: "standard" },
378
+ speed: {
379
+ medianSeconds: 18.2,
380
+ p95Seconds: 43.3,
381
+ source: "artificial-analysis-models"
382
+ },
383
+ artificialAnalysis: {
384
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
385
+ sourceUrls: AA_IMAGE_SOURCES,
386
+ textToImage: { rank: 17, elo: 1167, winRate: 0.63, pricePer1k: 40 },
387
+ editing: { rank: 17, elo: 1184, winRate: 0.58, pricePer1k: 40 }
388
+ }
389
+ },
390
+ sizeMode: "image_size_enum",
391
+ supportsAspect: true,
392
+ supportsResolution: false,
393
+ supportsEdit: true,
394
+ supportsNumImages: true,
395
+ supportsSeed: true,
396
+ supportsSyncMode: true,
397
+ maxReferenceImages: 10
398
+ },
399
+ "flux2-max": {
400
+ name: "FLUX.2 Max",
401
+ endpoint: "fal-ai/flux-2-max",
402
+ editEndpoint: "fal-ai/flux-2-max/edit",
403
+ type: "generation",
404
+ pricing: "$0.07/MP",
405
+ pricePerImageUsd: 0.07,
406
+ falPricing: {
407
+ endpointId: "fal-ai/flux-2-max",
408
+ unitPrice: 0.07,
409
+ unit: "megapixels",
410
+ currency: "USD",
411
+ estimatedCostPerImageUsd: 0.07,
412
+ source: "fal-pricing-api",
413
+ checkedAt: FAL_PRICING_CHECKED_AT
414
+ },
415
+ benchmark: {
416
+ useCase: "Best FLUX quality and high-end editing",
417
+ tiers: { quality: "best", speed: "slow", price: "premium" },
418
+ speed: {
419
+ medianSeconds: 31.6,
420
+ p95Seconds: 41,
421
+ source: "artificial-analysis-models"
422
+ },
423
+ artificialAnalysis: {
424
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
425
+ sourceUrls: AA_IMAGE_SOURCES,
426
+ textToImage: { rank: 8, elo: 1197, winRate: 0.67, pricePer1k: 70 },
427
+ editing: { rank: 10, elo: 1206, winRate: 0.6, pricePer1k: 140 }
428
+ }
429
+ },
430
+ sizeMode: "image_size_enum",
431
+ supportsAspect: true,
432
+ supportsResolution: false,
433
+ supportsEdit: true,
434
+ supportsNumImages: false,
435
+ supportsOutputFormat: true,
436
+ supportsSafetyTolerance: true,
437
+ supportsSafetyChecker: true,
438
+ supportsSeed: true,
439
+ supportsSyncMode: true,
440
+ maxReferenceImages: 10
441
+ },
442
+ "flux2-pro": {
443
+ name: "FLUX.2 Pro",
444
+ endpoint: "fal-ai/flux-2-pro",
445
+ editEndpoint: "fal-ai/flux-2-pro/edit",
446
+ type: "generation",
447
+ pricing: "$0.03/MP",
448
+ pricePerImageUsd: 0.03,
449
+ falPricing: {
450
+ endpointId: "fal-ai/flux-2-pro",
451
+ unitPrice: 0.03,
452
+ unit: "processed megapixels",
453
+ currency: "USD",
454
+ estimatedCostPerImageUsd: 0.03,
455
+ source: "fal-pricing-api",
456
+ checkedAt: FAL_PRICING_CHECKED_AT
457
+ },
458
+ benchmark: {
459
+ useCase: "Production FLUX quality at a low per-megapixel price",
460
+ tiers: { quality: "best", speed: "balanced", price: "budget" },
461
+ speed: {
462
+ medianSeconds: 16.1,
463
+ p95Seconds: 23.9,
464
+ source: "artificial-analysis-models"
465
+ },
466
+ artificialAnalysis: {
467
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
468
+ sourceUrls: AA_IMAGE_SOURCES,
469
+ textToImage: { rank: 10, elo: 1186, winRate: 0.62, pricePer1k: 30 },
470
+ editing: { rank: 21, elo: 1170, pricePer1k: 30 }
471
+ }
472
+ },
473
+ sizeMode: "image_size_enum",
474
+ supportsAspect: true,
475
+ supportsResolution: false,
476
+ supportsEdit: true,
477
+ supportsNumImages: false,
478
+ supportsOutputFormat: true,
479
+ supportsSafetyTolerance: true,
480
+ supportsSafetyChecker: true,
481
+ supportsSeed: true,
482
+ supportsSyncMode: true,
483
+ maxReferenceImages: 10
484
+ },
485
+ "flux2-flex": {
486
+ name: "FLUX.2 Flex",
487
+ endpoint: "fal-ai/flux-2-flex",
488
+ editEndpoint: "fal-ai/flux-2-flex/edit",
489
+ type: "generation",
490
+ pricing: "$0.05/MP",
491
+ pricePerImageUsd: 0.05,
492
+ falPricing: {
493
+ endpointId: "fal-ai/flux-2-flex",
494
+ unitPrice: 0.05,
495
+ unit: "processed megapixels",
496
+ currency: "USD",
497
+ estimatedCostPerImageUsd: 0.05,
498
+ source: "fal-pricing-api",
499
+ checkedAt: FAL_PRICING_CHECKED_AT
500
+ },
501
+ benchmark: {
502
+ useCase: "FLUX quality with guidance and step controls",
503
+ tiers: { quality: "best", speed: "balanced", price: "standard" },
504
+ speed: {
505
+ medianSeconds: 15.3,
506
+ p95Seconds: 22.2,
507
+ source: "artificial-analysis-models"
508
+ },
509
+ artificialAnalysis: {
510
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
511
+ sourceUrls: AA_IMAGE_SOURCES,
512
+ textToImage: { rank: 13, elo: 1182, winRate: 0.61, pricePer1k: 60 },
513
+ editing: { rank: 22, elo: 1161, pricePer1k: 60 }
514
+ }
515
+ },
516
+ sizeMode: "image_size_enum",
517
+ supportsAspect: true,
518
+ supportsResolution: false,
519
+ supportsEdit: true,
520
+ supportsNumImages: false,
521
+ supportsOutputFormat: true,
522
+ supportedOutputFormats: ["jpeg", "png"],
523
+ supportsSafetyTolerance: true,
524
+ supportsSafetyChecker: true,
525
+ supportsSeed: true,
526
+ supportsGuidanceScale: true,
527
+ supportsInferenceSteps: true,
528
+ supportsSyncMode: true,
529
+ maxReferenceImages: 10
530
+ },
531
+ "flux2-dev": {
532
+ name: "FLUX.2 Dev",
533
+ endpoint: "fal-ai/flux-2",
534
+ editEndpoint: "fal-ai/flux-2/edit",
535
+ type: "generation",
536
+ pricing: "$0.00167/sec",
537
+ pricePerImageUsd: 0.012,
538
+ falPricing: {
539
+ endpointId: "fal-ai/flux-2",
540
+ unitPrice: 167e-5,
541
+ unit: "compute seconds",
542
+ currency: "USD",
543
+ estimatedCostPerImageUsd: 0.012,
544
+ source: "fal-pricing-api",
545
+ checkedAt: FAL_PRICING_CHECKED_AT
546
+ },
547
+ benchmark: {
548
+ useCase: "Open FLUX.2 quality with low average cost",
549
+ tiers: { quality: "better", speed: "fast", price: "variable" },
550
+ speed: {
551
+ medianSeconds: 4.6,
552
+ p95Seconds: 69.6,
553
+ source: "artificial-analysis-models"
554
+ },
555
+ artificialAnalysis: {
556
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
557
+ sourceUrls: AA_IMAGE_SOURCES,
558
+ textToImage: { rank: 19, elo: 1160, winRate: 0.57, pricePer1k: 12 }
559
+ }
560
+ },
561
+ sizeMode: "image_size_enum",
562
+ supportsAspect: true,
563
+ supportsResolution: false,
564
+ supportsEdit: true,
565
+ supportsNumImages: true,
566
+ supportsOutputFormat: true,
567
+ supportsSafetyTolerance: true,
568
+ supportsSafetyChecker: true,
569
+ supportsSeed: true,
570
+ supportsGuidanceScale: true,
571
+ supportsInferenceSteps: true,
572
+ supportsSyncMode: true,
573
+ maxReferenceImages: 10
574
+ },
575
+ flux: {
576
+ name: "FLUX Pro Ultra",
577
+ endpoint: "fal-ai/flux-pro/v1.1-ultra",
578
+ type: "generation",
579
+ pricing: "$0.06",
580
+ pricePerImageUsd: 0.06,
581
+ falPricing: {
582
+ endpointId: "fal-ai/flux-pro/v1.1-ultra",
583
+ unitPrice: 0.06,
584
+ unit: "images",
585
+ currency: "USD",
586
+ estimatedCostPerImageUsd: 0.06,
587
+ source: "fal-pricing-api",
588
+ checkedAt: FAL_PRICING_CHECKED_AT
589
+ },
590
+ sizeMode: "aspect_ratio",
591
+ supportsAspect: true,
592
+ supportsResolution: false,
593
+ supportsEdit: true,
594
+ supportsNumImages: true,
595
+ supportsOutputFormat: true,
596
+ supportsRaw: true,
597
+ supportsEnhancePrompt: true,
598
+ supportsSafetyTolerance: true,
599
+ supportsSeed: true,
600
+ supportsImagePromptStrength: true,
601
+ supportsSyncMode: true,
602
+ maxReferenceImages: 1
603
+ },
604
+ "flux-fast": {
605
+ name: "FLUX Schnell",
606
+ endpoint: "fal-ai/flux/schnell",
607
+ type: "generation",
608
+ pricing: "$0.003",
609
+ pricePerImageUsd: 3e-3,
610
+ falPricing: {
611
+ endpointId: "fal-ai/flux/schnell",
612
+ unitPrice: 3e-3,
613
+ unit: "megapixels",
614
+ currency: "USD",
615
+ estimatedCostPerImageUsd: 3e-3,
616
+ source: "fal-pricing-api",
617
+ checkedAt: FAL_PRICING_CHECKED_AT
618
+ },
619
+ sizeMode: "image_size_enum",
620
+ supportsAspect: true,
621
+ supportsResolution: false,
622
+ supportsEdit: false,
623
+ supportsNumImages: true,
624
+ supportsOutputFormat: true,
625
+ supportsGuidanceScale: true,
626
+ supportsInferenceSteps: true,
627
+ supportsSeed: true,
628
+ supportsSyncMode: true
629
+ },
630
+ recraft: {
631
+ name: "Recraft V3",
632
+ endpoint: "fal-ai/recraft-v3",
633
+ type: "generation",
634
+ pricing: "$0.04",
635
+ pricePerImageUsd: 0.04,
636
+ falPricing: {
637
+ endpointId: "fal-ai/recraft-v3",
638
+ unitPrice: 0.04,
639
+ unit: "images",
640
+ currency: "USD",
641
+ estimatedCostPerImageUsd: 0.04,
642
+ source: "fal-pricing-api",
643
+ checkedAt: FAL_PRICING_CHECKED_AT
644
+ },
645
+ sizeMode: "image_size_enum",
646
+ supportsAspect: true,
647
+ supportsResolution: false,
648
+ supportsEdit: false,
649
+ supportsNumImages: false,
650
+ supportsStyle: true,
651
+ supportsSyncMode: true
652
+ },
653
+ ideogram: {
654
+ name: "Ideogram V3",
655
+ endpoint: "fal-ai/ideogram/v3",
656
+ type: "generation",
657
+ pricing: "$0.03",
658
+ pricePerImageUsd: 0.03,
659
+ falPricing: {
660
+ endpointId: "fal-ai/ideogram/v3",
661
+ unitPrice: 0.03,
662
+ unit: "images",
663
+ currency: "USD",
664
+ estimatedCostPerImageUsd: 0.03,
665
+ source: "fal-pricing-api",
666
+ checkedAt: FAL_PRICING_CHECKED_AT
667
+ },
668
+ sizeMode: "image_size_enum",
669
+ supportsAspect: true,
670
+ supportsResolution: false,
671
+ supportsEdit: false,
672
+ supportsNumImages: true,
673
+ supportsExpandPrompt: true,
674
+ supportsNegativePrompt: true,
675
+ supportsRenderingSpeed: true,
676
+ supportsSeed: true,
677
+ supportsStyle: true,
678
+ supportsSyncMode: true
679
+ },
680
+ "grok-image": {
681
+ name: "Grok Imagine Image",
682
+ endpoint: "xai/grok-imagine-image",
683
+ editEndpoint: "xai/grok-imagine-image/edit",
684
+ type: "generation",
685
+ pricing: "$0.02",
686
+ pricePerImageUsd: 0.02,
687
+ falPricing: {
688
+ endpointId: "xai/grok-imagine-image",
689
+ unitPrice: 0.02,
690
+ unit: "images",
691
+ currency: "USD",
692
+ estimatedCostPerImageUsd: 0.02,
693
+ source: "fal-pricing-api",
694
+ checkedAt: FAL_PRICING_CHECKED_AT
695
+ },
696
+ benchmark: {
697
+ useCase: "Very fast, cheap generation and edits",
698
+ tiers: { quality: "best", speed: "fast", price: "budget" },
699
+ speed: {
700
+ medianSeconds: 5.1,
701
+ p95Seconds: 6.5,
702
+ source: "artificial-analysis-models"
703
+ },
704
+ artificialAnalysis: {
705
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
706
+ sourceUrls: AA_IMAGE_SOURCES,
707
+ textToImage: { rank: 12, elo: 1182, winRate: 0.6, pricePer1k: 20 },
708
+ editing: { rank: 7, elo: 1213, winRate: 0.6, pricePer1k: 20 }
709
+ }
710
+ },
711
+ sizeMode: "aspect_ratio",
712
+ supportsAspect: true,
713
+ supportsResolution: true,
714
+ supportsEdit: true,
715
+ supportsNumImages: true,
716
+ supportsOutputFormat: true,
717
+ supportsSyncMode: true,
718
+ maxReferenceImages: 4
719
+ },
720
+ qwen: {
721
+ name: "Qwen Image",
722
+ endpoint: "fal-ai/qwen-image",
723
+ type: "generation",
724
+ pricing: "$0.02/MP",
725
+ pricePerImageUsd: 0.02,
726
+ falPricing: {
727
+ endpointId: "fal-ai/qwen-image",
728
+ unitPrice: 0.02,
729
+ unit: "megapixels",
730
+ currency: "USD",
731
+ estimatedCostPerImageUsd: 0.02,
732
+ source: "fal-pricing-api",
733
+ checkedAt: FAL_PRICING_CHECKED_AT
734
+ },
735
+ benchmark: {
736
+ useCase: "Low-cost open-weight text and design generation",
737
+ tiers: { quality: "better", speed: "slow", price: "budget" },
738
+ speed: {
739
+ medianSeconds: 23.3,
740
+ p95Seconds: 491,
741
+ source: "artificial-analysis-models"
742
+ },
743
+ artificialAnalysis: {
744
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
745
+ sourceUrls: AA_IMAGE_SOURCES,
746
+ textToImage: { rank: 20, elo: 1158, winRate: 0.6, pricePer1k: 20 }
747
+ }
748
+ },
749
+ sizeMode: "image_size_enum",
750
+ supportsAspect: true,
751
+ supportsResolution: false,
752
+ supportsEdit: false,
753
+ supportsNumImages: true,
754
+ supportsOutputFormat: true,
755
+ supportsSeed: true,
756
+ supportsSyncMode: true
757
+ },
758
+ // ─── Video Models ─────────────────────────────────────────────
759
+ kling: {
760
+ name: "Kling v3 Pro",
761
+ endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
762
+ type: "video",
763
+ pricing: "$0.11/sec",
764
+ sizeMode: "none",
765
+ supportsAspect: false,
766
+ supportsResolution: false,
767
+ supportsEdit: false,
768
+ supportsNumImages: false
769
+ },
770
+ // ─── Utility Models ───────────────────────────────────────────
771
+ clarity: {
772
+ name: "Clarity Upscaler",
773
+ endpoint: "fal-ai/clarity-upscaler",
774
+ type: "utility",
775
+ pricing: "$0.02",
776
+ supportsAspect: false,
777
+ supportsResolution: false,
778
+ supportsEdit: false,
779
+ supportsNumImages: false
780
+ },
781
+ crystal: {
782
+ name: "Crystal Upscaler",
783
+ endpoint: "clarityai/crystal-upscaler",
784
+ type: "utility",
785
+ pricing: "$0.02",
786
+ supportsAspect: false,
787
+ supportsResolution: false,
788
+ supportsEdit: false,
789
+ supportsNumImages: false
790
+ },
791
+ rmbg: {
792
+ name: "BiRefNet (Background Removal)",
793
+ endpoint: "fal-ai/birefnet",
794
+ type: "utility",
795
+ pricing: "$0.02",
796
+ supportsAspect: false,
797
+ supportsResolution: false,
798
+ supportsEdit: false,
799
+ supportsNumImages: false
800
+ },
801
+ bria: {
802
+ name: "Bria RMBG 2.0",
803
+ endpoint: "fal-ai/bria/background/remove",
804
+ type: "utility",
805
+ pricing: "$0.02",
806
+ supportsAspect: false,
807
+ supportsResolution: false,
808
+ supportsEdit: false,
809
+ supportsNumImages: false
810
+ }
811
+ };
812
+ var GENERATION_MODELS = [
813
+ "gpt2",
814
+ "gpt",
815
+ "banana2",
816
+ "banana",
817
+ "gemini",
818
+ "gemini3",
819
+ "seedream4",
820
+ "seedream45",
821
+ "flux2-max",
822
+ "flux2-pro",
823
+ "flux2-flex",
824
+ "flux2-dev",
825
+ "flux",
826
+ "flux-fast",
827
+ "recraft",
828
+ "ideogram",
829
+ "grok-image",
830
+ "qwen"
831
+ ];
832
+ var UTILITY_MODELS = ["clarity", "crystal", "rmbg", "bria"];
833
+ var VIDEO_MODELS = ["kling"];
834
+ var RECRAFT_STYLES = [
835
+ // Realistic
836
+ "realistic_image",
837
+ "realistic_image/b_and_w",
838
+ "realistic_image/enterprise",
839
+ "realistic_image/hard_flash",
840
+ "realistic_image/hdr",
841
+ "realistic_image/motion_blur",
842
+ "realistic_image/natural_light",
843
+ "realistic_image/studio_portrait",
844
+ "realistic_image/long_exposure",
845
+ // Digital illustration
846
+ "digital_illustration",
847
+ "digital_illustration/2d_art_poster",
848
+ "digital_illustration/2d_art_poster_2",
849
+ "digital_illustration/engraving_color",
850
+ "digital_illustration/grain",
851
+ "digital_illustration/hand_drawn",
852
+ "digital_illustration/hand_drawn_outline",
853
+ "digital_illustration/handmade_3d",
854
+ "digital_illustration/infantile_sketch",
855
+ "digital_illustration/pixel_art",
856
+ "digital_illustration/psychedelic",
857
+ "digital_illustration/seamless",
858
+ "digital_illustration/voxel",
859
+ "digital_illustration/watercolor",
860
+ // Vector illustration
861
+ "vector_illustration",
862
+ "vector_illustration/cartoon",
863
+ "vector_illustration/doodle_line_art",
864
+ "vector_illustration/engraving",
865
+ "vector_illustration/flat_2",
866
+ "vector_illustration/kawaii",
867
+ "vector_illustration/line_art",
868
+ "vector_illustration/line_circuit",
869
+ "vector_illustration/linocut",
870
+ "vector_illustration/zentangle"
871
+ ];
872
+ var IDEOGRAM_STYLES = [
873
+ "AUTO",
874
+ "GENERAL",
875
+ "REALISTIC",
876
+ "DESIGN"
877
+ ];
878
+
879
+ // src/cost.ts
880
+ function estimateCost(model, resolution, numImages = 1) {
881
+ const configuredPrice = MODELS[model]?.pricePerImageUsd;
882
+ if (configuredPrice !== void 0) {
883
+ if ((model === "banana" || model === "gemini3") && resolution === "4K") {
884
+ return configuredPrice * 2 * numImages;
885
+ }
886
+ return configuredPrice * numImages;
887
+ }
888
+ switch (model) {
889
+ case "gpt2":
890
+ return 0.211 * numImages;
891
+ case "gpt":
892
+ return 0.133 * numImages;
893
+ case "banana":
894
+ case "gemini3":
895
+ return (resolution === "4K" ? 0.3 : 0.15) * numImages;
896
+ case "gemini":
897
+ return 0.039 * numImages;
898
+ case "flux":
899
+ case "ideogram":
900
+ return 0.06 * numImages;
901
+ case "recraft":
902
+ return 0.04 * numImages;
903
+ case "flux-fast":
904
+ return 3e-3 * numImages;
905
+ case "clarity":
906
+ case "crystal":
907
+ case "rmbg":
908
+ case "bria":
909
+ return 0.02 * numImages;
910
+ default:
911
+ return 0.1 * numImages;
912
+ }
913
+ }
914
+ function estimateVideoCost(durationSeconds = 5, generateAudio = true) {
915
+ const perSecond = generateAudio ? 0.168 : 0.112;
916
+ return perSecond * durationSeconds;
917
+ }
918
+
919
+ // src/env.ts
920
+ import { defineEnv } from "@howells/envy";
921
+ import { z } from "zod";
922
+ var motifEnvSchema = defineEnv({
923
+ optional: {
924
+ FAL_KEY: z.string().min(1)
925
+ }
926
+ });
927
+ function parseMotifEnv(input = process.env) {
928
+ return motifEnvSchema.parse(input);
929
+ }
930
+ function getFalKeyFromEnv(input = process.env) {
931
+ return parseMotifEnv(input).FAL_KEY;
932
+ }
933
+
934
+ // src/generate.ts
935
+ var GPT_IMAGE_SIZES = ["auto", "1024x1024", "1536x1024", "1024x1536"];
936
+ var FAL_IMAGE_SIZE_PRESETS = [
937
+ "auto",
938
+ "square_hd",
939
+ "square",
940
+ "portrait_4_3",
941
+ "portrait_16_9",
942
+ "landscape_4_3",
943
+ "landscape_16_9"
944
+ ];
945
+ function normalizeImageSize(imageSize, sizeMode) {
946
+ if (imageSize === void 0) {
947
+ return void 0;
948
+ }
949
+ if (typeof imageSize !== "string") {
950
+ if (sizeMode === "gpt_size") {
951
+ throw new Error("GPT Image 1.5 image_size must be auto or a fixed size");
952
+ }
953
+ return imageSize;
954
+ }
955
+ if (sizeMode === "gpt_size") {
956
+ if (!GPT_IMAGE_SIZES.includes(imageSize)) {
957
+ throw new Error(
958
+ `GPT Image 1.5 image_size must be one of ${GPT_IMAGE_SIZES.join(", ")}`
959
+ );
960
+ }
961
+ return imageSize;
962
+ }
963
+ if (FAL_IMAGE_SIZE_PRESETS.includes(imageSize)) {
964
+ return imageSize;
965
+ }
966
+ const match = imageSize.match(/^(\d+)x(\d+)$/);
967
+ if (match) {
968
+ const width = Number(match[1]);
969
+ const height = Number(match[2]);
970
+ return { width, height };
971
+ }
972
+ throw new Error(
973
+ `fal image_size must be one of ${FAL_IMAGE_SIZE_PRESETS.join(", ")} or WIDTHxHEIGHT`
974
+ );
975
+ }
976
+ function unsupported(config, option) {
977
+ throw new Error(`${config.name} does not support ${option}`);
978
+ }
979
+ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
980
+ if (options.editImageUrls?.length && config.maxReferenceImages !== void 0 && options.editImageUrls.length > config.maxReferenceImages) {
981
+ throw new Error(
982
+ `${config.name} supports at most ${config.maxReferenceImages} reference images`
983
+ );
984
+ }
985
+ if (options.editImageUrls?.length && !config.supportsEdit) {
986
+ unsupported(config, "image editing");
987
+ }
988
+ if (options.aspect !== void 0 && sizeMode === "none") {
989
+ unsupported(config, "aspect");
990
+ }
991
+ if (options.resolution !== void 0 && options.resolution !== "2K" && !config.supportsResolution) {
992
+ unsupported(config, "resolution");
993
+ }
994
+ if (options.numImages !== void 0 && options.numImages !== 1 && !config.supportsNumImages) {
995
+ unsupported(config, "numImages");
996
+ }
997
+ if (options.background !== void 0 && !config.supportsBackground) {
998
+ unsupported(config, "background");
999
+ }
1000
+ if (options.transparent && !(config.supportsBackground || config.supportsOutputFormat)) {
1001
+ unsupported(config, "transparent output");
1002
+ }
1003
+ if (options.inputFidelity !== void 0) {
1004
+ if (!hasEditImages) {
1005
+ throw new Error("inputFidelity requires editImageUrls");
1006
+ }
1007
+ if (config.sizeMode !== "gpt_size" && config.name !== "GPT Image 2") {
1008
+ unsupported(config, "inputFidelity");
1009
+ }
1010
+ }
1011
+ if (options.imageSize !== void 0 && sizeMode !== "gpt_size" && sizeMode !== "image_size_enum") {
1012
+ unsupported(config, "imageSize");
1013
+ }
1014
+ if (options.imagePromptStrength !== void 0 && !config.supportsImagePromptStrength) {
1015
+ unsupported(config, "imagePromptStrength");
1016
+ }
1017
+ if (options.imagePromptStrength !== void 0 && !hasEditImages) {
1018
+ throw new Error("imagePromptStrength requires editImageUrls");
1019
+ }
1020
+ if (options.maskImageUrl !== void 0 && !config.supportsMaskImage) {
1021
+ unsupported(config, "maskImageUrl");
1022
+ }
1023
+ if (options.maskImageUrl !== void 0 && !hasEditImages) {
1024
+ throw new Error("maskImageUrl requires editImageUrls");
1025
+ }
1026
+ if (options.seed !== void 0 && !config.supportsSeed) {
1027
+ unsupported(config, "seed");
1028
+ }
1029
+ if (options.outputFormat !== void 0 && !config.supportsOutputFormat) {
1030
+ unsupported(config, "outputFormat");
1031
+ }
1032
+ if (options.quality !== void 0 && !config.supportsQuality) {
1033
+ unsupported(config, "quality");
1034
+ }
1035
+ if (options.negativePrompt !== void 0 && !config.supportsNegativePrompt) {
1036
+ unsupported(config, "negativePrompt");
1037
+ }
1038
+ if (options.style !== void 0 && !config.supportsStyle) {
1039
+ unsupported(config, "style");
1040
+ }
1041
+ if (options.syncMode !== void 0 && !config.supportsSyncMode) {
1042
+ unsupported(config, "syncMode");
1043
+ }
1044
+ if (options.renderingSpeed !== void 0 && !config.supportsRenderingSpeed) {
1045
+ unsupported(config, "renderingSpeed");
1046
+ }
1047
+ if (options.guidanceScale !== void 0 && !config.supportsGuidanceScale) {
1048
+ unsupported(config, "guidanceScale");
1049
+ }
1050
+ if (options.numInferenceSteps !== void 0 && !config.supportsInferenceSteps) {
1051
+ unsupported(config, "numInferenceSteps");
1052
+ }
1053
+ if (options.raw !== void 0 && !config.supportsRaw) {
1054
+ unsupported(config, "raw");
1055
+ }
1056
+ if (options.enhancePrompt !== void 0 && !config.supportsEnhancePrompt) {
1057
+ unsupported(config, "enhancePrompt");
1058
+ }
1059
+ if (options.safetyTolerance !== void 0 && !config.supportsSafetyTolerance) {
1060
+ unsupported(config, "safetyTolerance");
1061
+ }
1062
+ if (options.enableWebSearch !== void 0 && !config.supportsWebSearch) {
1063
+ unsupported(config, "enableWebSearch");
1064
+ }
1065
+ if (options.enableGoogleSearch !== void 0 && !config.supportsGoogleSearch) {
1066
+ unsupported(config, "enableGoogleSearch");
1067
+ }
1068
+ if (options.enableSafetyChecker !== void 0 && !config.supportsSafetyChecker) {
1069
+ unsupported(config, "enableSafetyChecker");
1070
+ }
1071
+ if (options.limitGenerations !== void 0 && !config.supportsLimitGenerations) {
1072
+ unsupported(config, "limitGenerations");
1073
+ }
1074
+ if (options.thinkingLevel !== void 0 && !config.supportsThinkingLevel) {
1075
+ unsupported(config, "thinkingLevel");
1076
+ }
1077
+ if (options.expandPrompt !== void 0 && !config.supportsExpandPrompt) {
1078
+ unsupported(config, "expandPrompt");
1079
+ }
1080
+ }
1081
+ function buildGenerateBody(options) {
1082
+ const {
1083
+ prompt,
1084
+ model,
1085
+ aspect = "1:1",
1086
+ resolution = "2K",
1087
+ numImages = 1,
1088
+ background,
1089
+ editImageUrls,
1090
+ enableGoogleSearch,
1091
+ enableSafetyChecker,
1092
+ transparent,
1093
+ inputFidelity,
1094
+ imageSize,
1095
+ imagePromptStrength,
1096
+ limitGenerations,
1097
+ maskImageUrl,
1098
+ seed,
1099
+ outputFormat,
1100
+ quality,
1101
+ negativePrompt,
1102
+ style,
1103
+ syncMode,
1104
+ thinkingLevel,
1105
+ renderingSpeed,
1106
+ guidanceScale,
1107
+ numInferenceSteps,
1108
+ raw,
1109
+ enhancePrompt,
1110
+ safetyTolerance,
1111
+ enableWebSearch,
1112
+ expandPrompt
1113
+ } = options;
1114
+ const config = MODELS[model];
1115
+ if (!config) {
1116
+ throw new Error(`Unknown model: ${model}`);
1117
+ }
1118
+ let endpoint = config.endpoint;
1119
+ const body = { prompt };
1120
+ const hasEditImages = Boolean(editImageUrls?.length);
1121
+ const editImages = editImageUrls ?? [];
1122
+ const explicitAspect = options.aspect !== void 0;
1123
+ const sizeMode = config.sizeMode ?? "aspect_ratio";
1124
+ validateGenerateOptions(options, config, sizeMode, hasEditImages);
1125
+ const normalizedImageSize = normalizeImageSize(imageSize, sizeMode);
1126
+ switch (sizeMode) {
1127
+ case "gpt_size":
1128
+ body.image_size = normalizedImageSize ?? (hasEditImages && !explicitAspect ? "auto" : aspectToGptSize(aspect));
1129
+ break;
1130
+ case "image_size_enum":
1131
+ body.image_size = normalizedImageSize ?? aspectToFalImageSize(aspect);
1132
+ break;
1133
+ case "aspect_ratio":
1134
+ if (config.supportsAspect) {
1135
+ body.aspect_ratio = aspect;
1136
+ }
1137
+ if (config.supportsResolution) {
1138
+ body.resolution = model === "grok-image" ? resolution.toLowerCase() : resolution;
1139
+ }
1140
+ break;
1141
+ }
1142
+ if (config.supportsQuality) {
1143
+ body.quality = quality ?? "high";
1144
+ }
1145
+ if (config.supportsBackground && background) {
1146
+ body.background = background;
1147
+ }
1148
+ if (config.supportsBackground && transparent) {
1149
+ body.background = "transparent";
1150
+ }
1151
+ if (transparent && config.supportsOutputFormat) {
1152
+ body.output_format = "png";
1153
+ }
1154
+ if (syncMode !== void 0 && config.supportsSyncMode) {
1155
+ body.sync_mode = syncMode;
1156
+ }
1157
+ if (seed !== void 0 && config.supportsSeed) {
1158
+ body.seed = seed;
1159
+ }
1160
+ if (outputFormat && config.supportsOutputFormat && !transparent) {
1161
+ if (config.supportedOutputFormats && !config.supportedOutputFormats.includes(outputFormat)) {
1162
+ throw new Error(
1163
+ `${config.name} supports output formats: ${config.supportedOutputFormats.join(", ")}`
1164
+ );
1165
+ }
1166
+ body.output_format = outputFormat;
1167
+ }
1168
+ if (config.supportsRaw && raw !== void 0) {
1169
+ body.raw = raw;
1170
+ }
1171
+ if (config.supportsEnhancePrompt && enhancePrompt !== void 0) {
1172
+ body.enhance_prompt = enhancePrompt;
1173
+ }
1174
+ if (config.supportsImagePromptStrength && imagePromptStrength !== void 0) {
1175
+ body.image_prompt_strength = imagePromptStrength;
1176
+ }
1177
+ if (model === "flux") {
1178
+ if (safetyTolerance) body.safety_tolerance = safetyTolerance;
1179
+ }
1180
+ if (config.supportsGuidanceScale && guidanceScale !== void 0) {
1181
+ body.guidance_scale = guidanceScale;
1182
+ }
1183
+ if (config.supportsInferenceSteps && numInferenceSteps !== void 0) {
1184
+ body.num_inference_steps = numInferenceSteps;
1185
+ }
1186
+ if (config.supportsSafetyTolerance) {
1187
+ if (safetyTolerance) body.safety_tolerance = safetyTolerance;
1188
+ }
1189
+ if (config.supportsWebSearch) {
1190
+ if (enableWebSearch !== void 0) body.enable_web_search = enableWebSearch;
1191
+ }
1192
+ if (config.supportsGoogleSearch) {
1193
+ if (enableGoogleSearch !== void 0) {
1194
+ body.enable_google_search = enableGoogleSearch;
1195
+ }
1196
+ }
1197
+ if (config.supportsSafetyChecker) {
1198
+ if (enableSafetyChecker !== void 0) {
1199
+ body.enable_safety_checker = enableSafetyChecker;
1200
+ }
1201
+ }
1202
+ if (config.supportsLimitGenerations) {
1203
+ if (limitGenerations !== void 0) {
1204
+ body.limit_generations = limitGenerations;
1205
+ }
1206
+ }
1207
+ if (config.supportsThinkingLevel) {
1208
+ if (thinkingLevel) body.thinking_level = thinkingLevel;
1209
+ }
1210
+ if (config.supportsStyle && style) {
1211
+ body.style = style;
1212
+ }
1213
+ if (config.supportsNegativePrompt) {
1214
+ if (negativePrompt) body.negative_prompt = negativePrompt;
1215
+ }
1216
+ if (model === "ideogram") {
1217
+ if (style) body.style = style;
1218
+ }
1219
+ if (config.supportsRenderingSpeed) {
1220
+ if (renderingSpeed) body.rendering_speed = renderingSpeed;
1221
+ }
1222
+ if (config.supportsExpandPrompt) {
1223
+ if (expandPrompt !== void 0) body.expand_prompt = expandPrompt;
1224
+ }
1225
+ if (model === "flux" && hasEditImages) {
1226
+ body.image_url = editImages[0];
1227
+ body.image_prompt_strength = imagePromptStrength ?? 0.5;
1228
+ }
1229
+ if (config.supportsNumImages) {
1230
+ body.num_images = numImages;
1231
+ }
1232
+ if (hasEditImages && sizeMode !== "gpt_size" && model !== "flux") {
1233
+ if (!config.supportsEdit) {
1234
+ throw new Error(`Model ${model} does not support image editing`);
1235
+ }
1236
+ endpoint = config.editEndpoint ?? `${endpoint}/edit`;
1237
+ const editImagesField = config.editImagesField ?? "image_urls";
1238
+ body[editImagesField] = editImagesField === "image_url" ? editImages[0] : editImages;
1239
+ if (model === "gpt2" && inputFidelity) {
1240
+ body.input_fidelity = inputFidelity;
1241
+ }
1242
+ }
1243
+ if (hasEditImages && sizeMode === "gpt_size") {
1244
+ endpoint = config.editEndpoint ?? `${endpoint}/edit`;
1245
+ body.image_urls = editImages;
1246
+ if (inputFidelity) {
1247
+ body.input_fidelity = inputFidelity;
1248
+ }
1249
+ }
1250
+ if (maskImageUrl && config.supportsMaskImage) {
1251
+ body.mask_image_url = maskImageUrl;
1252
+ }
1253
+ return { endpoint, body };
1254
+ }
1255
+
1256
+ // src/leaderboards.ts
1257
+ var IMAGE_TEXT_TO_IMAGE_TOP_20 = {
1258
+ capturedAt: "2026-05-12",
1259
+ source: "https://artificialanalysis.ai/image/leaderboard/text-to-image",
1260
+ entries: [
1261
+ {
1262
+ rank: 1,
1263
+ creator: "OpenAI",
1264
+ model: "GPT Image 2 (high)",
1265
+ elo: 1337,
1266
+ apiPricing: "$211/1k images",
1267
+ isCurrent: true,
1268
+ routableModelId: "gpt2"
1269
+ },
1270
+ {
1271
+ rank: 2,
1272
+ creator: "OpenAI",
1273
+ model: "GPT Image 1.5 (high)",
1274
+ elo: 1268,
1275
+ apiPricing: "$133/1k images",
1276
+ isCurrent: true,
1277
+ routableModelId: "gpt"
1278
+ },
1279
+ {
1280
+ rank: 3,
1281
+ creator: "Google",
1282
+ model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1283
+ elo: 1263,
1284
+ apiPricing: "$67/1k images",
1285
+ isCurrent: true,
1286
+ routableModelId: "banana2"
1287
+ },
1288
+ {
1289
+ rank: 4,
1290
+ creator: "Sourceful",
1291
+ model: "Riverflow 2.0",
1292
+ elo: 1256,
1293
+ apiPricing: "$150/1k images",
1294
+ isCurrent: true
1295
+ },
1296
+ {
1297
+ rank: 5,
1298
+ creator: "Google",
1299
+ model: "Nano Banana Pro (Gemini 3 Pro Image)",
1300
+ elo: 1220,
1301
+ apiPricing: "$134/1k images",
1302
+ isCurrent: true,
1303
+ routableModelId: "banana"
1304
+ },
1305
+ {
1306
+ rank: 6,
1307
+ creator: "ByteDance Seed",
1308
+ model: "Seedream 4.0",
1309
+ elo: 1198,
1310
+ apiPricing: "$30/1k images",
1311
+ isCurrent: true,
1312
+ routableModelId: "seedream4"
1313
+ },
1314
+ {
1315
+ rank: 7,
1316
+ creator: "Microsoft",
1317
+ model: "MAI-Image-2",
1318
+ elo: 1198,
1319
+ apiPricing: "$50/1k images",
1320
+ isCurrent: true
1321
+ },
1322
+ {
1323
+ rank: 8,
1324
+ creator: "Black Forest Labs",
1325
+ model: "FLUX.2 [max]",
1326
+ elo: 1197,
1327
+ apiPricing: "$70/1k images",
1328
+ isCurrent: true,
1329
+ routableModelId: "flux2-max"
1330
+ },
1331
+ {
1332
+ rank: 9,
1333
+ creator: "Peanut",
1334
+ model: "Peanut (Open Weights Coming Soon)",
1335
+ elo: 1187,
1336
+ apiPricing: "Unavailable",
1337
+ isCurrent: true
1338
+ },
1339
+ {
1340
+ rank: 10,
1341
+ creator: "Black Forest Labs",
1342
+ model: "FLUX.2 [pro]",
1343
+ elo: 1186,
1344
+ apiPricing: "$30/1k images",
1345
+ isCurrent: true,
1346
+ routableModelId: "flux2-pro"
1347
+ },
1348
+ {
1349
+ rank: 11,
1350
+ creator: "Google",
1351
+ model: "Imagen 4 Ultra Preview 0606",
1352
+ elo: 1184,
1353
+ apiPricing: "$60/1k images",
1354
+ isCurrent: false
1355
+ },
1356
+ {
1357
+ rank: 12,
1358
+ creator: "xAI",
1359
+ model: "grok-imagine-image",
1360
+ elo: 1182,
1361
+ apiPricing: "$20/1k images",
1362
+ isCurrent: true,
1363
+ routableModelId: "grok-image"
1364
+ },
1365
+ {
1366
+ rank: 13,
1367
+ creator: "Black Forest Labs",
1368
+ model: "FLUX.2 [flex]",
1369
+ elo: 1182,
1370
+ apiPricing: "$60/1k images",
1371
+ isCurrent: true,
1372
+ routableModelId: "flux2-flex"
1373
+ },
1374
+ {
1375
+ rank: 14,
1376
+ creator: "ImagineArt",
1377
+ model: "ImagineArt 2.0",
1378
+ elo: 1181,
1379
+ apiPricing: "$30/1k images",
1380
+ isCurrent: true
1381
+ },
1382
+ {
1383
+ rank: 15,
1384
+ creator: "Google",
1385
+ model: "Imagen 4 Ultra",
1386
+ elo: 1171,
1387
+ apiPricing: "$60/1k images",
1388
+ isCurrent: true
1389
+ },
1390
+ {
1391
+ rank: 16,
1392
+ creator: "Google",
1393
+ model: "Imagen 4 Preview 0606",
1394
+ elo: 1169,
1395
+ apiPricing: "$40/1k images",
1396
+ isCurrent: false
1397
+ },
1398
+ {
1399
+ rank: 17,
1400
+ creator: "ByteDance Seed",
1401
+ model: "Seedream 4.5",
1402
+ elo: 1167,
1403
+ apiPricing: "$40/1k images",
1404
+ isCurrent: true,
1405
+ routableModelId: "seedream45"
1406
+ },
1407
+ {
1408
+ rank: 18,
1409
+ creator: "Fal",
1410
+ model: "FLUX.2 [dev] Turbo",
1411
+ elo: 1161,
1412
+ apiPricing: "$8/1k images",
1413
+ isCurrent: true
1414
+ },
1415
+ {
1416
+ rank: 19,
1417
+ creator: "Black Forest Labs",
1418
+ model: "FLUX.2 [dev]",
1419
+ elo: 1160,
1420
+ apiPricing: "$12/1k images",
1421
+ isCurrent: true,
1422
+ routableModelId: "flux2-dev"
1423
+ },
1424
+ {
1425
+ rank: 20,
1426
+ creator: "Alibaba",
1427
+ model: "Qwen Image Max 2512",
1428
+ elo: 1158,
1429
+ apiPricing: "$20/1k images",
1430
+ isCurrent: true,
1431
+ routableModelId: "qwen"
1432
+ }
1433
+ ]
1434
+ };
1435
+ var IMAGE_EDITING_TOP_20 = {
1436
+ capturedAt: "2026-05-12",
1437
+ source: "https://artificialanalysis.ai/image/leaderboard/editing",
1438
+ entries: [
1439
+ {
1440
+ rank: 1,
1441
+ creator: "Sourceful",
1442
+ model: "Riverflow 2.0",
1443
+ elo: 1286,
1444
+ apiPricing: "$150/1k images",
1445
+ isCurrent: true
1446
+ },
1447
+ {
1448
+ rank: 2,
1449
+ creator: "OpenAI",
1450
+ model: "GPT Image 1.5 (high)",
1451
+ elo: 1262,
1452
+ apiPricing: "$133/1k images",
1453
+ isCurrent: true,
1454
+ routableModelId: "gpt"
1455
+ },
1456
+ {
1457
+ rank: 3,
1458
+ creator: "OpenAI",
1459
+ model: "GPT Image 2 (high)",
1460
+ elo: 1249,
1461
+ apiPricing: "$211/1k images",
1462
+ isCurrent: true,
1463
+ routableModelId: "gpt2"
1464
+ },
1465
+ {
1466
+ rank: 4,
1467
+ creator: "Google",
1468
+ model: "Nano Banana Pro (Gemini 3 Pro Image)",
1469
+ elo: 1241,
1470
+ apiPricing: "$134/1k images",
1471
+ isCurrent: true,
1472
+ routableModelId: "banana"
1473
+ },
1474
+ {
1475
+ rank: 5,
1476
+ creator: "Google",
1477
+ model: "Nano Banana 2 (Gemini 3.1 Flash Image Preview)",
1478
+ elo: 1231,
1479
+ apiPricing: "$67/1k images",
1480
+ isCurrent: true,
1481
+ routableModelId: "banana2"
1482
+ },
1483
+ {
1484
+ rank: 6,
1485
+ creator: "Tencent",
1486
+ model: "HunyuanImage 3.0 Instruct (Fal)",
1487
+ elo: 1222,
1488
+ apiPricing: "$90/1k images",
1489
+ isCurrent: true
1490
+ },
1491
+ {
1492
+ rank: 7,
1493
+ creator: "xAI",
1494
+ model: "grok-imagine-image",
1495
+ elo: 1213,
1496
+ apiPricing: "$20/1k images",
1497
+ isCurrent: true,
1498
+ routableModelId: "grok-image"
1499
+ },
1500
+ {
1501
+ rank: 8,
1502
+ creator: "xAI",
1503
+ model: "grok-imagine-image-pro",
1504
+ elo: 1212,
1505
+ apiPricing: "$70/1k images",
1506
+ isCurrent: true
1507
+ },
1508
+ {
1509
+ rank: 9,
1510
+ creator: "KlingAI",
1511
+ model: "Kling Image 3.0 Omni",
1512
+ elo: 1207,
1513
+ apiPricing: "Unavailable",
1514
+ isCurrent: true
1515
+ },
1516
+ {
1517
+ rank: 10,
1518
+ creator: "Black Forest Labs",
1519
+ model: "FLUX.2 [max]",
1520
+ elo: 1206,
1521
+ apiPricing: "$140/1k images",
1522
+ isCurrent: true,
1523
+ routableModelId: "flux2-max"
1524
+ },
1525
+ {
1526
+ rank: 11,
1527
+ creator: "Alibaba",
1528
+ model: "Wan 2.7 Pro",
1529
+ elo: 1200,
1530
+ apiPricing: "Unavailable",
1531
+ isCurrent: true
1532
+ },
1533
+ {
1534
+ rank: 12,
1535
+ creator: "KlingAI",
1536
+ model: "Kling Image 3.0",
1537
+ elo: 1196,
1538
+ apiPricing: "$28/1k images",
1539
+ isCurrent: true
1540
+ },
1541
+ {
1542
+ rank: 13,
1543
+ creator: "KlingAI",
1544
+ model: "Kling Image O1",
1545
+ elo: 1193,
1546
+ apiPricing: "$28/1k images",
1547
+ isCurrent: true
1548
+ },
1549
+ {
1550
+ rank: 14,
1551
+ creator: "Alibaba",
1552
+ model: "Wan 2.6 Image",
1553
+ elo: 1188,
1554
+ apiPricing: "$30/1k images",
1555
+ isCurrent: true
1556
+ },
1557
+ {
1558
+ rank: 15,
1559
+ creator: "Sourceful",
1560
+ model: "Riverflow 1",
1561
+ elo: 1184,
1562
+ apiPricing: "$66/1k images",
1563
+ isCurrent: true
1564
+ },
1565
+ {
1566
+ rank: 16,
1567
+ creator: "ByteDance Seed",
1568
+ model: "Seedream 4.0",
1569
+ elo: 1184,
1570
+ apiPricing: "$30/1k images",
1571
+ isCurrent: true,
1572
+ routableModelId: "seedream4"
1573
+ },
1574
+ {
1575
+ rank: 17,
1576
+ creator: "ByteDance Seed",
1577
+ model: "Seedream 4.5",
1578
+ elo: 1184,
1579
+ apiPricing: "$40/1k images",
1580
+ isCurrent: true,
1581
+ routableModelId: "seedream45"
1582
+ },
1583
+ {
1584
+ rank: 18,
1585
+ creator: "Alibaba",
1586
+ model: "Wan 2.7",
1587
+ elo: 1181,
1588
+ apiPricing: "$26/1k images",
1589
+ isCurrent: true
1590
+ },
1591
+ {
1592
+ rank: 19,
1593
+ creator: "Google",
1594
+ model: "Nano Banana (Gemini 2.5 Flash Image)",
1595
+ elo: 1173,
1596
+ apiPricing: "$39/1k images",
1597
+ isCurrent: true,
1598
+ routableModelId: "gemini"
1599
+ },
1600
+ {
1601
+ rank: 20,
1602
+ creator: "Reve",
1603
+ model: "Reve V1 (December)",
1604
+ elo: 1172,
1605
+ apiPricing: "$40/1k images",
1606
+ isCurrent: true
1607
+ }
1608
+ ]
1609
+ };
1610
+ var VIDEO_TEXT_TO_VIDEO_TOP_15 = {
1611
+ capturedAt: "2026-05-12",
1612
+ source: "https://artificialanalysis.ai/video/leaderboard/text-to-video",
1613
+ entries: [
1614
+ {
1615
+ rank: 1,
1616
+ creator: "Alibaba-ATH",
1617
+ model: "HappyHorse-1.0",
1618
+ elo: 1354,
1619
+ apiPricing: "$14.40/min"
1620
+ },
1621
+ {
1622
+ rank: 2,
1623
+ creator: "ByteDance Seed",
1624
+ model: "Dreamina Seedance 2.0 720p",
1625
+ elo: 1273,
1626
+ apiPricing: "No API available"
1627
+ },
1628
+ {
1629
+ rank: 3,
1630
+ creator: "KlingAI",
1631
+ model: "Kling 3.0 1080p (Pro)",
1632
+ elo: 1249,
1633
+ apiPricing: "$13.44/min"
1634
+ },
1635
+ {
1636
+ rank: 4,
1637
+ creator: "KlingAI",
1638
+ model: "Kling 3.0 Omni 1080p (Pro)",
1639
+ elo: 1233,
1640
+ apiPricing: "$13.44/min"
1641
+ },
1642
+ {
1643
+ rank: 5,
1644
+ creator: "xAI",
1645
+ model: "grok-imagine-video",
1646
+ elo: 1233,
1647
+ apiPricing: "$4.20/min"
1648
+ },
1649
+ {
1650
+ rank: 6,
1651
+ creator: "Vidu",
1652
+ model: "Vidu Q3 Pro",
1653
+ elo: 1225,
1654
+ apiPricing: "$9.60/min"
1655
+ },
1656
+ {
1657
+ rank: 7,
1658
+ creator: "Video Rebirth",
1659
+ model: "Bach-1.0 Preview",
1660
+ elo: 1224,
1661
+ apiPricing: "$3.00/min"
1662
+ },
1663
+ {
1664
+ rank: 8,
1665
+ creator: "KlingAI",
1666
+ model: "Kling 3.0 Omni 720p (Standard)",
1667
+ elo: 1224,
1668
+ apiPricing: "$10.08/min"
1669
+ },
1670
+ {
1671
+ rank: 9,
1672
+ creator: "PixVerse",
1673
+ model: "PixVerse V6",
1674
+ elo: 1222,
1675
+ apiPricing: "$5.40/min"
1676
+ },
1677
+ {
1678
+ rank: 10,
1679
+ creator: "PixVerse",
1680
+ model: "PixVerse V5.6",
1681
+ elo: 1221,
1682
+ apiPricing: "$9.00/min"
1683
+ },
1684
+ {
1685
+ rank: 11,
1686
+ creator: "Runway",
1687
+ model: "Runway Gen-4.5",
1688
+ elo: 1220,
1689
+ apiPricing: "No API available"
1690
+ },
1691
+ {
1692
+ rank: 12,
1693
+ creator: "Google",
1694
+ model: "Veo 3",
1695
+ elo: 1218,
1696
+ apiPricing: "$12.00/min"
1697
+ },
1698
+ {
1699
+ rank: 13,
1700
+ creator: "KlingAI",
1701
+ model: "Kling 3.0 720p (Standard)",
1702
+ elo: 1215,
1703
+ apiPricing: "$10.08/min"
1704
+ },
1705
+ {
1706
+ rank: 14,
1707
+ creator: "Google",
1708
+ model: "Veo 3.1 Lite",
1709
+ elo: 1214,
1710
+ apiPricing: "$3.00/min"
1711
+ },
1712
+ {
1713
+ rank: 15,
1714
+ creator: "KlingAI",
1715
+ model: "Kling O1 Pro (January)",
1716
+ elo: 1209,
1717
+ apiPricing: "$10.08/min"
1718
+ }
1719
+ ]
1720
+ };
1721
+ var VIDEO_IMAGE_TO_VIDEO_TOP_15 = {
1722
+ capturedAt: "2026-05-12",
1723
+ source: "https://artificialanalysis.ai/video/leaderboard/image-to-video",
1724
+ entries: [
1725
+ {
1726
+ rank: 1,
1727
+ creator: "Alibaba-ATH",
1728
+ model: "HappyHorse-1.0",
1729
+ elo: 1395,
1730
+ apiPricing: "Coming soon"
1731
+ },
1732
+ {
1733
+ rank: 2,
1734
+ creator: "ByteDance Seed",
1735
+ model: "Dreamina Seedance 2.0 720p",
1736
+ elo: 1348,
1737
+ apiPricing: "No API available"
1738
+ },
1739
+ {
1740
+ rank: 3,
1741
+ creator: "xAI",
1742
+ model: "grok-imagine-video",
1743
+ elo: 1326,
1744
+ apiPricing: "$4.20/min"
1745
+ },
1746
+ {
1747
+ rank: 4,
1748
+ creator: "PixVerse",
1749
+ model: "PixVerse V6",
1750
+ elo: 1322,
1751
+ apiPricing: "$5.40/min"
1752
+ },
1753
+ {
1754
+ rank: 5,
1755
+ creator: "Vidu",
1756
+ model: "Vidu Q3 Pro",
1757
+ elo: 1287,
1758
+ apiPricing: "$9.60/min"
1759
+ },
1760
+ {
1761
+ rank: 6,
1762
+ creator: "KlingAI",
1763
+ model: "Kling 2.5 Turbo 1080p",
1764
+ elo: 1283,
1765
+ apiPricing: "$4.20/min"
1766
+ },
1767
+ {
1768
+ rank: 7,
1769
+ creator: "KlingAI",
1770
+ model: "Kling 3.0 1080p (Pro)",
1771
+ elo: 1280,
1772
+ apiPricing: "$13.44/min"
1773
+ },
1774
+ {
1775
+ rank: 8,
1776
+ creator: "PixVerse",
1777
+ model: "PixVerse V5.6",
1778
+ elo: 1279,
1779
+ apiPricing: "$9.00/min"
1780
+ },
1781
+ {
1782
+ rank: 9,
1783
+ creator: "KlingAI",
1784
+ model: "Kling 3.0 Omni 1080p (Pro)",
1785
+ elo: 1277,
1786
+ apiPricing: "$13.44/min"
1787
+ },
1788
+ {
1789
+ rank: 10,
1790
+ creator: "KlingAI",
1791
+ model: "Kling 2.6 Standard (January)",
1792
+ elo: 1271,
1793
+ apiPricing: "Coming soon"
1794
+ },
1795
+ {
1796
+ rank: 11,
1797
+ creator: "PixVerse",
1798
+ model: "PixVerse V5.5",
1799
+ elo: 1271,
1800
+ apiPricing: "$6.40/min"
1801
+ },
1802
+ {
1803
+ rank: 12,
1804
+ creator: "Google",
1805
+ model: "Veo 3.1 Fast",
1806
+ elo: 1268,
1807
+ apiPricing: "$6.00/min"
1808
+ },
1809
+ {
1810
+ rank: 13,
1811
+ creator: "Runway",
1812
+ model: "Runway Gen-4.5",
1813
+ elo: 1263,
1814
+ apiPricing: "No API available"
1815
+ },
1816
+ {
1817
+ rank: 14,
1818
+ creator: "KlingAI",
1819
+ model: "Kling 3.0 Omni 720p (Standard)",
1820
+ elo: 1263,
1821
+ apiPricing: "$10.08/min"
1822
+ },
1823
+ {
1824
+ rank: 15,
1825
+ creator: "KlingAI",
1826
+ model: "Kling 3.0 720p (Standard)",
1827
+ elo: 1263,
1828
+ apiPricing: "$10.08/min"
1829
+ }
1830
+ ]
1831
+ };
1832
+
1833
+ // src/server.ts
1834
+ import { err, ok } from "neverthrow";
1835
+
1836
+ // src/tools.ts
1837
+ var FAL_EXPLORE_CHECKED_AT = "2026-05-12";
1838
+ var FAL_TOOLS = {
1839
+ nsfw: {
1840
+ name: "NSFW Checker",
1841
+ endpoint: "fal-ai/x-ailab/nsfw",
1842
+ task: "vision moderation",
1843
+ category: "moderation",
1844
+ description: "Predict whether one or more images contain NSFW concepts.",
1845
+ inputKind: "images",
1846
+ inputField: "image_urls",
1847
+ outputKeys: ["has_nsfw_concepts"],
1848
+ pricing: "$0.001/image",
1849
+ sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw"
1850
+ },
1851
+ "topaz-image": {
1852
+ name: "Topaz Image Upscale",
1853
+ endpoint: "fal-ai/topaz/upscale/image",
1854
+ task: "image enhancement",
1855
+ category: "upscale",
1856
+ description: "Professional Topaz image enhancement and upscaling.",
1857
+ inputKind: "image",
1858
+ inputField: "image_url",
1859
+ outputKeys: ["image"],
1860
+ pricing: "$0.08+ by output megapixels",
1861
+ sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/image",
1862
+ defaultOptions: {
1863
+ model: "Standard V2",
1864
+ output_format: "jpeg",
1865
+ upscale_factor: 2
1866
+ }
1867
+ },
1868
+ "topaz-video": {
1869
+ name: "Topaz Video Upscale",
1870
+ endpoint: "fal-ai/topaz/upscale/video",
1871
+ task: "video enhancement",
1872
+ category: "upscale",
1873
+ description: "Professional Topaz video enhancement and upscaling.",
1874
+ inputKind: "video",
1875
+ inputField: "video_url",
1876
+ outputKeys: ["video"],
1877
+ pricing: "$0.01-$0.08/sec by output resolution",
1878
+ sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
1879
+ defaultOptions: {
1880
+ model: "Proteus",
1881
+ upscale_factor: 2
1882
+ }
1883
+ },
1884
+ "bria-rmbg": {
1885
+ name: "Bria RMBG 2.0",
1886
+ endpoint: "fal-ai/bria/background/remove",
1887
+ task: "image background removal",
1888
+ category: "background",
1889
+ description: "Commercial-safe background removal for images.",
1890
+ inputKind: "image",
1891
+ inputField: "image_url",
1892
+ outputKeys: ["image"],
1893
+ pricing: "$0.02/image",
1894
+ sourceUrl: "https://fal.ai/models/fal-ai/bria/background/remove"
1895
+ },
1896
+ birefnet: {
1897
+ name: "BirefNet Background Removal",
1898
+ endpoint: "fal-ai/birefnet/v2",
1899
+ task: "image background removal",
1900
+ category: "background",
1901
+ description: "High-resolution dichotomous image segmentation and background removal.",
1902
+ inputKind: "image",
1903
+ inputField: "image_url",
1904
+ outputKeys: ["image", "mask_image"],
1905
+ pricing: "$0/compute-second listed by fal",
1906
+ sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
1907
+ defaultOptions: {
1908
+ model: "General Use (Light)",
1909
+ operating_resolution: "1024x1024",
1910
+ output_format: "png",
1911
+ refine_foreground: true
1912
+ }
1913
+ },
1914
+ rembg: {
1915
+ name: "Remove Background",
1916
+ endpoint: "fal-ai/imageutils/rembg",
1917
+ task: "image background removal",
1918
+ category: "background",
1919
+ description: "Generic image background removal utility.",
1920
+ inputKind: "image",
1921
+ inputField: "image_url",
1922
+ outputKeys: ["image"],
1923
+ pricing: "$0/compute-second listed by fal",
1924
+ sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
1925
+ defaultOptions: {
1926
+ crop_to_bbox: false
1927
+ }
1928
+ },
1929
+ "bria-video-rmbg": {
1930
+ name: "Bria Video Background Removal",
1931
+ endpoint: "bria/video/background-removal",
1932
+ task: "video background removal",
1933
+ category: "background",
1934
+ description: "Remove video backgrounds with configurable output container.",
1935
+ inputKind: "video",
1936
+ inputField: "video_url",
1937
+ outputKeys: ["video"],
1938
+ pricing: "$0.14/sec",
1939
+ sourceUrl: "https://fal.ai/models/bria/video/background-removal",
1940
+ defaultOptions: {
1941
+ background_color: "Black",
1942
+ output_container_and_codec: "webm_vp9",
1943
+ preserve_audio: true
1944
+ }
1945
+ },
1946
+ lineart: {
1947
+ name: "Line Art Preprocessor",
1948
+ endpoint: "fal-ai/image-preprocessors/lineart",
1949
+ task: "image preprocessing",
1950
+ category: "preprocess",
1951
+ description: "Generate line art/control-style edges from an input image.",
1952
+ inputKind: "image",
1953
+ inputField: "image_url",
1954
+ outputKeys: ["image"],
1955
+ pricing: "$0/compute-second listed by fal",
1956
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/lineart",
1957
+ defaultOptions: {
1958
+ coarse: false
1959
+ }
1960
+ },
1961
+ "sam-preprocessor": {
1962
+ name: "SAM Preprocessor",
1963
+ endpoint: "fal-ai/image-preprocessors/sam",
1964
+ task: "segmentation preprocessing",
1965
+ category: "preprocess",
1966
+ description: "Generate a SAM segmentation map for ControlNet-style workflows.",
1967
+ inputKind: "image",
1968
+ inputField: "image_url",
1969
+ outputKeys: ["image"],
1970
+ pricing: "$0/compute-second listed by fal",
1971
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/sam"
1972
+ },
1973
+ "midas-depth": {
1974
+ name: "MiDaS Depth Estimation",
1975
+ endpoint: "fal-ai/imageutils/depth",
1976
+ task: "depth map",
1977
+ category: "depth",
1978
+ description: "Create MiDaS depth maps from input images.",
1979
+ inputKind: "image",
1980
+ inputField: "image_url",
1981
+ outputKeys: ["image"],
1982
+ pricing: "$0/compute-second listed by fal",
1983
+ sourceUrl: "https://fal.ai/models/fal-ai/imageutils/depth",
1984
+ defaultOptions: {
1985
+ a: Math.PI * 2,
1986
+ bg_th: 0.1
1987
+ }
1988
+ },
1989
+ "midas-preprocessor": {
1990
+ name: "MiDaS Preprocessor",
1991
+ endpoint: "fal-ai/image-preprocessors/midas",
1992
+ task: "depth and normal preprocessing",
1993
+ category: "preprocess",
1994
+ description: "Generate MiDaS depth and normal maps for image workflows.",
1995
+ inputKind: "image",
1996
+ inputField: "image_url",
1997
+ outputKeys: ["depth_map", "normal_map"],
1998
+ pricing: "$0/compute-second listed by fal",
1999
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/midas"
2000
+ },
2001
+ "marigold-depth": {
2002
+ name: "Marigold Depth Estimation",
2003
+ endpoint: "fal-ai/imageutils/marigold-depth",
2004
+ task: "depth map",
2005
+ category: "depth",
2006
+ description: "Create depth maps using Marigold depth estimation.",
2007
+ inputKind: "image",
2008
+ inputField: "image_url",
2009
+ outputKeys: ["image"],
2010
+ pricing: "$0/compute-second listed by fal",
2011
+ sourceUrl: "https://fal.ai/models/fal-ai/imageutils/marigold-depth",
2012
+ defaultOptions: {
2013
+ ensemble_size: 10,
2014
+ num_inference_steps: 10
2015
+ }
2016
+ },
2017
+ "depth-anything": {
2018
+ name: "Depth Anything v2 Preprocessor",
2019
+ endpoint: "fal-ai/image-preprocessors/depth-anything/v2",
2020
+ task: "depth preprocessing",
2021
+ category: "preprocess",
2022
+ description: "Generate Depth Anything v2 depth maps from input images.",
2023
+ inputKind: "image",
2024
+ inputField: "image_url",
2025
+ outputKeys: ["image"],
2026
+ pricing: "$0/compute-second listed by fal",
2027
+ sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/depth-anything/v2"
2028
+ },
2029
+ "sam2-auto": {
2030
+ name: "SAM 2 Auto Segment",
2031
+ endpoint: "fal-ai/sam2/auto-segment",
2032
+ task: "automatic image segmentation",
2033
+ category: "segmentation",
2034
+ description: "Automatically segment an image into combined and individual masks.",
2035
+ inputKind: "image",
2036
+ inputField: "image_url",
2037
+ outputKeys: ["combined_mask", "individual_masks"],
2038
+ pricing: "$0/compute-second listed by fal",
2039
+ sourceUrl: "https://fal.ai/models/fal-ai/sam2/auto-segment",
2040
+ defaultOptions: {
2041
+ min_mask_region_area: 100,
2042
+ output_format: "png",
2043
+ points_per_side: 32,
2044
+ pred_iou_thresh: 0.88,
2045
+ stability_score_thresh: 0.95
2046
+ }
2047
+ },
2048
+ "sam3-image": {
2049
+ name: "SAM 3 Image",
2050
+ endpoint: "fal-ai/sam-3/image",
2051
+ task: "promptable image segmentation",
2052
+ category: "segmentation",
2053
+ description: "Segment image objects with text, point, or box prompts.",
2054
+ inputKind: "image",
2055
+ inputField: "image_url",
2056
+ outputKeys: ["image", "masks", "metadata", "scores", "boxes"],
2057
+ pricing: "$0.005/request",
2058
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/image",
2059
+ defaultOptions: {
2060
+ apply_mask: true,
2061
+ max_masks: 3,
2062
+ output_format: "png"
2063
+ }
2064
+ },
2065
+ "sam3-image-rle": {
2066
+ name: "SAM 3 Image RLE",
2067
+ endpoint: "fal-ai/sam-3/image-rle",
2068
+ task: "promptable image segmentation to RLE",
2069
+ category: "segmentation",
2070
+ description: "Segment image objects and return run-length encoded masks.",
2071
+ inputKind: "image",
2072
+ inputField: "image_url",
2073
+ outputKeys: ["rle_masks", "metadata", "scores", "boxes"],
2074
+ pricing: "$0.005/request",
2075
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/image-rle",
2076
+ defaultOptions: {
2077
+ apply_mask: true,
2078
+ max_masks: 3
2079
+ }
2080
+ },
2081
+ "sam3-video": {
2082
+ name: "SAM 3 Video",
2083
+ endpoint: "fal-ai/sam-3/video",
2084
+ task: "promptable video segmentation",
2085
+ category: "segmentation",
2086
+ description: "Segment and track prompted objects across video frames.",
2087
+ inputKind: "video",
2088
+ inputField: "video_url",
2089
+ outputKeys: ["video", "boundingbox_frames_zip"],
2090
+ pricing: "$0.005/16 frames",
2091
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video",
2092
+ defaultOptions: {
2093
+ apply_mask: true,
2094
+ detection_threshold: 0.5,
2095
+ prompt: "person",
2096
+ video_output_type: "X264 (.mp4)"
2097
+ }
2098
+ },
2099
+ "sam3-video-rle": {
2100
+ name: "SAM 3 Video RLE",
2101
+ endpoint: "fal-ai/sam-3/video-rle",
2102
+ task: "promptable video segmentation to RLE",
2103
+ category: "segmentation",
2104
+ description: "Track prompted video objects and return RLE mask data.",
2105
+ inputKind: "video",
2106
+ inputField: "video_url",
2107
+ outputKeys: ["rle_masks", "metadata"],
2108
+ pricing: "$0.005/16 frames",
2109
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video-rle",
2110
+ defaultOptions: {
2111
+ apply_mask: true,
2112
+ detection_threshold: 0.5,
2113
+ prompt: "person"
2114
+ }
2115
+ },
2116
+ "sam3-1-video": {
2117
+ name: "SAM 3.1 Video",
2118
+ endpoint: "fal-ai/sam-3-1/video",
2119
+ task: "multi-object video segmentation",
2120
+ category: "segmentation",
2121
+ description: "SAM 3.1 video segmentation with Object Multiplex tracking for multiple objects.",
2122
+ inputKind: "video",
2123
+ inputField: "video_url",
2124
+ outputKeys: ["video", "boundingbox_frames_zip"],
2125
+ pricing: "fal pricing varies by frame count",
2126
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/video",
2127
+ defaultOptions: {
2128
+ apply_mask: true,
2129
+ prompt: "person"
2130
+ }
2131
+ },
2132
+ "sam3-3d-objects": {
2133
+ name: "SAM 3D Objects",
2134
+ endpoint: "fal-ai/sam-3/3d-objects",
2135
+ task: "single-image 3D object reconstruction",
2136
+ category: "3d",
2137
+ description: "Reconstruct one or more 3D objects from an image and prompts.",
2138
+ inputKind: "image",
2139
+ inputField: "image_url",
2140
+ outputKeys: [
2141
+ "gaussian_splat",
2142
+ "model_glb",
2143
+ "metadata",
2144
+ "individual_splats",
2145
+ "individual_glbs",
2146
+ "artifacts_zip"
2147
+ ],
2148
+ pricing: "$0.02/generation",
2149
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-objects",
2150
+ defaultOptions: {
2151
+ prompt: "car"
2152
+ }
2153
+ },
2154
+ "sam3-3d-body": {
2155
+ name: "SAM 3D Body",
2156
+ endpoint: "fal-ai/sam-3/3d-body",
2157
+ task: "single-image 3D body reconstruction",
2158
+ category: "3d",
2159
+ description: "Reconstruct human body meshes and keypoints from a single image.",
2160
+ inputKind: "image",
2161
+ inputField: "image_url",
2162
+ outputKeys: ["model_glb", "visualization", "meshes", "metadata"],
2163
+ pricing: "$0.015/inference",
2164
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-body",
2165
+ defaultOptions: {
2166
+ export_meshes: true,
2167
+ include_3d_keypoints: true,
2168
+ include_mhr_params: true
2169
+ }
2170
+ },
2171
+ "sam3-3d-align": {
2172
+ name: "SAM 3D Align",
2173
+ endpoint: "fal-ai/sam-3/3d-align",
2174
+ task: "3D scene alignment",
2175
+ category: "3d",
2176
+ description: "Align SAM 3D objects and bodies into a shared scene.",
2177
+ inputKind: "image",
2178
+ inputField: "image_url",
2179
+ outputKeys: ["scene_glb", "metadata", "artifacts_zip"],
2180
+ pricing: "fal pricing varies by scene",
2181
+ sourceUrl: "https://fal.ai/models/fal-ai/sam-3/3d-align"
2182
+ }
2183
+ };
2184
+ var FAL_TOOLS_CHECKED_AT = FAL_EXPLORE_CHECKED_AT;
2185
+ var FAL_TOOL_IDS = Object.keys(FAL_TOOLS);
2186
+ function isFalToolId(tool) {
2187
+ return Object.hasOwn(FAL_TOOLS, tool);
2188
+ }
2189
+ function buildFalToolRequest(options) {
2190
+ if (!isFalToolId(options.tool)) {
2191
+ throw new Error(`Unknown fal tool: ${options.tool}`);
2192
+ }
2193
+ const tool = FAL_TOOLS[options.tool];
2194
+ const values = options.inputs ?? (options.input ? [options.input] : []);
2195
+ if (values.length === 0) {
2196
+ throw new Error(`${options.tool} requires input media`);
2197
+ }
2198
+ const mediaValue = tool.inputKind === "images" ? values : values[0];
2199
+ const defaultOptions = "defaultOptions" in tool ? tool.defaultOptions : void 0;
2200
+ const body = {
2201
+ ...defaultOptions,
2202
+ ...options.options,
2203
+ [tool.inputField]: mediaValue
2204
+ };
2205
+ return { endpoint: tool.endpoint, body, tool };
2206
+ }
2207
+
2208
+ // src/server.ts
2209
+ var FAL_BASE_URL = "https://fal.run";
2210
+ var FAL_QUEUE_URL = "https://queue.fal.run";
2211
+ var FAL_REST_URL = "https://rest.alpha.fal.ai";
2212
+ function endpointFromQueueUrl(url, fallback) {
2213
+ if (!url) return fallback;
2214
+ try {
2215
+ const parsed = new URL(url);
2216
+ const match = parsed.pathname.match(/^\/(.+)\/requests\//);
2217
+ return match?.[1] ?? fallback;
2218
+ } catch {
2219
+ return fallback;
2220
+ }
2221
+ }
2222
+ var MotifServer = class {
2223
+ apiKey;
2224
+ timeout;
2225
+ retries;
2226
+ constructor(config) {
2227
+ if (typeof config === "string") {
2228
+ this.apiKey = config;
2229
+ this.timeout = 12e4;
2230
+ this.retries = 3;
2231
+ } else {
2232
+ this.apiKey = config.apiKey;
2233
+ this.timeout = config.timeout ?? 12e4;
2234
+ this.retries = config.retries ?? 3;
2235
+ }
2236
+ if (!this.apiKey) {
2237
+ throw new MotifError("API key is required", 0);
2238
+ }
2239
+ }
2240
+ /** ─── Synchronous Generation ──────────────────────────────── */
2241
+ /** Generate images synchronously (blocks until fal.ai returns). */
2242
+ async generate(options) {
2243
+ const config = MODELS[options.model];
2244
+ if (config?.useQueue) {
2245
+ return this.generateQueued(options);
2246
+ }
2247
+ const { endpoint, body } = buildGenerateBody(options);
2248
+ const response = await this.request(`${FAL_BASE_URL}/${endpoint}`, {
2249
+ method: "POST",
2250
+ body: JSON.stringify(body)
2251
+ });
2252
+ if (response.isErr()) {
2253
+ return err(response.error);
2254
+ }
2255
+ const data = await response.value.json();
2256
+ return this.normalizeResponse(data);
2257
+ }
2258
+ async generateQueued(options) {
2259
+ const job = await this.submitGeneration(options);
2260
+ if (job.isErr()) {
2261
+ return err(job.error);
2262
+ }
2263
+ const pollIntervalMs = 3e3;
2264
+ const maxAttempts = 160;
2265
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2266
+ const status = await this.getJobStatus(
2267
+ job.value.endpoint,
2268
+ job.value.requestId
2269
+ );
2270
+ if (status.isErr()) {
2271
+ return err(status.error);
2272
+ }
2273
+ if (status.value.status === "completed") {
2274
+ return this.getJobResult(job.value.endpoint, job.value.requestId);
2275
+ }
2276
+ if (status.value.status === "failed") {
2277
+ return err(
2278
+ new MotifError(status.value.error ?? "Queued generation failed", 0)
2279
+ );
2280
+ }
2281
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
2282
+ }
2283
+ return err(new MotifError("Queued generation timed out", 0));
2284
+ }
2285
+ /** ─── Queue-Based Generation ──────────────────────────────── */
2286
+ /** Submit a generation to the fal.ai queue (returns immediately). */
2287
+ async submitGeneration(options) {
2288
+ const { endpoint, body } = buildGenerateBody(options);
2289
+ const response = await this.request(`${FAL_QUEUE_URL}/${endpoint}`, {
2290
+ method: "POST",
2291
+ body: JSON.stringify(body)
2292
+ });
2293
+ if (response.isErr()) {
2294
+ return err(response.error);
2295
+ }
2296
+ const data = await response.value.json();
2297
+ return ok({
2298
+ requestId: data.request_id,
2299
+ endpoint: endpointFromQueueUrl(data.response_url, endpoint),
2300
+ estimatedCost: estimateCost(
2301
+ options.model,
2302
+ options.resolution,
2303
+ options.numImages
2304
+ )
2305
+ });
2306
+ }
2307
+ /** Check the status of a queued generation. */
2308
+ async getJobStatus(endpoint, requestId) {
2309
+ const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}/status?logs=1`;
2310
+ const response = await this.request(url);
2311
+ if (response.isErr()) {
2312
+ return err(response.error);
2313
+ }
2314
+ const data = await response.value.json();
2315
+ let status;
2316
+ if (data.status === "IN_QUEUE") {
2317
+ status = "queued";
2318
+ } else if (data.status === "IN_PROGRESS") {
2319
+ status = "processing";
2320
+ } else if (data.status === "COMPLETED") {
2321
+ status = "completed";
2322
+ } else if (data.status === "FAILED" || data.status === "ERROR" || data.status === "CANCELED") {
2323
+ status = "failed";
2324
+ } else {
2325
+ return err(new MotifError(`Unknown job status: ${data.status}`, 0));
2326
+ }
2327
+ return ok({
2328
+ status,
2329
+ error: data.error ?? data.detail,
2330
+ queuePosition: data.queue_position,
2331
+ logs: data.logs
2332
+ });
2333
+ }
2334
+ /** Fetch the completed result from the queue. */
2335
+ async getJobResult(endpoint, requestId) {
2336
+ const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
2337
+ const response = await this.request(url);
2338
+ if (response.isErr()) {
2339
+ return err(response.error);
2340
+ }
2341
+ const data = await response.value.json();
2342
+ return this.normalizeResponse(data);
2343
+ }
2344
+ /** ─── Processing ──────────────────────────────────────────── */
2345
+ /** Upscale an image using clarity or crystal upscaler. */
2346
+ async upscale(options) {
2347
+ const {
2348
+ imageUrl,
2349
+ model = "clarity",
2350
+ scaleFactor,
2351
+ creativity,
2352
+ resemblance,
2353
+ prompt: upscalePrompt,
2354
+ negativePrompt,
2355
+ numInferenceSteps,
2356
+ guidanceScale
2357
+ } = options;
2358
+ const config = MODELS[model];
2359
+ if (!config || config.type !== "utility") {
2360
+ return err(new MotifError(`Invalid upscale model: ${model}`, 0));
2361
+ }
2362
+ const body = { image_url: imageUrl };
2363
+ if (model === "crystal") {
2364
+ if (scaleFactor !== void 0) body.scale_factor = scaleFactor;
2365
+ if (creativity !== void 0) body.creativity = creativity;
2366
+ } else {
2367
+ if (scaleFactor !== void 0) body.upscale_factor = scaleFactor;
2368
+ if (creativity !== void 0) body.creativity = creativity;
2369
+ if (resemblance !== void 0) body.resemblance = resemblance;
2370
+ if (upscalePrompt) body.prompt = upscalePrompt;
2371
+ if (negativePrompt) body.negative_prompt = negativePrompt;
2372
+ if (numInferenceSteps !== void 0)
2373
+ body.num_inference_steps = numInferenceSteps;
2374
+ if (guidanceScale !== void 0) body.guidance_scale = guidanceScale;
2375
+ }
2376
+ const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
2377
+ method: "POST",
2378
+ body: JSON.stringify(body)
2379
+ });
2380
+ if (response.isErr()) {
2381
+ return err(response.error);
2382
+ }
2383
+ const data = await response.value.json();
2384
+ return this.normalizeResponse(data);
2385
+ }
2386
+ /** Remove the background from an image. */
2387
+ async removeBackground(options) {
2388
+ const {
2389
+ imageUrl,
2390
+ model = "rmbg",
2391
+ variant,
2392
+ operatingResolution,
2393
+ outputFormat,
2394
+ refineForeground,
2395
+ outputMask
2396
+ } = options;
2397
+ const config = MODELS[model];
2398
+ if (!config) {
2399
+ return err(
2400
+ new MotifError(`Invalid background removal model: ${model}`, 0)
2401
+ );
2402
+ }
2403
+ const rbBody = { image_url: imageUrl };
2404
+ if (model === "rmbg") {
2405
+ if (variant) rbBody.model = variant;
2406
+ if (operatingResolution)
2407
+ rbBody.operating_resolution = operatingResolution;
2408
+ if (outputFormat) rbBody.output_format = outputFormat;
2409
+ if (refineForeground !== void 0)
2410
+ rbBody.refine_foreground = refineForeground;
2411
+ if (outputMask !== void 0) rbBody.output_mask = outputMask;
2412
+ }
2413
+ const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
2414
+ method: "POST",
2415
+ body: JSON.stringify(rbBody)
2416
+ });
2417
+ if (response.isErr()) {
2418
+ return err(response.error);
2419
+ }
2420
+ const data = await response.value.json();
2421
+ return this.normalizeResponse(data);
2422
+ }
2423
+ /** ─── Video Generation ───────────────────────────────────── */
2424
+ /**
2425
+ * Generate a video from an image using Kling v3 Pro.
2426
+ * This uses the queue API since video generation takes 30-120s.
2427
+ * Returns immediately with a job — poll with getJobStatus/getVideoResult.
2428
+ */
2429
+ async submitVideo(options) {
2430
+ const {
2431
+ imageUrl,
2432
+ prompt,
2433
+ duration = 5,
2434
+ generateAudio = true,
2435
+ endImageUrl,
2436
+ negativePrompt,
2437
+ cfgScale
2438
+ } = options;
2439
+ const config = MODELS.kling;
2440
+ if (!config) {
2441
+ return err(new MotifError("Kling video model not found", 0));
2442
+ }
2443
+ const body = {
2444
+ start_image_url: imageUrl,
2445
+ prompt,
2446
+ duration: String(duration),
2447
+ generate_audio: generateAudio
2448
+ };
2449
+ if (endImageUrl) {
2450
+ body.end_image_url = endImageUrl;
2451
+ }
2452
+ if (negativePrompt) body.negative_prompt = negativePrompt;
2453
+ if (cfgScale !== void 0) body.cfg_scale = cfgScale;
2454
+ const response = await this.request(`${FAL_QUEUE_URL}/${config.endpoint}`, {
2455
+ method: "POST",
2456
+ body: JSON.stringify(body)
2457
+ });
2458
+ if (response.isErr()) {
2459
+ return err(response.error);
2460
+ }
2461
+ const data = await response.value.json();
2462
+ return ok({
2463
+ requestId: data.request_id,
2464
+ endpoint: endpointFromQueueUrl(data.response_url, config.endpoint),
2465
+ estimatedCost: estimateVideoCost(duration, generateAudio)
2466
+ });
2467
+ }
2468
+ /** Fetch the completed video result from the queue. */
2469
+ async getVideoResult(endpoint, requestId) {
2470
+ const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
2471
+ const response = await this.request(url);
2472
+ if (response.isErr()) {
2473
+ return err(response.error);
2474
+ }
2475
+ const data = await response.value.json();
2476
+ if (!data.video) {
2477
+ return err(new MotifError("No video in response", 0));
2478
+ }
2479
+ return ok({
2480
+ url: data.video.url,
2481
+ contentType: data.video.content_type,
2482
+ fileName: data.video.file_name,
2483
+ fileSize: data.video.file_size
2484
+ });
2485
+ }
2486
+ /** ─── File Upload ─────────────────────────────────────────── */
2487
+ /**
2488
+ * Upload a file to fal.ai CDN storage and return the public URL.
2489
+ * Uses the two-step initiate + PUT flow.
2490
+ */
2491
+ async uploadToFalCdn(file, options) {
2492
+ const initiateResponse = await this.request(
2493
+ `${FAL_REST_URL}/storage/upload/initiate?storage_type=fal-cdn-v3`,
2494
+ {
2495
+ method: "POST",
2496
+ body: JSON.stringify({
2497
+ content_type: options.contentType,
2498
+ file_name: options.fileName
2499
+ })
2500
+ }
2501
+ );
2502
+ if (initiateResponse.isErr()) {
2503
+ return err(initiateResponse.error);
2504
+ }
2505
+ const { file_url, upload_url } = await initiateResponse.value.json();
2506
+ let putResponse;
2507
+ try {
2508
+ const body = Buffer.from(
2509
+ file instanceof Uint8Array ? file : new Uint8Array(file)
2510
+ );
2511
+ putResponse = await fetch(upload_url, {
2512
+ method: "PUT",
2513
+ headers: { "Content-Type": options.contentType },
2514
+ body
2515
+ });
2516
+ } catch (error) {
2517
+ return err(
2518
+ new MotifError(
2519
+ `Upload PUT failed: ${error instanceof Error ? error.message : String(error)}`,
2520
+ 0
2521
+ )
2522
+ );
2523
+ }
2524
+ if (!putResponse.ok) {
2525
+ return err(
2526
+ new MotifError(
2527
+ `Upload PUT failed: ${putResponse.status}`,
2528
+ putResponse.status
2529
+ )
2530
+ );
2531
+ }
2532
+ return ok(file_url);
2533
+ }
2534
+ /** ─── Utilities ───────────────────────────────────────────── */
2535
+ /** Run a registered fal utility/tool endpoint. */
2536
+ async runTool(options) {
2537
+ let request;
2538
+ try {
2539
+ request = buildFalToolRequest(options);
2540
+ } catch (error) {
2541
+ return err(
2542
+ new MotifError(
2543
+ error instanceof Error ? error.message : String(error),
2544
+ 0
2545
+ )
2546
+ );
2547
+ }
2548
+ const response = await this.request(`${FAL_BASE_URL}/${request.endpoint}`, {
2549
+ method: "POST",
2550
+ body: JSON.stringify(request.body)
2551
+ });
2552
+ if (response.isErr()) {
2553
+ return err(response.error);
2554
+ }
2555
+ return ok(await response.value.json());
2556
+ }
2557
+ /** Estimate cost for a generation (no API call). */
2558
+ estimateCost(model, resolution, numImages) {
2559
+ return estimateCost(model, resolution, numImages);
2560
+ }
2561
+ /** Build the fal.ai request body without sending it. */
2562
+ buildRequestBody(options) {
2563
+ return buildGenerateBody(options);
2564
+ }
2565
+ /** Model registry. */
2566
+ get models() {
2567
+ return MODELS;
2568
+ }
2569
+ /** Generation model keys. */
2570
+ get generationModels() {
2571
+ return GENERATION_MODELS;
2572
+ }
2573
+ /** Utility model keys. */
2574
+ get utilityModels() {
2575
+ return UTILITY_MODELS;
2576
+ }
2577
+ /** Registered fal utility/tool endpoints. */
2578
+ get tools() {
2579
+ return FAL_TOOLS;
2580
+ }
2581
+ /** ─── Private ─────────────────────────────────────────────── */
2582
+ /** Authenticated fetch to fal.ai APIs with retry logic. */
2583
+ async request(url, options = {}) {
2584
+ let lastError = null;
2585
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
2586
+ const controller = new AbortController();
2587
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
2588
+ try {
2589
+ const response = await fetch(url, {
2590
+ ...options,
2591
+ signal: controller.signal,
2592
+ headers: {
2593
+ Authorization: `Key ${this.apiKey}`,
2594
+ "Content-Type": "application/json",
2595
+ ...options.headers
2596
+ }
2597
+ });
2598
+ clearTimeout(timeoutId);
2599
+ if ((response.status === 429 || response.status >= 500) && attempt < this.retries) {
2600
+ const delay = 1e3 * 2 ** attempt;
2601
+ await new Promise((r) => setTimeout(r, delay));
2602
+ continue;
2603
+ }
2604
+ if (!response.ok) {
2605
+ const text = await response.text();
2606
+ return err(
2607
+ new MotifError(
2608
+ `Request failed: ${response.status} ${text}`,
2609
+ response.status
2610
+ )
2611
+ );
2612
+ }
2613
+ return ok(response);
2614
+ } catch (error) {
2615
+ clearTimeout(timeoutId);
2616
+ if (error instanceof MotifError) {
2617
+ return err(error);
2618
+ }
2619
+ lastError = new MotifError(
2620
+ error instanceof Error ? error.message : String(error),
2621
+ 0
2622
+ );
2623
+ if (attempt < this.retries) {
2624
+ const delay = 1e3 * 2 ** attempt;
2625
+ await new Promise((r) => setTimeout(r, delay));
2626
+ }
2627
+ }
2628
+ }
2629
+ return err(lastError ?? new MotifError("Request failed after retries", 0));
2630
+ }
2631
+ /**
2632
+ * Normalize fal.ai responses.
2633
+ * Some APIs return `{ image: {...} }` instead of `{ images: [...] }`.
2634
+ */
2635
+ normalizeResponse(data) {
2636
+ const obj = data;
2637
+ if ("detail" in obj) {
2638
+ return err(
2639
+ new MotifError(obj.detail, 0, "FAL_ERROR")
2640
+ );
2641
+ }
2642
+ if ("image" in obj && !("images" in obj)) {
2643
+ return ok({
2644
+ images: [obj.image],
2645
+ seed: obj.seed,
2646
+ prompt: obj.prompt
2647
+ });
2648
+ }
2649
+ return ok(obj);
2650
+ }
2651
+ };
2652
+ var MotifError = class extends Error {
2653
+ status;
2654
+ code;
2655
+ constructor(message, status, code) {
2656
+ super(message);
2657
+ this.name = "MotifError";
2658
+ this.status = status;
2659
+ this.code = code;
2660
+ }
2661
+ };
2662
+ export {
2663
+ ASPECT_RATIOS,
2664
+ FAL_TOOLS,
2665
+ FAL_TOOLS_CHECKED_AT,
2666
+ FAL_TOOL_IDS,
2667
+ FORMAT_PRESETS,
2668
+ GENERATION_MODELS,
2669
+ IDEOGRAM_STYLES,
2670
+ IMAGE_EDITING_TOP_20,
2671
+ IMAGE_TEXT_TO_IMAGE_TOP_20,
2672
+ MODELS,
2673
+ MotifError,
2674
+ MotifServer,
2675
+ RECRAFT_STYLES,
2676
+ RESOLUTIONS,
2677
+ UTILITY_MODELS,
2678
+ VIDEO_IMAGE_TO_VIDEO_TOP_15,
2679
+ VIDEO_MODELS,
2680
+ VIDEO_TEXT_TO_VIDEO_TOP_15,
2681
+ aspectToFalImageSize,
2682
+ aspectToGptSize,
2683
+ buildFalToolRequest,
2684
+ buildGenerateBody,
2685
+ err2 as err,
2686
+ estimateCost,
2687
+ estimateVideoCost,
2688
+ getFalKeyFromEnv,
2689
+ isFalToolId,
2690
+ motifEnvSchema,
2691
+ ok2 as ok,
2692
+ parseMotifEnv
2693
+ };