@howells/motif-sdk 1.3.1 → 4.0.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/image.js CHANGED
@@ -4,12 +4,133 @@ import { createFal } from "@ai-sdk/fal";
4
4
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
5
5
  import { createOpenAI } from "@ai-sdk/openai";
6
6
  import { createReplicate } from "@ai-sdk/replicate";
7
+ //#region src/errors.ts
8
+ /**
9
+ * `MotifError` and its coercion helper.
10
+ *
11
+ * Extracted from `server.ts` so `server-tools.ts` can construct errors without
12
+ * importing back into `server.ts` — that import was a dependency cycle. Both
13
+ * `server.ts` and `server-tools.ts` import from here, and `server.ts`
14
+ * re-exports `MotifError` so the package's public surface is unchanged.
15
+ */
16
+ var MotifError = class extends Error {
17
+ status;
18
+ code;
19
+ /** fal's request-correlation id (from the `x-fal-request-id` header or the
20
+ * error body). Ties a failure back to fal's dashboard/support. */
21
+ requestId;
22
+ /** Structured context for the failure, e.g. the refused field. */
23
+ details;
24
+ constructor(message, status, code, requestId, details) {
25
+ super(message);
26
+ this.name = "MotifError";
27
+ this.status = status;
28
+ this.code = code;
29
+ this.requestId = requestId;
30
+ this.details = details;
31
+ }
32
+ };
33
+ /** Error code for a fal account that is locked, usually for lack of credit. */
34
+ const ACCOUNT_LOCKED = "ACCOUNT_LOCKED";
35
+ const ACCOUNT_LOCKED_DETAIL = "User is locked";
36
+ /**
37
+ * Whether a fal HTTP failure means the account is locked.
38
+ *
39
+ * fal answers `403 {"detail":"User is locked. Reason: TOP_UP."}` when the
40
+ * account has run out of credit. Retrying cannot help, and the key is valid.
41
+ */
42
+ function isFalAccountLocked(status, body) {
43
+ return status === 403 && body.includes(ACCOUNT_LOCKED_DETAIL);
44
+ }
45
+ /**
46
+ * Build the `MotifError` for a non-OK fal response, recognising a locked
47
+ * account as `ACCOUNT_LOCKED`.
48
+ */
49
+ function falHttpError(status, body, requestId) {
50
+ if (isFalAccountLocked(status, body)) return new MotifError(`fal account is locked because it is out of credit (fal said: ${body})`, status, ACCOUNT_LOCKED, requestId);
51
+ return new MotifError(`Request failed: ${status} ${body}`, status, void 0, requestId);
52
+ }
53
+ //#endregion
7
54
  //#region src/models.ts
8
55
  const AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-08-23";
9
56
  const AA_IMAGE_SOURCES = ["https://artificialanalysis.ai/image/leaderboard/text-to-image", "https://artificialanalysis.ai/image/leaderboard/editing"];
10
57
  const FAL_PRICING_CHECKED_AT = "2026-05-12";
11
58
  const FAL_PRICING_CHECKED_JUL_2026 = "2026-07-11";
59
+ const FAL_PRICING_CHECKED_AUG_2026 = "2026-08-23";
60
+ const FAL_PRICING_CHECKED_SEP_2026 = "2026-09-16";
12
61
  const MODELS = {
62
+ flare: {
63
+ customImageSize: {
64
+ maxEdge: 3840,
65
+ maxPixels: 8294400,
66
+ maxRatio: 3,
67
+ minPixels: 655360,
68
+ multipleOf: 16
69
+ },
70
+ editEndpoint: "openai/gpt-image-2.5/flare/edit",
71
+ endpoint: "openai/gpt-image-2.5/flare/text-to-image",
72
+ maxReferenceImages: 16,
73
+ name: "GPT Image 2.5 Flare",
74
+ pricing: "Token-based: text $5/M input, $1.25/M cached, $10/M output; image $8/M input, $2/M cached, $30/M output",
75
+ pricePerImageUsd: null,
76
+ sizeMode: "image_size_enum",
77
+ supportsAspect: true,
78
+ supportsBackground: true,
79
+ supportsEdit: true,
80
+ supportsMaskImage: true,
81
+ maskImageField: "mask_url",
82
+ supportsNumImages: true,
83
+ supportsOutputFormat: true,
84
+ supportsQuality: true,
85
+ supportedQualities: [
86
+ "auto",
87
+ "low",
88
+ "medium",
89
+ "high",
90
+ "xhigh",
91
+ "max"
92
+ ],
93
+ supportsResolution: false,
94
+ supportsSyncMode: true,
95
+ type: "generation",
96
+ useQueue: true
97
+ },
98
+ sunburst: {
99
+ customImageSize: {
100
+ maxEdge: 3840,
101
+ maxPixels: 8294400,
102
+ maxRatio: 3,
103
+ minPixels: 655360,
104
+ multipleOf: 16
105
+ },
106
+ editEndpoint: "openai/gpt-image-2.5/sunburst/edit",
107
+ endpoint: "openai/gpt-image-2.5/sunburst/text-to-image",
108
+ maxReferenceImages: 16,
109
+ name: "GPT Image 2.5 Sunburst",
110
+ pricing: "Token-based: text $5/M input, $1.25/M cached, $10/M output; image $8/M input, $2/M cached, $30/M output",
111
+ pricePerImageUsd: null,
112
+ sizeMode: "image_size_enum",
113
+ supportsAspect: true,
114
+ supportsBackground: true,
115
+ supportsEdit: true,
116
+ supportsMaskImage: true,
117
+ maskImageField: "mask_url",
118
+ supportsNumImages: true,
119
+ supportsOutputFormat: true,
120
+ supportsQuality: true,
121
+ supportedQualities: [
122
+ "auto",
123
+ "low",
124
+ "medium",
125
+ "high",
126
+ "xhigh",
127
+ "max"
128
+ ],
129
+ supportsResolution: false,
130
+ supportsSyncMode: true,
131
+ type: "generation",
132
+ useQueue: true
133
+ },
13
134
  gpt2: {
14
135
  benchmark: {
15
136
  artificialAnalysis: {
@@ -36,7 +157,14 @@ const MODELS = {
36
157
  },
37
158
  useCase: "Highest-ranked text-to-image quality and transparent PNGs"
38
159
  },
39
- editEndpoint: "openai/gpt-image-2/image-to-image",
160
+ customImageSize: {
161
+ maxEdge: 3840,
162
+ maxPixels: 8294400,
163
+ maxRatio: 3,
164
+ minPixels: 655360,
165
+ multipleOf: 16
166
+ },
167
+ editEndpoint: "openai/gpt-image-2/edit",
40
168
  endpoint: "openai/gpt-image-2",
41
169
  falPricing: {
42
170
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -55,11 +183,18 @@ const MODELS = {
55
183
  supportsAspect: true,
56
184
  supportsEdit: true,
57
185
  supportsMaskImage: true,
186
+ maskImageField: "mask_url",
58
187
  supportsNumImages: true,
59
188
  supportsOutputFormat: true,
60
189
  supportsQuality: true,
61
190
  supportsResolution: false,
62
191
  supportsSyncMode: true,
192
+ transparencyRoute: {
193
+ apiKeyEnv: "OPENAI_API_KEY",
194
+ model: "gpt-image-2",
195
+ provider: "openai",
196
+ supportsEdit: true
197
+ },
63
198
  type: "generation",
64
199
  useQueue: true
65
200
  },
@@ -209,6 +344,24 @@ const MODELS = {
209
344
  pricePerImageUsd: .15,
210
345
  pricing: "$0.15",
211
346
  sizeMode: "aspect_ratio",
347
+ supportedAspects: [
348
+ "auto",
349
+ "21:9",
350
+ "16:9",
351
+ "3:2",
352
+ "4:3",
353
+ "5:4",
354
+ "1:1",
355
+ "4:5",
356
+ "3:4",
357
+ "2:3",
358
+ "9:16"
359
+ ],
360
+ supportedResolutions: [
361
+ "1K",
362
+ "2K",
363
+ "4K"
364
+ ],
212
365
  supportsAspect: true,
213
366
  supportsEdit: true,
214
367
  supportsGoogleSearch: true,
@@ -247,6 +400,18 @@ const MODELS = {
247
400
  pricePerImageUsd: .0398,
248
401
  pricing: "$0.04",
249
402
  sizeMode: "aspect_ratio",
403
+ supportedAspects: [
404
+ "21:9",
405
+ "16:9",
406
+ "3:2",
407
+ "4:3",
408
+ "5:4",
409
+ "1:1",
410
+ "4:5",
411
+ "3:4",
412
+ "2:3",
413
+ "9:16"
414
+ ],
250
415
  supportsAspect: true,
251
416
  supportsEdit: true,
252
417
  supportsNumImages: true,
@@ -286,6 +451,24 @@ const MODELS = {
286
451
  pricePerImageUsd: .15,
287
452
  pricing: "$0.15",
288
453
  sizeMode: "aspect_ratio",
454
+ supportedAspects: [
455
+ "auto",
456
+ "21:9",
457
+ "16:9",
458
+ "3:2",
459
+ "4:3",
460
+ "5:4",
461
+ "1:1",
462
+ "4:5",
463
+ "3:4",
464
+ "2:3",
465
+ "9:16"
466
+ ],
467
+ supportedResolutions: [
468
+ "1K",
469
+ "2K",
470
+ "4K"
471
+ ],
289
472
  supportsAspect: true,
290
473
  supportsEdit: true,
291
474
  supportsNumImages: true,
@@ -323,6 +506,10 @@ const MODELS = {
323
506
  },
324
507
  useCase: "High-ranked budget generation and large multi-reference edits"
325
508
  },
509
+ customImageSize: {
510
+ maxPixels: 16777216,
511
+ minPixels: 921600
512
+ },
326
513
  editEndpoint: "fal-ai/bytedance/seedream/v4/edit",
327
514
  endpoint: "fal-ai/bytedance/seedream/v4/text-to-image",
328
515
  falPricing: {
@@ -373,6 +560,10 @@ const MODELS = {
373
560
  },
374
561
  useCase: "Cheap current Seedream model with good animation/art scores"
375
562
  },
563
+ customImageSize: {
564
+ maxPixels: 16777216,
565
+ minPixels: 3686400
566
+ },
376
567
  editEndpoint: "fal-ai/bytedance/seedream/v4.5/edit",
377
568
  endpoint: "fal-ai/bytedance/seedream/v4.5/text-to-image",
378
569
  falPricing: {
@@ -410,6 +601,11 @@ const MODELS = {
410
601
  rank: 10
411
602
  }
412
603
  } },
604
+ customImageSize: {
605
+ maxPixels: 4194304,
606
+ maxRatio: 16,
607
+ minPixels: 1048576
608
+ },
413
609
  name: "Seedream 5.0 Pro",
414
610
  endpoint: "bytedance/seedream/v5/pro/text-to-image",
415
611
  editEndpoint: "bytedance/seedream/v5/pro/edit",
@@ -443,16 +639,20 @@ const MODELS = {
443
639
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
444
640
  sourceUrls: AA_IMAGE_SOURCES
445
641
  } },
642
+ customImageSize: {
643
+ maxPixels: 16777216,
644
+ minPixels: 3686400
645
+ },
446
646
  name: "Seedream 5.0 Lite",
447
- endpoint: "fal-ai/bytedance/seedream/v5/lite/text-to-image",
448
- editEndpoint: "fal-ai/bytedance/seedream/v5/lite/edit",
647
+ endpoint: "bytedance/seedream/v5/lite/text-to-image",
648
+ editEndpoint: "bytedance/seedream/v5/lite/edit",
449
649
  type: "generation",
450
650
  pricing: "$0.035",
451
651
  pricePerImageUsd: .035,
452
652
  falPricing: {
453
653
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
454
654
  currency: "USD",
455
- endpointId: "fal-ai/bytedance/seedream/v5/lite/text-to-image",
655
+ endpointId: "bytedance/seedream/v5/lite/text-to-image",
456
656
  estimatedCostPerImageUsd: .035,
457
657
  source: "fal-pricing-api",
458
658
  unit: "images",
@@ -493,6 +693,12 @@ const MODELS = {
493
693
  },
494
694
  useCase: "Best FLUX quality and high-end editing"
495
695
  },
696
+ customImageSize: {
697
+ maxEdge: 2560,
698
+ maxPixels: 4194304,
699
+ minEdge: 256,
700
+ multipleOf: 16
701
+ },
496
702
  editEndpoint: "fal-ai/flux-2-max/edit",
497
703
  endpoint: "fal-ai/flux-2-max",
498
704
  falPricing: {
@@ -546,6 +752,12 @@ const MODELS = {
546
752
  },
547
753
  useCase: "Production FLUX quality at a low per-megapixel price"
548
754
  },
755
+ customImageSize: {
756
+ maxEdge: 2560,
757
+ maxPixels: 4194304,
758
+ minEdge: 256,
759
+ multipleOf: 16
760
+ },
549
761
  editEndpoint: "fal-ai/flux-2-pro/edit",
550
762
  endpoint: "fal-ai/flux-2-pro",
551
763
  falPricing: {
@@ -595,6 +807,12 @@ const MODELS = {
595
807
  },
596
808
  useCase: "FLUX quality with guidance and step controls"
597
809
  },
810
+ customImageSize: {
811
+ maxEdge: 2560,
812
+ maxPixels: 4194304,
813
+ minEdge: 256,
814
+ multipleOf: 16
815
+ },
598
816
  editEndpoint: "fal-ai/flux-2-flex/edit",
599
817
  endpoint: "fal-ai/flux-2-flex",
600
818
  falPricing: {
@@ -643,6 +861,10 @@ const MODELS = {
643
861
  },
644
862
  useCase: "Open FLUX.2 quality with low average cost"
645
863
  },
864
+ customImageSize: {
865
+ maxEdge: 2048,
866
+ minEdge: 512
867
+ },
646
868
  editEndpoint: "fal-ai/flux-2/edit",
647
869
  endpoint: "fal-ai/flux-2",
648
870
  falPricing: {
@@ -673,6 +895,10 @@ const MODELS = {
673
895
  type: "generation"
674
896
  },
675
897
  "flux2-turbo": {
898
+ customImageSize: {
899
+ maxEdge: 2048,
900
+ minEdge: 512
901
+ },
676
902
  endpoint: "fal-ai/flux-2/turbo",
677
903
  falPricing: {
678
904
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
@@ -728,6 +954,7 @@ const MODELS = {
728
954
  type: "generation"
729
955
  },
730
956
  "flux-fast": {
957
+ customImageSize: {},
731
958
  endpoint: "fal-ai/flux/schnell",
732
959
  falPricing: {
733
960
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -754,11 +981,12 @@ const MODELS = {
754
981
  type: "generation"
755
982
  },
756
983
  recraft: {
757
- endpoint: "fal-ai/recraft-v3",
984
+ customImageSize: {},
985
+ endpoint: "fal-ai/recraft/v3/text-to-image",
758
986
  falPricing: {
759
987
  checkedAt: FAL_PRICING_CHECKED_AT,
760
988
  currency: "USD",
761
- endpointId: "fal-ai/recraft-v3",
989
+ endpointId: "fal-ai/recraft/v3/text-to-image",
762
990
  estimatedCostPerImageUsd: .04,
763
991
  source: "fal-pricing-api",
764
992
  unit: "images",
@@ -800,7 +1028,7 @@ const MODELS = {
800
1028
  editEndpoint: "fal-ai/reve/edit",
801
1029
  endpoint: "fal-ai/reve/text-to-image",
802
1030
  falPricing: {
803
- checkedAt: "2026-08-23",
1031
+ checkedAt: FAL_PRICING_CHECKED_AUG_2026,
804
1032
  currency: "USD",
805
1033
  endpointId: "fal-ai/reve/text-to-image",
806
1034
  estimatedCostPerImageUsd: .04,
@@ -813,6 +1041,15 @@ const MODELS = {
813
1041
  pricePerImageUsd: .04,
814
1042
  pricing: "$0.04",
815
1043
  sizeMode: "aspect_ratio",
1044
+ supportedAspects: [
1045
+ "16:9",
1046
+ "9:16",
1047
+ "3:2",
1048
+ "2:3",
1049
+ "4:3",
1050
+ "3:4",
1051
+ "1:1"
1052
+ ],
816
1053
  supportsAspect: true,
817
1054
  supportsEdit: true,
818
1055
  supportsNumImages: true,
@@ -831,6 +1068,7 @@ const MODELS = {
831
1068
  rank: 25
832
1069
  }
833
1070
  } },
1071
+ customImageSize: {},
834
1072
  endpoint: "fal-ai/recraft/v4/text-to-image",
835
1073
  falPricing: {
836
1074
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
@@ -849,11 +1087,11 @@ const MODELS = {
849
1087
  supportsEdit: false,
850
1088
  supportsNumImages: false,
851
1089
  supportsResolution: false,
852
- supportsStyle: true,
853
1090
  supportsSyncMode: true,
854
1091
  type: "generation"
855
1092
  },
856
1093
  ideogram: {
1094
+ customImageSize: {},
857
1095
  endpoint: "fal-ai/ideogram/v3",
858
1096
  falPricing: {
859
1097
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -889,6 +1127,11 @@ const MODELS = {
889
1127
  rank: 19
890
1128
  }
891
1129
  } },
1130
+ customImageSize: {
1131
+ maxEdge: 3840,
1132
+ minEdge: 512,
1133
+ multipleOf: 16
1134
+ },
892
1135
  name: "Ideogram V4",
893
1136
  endpoint: "ideogram/v4",
894
1137
  type: "generation",
@@ -952,11 +1195,21 @@ const MODELS = {
952
1195
  unit: "images",
953
1196
  unitPrice: .02
954
1197
  },
955
- maxReferenceImages: 4,
1198
+ maxReferenceImages: 3,
956
1199
  name: "Grok Imagine Image",
957
1200
  pricePerImageUsd: .02,
958
1201
  pricing: "$0.02",
959
1202
  sizeMode: "aspect_ratio",
1203
+ supportedAspects: [
1204
+ "16:9",
1205
+ "4:3",
1206
+ "3:2",
1207
+ "1:1",
1208
+ "2:3",
1209
+ "3:4",
1210
+ "9:16"
1211
+ ],
1212
+ supportedResolutions: ["1K", "2K"],
960
1213
  supportsAspect: true,
961
1214
  supportsEdit: true,
962
1215
  supportsNumImages: true,
@@ -983,6 +1236,7 @@ const MODELS = {
983
1236
  },
984
1237
  useCase: "Low-cost open-weight text and design generation"
985
1238
  },
1239
+ customImageSize: {},
986
1240
  endpoint: "fal-ai/qwen-image",
987
1241
  falPricing: {
988
1242
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -1027,19 +1281,23 @@ const MODELS = {
1027
1281
  },
1028
1282
  useCase: "Strong quality per dollar - 11th on text-to-image at $30/1k"
1029
1283
  },
1030
- endpoint: "fal-ai/qwen-image-3/text-to-image",
1284
+ customImageSize: {
1285
+ maxPixels: 4194304,
1286
+ minPixels: 262144
1287
+ },
1288
+ endpoint: "alibaba/qwen-image-3/text-to-image",
1031
1289
  falPricing: {
1032
- checkedAt: "2026-08-05",
1290
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1033
1291
  currency: "USD",
1034
- endpointId: "fal-ai/qwen-image-3/text-to-image",
1035
- estimatedCostPerImageUsd: .02,
1292
+ endpointId: "alibaba/qwen-image-3/text-to-image",
1293
+ estimatedCostPerImageUsd: .04,
1036
1294
  source: "fal-pricing-api",
1037
- unit: "megapixels",
1038
- unitPrice: .02
1295
+ unit: "images",
1296
+ unitPrice: .04
1039
1297
  },
1040
1298
  name: "Qwen Image 3",
1041
- pricePerImageUsd: .02,
1042
- pricing: "$0.02/MP (assumed from qwen v1; fal pricing page bot-gated at check time)",
1299
+ pricePerImageUsd: .04,
1300
+ pricing: "$0.04 (1K) / $0.075 (2K)",
1043
1301
  sizeMode: "image_size_enum",
1044
1302
  supportsAspect: true,
1045
1303
  supportsEdit: false,
@@ -1051,6 +1309,179 @@ const MODELS = {
1051
1309
  supportsSeed: true,
1052
1310
  type: "generation"
1053
1311
  },
1312
+ "mai-image-2.5-pro": {
1313
+ benchmark: {
1314
+ artificialAnalysis: {
1315
+ editing: {
1316
+ elo: 1272,
1317
+ rank: 1
1318
+ },
1319
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
1320
+ sourceUrls: AA_IMAGE_SOURCES,
1321
+ textToImage: {
1322
+ elo: 1293,
1323
+ rank: 7
1324
+ }
1325
+ },
1326
+ useCase: "Highest-ranked image editor, single reference image"
1327
+ },
1328
+ editEndpoint: "microsoft/mai-image-2.5-pro/edit",
1329
+ editImagesField: "image_url",
1330
+ endpoint: "microsoft/mai-image-2.5-pro",
1331
+ falPricing: {
1332
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1333
+ currency: "USD",
1334
+ endpointId: "microsoft/mai-image-2.5-pro",
1335
+ estimatedCostPerImageUsd: .17,
1336
+ source: "fal-pricing-api",
1337
+ unit: "images",
1338
+ unitPrice: .17
1339
+ },
1340
+ maxReferenceImages: 1,
1341
+ name: "MAI Image 2.5 Pro",
1342
+ pricePerImageUsd: .17,
1343
+ pricing: "~$0.17 (edit ~$0.18-$0.27)",
1344
+ sizeMode: "aspect_ratio",
1345
+ supportedAspects: [
1346
+ "auto",
1347
+ "1:1",
1348
+ "4:3",
1349
+ "3:4",
1350
+ "16:9",
1351
+ "9:16",
1352
+ "3:2",
1353
+ "2:3"
1354
+ ],
1355
+ supportsAspect: true,
1356
+ supportsEdit: true,
1357
+ supportsNumImages: true,
1358
+ supportsOutputFormat: true,
1359
+ supportsResolution: false,
1360
+ supportsSyncMode: true,
1361
+ type: "generation"
1362
+ },
1363
+ "banana2-lite": {
1364
+ benchmark: { artificialAnalysis: {
1365
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
1366
+ sourceUrls: AA_IMAGE_SOURCES,
1367
+ textToImage: {
1368
+ elo: 1289,
1369
+ rank: 8
1370
+ }
1371
+ } },
1372
+ endpoint: "google/nano-banana-2-lite",
1373
+ name: "Nano Banana 2 Lite",
1374
+ pricing: "Token-based: text $0.3125/M input, $1.875/M output; image $0.3125/M input, $37.50/M output; fixed 1K output (~$0.048)",
1375
+ pricePerImageUsd: .048,
1376
+ sizeMode: "aspect_ratio",
1377
+ supportsAspect: true,
1378
+ supportsEdit: false,
1379
+ supportsLimitGenerations: true,
1380
+ supportsNumImages: true,
1381
+ supportsOutputFormat: true,
1382
+ supportsResolution: false,
1383
+ supportsSafetyTolerance: true,
1384
+ supportsSeed: true,
1385
+ supportsSyncMode: true,
1386
+ supportsThinkingLevel: true,
1387
+ type: "generation"
1388
+ },
1389
+ "ideogram3-transparent": {
1390
+ endpoint: "fal-ai/ideogram/v3/generate-transparent",
1391
+ falPricing: {
1392
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1393
+ currency: "USD",
1394
+ endpointId: "fal-ai/ideogram/v3/generate-transparent",
1395
+ estimatedCostPerImageUsd: .06,
1396
+ source: "fal-pricing-api",
1397
+ unit: "images",
1398
+ unitPrice: .06
1399
+ },
1400
+ name: "Ideogram V3 Transparent",
1401
+ pricePerImageUsd: .06,
1402
+ pricing: "$0.03 TURBO / $0.06 BALANCED / $0.09 QUALITY",
1403
+ sizeMode: "aspect_ratio",
1404
+ supportedAspects: [
1405
+ "16:9",
1406
+ "9:16",
1407
+ "3:2",
1408
+ "2:3",
1409
+ "4:3",
1410
+ "3:4",
1411
+ "5:4",
1412
+ "4:5",
1413
+ "1:1"
1414
+ ],
1415
+ supportsAspect: true,
1416
+ supportsEdit: false,
1417
+ supportsExpandPrompt: true,
1418
+ supportsNegativePrompt: true,
1419
+ supportsNumImages: true,
1420
+ supportsRenderingSpeed: true,
1421
+ supportsResolution: false,
1422
+ supportsSeed: true,
1423
+ supportsSyncMode: true,
1424
+ transparentOutput: true,
1425
+ type: "generation"
1426
+ },
1427
+ recraft41: {
1428
+ customImageSize: {},
1429
+ endpoint: "fal-ai/recraft/v4.1/text-to-image",
1430
+ falPricing: {
1431
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1432
+ currency: "USD",
1433
+ endpointId: "fal-ai/recraft/v4.1/text-to-image",
1434
+ estimatedCostPerImageUsd: .035,
1435
+ source: "fal-pricing-api",
1436
+ unit: "images",
1437
+ unitPrice: .035
1438
+ },
1439
+ name: "Recraft V4.1",
1440
+ pricePerImageUsd: .035,
1441
+ pricing: "$0.035",
1442
+ sizeMode: "image_size_enum",
1443
+ supportsAspect: true,
1444
+ supportsEdit: false,
1445
+ supportsNumImages: false,
1446
+ supportsResolution: false,
1447
+ supportsSafetyChecker: true,
1448
+ type: "generation"
1449
+ },
1450
+ "grok-image-2": {
1451
+ editEndpoint: "xai/grok-imagine-image/v2.0/edit",
1452
+ endpoint: "xai/grok-imagine-image/v2.0/text-to-image",
1453
+ falPricing: {
1454
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1455
+ currency: "USD",
1456
+ endpointId: "xai/grok-imagine-image/v2.0/text-to-image",
1457
+ estimatedCostPerImageUsd: .06,
1458
+ source: "fal-pricing-api",
1459
+ unit: "images",
1460
+ unitPrice: .06
1461
+ },
1462
+ maxReferenceImages: 3,
1463
+ name: "Grok Imagine Image 2.0",
1464
+ pricePerImageUsd: .06,
1465
+ pricing: "$0.06 (1K) / $0.08 (2K) at medium quality",
1466
+ sizeMode: "aspect_ratio",
1467
+ supportedAspects: [
1468
+ "16:9",
1469
+ "4:3",
1470
+ "3:2",
1471
+ "1:1",
1472
+ "2:3",
1473
+ "3:4",
1474
+ "9:16"
1475
+ ],
1476
+ supportedResolutions: ["1K", "2K"],
1477
+ supportsAspect: true,
1478
+ supportsEdit: true,
1479
+ supportsNumImages: true,
1480
+ supportsOutputFormat: true,
1481
+ supportsResolution: true,
1482
+ supportsSyncMode: true,
1483
+ type: "generation"
1484
+ },
1054
1485
  kling: {
1055
1486
  endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
1056
1487
  name: "Kling v3 Pro",
@@ -1062,6 +1493,17 @@ const MODELS = {
1062
1493
  supportsResolution: false,
1063
1494
  type: "video"
1064
1495
  },
1496
+ "kling-turbo": {
1497
+ endpoint: "fal-ai/kling-video/v3/turbo/pro/image-to-video",
1498
+ name: "Kling v3 Turbo Pro",
1499
+ pricing: "$0.14/sec",
1500
+ sizeMode: "none",
1501
+ supportsAspect: false,
1502
+ supportsEdit: false,
1503
+ supportsNumImages: false,
1504
+ supportsResolution: false,
1505
+ type: "video"
1506
+ },
1065
1507
  clarity: {
1066
1508
  endpoint: "fal-ai/clarity-upscaler",
1067
1509
  name: "Clarity Upscaler",
@@ -1104,6 +1546,8 @@ const MODELS = {
1104
1546
  }
1105
1547
  };
1106
1548
  [
1549
+ "flare",
1550
+ "sunburst",
1107
1551
  "gpt2",
1108
1552
  "gpt",
1109
1553
  "banana2",
@@ -1126,33 +1570,202 @@ const MODELS = {
1126
1570
  "ideogram",
1127
1571
  "ideogram4",
1128
1572
  "grok-image",
1573
+ "grok-image-2",
1129
1574
  "qwen",
1130
- "qwen3"
1575
+ "qwen3",
1576
+ "mai-image-2.5-pro",
1577
+ "banana2-lite",
1578
+ "ideogram3-transparent",
1579
+ "recraft41"
1131
1580
  ].filter((id) => MODELS[id]?.supportsEdit === true);
1132
1581
  //#endregion
1133
- //#region src/errors.ts
1134
- /**
1135
- * `MotifError` and its coercion helper.
1136
- *
1137
- * Extracted from `server.ts` so `server-tools.ts` can construct errors without
1138
- * importing back into `server.ts` — that import was a dependency cycle. Both
1139
- * `server.ts` and `server-tools.ts` import from here, and `server.ts`
1140
- * re-exports `MotifError` so the package's public surface is unchanged.
1141
- */
1142
- var MotifError = class extends Error {
1143
- status;
1144
- code;
1145
- /** fal's request-correlation id (from the `x-fal-request-id` header or the
1146
- * error body). Ties a failure back to fal's dashboard/support. */
1147
- requestId;
1148
- constructor(message, status, code, requestId) {
1149
- super(message);
1150
- this.name = "MotifError";
1151
- this.status = status;
1152
- this.code = code;
1153
- this.requestId = requestId;
1154
- }
1582
+ //#region src/capabilities.ts
1583
+ /** Generation options a model can refuse, with the registry test for each. */
1584
+ const OPTION_CAPABILITIES = {
1585
+ aspect: (config) => (config.sizeMode ?? "aspect_ratio") !== "none",
1586
+ background: (config) => config.supportsBackground === true,
1587
+ enableGoogleSearch: (config) => config.supportsGoogleSearch === true,
1588
+ enableSafetyChecker: (config) => config.supportsSafetyChecker === true,
1589
+ enableWebSearch: (config) => config.supportsWebSearch === true,
1590
+ enhancePrompt: (config) => config.supportsEnhancePrompt === true,
1591
+ expandPrompt: (config) => config.supportsExpandPrompt === true,
1592
+ guidanceScale: (config) => config.supportsGuidanceScale === true,
1593
+ "image editing": (config) => config.supportsEdit,
1594
+ imagePromptStrength: (config) => config.supportsImagePromptStrength === true,
1595
+ imageSize: (config) => config.sizeMode === "gpt_size" || config.sizeMode === "image_size_enum",
1596
+ inputFidelity: (config) => config.sizeMode === "gpt_size",
1597
+ limitGenerations: (config) => config.supportsLimitGenerations === true,
1598
+ maskImageUrl: (config) => config.supportsMaskImage === true,
1599
+ negativePrompt: (config) => config.supportsNegativePrompt === true,
1600
+ numImages: (config) => config.supportsNumImages,
1601
+ numInferenceSteps: (config) => config.supportsInferenceSteps === true,
1602
+ outputFormat: (config) => config.supportsOutputFormat === true,
1603
+ quality: (config) => config.supportsQuality === true,
1604
+ raw: (config) => config.supportsRaw === true,
1605
+ renderingSpeed: (config) => config.supportsRenderingSpeed === true,
1606
+ resolution: (config) => config.supportsResolution,
1607
+ safetyTolerance: (config) => config.supportsSafetyTolerance === true,
1608
+ seed: (config) => config.supportsSeed === true,
1609
+ style: (config) => config.supportsStyle === true,
1610
+ syncMode: (config) => config.supportsSyncMode === true,
1611
+ thinkingLevel: (config) => config.supportsThinkingLevel === true,
1612
+ "transparent output": (config) => config.supportsBackground === true || config.transparentOutput === true
1155
1613
  };
1614
+ function isModelOption(key) {
1615
+ return Object.hasOwn(OPTION_CAPABILITIES, key);
1616
+ }
1617
+ Object.keys(OPTION_CAPABILITIES).filter(isModelOption);
1618
+ ({
1619
+ look: [
1620
+ {
1621
+ acceptsMood: true,
1622
+ aspect: "1:1",
1623
+ clause: "Editorial photograph in the register of Atelier Ellis, Aman hotels, Kinfolk magazine and Aesop, pigment-rich mineral colour on named matte surfaces, quiet composition with generous negative space, shallow depth of field, shot on film with fine grain, restrained and materially rich. No text, no logos, no people",
1624
+ description: "Quiet, materially rich editorial photography for brand and mood imagery.",
1625
+ id: "editorial",
1626
+ label: "Quiet editorial",
1627
+ model: "flux2-pro"
1628
+ },
1629
+ {
1630
+ acceptsMood: true,
1631
+ aspect: "1:1",
1632
+ clause: "Editorial still life on a warm bone plaster ground, chalky unglazed surfaces, a long soft shadow, generous empty space, shot on film with fine grain. No text, no logos, no people",
1633
+ description: "Objects and material samples on a plaster ground, for product and swatch shots.",
1634
+ id: "still-life",
1635
+ label: "Material still life",
1636
+ model: "flux2-pro"
1637
+ },
1638
+ {
1639
+ acceptsMood: true,
1640
+ aspect: "3:2",
1641
+ clause: "Interior photograph shot square-on at eye level on a 35mm lens, warm off-white plaster, wide oak floorboards, linen, brass and a little pattern, light, bright and layered, collected rather than styled, slightly imperfect and lived-in rather than showroom-perfect, photographic realism. No text, no logos, no people",
1642
+ description: "Bright, collected rooms that feel lived in, for interior scenes.",
1643
+ id: "lived-in",
1644
+ label: "Lived-in interior",
1645
+ model: "flux2-pro"
1646
+ },
1647
+ {
1648
+ acceptsMood: true,
1649
+ aspect: "4:5",
1650
+ clause: "Architectural editorial photograph at full room scale, honest materials meeting precise detailing, one hero element genuinely installed, plausible light and shadow, generous negative space, empty of people. No text, no logos",
1651
+ description: "Whole rooms with one product installed, for showing a material at scale.",
1652
+ id: "architectural",
1653
+ label: "Architectural scale",
1654
+ model: "banana"
1655
+ },
1656
+ {
1657
+ acceptsMood: true,
1658
+ aspect: "4:3",
1659
+ clause: "Amateur phone photo of a real home taken by the homeowner, slightly wonky framing, unstyled domestic photography, ordinary exposure. No text, no people",
1660
+ description: "Unstyled phone snapshots of real homes, for believable before and after shots.",
1661
+ id: "homeowner",
1662
+ label: "Homeowner snapshot",
1663
+ model: "seedream45"
1664
+ },
1665
+ {
1666
+ acceptsMood: true,
1667
+ aspect: "1:1",
1668
+ clause: "Stylised architectural illustration of the room, colour laid as flat planes on walls, joinery and trim, fine hand-drawn line with a gentle gouache wash, clearly a drawing of a design decision rather than a photograph. No text, no people",
1669
+ description: "Line and gouache room drawings, for showing a colour scheme as a design idea.",
1670
+ experimental: true,
1671
+ id: "drawing",
1672
+ label: "Palette drawing",
1673
+ model: "gpt2"
1674
+ },
1675
+ {
1676
+ acceptsMood: false,
1677
+ aspect: "1:1",
1678
+ clause: "Straight-on orthographic photograph of the surface filling the entire frame edge to edge, even shadowless studio light, crisp macro texture, colour-accurate. No text, no logos",
1679
+ description: "Flat, edge-to-edge surface photographs, for textures and material swatches.",
1680
+ id: "plate",
1681
+ label: "Flat plate",
1682
+ model: "flux2-pro"
1683
+ },
1684
+ {
1685
+ acceptsMood: false,
1686
+ aspect: "1:1",
1687
+ clause: "Fine hand-engraved botanical plate with delicate hatching and dry brush, grey ink only, reaching near-black at its densest, on matte uncoated stock under flat even light, cropped mid-motif and running past all four edges, never simplified or cartoonish. No text",
1688
+ description: "Grey-ink botanical engravings that run off the edges, for patterns and backgrounds.",
1689
+ id: "engraved",
1690
+ label: "Engraved grey ink",
1691
+ model: "gpt2"
1692
+ },
1693
+ {
1694
+ acceptsMood: false,
1695
+ aspect: "2:3",
1696
+ clause: "Tightly cropped photograph of a single piece of late-1940s American printed matter, flat and square-on in even light, every pixel paper, letterpress and wood type, sun-faded ink, foxing, soft creases and thumbtack holes, era-correct typography, nothing that looks like a digital photo run through a filter",
1697
+ description: "Aged mid-century printed matter such as posters and cards, where the lettering matters.",
1698
+ id: "ephemera",
1699
+ label: "Period ephemera",
1700
+ model: "ideogram4"
1701
+ },
1702
+ {
1703
+ acceptsMood: false,
1704
+ aspect: "3:4",
1705
+ clause: "Physical mineral pigment and chalk gesso on coarse natural linen, two or three confident gestures, warm ivory, oatmeal, putty and soft charcoal, flat diffuse museum reproduction lighting, shown unframed. No text",
1706
+ description: "Loose abstract paintings on linen, for wall art and calm backgrounds.",
1707
+ id: "canvas",
1708
+ label: "Linen abstract",
1709
+ model: "banana"
1710
+ },
1711
+ {
1712
+ acceptsMood: true,
1713
+ aspect: "1:1",
1714
+ clause: "Editorial documentary portrait, muted warm palette, waist-up, unposed, plain clothing with no logos. No text",
1715
+ description: "Natural, unposed documentary portraits of people. Pair with a mood for the light.",
1716
+ id: "portrait",
1717
+ label: "Documentary portrait",
1718
+ model: "seedream45"
1719
+ },
1720
+ {
1721
+ acceptsMood: false,
1722
+ aspect: "1:1",
1723
+ clause: "A single matte object centred with generous empty space, soft diffused studio light, minimal and quiet, one committed colour. No text, no logos, no people",
1724
+ description: "One object in one colour on a clean ground, for icons and simple product shots.",
1725
+ id: "object",
1726
+ label: "Studio object",
1727
+ model: "flux2-pro"
1728
+ }
1729
+ ],
1730
+ mood: [
1731
+ {
1732
+ clause: "soft natural daylight from a window out of frame, gentle falloff into the corners, low contrast, even diffused light with soft, held highlights",
1733
+ description: "Soft, even daylight from a window. The safe default.",
1734
+ id: "window",
1735
+ label: "Window light"
1736
+ },
1737
+ {
1738
+ clause: "early morning light through tall glazing, cool and clear",
1739
+ description: "Cool, clear early morning light.",
1740
+ id: "dawn",
1741
+ label: "Dawn"
1742
+ },
1743
+ {
1744
+ clause: "low raking daylight from the left, long soft shadows that reveal texture",
1745
+ description: "Low side light that brings out surface texture.",
1746
+ id: "raking",
1747
+ label: "Raking light"
1748
+ },
1749
+ {
1750
+ clause: "overcast afternoon with rain on a tall window, soft even grey light",
1751
+ description: "Soft grey light on a rainy afternoon.",
1752
+ id: "overcast",
1753
+ label: "Overcast"
1754
+ },
1755
+ {
1756
+ clause: "evening, warm practical lamps around 2400K, candles and a lit fire, cosy and warm, never gloomy",
1757
+ description: "Warm evening light from lamps, candles and a fire.",
1758
+ id: "lamplit",
1759
+ label: "Lamplit evening"
1760
+ },
1761
+ {
1762
+ clause: "night, one warm low practical light, deep shadow, candlelit",
1763
+ description: "Dark night scene lit by one warm low light.",
1764
+ id: "nocturne",
1765
+ label: "Nocturne"
1766
+ }
1767
+ ]
1768
+ }).look;
1156
1769
  //#endregion
1157
1770
  //#region src/tool-registry/analysis.ts
1158
1771
  const ANALYSIS_TOOLS = {
@@ -1164,7 +1777,11 @@ const ANALYSIS_TOOLS = {
1164
1777
  inputKind: "images",
1165
1778
  name: "GOT-OCR 2.0",
1166
1779
  outputKeys: ["outputs"],
1167
- price: { kind: "metered" },
1780
+ price: {
1781
+ kind: "call",
1782
+ per: ["input_image_urls"],
1783
+ usd: .05
1784
+ },
1168
1785
  pricing: "$0.05/image",
1169
1786
  queued: true,
1170
1787
  sourceUrl: "https://fal.ai/models/fal-ai/got-ocr/v2",
@@ -1178,7 +1795,13 @@ const ANALYSIS_TOOLS = {
1178
1795
  inputKind: "image",
1179
1796
  name: "Moondream 3 Caption",
1180
1797
  outputKeys: ["output"],
1181
- price: { kind: "metered" },
1798
+ price: {
1799
+ inputPerMillion: .4,
1800
+ inputTokens: 737,
1801
+ kind: "token",
1802
+ outputPerMillion: 3.5,
1803
+ outputTokens: 200
1804
+ },
1182
1805
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1183
1806
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/caption",
1184
1807
  task: "image captioning"
@@ -1191,7 +1814,13 @@ const ANALYSIS_TOOLS = {
1191
1814
  inputKind: "image",
1192
1815
  name: "Moondream 3 Detect",
1193
1816
  outputKeys: ["objects", "image"],
1194
- price: { kind: "metered" },
1817
+ price: {
1818
+ inputPerMillion: .4,
1819
+ inputTokens: 737,
1820
+ kind: "token",
1821
+ outputPerMillion: 3.5,
1822
+ outputTokens: 100
1823
+ },
1195
1824
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1196
1825
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/detect",
1197
1826
  task: "object detection"
@@ -1204,7 +1833,13 @@ const ANALYSIS_TOOLS = {
1204
1833
  inputKind: "image",
1205
1834
  name: "Moondream 3 Point",
1206
1835
  outputKeys: ["points", "image"],
1207
- price: { kind: "metered" },
1836
+ price: {
1837
+ inputPerMillion: .4,
1838
+ inputTokens: 737,
1839
+ kind: "token",
1840
+ outputPerMillion: 3.5,
1841
+ outputTokens: 100
1842
+ },
1208
1843
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1209
1844
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/point",
1210
1845
  task: "object pointing"
@@ -1217,7 +1852,13 @@ const ANALYSIS_TOOLS = {
1217
1852
  inputKind: "image",
1218
1853
  name: "Moondream 3 Query",
1219
1854
  outputKeys: ["output", "reasoning"],
1220
- price: { kind: "metered" },
1855
+ price: {
1856
+ inputPerMillion: .4,
1857
+ inputTokens: 737,
1858
+ kind: "token",
1859
+ outputPerMillion: 3.5,
1860
+ outputTokens: 500
1861
+ },
1221
1862
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1222
1863
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/query",
1223
1864
  task: "visual question answering"
@@ -1230,7 +1871,11 @@ const ANALYSIS_TOOLS = {
1230
1871
  inputKind: "images",
1231
1872
  name: "NSFW Checker",
1232
1873
  outputKeys: ["has_nsfw_concepts"],
1233
- price: { kind: "metered" },
1874
+ price: {
1875
+ kind: "call",
1876
+ per: ["image_urls"],
1877
+ usd: .001
1878
+ },
1234
1879
  pricing: "$0.001/image",
1235
1880
  sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw",
1236
1881
  task: "vision moderation"
@@ -1314,6 +1959,34 @@ const ASSET_TOOLS = {
1314
1959
  sourceUrl: "https://fal.ai/models/fal-ai/image2svg",
1315
1960
  task: "raster to vector tracing"
1316
1961
  },
1962
+ "meshy-v7": {
1963
+ category: "3d",
1964
+ description: "Make a textured 3D mesh from one image, optionally rigged for animation.",
1965
+ endpoint: "meshy/v7/image-to-3d",
1966
+ inputField: "image_url",
1967
+ inputKind: "image",
1968
+ name: "Meshy v7 Image to 3D",
1969
+ outputKeys: [
1970
+ "model_glb",
1971
+ "model_urls",
1972
+ "thumbnail",
1973
+ "rigged_character_glb",
1974
+ "rigged_character_fbx"
1975
+ ],
1976
+ price: {
1977
+ extras: {
1978
+ enable_animation: .12,
1979
+ enable_rigging: .2,
1980
+ ultra_mode: .2
1981
+ },
1982
+ kind: "call",
1983
+ usd: 1.2
1984
+ },
1985
+ pricing: "$1.20/generation at the default textured model; $0.80 untextured, $1.40 ultra, plus $0.20 for rigging and $0.12 for animation",
1986
+ queued: true,
1987
+ sourceUrl: "https://fal.ai/models/meshy/v7/image-to-3d",
1988
+ task: "single-image 3D reconstruction"
1989
+ },
1317
1990
  patina: {
1318
1991
  category: "material",
1319
1992
  description: "Decompose a surface photograph into PBR maps: basecolor, normal, roughness, metalness, height.",
@@ -1332,7 +2005,12 @@ const ASSET_TOOLS = {
1332
2005
  ],
1333
2006
  fromOption: "maps"
1334
2007
  } },
1335
- price: { kind: "metered" },
2008
+ price: {
2009
+ base: .01,
2010
+ kind: "maps",
2011
+ perMapMegapixel: .01,
2012
+ perMegapixel: 0
2013
+ },
1336
2014
  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",
1337
2015
  queued: true,
1338
2016
  sourceUrl: "https://fal.ai/models/fal-ai/patina",
@@ -1356,8 +2034,17 @@ const ASSET_TOOLS = {
1356
2034
  ],
1357
2035
  fromOption: "maps"
1358
2036
  } },
1359
- price: { kind: "metered" },
1360
- pricing: "$0.10 base only; add $0.02/megapixel plus $0.01/megapixel per map, so 1MP with all 5 maps is $0.17",
2037
+ price: {
2038
+ base: .1,
2039
+ kind: "maps",
2040
+ perMapMegapixel: .01,
2041
+ perMegapixel: .02,
2042
+ upscale: {
2043
+ "2": .004,
2044
+ "4": .016
2045
+ }
2046
+ },
2047
+ pricing: "$0.10 plus $0.02/megapixel plus $0.01/megapixel per map, so 1MP with all 5 maps is $0.17; upscaling adds $0.004 (2x) or $0.016 (4x) per pre-upscale megapixel per map",
1361
2048
  queued: true,
1362
2049
  sourceUrl: "https://fal.ai/models/fal-ai/patina/material/extract",
1363
2050
  task: "tiling material extraction"
@@ -1370,8 +2057,11 @@ const ASSET_TOOLS = {
1370
2057
  inputKind: "image",
1371
2058
  name: "Qwen Image Layered",
1372
2059
  outputKeys: ["images"],
1373
- price: { kind: "metered" },
1374
- 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",
2060
+ price: {
2061
+ kind: "call",
2062
+ usd: .05
2063
+ },
2064
+ pricing: "$0.05 per image, one input image a call",
1375
2065
  queued: true,
1376
2066
  sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-layered",
1377
2067
  task: "image layer decomposition"
@@ -1438,7 +2128,6 @@ const ASSET_TOOLS = {
1438
2128
  },
1439
2129
  "sam3-3d-objects": {
1440
2130
  category: "3d",
1441
- defaultOptions: { prompt: "car" },
1442
2131
  description: "Reconstruct one or more 3D objects from an image and prompts.",
1443
2132
  endpoint: "fal-ai/sam-3/3d-objects",
1444
2133
  inputField: "image_url",
@@ -1544,7 +2233,10 @@ const BACKGROUND_TOOLS = {
1544
2233
  inputKind: "image",
1545
2234
  name: "BirefNet Background Removal",
1546
2235
  outputKeys: ["image", "mask_image"],
1547
- price: { kind: "metered" },
2236
+ price: {
2237
+ kind: "call",
2238
+ usd: 0
2239
+ },
1548
2240
  pricing: "$0/compute-second listed by fal",
1549
2241
  sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
1550
2242
  task: "image background removal"
@@ -1580,13 +2272,51 @@ const BACKGROUND_TOOLS = {
1580
2272
  outputKeys: ["video"],
1581
2273
  price: {
1582
2274
  kind: "second",
1583
- usd: .00425
2275
+ usd: .14
1584
2276
  },
1585
- pricing: "$0.00425/sec",
2277
+ pricing: "$0.14/sec",
1586
2278
  queued: true,
1587
2279
  sourceUrl: "https://fal.ai/models/bria/video/background-removal",
1588
2280
  task: "video background removal"
1589
2281
  },
2282
+ "bria-video-rmbg-v3": {
2283
+ category: "background",
2284
+ defaultOptions: {
2285
+ background_color: "Black",
2286
+ output_container_and_codec: "webm_vp9",
2287
+ preserve_audio: true
2288
+ },
2289
+ description: "Remove video backgrounds with Bria's VRMBG 3.0, with configurable output container.",
2290
+ endpoint: "bria/video/background-removal/v3",
2291
+ inputField: "video_url",
2292
+ inputKind: "video",
2293
+ name: "Bria Video Background Removal 3.0",
2294
+ outputKeys: ["video"],
2295
+ price: {
2296
+ kind: "second",
2297
+ usd: .05
2298
+ },
2299
+ pricing: "$0.05/sec",
2300
+ queued: true,
2301
+ sourceUrl: "https://fal.ai/models/bria/video/background-removal/v3",
2302
+ task: "video background removal"
2303
+ },
2304
+ "control-light": {
2305
+ category: "restoration",
2306
+ description: "Brighten a dark or underexposed photo, recovering detail while keeping its identity, geometry and colour.",
2307
+ endpoint: "fal-ai/control-light",
2308
+ inputField: "image_url",
2309
+ inputKind: "image",
2310
+ name: "Control Light",
2311
+ outputKeys: ["images"],
2312
+ price: {
2313
+ kind: "megapixel",
2314
+ usd: .03
2315
+ },
2316
+ pricing: "$0.03/megapixel",
2317
+ sourceUrl: "https://fal.ai/models/fal-ai/control-light",
2318
+ task: "low-light enhancement"
2319
+ },
1590
2320
  ddcolor: {
1591
2321
  category: "restoration",
1592
2322
  description: "Colourise black-and-white photographs.",
@@ -1612,7 +2342,10 @@ const BACKGROUND_TOOLS = {
1612
2342
  inputKind: "image",
1613
2343
  name: "Remove Background",
1614
2344
  outputKeys: ["image"],
1615
- price: { kind: "metered" },
2345
+ price: {
2346
+ kind: "call",
2347
+ usd: 0
2348
+ },
1616
2349
  pricing: "$0/compute-second listed by fal",
1617
2350
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
1618
2351
  task: "image background removal"
@@ -1645,8 +2378,9 @@ const BACKGROUND_TOOLS = {
1645
2378
  name: "Topaz Adjust",
1646
2379
  outputKeys: ["image"],
1647
2380
  price: {
1648
- kind: "megapixel",
1649
- usd: .08 / 24
2381
+ kind: "megapixel-step",
2382
+ megapixels: 24,
2383
+ usd: .08
1650
2384
  },
1651
2385
  pricing: "$0.08 per 24 output megapixels",
1652
2386
  queued: true,
@@ -1663,10 +2397,11 @@ const BACKGROUND_TOOLS = {
1663
2397
  name: "Topaz Creative Upscale",
1664
2398
  outputKeys: ["image"],
1665
2399
  price: {
1666
- kind: "megapixel",
1667
- usd: .96 / 24
2400
+ kind: "megapixel-step",
2401
+ megapixels: 2,
2402
+ usd: .08
1668
2403
  },
1669
- pricing: "$0.96 per 24 output megapixels",
2404
+ pricing: "$0.08 per started 2 output megapixels, any Bloom model",
1670
2405
  queued: true,
1671
2406
  sourceUrl: "https://fal.ai/models/topaz/upscale/image/creative",
1672
2407
  task: "creative image upscaling"
@@ -1681,8 +2416,9 @@ const BACKGROUND_TOOLS = {
1681
2416
  name: "Topaz Denoise",
1682
2417
  outputKeys: ["image"],
1683
2418
  price: {
1684
- kind: "megapixel",
1685
- usd: .08 / 24
2419
+ kind: "megapixel-step",
2420
+ megapixels: 24,
2421
+ usd: .08
1686
2422
  },
1687
2423
  pricing: "$0.08 per 24 output megapixels at the default Normal model; $0.16 with Denoise Max",
1688
2424
  queued: true,
@@ -1699,10 +2435,11 @@ const BACKGROUND_TOOLS = {
1699
2435
  name: "Topaz Generative Upscale",
1700
2436
  outputKeys: ["image"],
1701
2437
  price: {
1702
- kind: "megapixel",
1703
- usd: .24 / 24
2438
+ kind: "megapixel-step",
2439
+ megapixels: 8,
2440
+ usd: .08
1704
2441
  },
1705
- pricing: "$0.24 per 24 output megapixels at the default Wonder 3; $0.48 with Wonder, Wonder 2, Standard MAX, Redefine or Recover 3",
2442
+ pricing: "$0.08 per started 8 output megapixels at the default Wonder 3; per started 4 with Wonder, Wonder 2, Standard MAX, Redefine or Recover 3",
1706
2443
  queued: true,
1707
2444
  sourceUrl: "https://fal.ai/models/topaz/upscale/image/generative",
1708
2445
  task: "generative image upscaling"
@@ -1721,8 +2458,9 @@ const BACKGROUND_TOOLS = {
1721
2458
  name: "Topaz Image Upscale",
1722
2459
  outputKeys: ["image"],
1723
2460
  price: {
1724
- kind: "megapixel",
1725
- usd: .08 / 24
2461
+ kind: "megapixel-step",
2462
+ megapixels: 24,
2463
+ usd: .08
1726
2464
  },
1727
2465
  pricing: "$0.08 for output up to 24MP; $0.16 to 48MP, $0.32 to 96MP, up to $1.36 at 512MP",
1728
2466
  queued: true,
@@ -1739,8 +2477,9 @@ const BACKGROUND_TOOLS = {
1739
2477
  name: "Topaz Precision Upscale",
1740
2478
  outputKeys: ["image"],
1741
2479
  price: {
1742
- kind: "megapixel",
1743
- usd: .08 / 24
2480
+ kind: "megapixel-step",
2481
+ megapixels: 24,
2482
+ usd: .08
1744
2483
  },
1745
2484
  pricing: "$0.08 per 24 output megapixels, any precision model",
1746
2485
  queued: true,
@@ -1757,8 +2496,9 @@ const BACKGROUND_TOOLS = {
1757
2496
  name: "Topaz Restore",
1758
2497
  outputKeys: ["image"],
1759
2498
  price: {
1760
- kind: "megapixel",
1761
- usd: .48 / 24
2499
+ kind: "megapixel-step",
2500
+ megapixels: 24,
2501
+ usd: .48
1762
2502
  },
1763
2503
  pricing: "$0.48 per 24 output megapixels at the default Recover 3; $0.08 with Dust-Scratch V2",
1764
2504
  queued: true,
@@ -1775,8 +2515,9 @@ const BACKGROUND_TOOLS = {
1775
2515
  name: "Topaz Sharpen",
1776
2516
  outputKeys: ["image"],
1777
2517
  price: {
1778
- kind: "megapixel",
1779
- usd: .08 / 24
2518
+ kind: "megapixel-step",
2519
+ megapixels: 24,
2520
+ usd: .08
1780
2521
  },
1781
2522
  pricing: "$0.08 per 24 output megapixels at the default Standard model; $0.16 with Super Focus",
1782
2523
  queued: true,
@@ -1792,8 +2533,9 @@ const BACKGROUND_TOOLS = {
1792
2533
  name: "Topaz Transparent Upscale",
1793
2534
  outputKeys: ["image"],
1794
2535
  price: {
1795
- kind: "megapixel",
1796
- usd: .08 / 24
2536
+ kind: "megapixel-step",
2537
+ megapixels: 24,
2538
+ usd: .08
1797
2539
  },
1798
2540
  pricing: "$0.08 per 24 output megapixels",
1799
2541
  queued: true,
@@ -1813,10 +2555,22 @@ const BACKGROUND_TOOLS = {
1813
2555
  name: "Topaz Video Upscale",
1814
2556
  outputKeys: ["video"],
1815
2557
  price: {
1816
- kind: "second",
1817
- usd: .01
2558
+ doubleAtFps: 60,
2559
+ halfWithModel: "Gaia 2",
2560
+ kind: "video-second",
2561
+ tiers: [
2562
+ {
2563
+ upTo: 720,
2564
+ usd: .01
2565
+ },
2566
+ {
2567
+ upTo: 1080,
2568
+ usd: .02
2569
+ },
2570
+ { usd: .08 }
2571
+ ]
1818
2572
  },
1819
- pricing: "$0.01/sec up to 720p; $0.02 to 1080p, $0.08 above, doubled at 60fps, halved with Gaia 2",
2573
+ pricing: "$0.01/sec up to 720p output; $0.02 to 1080p, $0.08 above, doubled at 60fps, halved with Gaia 2",
1820
2574
  queued: true,
1821
2575
  sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
1822
2576
  task: "video enhancement"
@@ -1867,6 +2621,7 @@ const EDITING_TOOLS = {
1867
2621
  outputKeys: ["images"],
1868
2622
  price: {
1869
2623
  kind: "call",
2624
+ perImage: true,
1870
2625
  usd: .04
1871
2626
  },
1872
2627
  pricing: "$0.04/generation",
@@ -1897,7 +2652,11 @@ const EDITING_TOOLS = {
1897
2652
  inputKind: "image",
1898
2653
  name: "FLUX.2 Pro Outpaint",
1899
2654
  outputKeys: ["images"],
1900
- price: { kind: "metered" },
2655
+ price: {
2656
+ extra: .015,
2657
+ first: .03,
2658
+ kind: "megapixel-first"
2659
+ },
1901
2660
  pricing: "$0.03 for the first output megapixel, then $0.015 per extra megapixel of input and output, rounded up",
1902
2661
  sourceUrl: "https://fal.ai/models/fal-ai/flux-2-pro/outpaint",
1903
2662
  task: "image outpainting"
@@ -1928,6 +2687,7 @@ const EDITING_TOOLS = {
1928
2687
  outputKeys: ["images"],
1929
2688
  price: {
1930
2689
  kind: "call",
2690
+ perImage: true,
1931
2691
  usd: .06
1932
2692
  },
1933
2693
  pricing: "$0.06/image at the default BALANCED speed; $0.03 turbo, $0.09 quality",
@@ -2022,12 +2782,35 @@ const EDITING_TOOLS = {
2022
2782
  inputKind: "image",
2023
2783
  name: "Smart Resize",
2024
2784
  outputKeys: ["images", "results"],
2025
- price: { kind: "metered" },
2785
+ price: {
2786
+ fee: .05,
2787
+ kind: "call",
2788
+ multipliers: { resolution: { "4K": 2 } },
2789
+ per: ["target_sizes", "num_images_per_size"],
2790
+ usd: .15
2791
+ },
2026
2792
  pricing: "$0.15 per output image, doubled at 4K, plus a $0.05 vision analysis fee per request",
2027
2793
  queued: true,
2028
2794
  sourceUrl: "https://fal.ai/models/fal-ai/smart-resize",
2029
2795
  task: "multi-size recomposition"
2030
2796
  },
2797
+ "telestyle-v2": {
2798
+ category: "restyle",
2799
+ description: "Redraw a content image in the style of a second, style image.",
2800
+ endpoint: "fal-ai/telestyle-v2",
2801
+ inputField: "content_image_url",
2802
+ inputKind: "image",
2803
+ name: "TeleStyle v2",
2804
+ outputKeys: ["images"],
2805
+ price: {
2806
+ kind: "megapixel",
2807
+ usd: .035
2808
+ },
2809
+ pricing: "$0.035/megapixel",
2810
+ referenceField: "style_image_url",
2811
+ sourceUrl: "https://fal.ai/models/fal-ai/telestyle-v2",
2812
+ task: "style transfer from a reference image"
2813
+ },
2031
2814
  "text-removal": {
2032
2815
  category: "erase",
2033
2816
  defaultOptions: { output_format: "png" },
@@ -2044,6 +2827,25 @@ const EDITING_TOOLS = {
2044
2827
  pricing: "$0.04/image",
2045
2828
  sourceUrl: "https://fal.ai/models/fal-ai/image-editing/text-removal",
2046
2829
  task: "text removal"
2830
+ },
2831
+ "virtual-try-on": {
2832
+ category: "try-on",
2833
+ description: "Dress the person in one image in the garment shown in another.",
2834
+ endpoint: "google/virtual-try-on",
2835
+ inputField: "person_image_url",
2836
+ inputKind: "image",
2837
+ name: "Google Virtual Try-On",
2838
+ outputKeys: ["images"],
2839
+ price: {
2840
+ kind: "call",
2841
+ perImage: true,
2842
+ usd: .075
2843
+ },
2844
+ pricing: "$0.075/image",
2845
+ queued: true,
2846
+ referenceField: "product_image_url",
2847
+ sourceUrl: "https://fal.ai/models/google/virtual-try-on",
2848
+ task: "virtual garment try-on"
2047
2849
  }
2048
2850
  };
2049
2851
  //#endregion
@@ -2057,7 +2859,10 @@ const STRUCTURE_TOOLS = {
2057
2859
  inputKind: "image",
2058
2860
  name: "Depth Anything v2 Preprocessor",
2059
2861
  outputKeys: ["image"],
2060
- price: { kind: "metered" },
2862
+ price: {
2863
+ kind: "call",
2864
+ usd: 0
2865
+ },
2061
2866
  pricing: "$0/compute-second listed by fal",
2062
2867
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/depth-anything/v2",
2063
2868
  task: "depth preprocessing"
@@ -2086,7 +2891,10 @@ const STRUCTURE_TOOLS = {
2086
2891
  inputKind: "image",
2087
2892
  name: "HED Edge Preprocessor",
2088
2893
  outputKeys: ["image"],
2089
- price: { kind: "metered" },
2894
+ price: {
2895
+ kind: "call",
2896
+ usd: 0
2897
+ },
2090
2898
  pricing: "$0/compute-second listed by fal",
2091
2899
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/hed",
2092
2900
  task: "edge preprocessing"
@@ -2100,7 +2908,10 @@ const STRUCTURE_TOOLS = {
2100
2908
  inputKind: "image",
2101
2909
  name: "Line Art Preprocessor",
2102
2910
  outputKeys: ["image"],
2103
- price: { kind: "metered" },
2911
+ price: {
2912
+ kind: "call",
2913
+ usd: 0
2914
+ },
2104
2915
  pricing: "$0/compute-second listed by fal",
2105
2916
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/lineart",
2106
2917
  task: "image preprocessing"
@@ -2117,7 +2928,10 @@ const STRUCTURE_TOOLS = {
2117
2928
  inputKind: "image",
2118
2929
  name: "Marigold Depth Estimation",
2119
2930
  outputKeys: ["image"],
2120
- price: { kind: "metered" },
2931
+ price: {
2932
+ kind: "call",
2933
+ usd: 0
2934
+ },
2121
2935
  pricing: "$0/compute-second listed by fal",
2122
2936
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/marigold-depth",
2123
2937
  task: "depth map"
@@ -2134,7 +2948,10 @@ const STRUCTURE_TOOLS = {
2134
2948
  inputKind: "image",
2135
2949
  name: "MiDaS Depth Estimation",
2136
2950
  outputKeys: ["image"],
2137
- price: { kind: "metered" },
2951
+ price: {
2952
+ kind: "call",
2953
+ usd: 0
2954
+ },
2138
2955
  pricing: "$0/compute-second listed by fal",
2139
2956
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/depth",
2140
2957
  task: "depth map"
@@ -2147,7 +2964,10 @@ const STRUCTURE_TOOLS = {
2147
2964
  inputKind: "image",
2148
2965
  name: "MiDaS Preprocessor",
2149
2966
  outputKeys: ["depth_map", "normal_map"],
2150
- price: { kind: "metered" },
2967
+ price: {
2968
+ kind: "call",
2969
+ usd: 0
2970
+ },
2151
2971
  pricing: "$0/compute-second listed by fal",
2152
2972
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/midas",
2153
2973
  task: "depth and normal preprocessing"
@@ -2160,7 +2980,10 @@ const STRUCTURE_TOOLS = {
2160
2980
  inputKind: "image",
2161
2981
  name: "M-LSD Line Preprocessor",
2162
2982
  outputKeys: ["image"],
2163
- price: { kind: "metered" },
2983
+ price: {
2984
+ kind: "call",
2985
+ usd: 0
2986
+ },
2164
2987
  pricing: "$0/compute-second listed by fal",
2165
2988
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/mlsd",
2166
2989
  task: "line segment preprocessing"
@@ -2173,7 +2996,10 @@ const STRUCTURE_TOOLS = {
2173
2996
  inputKind: "image",
2174
2997
  name: "PiDiNet Edge Preprocessor",
2175
2998
  outputKeys: ["image"],
2176
- price: { kind: "metered" },
2999
+ price: {
3000
+ kind: "call",
3001
+ usd: 0
3002
+ },
2177
3003
  pricing: "$0/compute-second listed by fal",
2178
3004
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/pidi",
2179
3005
  task: "edge preprocessing"
@@ -2186,7 +3012,10 @@ const STRUCTURE_TOOLS = {
2186
3012
  inputKind: "image",
2187
3013
  name: "SAM Preprocessor",
2188
3014
  outputKeys: ["image"],
2189
- price: { kind: "metered" },
3015
+ price: {
3016
+ kind: "call",
3017
+ usd: 0
3018
+ },
2190
3019
  pricing: "$0/compute-second listed by fal",
2191
3020
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/sam",
2192
3021
  task: "segmentation preprocessing"
@@ -2206,7 +3035,10 @@ const STRUCTURE_TOOLS = {
2206
3035
  inputKind: "image",
2207
3036
  name: "SAM 2 Auto Segment",
2208
3037
  outputKeys: ["combined_mask", "individual_masks"],
2209
- price: { kind: "metered" },
3038
+ price: {
3039
+ kind: "call",
3040
+ usd: 0
3041
+ },
2210
3042
  pricing: "$0/compute-second listed by fal",
2211
3043
  queued: true,
2212
3044
  sourceUrl: "https://fal.ai/models/fal-ai/sam2/auto-segment",
@@ -2251,7 +3083,11 @@ const STRUCTURE_TOOLS = {
2251
3083
  inputKind: "video",
2252
3084
  name: "SAM 3.1 Video",
2253
3085
  outputKeys: ["video", "boundingbox_frames_zip"],
2254
- price: { kind: "metered" },
3086
+ price: {
3087
+ frames: 16,
3088
+ kind: "frames",
3089
+ usd: .01
3090
+ },
2255
3091
  pricing: "$0.01/16 frames of video input",
2256
3092
  queued: true,
2257
3093
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/video",
@@ -2323,7 +3159,11 @@ const STRUCTURE_TOOLS = {
2323
3159
  inputKind: "video",
2324
3160
  name: "SAM 3 Video",
2325
3161
  outputKeys: ["video", "boundingbox_frames_zip"],
2326
- price: { kind: "metered" },
3162
+ price: {
3163
+ frames: 16,
3164
+ kind: "frames",
3165
+ usd: .005
3166
+ },
2327
3167
  pricing: "$0.005/16 frames of video input",
2328
3168
  queued: true,
2329
3169
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video",
@@ -2342,7 +3182,11 @@ const STRUCTURE_TOOLS = {
2342
3182
  inputKind: "video",
2343
3183
  name: "SAM 3 Video RLE",
2344
3184
  outputKeys: ["video", "boundingbox_frames_zip"],
2345
- price: { kind: "metered" },
3185
+ price: {
3186
+ frames: 16,
3187
+ kind: "frames",
3188
+ usd: .005
3189
+ },
2346
3190
  pricing: "$0.005/16 frames of video",
2347
3191
  queued: true,
2348
3192
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video-rle",
@@ -2356,7 +3200,10 @@ const STRUCTURE_TOOLS = {
2356
3200
  inputKind: "image",
2357
3201
  name: "Scribble Preprocessor",
2358
3202
  outputKeys: ["image"],
2359
- price: { kind: "metered" },
3203
+ price: {
3204
+ kind: "call",
3205
+ usd: 0
3206
+ },
2360
3207
  pricing: "$0/compute-second listed by fal",
2361
3208
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/scribble",
2362
3209
  task: "scribble preprocessing"
@@ -2369,7 +3216,10 @@ const STRUCTURE_TOOLS = {
2369
3216
  inputKind: "image",
2370
3217
  name: "TEED Edge Preprocessor",
2371
3218
  outputKeys: ["image"],
2372
- price: { kind: "metered" },
3219
+ price: {
3220
+ kind: "call",
3221
+ usd: 0
3222
+ },
2373
3223
  pricing: "$0/compute-second listed by fal",
2374
3224
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/teed",
2375
3225
  task: "edge preprocessing"
@@ -2382,7 +3232,10 @@ const STRUCTURE_TOOLS = {
2382
3232
  inputKind: "image",
2383
3233
  name: "ZoeDepth Preprocessor",
2384
3234
  outputKeys: ["image"],
2385
- price: { kind: "metered" },
3235
+ price: {
3236
+ kind: "call",
3237
+ usd: 0
3238
+ },
2386
3239
  pricing: "$0/compute-second listed by fal",
2387
3240
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/zoe",
2388
3241
  task: "depth preprocessing"
@@ -2399,6 +3252,17 @@ const FAL_TOOLS = {
2399
3252
  };
2400
3253
  Object.keys(FAL_TOOLS);
2401
3254
  //#endregion
3255
+ //#region src/image/fetch.ts
3256
+ function urlOf(input) {
3257
+ if (typeof input === "string") return input;
3258
+ return input instanceof URL ? input.href : input.url;
3259
+ }
3260
+ /** Adapt a configured fetch for a provider factory; undefined keeps global fetch. */
3261
+ function toProviderFetch(fetch) {
3262
+ if (fetch === void 0) return {};
3263
+ return { fetch: async (input, init) => await fetch(urlOf(input), init ?? {}) };
3264
+ }
3265
+ //#endregion
2402
3266
  //#region src/image/fal.ts
2403
3267
  /**
2404
3268
  * fal provider adapter.
@@ -2416,35 +3280,22 @@ Object.keys(FAL_TOOLS);
2416
3280
  * need a specific size. Example:
2417
3281
  * img.generate({
2418
3282
  * provider: "fal",
2419
- * tier: "balanced",
3283
+ * model: "fal-ai/gpt-image-1.5",
2420
3284
  * prompt: "...",
2421
3285
  * providerOptions: { fal: { image_size: "1024x1024" } },
2422
3286
  * });
2423
3287
  */
2424
3288
  /**
2425
- * fal model ids by tier. `fast` uses FLUX Pro Ultra (fast, cheap); the higher
2426
- * tiers use fal's gpt-image endpoint for its edit quality. Both are proven fal
2427
- * endpoints that also exist in the `../models` registry snapshot.
3289
+ * fal endpoint ids proven to work with this adapter and priced in
3290
+ * {@link FAL_IMAGE_PRICE_USD} below (FLUX Pro Ultra + gpt-image).
2428
3291
  */
2429
3292
  const FAL_FLUX_MODEL = "fal-ai/flux-pro/v1.1-ultra";
2430
3293
  const FAL_GPT_IMAGE_MODEL = "fal-ai/gpt-image-1.5";
2431
- /**
2432
- * Tier → fal model id. For fal, explicit `model:` endpoint ids are the primary
2433
- * path (any fal endpoint resolves via passthrough); this tier map is a
2434
- * convenience covering the two most common (FLUX Pro Ultra + gpt-image).
2435
- */
2436
- const FAL_TIER_MODELS = {
2437
- fast: FAL_FLUX_MODEL,
2438
- balanced: FAL_GPT_IMAGE_MODEL,
2439
- quality: FAL_GPT_IMAGE_MODEL,
2440
- hero: FAL_GPT_IMAGE_MODEL
2441
- };
2442
3294
  /** Env var read for the fal key when `apiKey` is not supplied in config. */
2443
3295
  const FAL_API_KEY_ENV = "FAL_KEY";
2444
3296
  /**
2445
- * Static fal USD/image, keyed by the fal ENDPOINT id. Explicit `model:` endpoint
2446
- * ids are fal's primary path (the tier map covers only two), so cost tracking
2447
- * must price the endpoints consumers actually pass — kiln uses
3297
+ * Static fal USD/image, keyed by the fal ENDPOINT id. Cost tracking must price
3298
+ * the endpoints consumers actually pass — kiln uses
2448
3299
  * `fal-ai/flux/schnell`, `fal-ai/flux-2-pro`, `fal-ai/flux-2-max`. Every price is
2449
3300
  * sourced from the fal registry snapshot in `../models`
2450
3301
  * (`MODELS[...].pricePerImageUsd`) so this table stays in sync rather than
@@ -2471,12 +3322,17 @@ const FAL_IMAGE_PRICE_USD = {
2471
3322
  "fal-ai/bytedance/seedream/v4/text-to-image": MODELS.seedream4?.pricePerImageUsd ?? .03,
2472
3323
  "fal-ai/bytedance/seedream/v4.5/text-to-image": MODELS.seedream45?.pricePerImageUsd ?? .04,
2473
3324
  "bytedance/seedream/v5/pro/text-to-image": MODELS.seedream5?.pricePerImageUsd ?? .0675,
2474
- "fal-ai/bytedance/seedream/v5/lite/text-to-image": MODELS["seedream5-lite"]?.pricePerImageUsd ?? .035,
2475
- "fal-ai/recraft-v3": MODELS.recraft?.pricePerImageUsd ?? .04,
3325
+ "bytedance/seedream/v5/lite/text-to-image": MODELS["seedream5-lite"]?.pricePerImageUsd ?? .035,
3326
+ "fal-ai/recraft/v3/text-to-image": MODELS.recraft?.pricePerImageUsd ?? .04,
2476
3327
  "fal-ai/recraft/v4/text-to-image": MODELS.recraft4?.pricePerImageUsd ?? .04,
3328
+ "fal-ai/recraft/v4.1/text-to-image": MODELS.recraft41?.pricePerImageUsd ?? .035,
2477
3329
  "fal-ai/ideogram/v3": MODELS.ideogram?.pricePerImageUsd ?? .03,
2478
3330
  "ideogram/v4": MODELS.ideogram4?.pricePerImageUsd ?? .03,
2479
3331
  "xai/grok-imagine-image": MODELS["grok-image"]?.pricePerImageUsd ?? .02,
3332
+ "xai/grok-imagine-image/v2.0/text-to-image": MODELS["grok-image-2"]?.pricePerImageUsd ?? .06,
3333
+ "microsoft/mai-image-2.5-pro": MODELS["mai-image-2.5-pro"]?.pricePerImageUsd ?? .17,
3334
+ "fal-ai/ideogram/v3/generate-transparent": MODELS["ideogram3-transparent"]?.pricePerImageUsd ?? .06,
3335
+ "alibaba/qwen-image-3/text-to-image": MODELS.qwen3?.pricePerImageUsd ?? .04,
2480
3336
  "fal-ai/qwen-image": MODELS.qwen?.pricePerImageUsd ?? .02
2481
3337
  };
2482
3338
  /**
@@ -2484,15 +3340,17 @@ const FAL_IMAGE_PRICE_USD = {
2484
3340
  * var. Throws `MotifError` when neither is present (callers translate this into
2485
3341
  * a `Result.err`).
2486
3342
  */
2487
- function resolveModel$3(modelId, apiKey) {
3343
+ function resolveModel$3(modelId, apiKey, fetch) {
2488
3344
  const key = apiKey ?? process.env["FAL_KEY"];
2489
3345
  if (key === void 0 || key === "") throw new MotifError(`fal image generation requires an API key (config.fal.apiKey or ${FAL_API_KEY_ENV})`, 0);
2490
- return createFal({ apiKey: key }).image(modelId);
3346
+ return createFal({
3347
+ apiKey: key,
3348
+ ...toProviderFetch(fetch)
3349
+ }).image(modelId);
2491
3350
  }
2492
3351
  /** The fal provider adapter registered in the provider registry. */
2493
3352
  const falAdapter = {
2494
3353
  id: "fal",
2495
- tierModels: FAL_TIER_MODELS,
2496
3354
  apiKeyEnv: FAL_API_KEY_ENV,
2497
3355
  resolveModel: resolveModel$3,
2498
3356
  priceUsdByModel: FAL_IMAGE_PRICE_USD
@@ -2508,19 +3366,6 @@ const falAdapter = {
2508
3366
  * multi-image-in → image-out editing (with an optional mask), which is the core
2509
3367
  * operation this layer normalizes.
2510
3368
  */
2511
- /**
2512
- * Tier → Gemini image model id.
2513
- *
2514
- * Seeded from Material Desk's `RENDER_IMAGE_MODEL_BY_QUALITY` (the driving
2515
- * consumer, see the design doc). `gemini-2.5-flash-image` is the proven-reachable
2516
- * floor; the preview ids may require allowlist/tier access.
2517
- */
2518
- const GOOGLE_TIER_MODELS = {
2519
- fast: "gemini-2.5-flash-image",
2520
- balanced: "gemini-3.1-flash-image-preview",
2521
- quality: "gemini-3-pro-image-preview",
2522
- hero: "gemini-3-pro-image-preview"
2523
- };
2524
3369
  /** Env var read for the Google API key when `apiKey` is not supplied in config. */
2525
3370
  const GOOGLE_API_KEY_ENV = "GOOGLE_GENERATIVE_AI_API_KEY";
2526
3371
  /**
@@ -2550,15 +3395,17 @@ const GOOGLE_IMAGE_PRICE_USD = {
2550
3395
  * `GOOGLE_GENERATIVE_AI_API_KEY` env var. Throws `MotifError` when neither is
2551
3396
  * present (callers translate this into a `Result.err`).
2552
3397
  */
2553
- function resolveModel$2(modelId, apiKey) {
3398
+ function resolveModel$2(modelId, apiKey, fetch) {
2554
3399
  const key = apiKey ?? process.env["GOOGLE_GENERATIVE_AI_API_KEY"];
2555
3400
  if (key === void 0 || key === "") throw new MotifError(`Google image generation requires an API key (config.google.apiKey or ${GOOGLE_API_KEY_ENV})`, 0);
2556
- return createGoogleGenerativeAI({ apiKey: key }).image(modelId);
3401
+ return createGoogleGenerativeAI({
3402
+ apiKey: key,
3403
+ ...toProviderFetch(fetch)
3404
+ }).image(modelId);
2557
3405
  }
2558
3406
  /** The Google (Gemini) provider adapter registered in the provider registry. */
2559
3407
  const googleAdapter = {
2560
3408
  id: "google",
2561
- tierModels: GOOGLE_TIER_MODELS,
2562
3409
  apiKeyEnv: GOOGLE_API_KEY_ENV,
2563
3410
  resolveModel: resolveModel$2,
2564
3411
  priceUsdByModel: GOOGLE_IMAGE_PRICE_USD
@@ -2572,20 +3419,6 @@ const googleAdapter = {
2572
3419
  * performs no network I/O — the request only happens when `generateImage`
2573
3420
  * invokes `model.doGenerate`.
2574
3421
  */
2575
- /**
2576
- * OpenAI's image API currently exposes a single gpt-image model, so every tier
2577
- * maps to `gpt-image-1`. Tiers will later differ by `quality`
2578
- * (low/medium/high/auto) passed via `providerOptions.openai` (Phase 1c); until
2579
- * then the tier only selects this one model.
2580
- */
2581
- const OPENAI_MODEL = "gpt-image-1";
2582
- /** Tier → OpenAI image model id (all tiers → the single gpt-image model). */
2583
- const OPENAI_TIER_MODELS = {
2584
- fast: OPENAI_MODEL,
2585
- balanced: OPENAI_MODEL,
2586
- quality: OPENAI_MODEL,
2587
- hero: OPENAI_MODEL
2588
- };
2589
3422
  /** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
2590
3423
  const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
2591
3424
  /**
@@ -2597,6 +3430,11 @@ const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
2597
3430
  * Source: https://platform.openai.com/docs/pricing (image generation)
2598
3431
  * Sanity anchor: the Phase 0 benchmark measured gpt-image direct at $0.042
2599
3432
  * (vs $0.133 via fal — see docs/design/provider-agnostic-image-layer.md §10).
3433
+ *
3434
+ * GPT Image 2.5 is token-priced, with no published per-image estimate. Leave
3435
+ * these models absent so cost remains unknown unless supplied by the provider.
3436
+ * https://developers.openai.com/api/docs/models/gpt-image-2.5-flare
3437
+ * https://developers.openai.com/api/docs/models/gpt-image-2.5-sunburst
2600
3438
  */
2601
3439
  const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
2602
3440
  /**
@@ -2604,15 +3442,17 @@ const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
2604
3442
  * `OPENAI_API_KEY` env var. Throws `MotifError` when neither is present
2605
3443
  * (callers translate this into a `Result.err`).
2606
3444
  */
2607
- function resolveModel$1(modelId, apiKey) {
3445
+ function resolveModel$1(modelId, apiKey, fetch) {
2608
3446
  const key = apiKey ?? process.env["OPENAI_API_KEY"];
2609
3447
  if (key === void 0 || key === "") throw new MotifError(`OpenAI image generation requires an API key (config.openai.apiKey or ${OPENAI_API_KEY_ENV})`, 0);
2610
- return createOpenAI({ apiKey: key }).image(modelId);
3448
+ return createOpenAI({
3449
+ apiKey: key,
3450
+ ...toProviderFetch(fetch)
3451
+ }).image(modelId);
2611
3452
  }
2612
3453
  /** The OpenAI provider adapter registered in the provider registry. */
2613
3454
  const openaiAdapter = {
2614
3455
  id: "openai",
2615
- tierModels: OPENAI_TIER_MODELS,
2616
3456
  apiKeyEnv: OPENAI_API_KEY_ENV,
2617
3457
  resolveModel: resolveModel$1,
2618
3458
  priceUsdByModel: OPENAI_IMAGE_PRICE_USD
@@ -2629,19 +3469,6 @@ const openaiAdapter = {
2629
3469
  * NOTE: Replicate's SDK names the credential option `apiToken` (not `apiKey`),
2630
3470
  * and reads `REPLICATE_API_TOKEN` from the environment.
2631
3471
  */
2632
- /**
2633
- * Replicate is wired to a single high-quality model for now, so every tier maps
2634
- * to FLUX 1.1 Pro Ultra. (The benchmark found Replicate ~1.45× faster than fal
2635
- * for this model at the same price — see the design doc §10.)
2636
- */
2637
- const REPLICATE_MODEL = "black-forest-labs/flux-1.1-pro-ultra";
2638
- /** Tier → Replicate model id (all tiers → FLUX 1.1 Pro Ultra for now). */
2639
- const REPLICATE_TIER_MODELS = {
2640
- fast: REPLICATE_MODEL,
2641
- balanced: REPLICATE_MODEL,
2642
- quality: REPLICATE_MODEL,
2643
- hero: REPLICATE_MODEL
2644
- };
2645
3472
  /** Env var read for the Replicate API token when `apiToken` is not in config. */
2646
3473
  const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
2647
3474
  /**
@@ -2658,10 +3485,13 @@ const REPLICATE_IMAGE_PRICE_USD = { "black-forest-labs/flux-1.1-pro-ultra": .06
2658
3485
  * token), else the `REPLICATE_API_TOKEN` env var. Throws `MotifError` when
2659
3486
  * neither is present (callers translate this into a `Result.err`).
2660
3487
  */
2661
- function resolveModel(modelId, apiKey) {
3488
+ function resolveModel(modelId, apiKey, fetch) {
2662
3489
  const token = apiKey ?? process.env["REPLICATE_API_TOKEN"];
2663
3490
  if (token === void 0 || token === "") throw new MotifError(`Replicate image generation requires an API token (config.replicate.apiToken or ${REPLICATE_API_KEY_ENV})`, 0);
2664
- return createReplicate({ apiToken: token }).image(modelId);
3491
+ return createReplicate({
3492
+ apiToken: token,
3493
+ ...toProviderFetch(fetch)
3494
+ }).image(modelId);
2665
3495
  }
2666
3496
  //#endregion
2667
3497
  //#region src/image/provider.ts
@@ -2675,7 +3505,6 @@ const PROVIDERS = {
2675
3505
  openai: openaiAdapter,
2676
3506
  replicate: {
2677
3507
  id: "replicate",
2678
- tierModels: REPLICATE_TIER_MODELS,
2679
3508
  apiKeyEnv: REPLICATE_API_KEY_ENV,
2680
3509
  resolveModel,
2681
3510
  priceUsdByModel: REPLICATE_IMAGE_PRICE_USD
@@ -2719,6 +3548,9 @@ function costFromProviderMetadata(providerMetadata) {
2719
3548
  }
2720
3549
  }
2721
3550
  /** Static per-image USD for a (provider, model), or undefined if unknown. */
3551
+ function providerPricePerImageUsd(provider, modelId) {
3552
+ return tablePricePerImage(provider, modelId);
3553
+ }
2722
3554
  function tablePricePerImage(provider, modelId) {
2723
3555
  const adapter = PROVIDERS[provider];
2724
3556
  if (adapter === void 0) return;
@@ -2753,8 +3585,8 @@ function costForImages(provider, modelId, providerMetadata, imageCount) {
2753
3585
  * `@howells/motif-sdk/image` — provider-agnostic image generation + editing.
2754
3586
  *
2755
3587
  * ESM-only subpath export, built on the Vercel AI SDK image interface
2756
- * (`generateImage`, `@ai-sdk/*`). Additive to the fal-specific `FalClient`
2757
- * surface; reuses the SDK's Result convention (`Result<T, MotifError>` — no
3588
+ * (`generateImage`, `@ai-sdk/*`). The caller names the provider and model;
3589
+ * reuses the SDK's Result convention (`Result<T, MotifError>` — no
2758
3590
  * thrown exceptions). Google (Gemini) is the only provider in Phase 1a.
2759
3591
  *
2760
3592
  * @example
@@ -2762,11 +3594,10 @@ function costForImages(provider, modelId, providerMetadata, imageCount) {
2762
3594
  * import { createMotifImage } from "@howells/motif-sdk/image";
2763
3595
  *
2764
3596
  * const img = createMotifImage({ defaultProvider: "google" });
2765
- * const r = await img.generate({ tier: "fast", prompt: "a bare concrete wall" });
3597
+ * const r = await img.generate({ model: "gemini-2.5-flash-image", prompt: "a bare concrete wall" });
2766
3598
  * if (r.isOk()) console.log(r.value.images[0].mediaType, r.value.cost);
2767
3599
  * ```
2768
3600
  */
2769
- const DEFAULT_TIER = "balanced";
2770
3601
  const DEFAULT_PROVIDER = "google";
2771
3602
  /**
2772
3603
  * Create a provider-agnostic image client.
@@ -2793,8 +3624,8 @@ function createMotifImage(config = {}, deps = {}) {
2793
3624
  async function generate(opts) {
2794
3625
  const provider = resolveProvider(opts.provider);
2795
3626
  try {
2796
- const modelId = resolveModelId(provider, opts.model, opts.tier);
2797
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider));
3627
+ const modelId = opts.model;
3628
+ const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
2798
3629
  const result = await generateImageFn({
2799
3630
  model,
2800
3631
  prompt: opts.prompt,
@@ -2802,6 +3633,7 @@ function createMotifImage(config = {}, deps = {}) {
2802
3633
  ...opts.size === void 0 ? {} : { size: opts.size },
2803
3634
  ...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
2804
3635
  ...opts.seed === void 0 ? {} : { seed: opts.seed },
3636
+ ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
2805
3637
  ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
2806
3638
  ...opts.headers === void 0 ? {} : { headers: opts.headers },
2807
3639
  ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
@@ -2814,8 +3646,8 @@ function createMotifImage(config = {}, deps = {}) {
2814
3646
  async function edit(opts) {
2815
3647
  const provider = resolveProvider(opts.provider);
2816
3648
  try {
2817
- const modelId = resolveModelId(provider, opts.model, opts.tier);
2818
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider));
3649
+ const modelId = opts.model;
3650
+ const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
2819
3651
  const result = await generateImageFn({
2820
3652
  model,
2821
3653
  prompt: {
@@ -2825,6 +3657,7 @@ function createMotifImage(config = {}, deps = {}) {
2825
3657
  },
2826
3658
  ...opts.n === void 0 ? {} : { n: opts.n },
2827
3659
  ...opts.seed === void 0 ? {} : { seed: opts.seed },
3660
+ ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
2828
3661
  ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
2829
3662
  ...opts.headers === void 0 ? {} : { headers: opts.headers },
2830
3663
  ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
@@ -2910,14 +3743,8 @@ async function runBestOfN(opts, generate, edit) {
2910
3743
  }
2911
3744
  }
2912
3745
  /** Default provider dispatch: resolve the model through the provider registry. */
2913
- function defaultResolveModel(provider, modelId, apiKey) {
2914
- return getProviderAdapter(provider).resolveModel(modelId, apiKey);
2915
- }
2916
- /** Resolve the model id: explicit `model` wins, else the provider's tier map. */
2917
- function resolveModelId(provider, model, tier) {
2918
- if (model !== void 0 && model !== "") return model;
2919
- const resolvedTier = tier ?? DEFAULT_TIER;
2920
- return getProviderAdapter(provider).tierModels[resolvedTier];
3746
+ function defaultResolveModel(provider, modelId, apiKey, fetch) {
3747
+ return getProviderAdapter(provider).resolveModel(modelId, apiKey, fetch);
2921
3748
  }
2922
3749
  /** Map the AI SDK result → normalized MotifImageResult (with cost + requestId). */
2923
3750
  function toMotifImageResult(result, provider, model) {
@@ -3009,7 +3836,10 @@ function toMotifError(error) {
3009
3836
  if (error instanceof MotifError) return error;
3010
3837
  const message = error instanceof Error ? error.message : String(error);
3011
3838
  const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
3012
- return new MotifError(message, error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : 0, code);
3839
+ const status = error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : 0;
3840
+ const body = error instanceof Error && "responseBody" in error && typeof error.responseBody === "string" ? error.responseBody : message;
3841
+ if (isFalAccountLocked(status, body)) return falHttpError(status, body);
3842
+ return new MotifError(message, status, code);
3013
3843
  }
3014
3844
  //#endregion
3015
- export { FAL_API_KEY_ENV, FAL_TIER_MODELS, GOOGLE_API_KEY_ENV, GOOGLE_TIER_MODELS, OPENAI_API_KEY_ENV, OPENAI_TIER_MODELS, PROVIDERS, REPLICATE_API_KEY_ENV, REPLICATE_TIER_MODELS, costForImages, costFromProviderMetadata, createMotifImage, getProviderAdapter };
3845
+ export { FAL_API_KEY_ENV, GOOGLE_API_KEY_ENV, OPENAI_API_KEY_ENV, PROVIDERS, REPLICATE_API_KEY_ENV, costForImages, costFromProviderMetadata, createMotifImage, getProviderAdapter, providerPricePerImageUsd };