@howells/motif-sdk 2.0.0 → 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,13 +4,69 @@ 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 = {
13
62
  flare: {
63
+ customImageSize: {
64
+ maxEdge: 3840,
65
+ maxPixels: 8294400,
66
+ maxRatio: 3,
67
+ minPixels: 655360,
68
+ multipleOf: 16
69
+ },
14
70
  editEndpoint: "openai/gpt-image-2.5/flare/edit",
15
71
  endpoint: "openai/gpt-image-2.5/flare/text-to-image",
16
72
  maxReferenceImages: 16,
@@ -40,6 +96,13 @@ const MODELS = {
40
96
  useQueue: true
41
97
  },
42
98
  sunburst: {
99
+ customImageSize: {
100
+ maxEdge: 3840,
101
+ maxPixels: 8294400,
102
+ maxRatio: 3,
103
+ minPixels: 655360,
104
+ multipleOf: 16
105
+ },
43
106
  editEndpoint: "openai/gpt-image-2.5/sunburst/edit",
44
107
  endpoint: "openai/gpt-image-2.5/sunburst/text-to-image",
45
108
  maxReferenceImages: 16,
@@ -94,7 +157,14 @@ const MODELS = {
94
157
  },
95
158
  useCase: "Highest-ranked text-to-image quality and transparent PNGs"
96
159
  },
97
- 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",
98
168
  endpoint: "openai/gpt-image-2",
99
169
  falPricing: {
100
170
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -113,11 +183,18 @@ const MODELS = {
113
183
  supportsAspect: true,
114
184
  supportsEdit: true,
115
185
  supportsMaskImage: true,
186
+ maskImageField: "mask_url",
116
187
  supportsNumImages: true,
117
188
  supportsOutputFormat: true,
118
189
  supportsQuality: true,
119
190
  supportsResolution: false,
120
191
  supportsSyncMode: true,
192
+ transparencyRoute: {
193
+ apiKeyEnv: "OPENAI_API_KEY",
194
+ model: "gpt-image-2",
195
+ provider: "openai",
196
+ supportsEdit: true
197
+ },
121
198
  type: "generation",
122
199
  useQueue: true
123
200
  },
@@ -267,6 +344,24 @@ const MODELS = {
267
344
  pricePerImageUsd: .15,
268
345
  pricing: "$0.15",
269
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
+ ],
270
365
  supportsAspect: true,
271
366
  supportsEdit: true,
272
367
  supportsGoogleSearch: true,
@@ -305,6 +400,18 @@ const MODELS = {
305
400
  pricePerImageUsd: .0398,
306
401
  pricing: "$0.04",
307
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
+ ],
308
415
  supportsAspect: true,
309
416
  supportsEdit: true,
310
417
  supportsNumImages: true,
@@ -344,6 +451,24 @@ const MODELS = {
344
451
  pricePerImageUsd: .15,
345
452
  pricing: "$0.15",
346
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
+ ],
347
472
  supportsAspect: true,
348
473
  supportsEdit: true,
349
474
  supportsNumImages: true,
@@ -381,6 +506,10 @@ const MODELS = {
381
506
  },
382
507
  useCase: "High-ranked budget generation and large multi-reference edits"
383
508
  },
509
+ customImageSize: {
510
+ maxPixels: 16777216,
511
+ minPixels: 921600
512
+ },
384
513
  editEndpoint: "fal-ai/bytedance/seedream/v4/edit",
385
514
  endpoint: "fal-ai/bytedance/seedream/v4/text-to-image",
386
515
  falPricing: {
@@ -431,6 +560,10 @@ const MODELS = {
431
560
  },
432
561
  useCase: "Cheap current Seedream model with good animation/art scores"
433
562
  },
563
+ customImageSize: {
564
+ maxPixels: 16777216,
565
+ minPixels: 3686400
566
+ },
434
567
  editEndpoint: "fal-ai/bytedance/seedream/v4.5/edit",
435
568
  endpoint: "fal-ai/bytedance/seedream/v4.5/text-to-image",
436
569
  falPricing: {
@@ -468,6 +601,11 @@ const MODELS = {
468
601
  rank: 10
469
602
  }
470
603
  } },
604
+ customImageSize: {
605
+ maxPixels: 4194304,
606
+ maxRatio: 16,
607
+ minPixels: 1048576
608
+ },
471
609
  name: "Seedream 5.0 Pro",
472
610
  endpoint: "bytedance/seedream/v5/pro/text-to-image",
473
611
  editEndpoint: "bytedance/seedream/v5/pro/edit",
@@ -501,16 +639,20 @@ const MODELS = {
501
639
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
502
640
  sourceUrls: AA_IMAGE_SOURCES
503
641
  } },
642
+ customImageSize: {
643
+ maxPixels: 16777216,
644
+ minPixels: 3686400
645
+ },
504
646
  name: "Seedream 5.0 Lite",
505
- endpoint: "fal-ai/bytedance/seedream/v5/lite/text-to-image",
506
- editEndpoint: "fal-ai/bytedance/seedream/v5/lite/edit",
647
+ endpoint: "bytedance/seedream/v5/lite/text-to-image",
648
+ editEndpoint: "bytedance/seedream/v5/lite/edit",
507
649
  type: "generation",
508
650
  pricing: "$0.035",
509
651
  pricePerImageUsd: .035,
510
652
  falPricing: {
511
653
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
512
654
  currency: "USD",
513
- endpointId: "fal-ai/bytedance/seedream/v5/lite/text-to-image",
655
+ endpointId: "bytedance/seedream/v5/lite/text-to-image",
514
656
  estimatedCostPerImageUsd: .035,
515
657
  source: "fal-pricing-api",
516
658
  unit: "images",
@@ -551,6 +693,12 @@ const MODELS = {
551
693
  },
552
694
  useCase: "Best FLUX quality and high-end editing"
553
695
  },
696
+ customImageSize: {
697
+ maxEdge: 2560,
698
+ maxPixels: 4194304,
699
+ minEdge: 256,
700
+ multipleOf: 16
701
+ },
554
702
  editEndpoint: "fal-ai/flux-2-max/edit",
555
703
  endpoint: "fal-ai/flux-2-max",
556
704
  falPricing: {
@@ -604,6 +752,12 @@ const MODELS = {
604
752
  },
605
753
  useCase: "Production FLUX quality at a low per-megapixel price"
606
754
  },
755
+ customImageSize: {
756
+ maxEdge: 2560,
757
+ maxPixels: 4194304,
758
+ minEdge: 256,
759
+ multipleOf: 16
760
+ },
607
761
  editEndpoint: "fal-ai/flux-2-pro/edit",
608
762
  endpoint: "fal-ai/flux-2-pro",
609
763
  falPricing: {
@@ -653,6 +807,12 @@ const MODELS = {
653
807
  },
654
808
  useCase: "FLUX quality with guidance and step controls"
655
809
  },
810
+ customImageSize: {
811
+ maxEdge: 2560,
812
+ maxPixels: 4194304,
813
+ minEdge: 256,
814
+ multipleOf: 16
815
+ },
656
816
  editEndpoint: "fal-ai/flux-2-flex/edit",
657
817
  endpoint: "fal-ai/flux-2-flex",
658
818
  falPricing: {
@@ -701,6 +861,10 @@ const MODELS = {
701
861
  },
702
862
  useCase: "Open FLUX.2 quality with low average cost"
703
863
  },
864
+ customImageSize: {
865
+ maxEdge: 2048,
866
+ minEdge: 512
867
+ },
704
868
  editEndpoint: "fal-ai/flux-2/edit",
705
869
  endpoint: "fal-ai/flux-2",
706
870
  falPricing: {
@@ -731,6 +895,10 @@ const MODELS = {
731
895
  type: "generation"
732
896
  },
733
897
  "flux2-turbo": {
898
+ customImageSize: {
899
+ maxEdge: 2048,
900
+ minEdge: 512
901
+ },
734
902
  endpoint: "fal-ai/flux-2/turbo",
735
903
  falPricing: {
736
904
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
@@ -786,6 +954,7 @@ const MODELS = {
786
954
  type: "generation"
787
955
  },
788
956
  "flux-fast": {
957
+ customImageSize: {},
789
958
  endpoint: "fal-ai/flux/schnell",
790
959
  falPricing: {
791
960
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -812,11 +981,12 @@ const MODELS = {
812
981
  type: "generation"
813
982
  },
814
983
  recraft: {
815
- endpoint: "fal-ai/recraft-v3",
984
+ customImageSize: {},
985
+ endpoint: "fal-ai/recraft/v3/text-to-image",
816
986
  falPricing: {
817
987
  checkedAt: FAL_PRICING_CHECKED_AT,
818
988
  currency: "USD",
819
- endpointId: "fal-ai/recraft-v3",
989
+ endpointId: "fal-ai/recraft/v3/text-to-image",
820
990
  estimatedCostPerImageUsd: .04,
821
991
  source: "fal-pricing-api",
822
992
  unit: "images",
@@ -858,7 +1028,7 @@ const MODELS = {
858
1028
  editEndpoint: "fal-ai/reve/edit",
859
1029
  endpoint: "fal-ai/reve/text-to-image",
860
1030
  falPricing: {
861
- checkedAt: "2026-08-23",
1031
+ checkedAt: FAL_PRICING_CHECKED_AUG_2026,
862
1032
  currency: "USD",
863
1033
  endpointId: "fal-ai/reve/text-to-image",
864
1034
  estimatedCostPerImageUsd: .04,
@@ -871,6 +1041,15 @@ const MODELS = {
871
1041
  pricePerImageUsd: .04,
872
1042
  pricing: "$0.04",
873
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
+ ],
874
1053
  supportsAspect: true,
875
1054
  supportsEdit: true,
876
1055
  supportsNumImages: true,
@@ -889,6 +1068,7 @@ const MODELS = {
889
1068
  rank: 25
890
1069
  }
891
1070
  } },
1071
+ customImageSize: {},
892
1072
  endpoint: "fal-ai/recraft/v4/text-to-image",
893
1073
  falPricing: {
894
1074
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
@@ -907,11 +1087,11 @@ const MODELS = {
907
1087
  supportsEdit: false,
908
1088
  supportsNumImages: false,
909
1089
  supportsResolution: false,
910
- supportsStyle: true,
911
1090
  supportsSyncMode: true,
912
1091
  type: "generation"
913
1092
  },
914
1093
  ideogram: {
1094
+ customImageSize: {},
915
1095
  endpoint: "fal-ai/ideogram/v3",
916
1096
  falPricing: {
917
1097
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -947,6 +1127,11 @@ const MODELS = {
947
1127
  rank: 19
948
1128
  }
949
1129
  } },
1130
+ customImageSize: {
1131
+ maxEdge: 3840,
1132
+ minEdge: 512,
1133
+ multipleOf: 16
1134
+ },
950
1135
  name: "Ideogram V4",
951
1136
  endpoint: "ideogram/v4",
952
1137
  type: "generation",
@@ -1010,11 +1195,21 @@ const MODELS = {
1010
1195
  unit: "images",
1011
1196
  unitPrice: .02
1012
1197
  },
1013
- maxReferenceImages: 4,
1198
+ maxReferenceImages: 3,
1014
1199
  name: "Grok Imagine Image",
1015
1200
  pricePerImageUsd: .02,
1016
1201
  pricing: "$0.02",
1017
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"],
1018
1213
  supportsAspect: true,
1019
1214
  supportsEdit: true,
1020
1215
  supportsNumImages: true,
@@ -1041,6 +1236,7 @@ const MODELS = {
1041
1236
  },
1042
1237
  useCase: "Low-cost open-weight text and design generation"
1043
1238
  },
1239
+ customImageSize: {},
1044
1240
  endpoint: "fal-ai/qwen-image",
1045
1241
  falPricing: {
1046
1242
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -1085,19 +1281,23 @@ const MODELS = {
1085
1281
  },
1086
1282
  useCase: "Strong quality per dollar - 11th on text-to-image at $30/1k"
1087
1283
  },
1088
- 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",
1089
1289
  falPricing: {
1090
- checkedAt: "2026-08-05",
1290
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1091
1291
  currency: "USD",
1092
- endpointId: "fal-ai/qwen-image-3/text-to-image",
1093
- estimatedCostPerImageUsd: .02,
1292
+ endpointId: "alibaba/qwen-image-3/text-to-image",
1293
+ estimatedCostPerImageUsd: .04,
1094
1294
  source: "fal-pricing-api",
1095
- unit: "megapixels",
1096
- unitPrice: .02
1295
+ unit: "images",
1296
+ unitPrice: .04
1097
1297
  },
1098
1298
  name: "Qwen Image 3",
1099
- pricePerImageUsd: .02,
1100
- 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)",
1101
1301
  sizeMode: "image_size_enum",
1102
1302
  supportsAspect: true,
1103
1303
  supportsEdit: false,
@@ -1109,6 +1309,179 @@ const MODELS = {
1109
1309
  supportsSeed: true,
1110
1310
  type: "generation"
1111
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
+ },
1112
1485
  kling: {
1113
1486
  endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
1114
1487
  name: "Kling v3 Pro",
@@ -1120,6 +1493,17 @@ const MODELS = {
1120
1493
  supportsResolution: false,
1121
1494
  type: "video"
1122
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
+ },
1123
1507
  clarity: {
1124
1508
  endpoint: "fal-ai/clarity-upscaler",
1125
1509
  name: "Clarity Upscaler",
@@ -1186,33 +1570,202 @@ const MODELS = {
1186
1570
  "ideogram",
1187
1571
  "ideogram4",
1188
1572
  "grok-image",
1573
+ "grok-image-2",
1189
1574
  "qwen",
1190
- "qwen3"
1575
+ "qwen3",
1576
+ "mai-image-2.5-pro",
1577
+ "banana2-lite",
1578
+ "ideogram3-transparent",
1579
+ "recraft41"
1191
1580
  ].filter((id) => MODELS[id]?.supportsEdit === true);
1192
1581
  //#endregion
1193
- //#region src/errors.ts
1194
- /**
1195
- * `MotifError` and its coercion helper.
1196
- *
1197
- * Extracted from `server.ts` so `server-tools.ts` can construct errors without
1198
- * importing back into `server.ts` — that import was a dependency cycle. Both
1199
- * `server.ts` and `server-tools.ts` import from here, and `server.ts`
1200
- * re-exports `MotifError` so the package's public surface is unchanged.
1201
- */
1202
- var MotifError = class extends Error {
1203
- status;
1204
- code;
1205
- /** fal's request-correlation id (from the `x-fal-request-id` header or the
1206
- * error body). Ties a failure back to fal's dashboard/support. */
1207
- requestId;
1208
- constructor(message, status, code, requestId) {
1209
- super(message);
1210
- this.name = "MotifError";
1211
- this.status = status;
1212
- this.code = code;
1213
- this.requestId = requestId;
1214
- }
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
1215
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;
1216
1769
  //#endregion
1217
1770
  //#region src/tool-registry/analysis.ts
1218
1771
  const ANALYSIS_TOOLS = {
@@ -1224,7 +1777,11 @@ const ANALYSIS_TOOLS = {
1224
1777
  inputKind: "images",
1225
1778
  name: "GOT-OCR 2.0",
1226
1779
  outputKeys: ["outputs"],
1227
- price: { kind: "metered" },
1780
+ price: {
1781
+ kind: "call",
1782
+ per: ["input_image_urls"],
1783
+ usd: .05
1784
+ },
1228
1785
  pricing: "$0.05/image",
1229
1786
  queued: true,
1230
1787
  sourceUrl: "https://fal.ai/models/fal-ai/got-ocr/v2",
@@ -1238,7 +1795,13 @@ const ANALYSIS_TOOLS = {
1238
1795
  inputKind: "image",
1239
1796
  name: "Moondream 3 Caption",
1240
1797
  outputKeys: ["output"],
1241
- price: { kind: "metered" },
1798
+ price: {
1799
+ inputPerMillion: .4,
1800
+ inputTokens: 737,
1801
+ kind: "token",
1802
+ outputPerMillion: 3.5,
1803
+ outputTokens: 200
1804
+ },
1242
1805
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1243
1806
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/caption",
1244
1807
  task: "image captioning"
@@ -1251,7 +1814,13 @@ const ANALYSIS_TOOLS = {
1251
1814
  inputKind: "image",
1252
1815
  name: "Moondream 3 Detect",
1253
1816
  outputKeys: ["objects", "image"],
1254
- price: { kind: "metered" },
1817
+ price: {
1818
+ inputPerMillion: .4,
1819
+ inputTokens: 737,
1820
+ kind: "token",
1821
+ outputPerMillion: 3.5,
1822
+ outputTokens: 100
1823
+ },
1255
1824
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1256
1825
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/detect",
1257
1826
  task: "object detection"
@@ -1264,7 +1833,13 @@ const ANALYSIS_TOOLS = {
1264
1833
  inputKind: "image",
1265
1834
  name: "Moondream 3 Point",
1266
1835
  outputKeys: ["points", "image"],
1267
- price: { kind: "metered" },
1836
+ price: {
1837
+ inputPerMillion: .4,
1838
+ inputTokens: 737,
1839
+ kind: "token",
1840
+ outputPerMillion: 3.5,
1841
+ outputTokens: 100
1842
+ },
1268
1843
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1269
1844
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/point",
1270
1845
  task: "object pointing"
@@ -1277,7 +1852,13 @@ const ANALYSIS_TOOLS = {
1277
1852
  inputKind: "image",
1278
1853
  name: "Moondream 3 Query",
1279
1854
  outputKeys: ["output", "reasoning"],
1280
- price: { kind: "metered" },
1855
+ price: {
1856
+ inputPerMillion: .4,
1857
+ inputTokens: 737,
1858
+ kind: "token",
1859
+ outputPerMillion: 3.5,
1860
+ outputTokens: 500
1861
+ },
1281
1862
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1282
1863
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/query",
1283
1864
  task: "visual question answering"
@@ -1290,7 +1871,11 @@ const ANALYSIS_TOOLS = {
1290
1871
  inputKind: "images",
1291
1872
  name: "NSFW Checker",
1292
1873
  outputKeys: ["has_nsfw_concepts"],
1293
- price: { kind: "metered" },
1874
+ price: {
1875
+ kind: "call",
1876
+ per: ["image_urls"],
1877
+ usd: .001
1878
+ },
1294
1879
  pricing: "$0.001/image",
1295
1880
  sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw",
1296
1881
  task: "vision moderation"
@@ -1374,6 +1959,34 @@ const ASSET_TOOLS = {
1374
1959
  sourceUrl: "https://fal.ai/models/fal-ai/image2svg",
1375
1960
  task: "raster to vector tracing"
1376
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
+ },
1377
1990
  patina: {
1378
1991
  category: "material",
1379
1992
  description: "Decompose a surface photograph into PBR maps: basecolor, normal, roughness, metalness, height.",
@@ -1392,7 +2005,12 @@ const ASSET_TOOLS = {
1392
2005
  ],
1393
2006
  fromOption: "maps"
1394
2007
  } },
1395
- price: { kind: "metered" },
2008
+ price: {
2009
+ base: .01,
2010
+ kind: "maps",
2011
+ perMapMegapixel: .01,
2012
+ perMegapixel: 0
2013
+ },
1396
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",
1397
2015
  queued: true,
1398
2016
  sourceUrl: "https://fal.ai/models/fal-ai/patina",
@@ -1416,8 +2034,17 @@ const ASSET_TOOLS = {
1416
2034
  ],
1417
2035
  fromOption: "maps"
1418
2036
  } },
1419
- price: { kind: "metered" },
1420
- 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",
1421
2048
  queued: true,
1422
2049
  sourceUrl: "https://fal.ai/models/fal-ai/patina/material/extract",
1423
2050
  task: "tiling material extraction"
@@ -1430,8 +2057,11 @@ const ASSET_TOOLS = {
1430
2057
  inputKind: "image",
1431
2058
  name: "Qwen Image Layered",
1432
2059
  outputKeys: ["images"],
1433
- price: { kind: "metered" },
1434
- 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",
1435
2065
  queued: true,
1436
2066
  sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-layered",
1437
2067
  task: "image layer decomposition"
@@ -1498,7 +2128,6 @@ const ASSET_TOOLS = {
1498
2128
  },
1499
2129
  "sam3-3d-objects": {
1500
2130
  category: "3d",
1501
- defaultOptions: { prompt: "car" },
1502
2131
  description: "Reconstruct one or more 3D objects from an image and prompts.",
1503
2132
  endpoint: "fal-ai/sam-3/3d-objects",
1504
2133
  inputField: "image_url",
@@ -1604,7 +2233,10 @@ const BACKGROUND_TOOLS = {
1604
2233
  inputKind: "image",
1605
2234
  name: "BirefNet Background Removal",
1606
2235
  outputKeys: ["image", "mask_image"],
1607
- price: { kind: "metered" },
2236
+ price: {
2237
+ kind: "call",
2238
+ usd: 0
2239
+ },
1608
2240
  pricing: "$0/compute-second listed by fal",
1609
2241
  sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
1610
2242
  task: "image background removal"
@@ -1640,13 +2272,51 @@ const BACKGROUND_TOOLS = {
1640
2272
  outputKeys: ["video"],
1641
2273
  price: {
1642
2274
  kind: "second",
1643
- usd: .00425
2275
+ usd: .14
1644
2276
  },
1645
- pricing: "$0.00425/sec",
2277
+ pricing: "$0.14/sec",
1646
2278
  queued: true,
1647
2279
  sourceUrl: "https://fal.ai/models/bria/video/background-removal",
1648
2280
  task: "video background removal"
1649
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
+ },
1650
2320
  ddcolor: {
1651
2321
  category: "restoration",
1652
2322
  description: "Colourise black-and-white photographs.",
@@ -1672,7 +2342,10 @@ const BACKGROUND_TOOLS = {
1672
2342
  inputKind: "image",
1673
2343
  name: "Remove Background",
1674
2344
  outputKeys: ["image"],
1675
- price: { kind: "metered" },
2345
+ price: {
2346
+ kind: "call",
2347
+ usd: 0
2348
+ },
1676
2349
  pricing: "$0/compute-second listed by fal",
1677
2350
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
1678
2351
  task: "image background removal"
@@ -1705,8 +2378,9 @@ const BACKGROUND_TOOLS = {
1705
2378
  name: "Topaz Adjust",
1706
2379
  outputKeys: ["image"],
1707
2380
  price: {
1708
- kind: "megapixel",
1709
- usd: .08 / 24
2381
+ kind: "megapixel-step",
2382
+ megapixels: 24,
2383
+ usd: .08
1710
2384
  },
1711
2385
  pricing: "$0.08 per 24 output megapixels",
1712
2386
  queued: true,
@@ -1723,10 +2397,11 @@ const BACKGROUND_TOOLS = {
1723
2397
  name: "Topaz Creative Upscale",
1724
2398
  outputKeys: ["image"],
1725
2399
  price: {
1726
- kind: "megapixel",
1727
- usd: .96 / 24
2400
+ kind: "megapixel-step",
2401
+ megapixels: 2,
2402
+ usd: .08
1728
2403
  },
1729
- pricing: "$0.96 per 24 output megapixels",
2404
+ pricing: "$0.08 per started 2 output megapixels, any Bloom model",
1730
2405
  queued: true,
1731
2406
  sourceUrl: "https://fal.ai/models/topaz/upscale/image/creative",
1732
2407
  task: "creative image upscaling"
@@ -1741,8 +2416,9 @@ const BACKGROUND_TOOLS = {
1741
2416
  name: "Topaz Denoise",
1742
2417
  outputKeys: ["image"],
1743
2418
  price: {
1744
- kind: "megapixel",
1745
- usd: .08 / 24
2419
+ kind: "megapixel-step",
2420
+ megapixels: 24,
2421
+ usd: .08
1746
2422
  },
1747
2423
  pricing: "$0.08 per 24 output megapixels at the default Normal model; $0.16 with Denoise Max",
1748
2424
  queued: true,
@@ -1759,10 +2435,11 @@ const BACKGROUND_TOOLS = {
1759
2435
  name: "Topaz Generative Upscale",
1760
2436
  outputKeys: ["image"],
1761
2437
  price: {
1762
- kind: "megapixel",
1763
- usd: .24 / 24
2438
+ kind: "megapixel-step",
2439
+ megapixels: 8,
2440
+ usd: .08
1764
2441
  },
1765
- 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",
1766
2443
  queued: true,
1767
2444
  sourceUrl: "https://fal.ai/models/topaz/upscale/image/generative",
1768
2445
  task: "generative image upscaling"
@@ -1781,8 +2458,9 @@ const BACKGROUND_TOOLS = {
1781
2458
  name: "Topaz Image Upscale",
1782
2459
  outputKeys: ["image"],
1783
2460
  price: {
1784
- kind: "megapixel",
1785
- usd: .08 / 24
2461
+ kind: "megapixel-step",
2462
+ megapixels: 24,
2463
+ usd: .08
1786
2464
  },
1787
2465
  pricing: "$0.08 for output up to 24MP; $0.16 to 48MP, $0.32 to 96MP, up to $1.36 at 512MP",
1788
2466
  queued: true,
@@ -1799,8 +2477,9 @@ const BACKGROUND_TOOLS = {
1799
2477
  name: "Topaz Precision Upscale",
1800
2478
  outputKeys: ["image"],
1801
2479
  price: {
1802
- kind: "megapixel",
1803
- usd: .08 / 24
2480
+ kind: "megapixel-step",
2481
+ megapixels: 24,
2482
+ usd: .08
1804
2483
  },
1805
2484
  pricing: "$0.08 per 24 output megapixels, any precision model",
1806
2485
  queued: true,
@@ -1817,8 +2496,9 @@ const BACKGROUND_TOOLS = {
1817
2496
  name: "Topaz Restore",
1818
2497
  outputKeys: ["image"],
1819
2498
  price: {
1820
- kind: "megapixel",
1821
- usd: .48 / 24
2499
+ kind: "megapixel-step",
2500
+ megapixels: 24,
2501
+ usd: .48
1822
2502
  },
1823
2503
  pricing: "$0.48 per 24 output megapixels at the default Recover 3; $0.08 with Dust-Scratch V2",
1824
2504
  queued: true,
@@ -1835,8 +2515,9 @@ const BACKGROUND_TOOLS = {
1835
2515
  name: "Topaz Sharpen",
1836
2516
  outputKeys: ["image"],
1837
2517
  price: {
1838
- kind: "megapixel",
1839
- usd: .08 / 24
2518
+ kind: "megapixel-step",
2519
+ megapixels: 24,
2520
+ usd: .08
1840
2521
  },
1841
2522
  pricing: "$0.08 per 24 output megapixels at the default Standard model; $0.16 with Super Focus",
1842
2523
  queued: true,
@@ -1852,8 +2533,9 @@ const BACKGROUND_TOOLS = {
1852
2533
  name: "Topaz Transparent Upscale",
1853
2534
  outputKeys: ["image"],
1854
2535
  price: {
1855
- kind: "megapixel",
1856
- usd: .08 / 24
2536
+ kind: "megapixel-step",
2537
+ megapixels: 24,
2538
+ usd: .08
1857
2539
  },
1858
2540
  pricing: "$0.08 per 24 output megapixels",
1859
2541
  queued: true,
@@ -1873,10 +2555,22 @@ const BACKGROUND_TOOLS = {
1873
2555
  name: "Topaz Video Upscale",
1874
2556
  outputKeys: ["video"],
1875
2557
  price: {
1876
- kind: "second",
1877
- 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
+ ]
1878
2572
  },
1879
- 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",
1880
2574
  queued: true,
1881
2575
  sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
1882
2576
  task: "video enhancement"
@@ -1927,6 +2621,7 @@ const EDITING_TOOLS = {
1927
2621
  outputKeys: ["images"],
1928
2622
  price: {
1929
2623
  kind: "call",
2624
+ perImage: true,
1930
2625
  usd: .04
1931
2626
  },
1932
2627
  pricing: "$0.04/generation",
@@ -1957,7 +2652,11 @@ const EDITING_TOOLS = {
1957
2652
  inputKind: "image",
1958
2653
  name: "FLUX.2 Pro Outpaint",
1959
2654
  outputKeys: ["images"],
1960
- price: { kind: "metered" },
2655
+ price: {
2656
+ extra: .015,
2657
+ first: .03,
2658
+ kind: "megapixel-first"
2659
+ },
1961
2660
  pricing: "$0.03 for the first output megapixel, then $0.015 per extra megapixel of input and output, rounded up",
1962
2661
  sourceUrl: "https://fal.ai/models/fal-ai/flux-2-pro/outpaint",
1963
2662
  task: "image outpainting"
@@ -1988,6 +2687,7 @@ const EDITING_TOOLS = {
1988
2687
  outputKeys: ["images"],
1989
2688
  price: {
1990
2689
  kind: "call",
2690
+ perImage: true,
1991
2691
  usd: .06
1992
2692
  },
1993
2693
  pricing: "$0.06/image at the default BALANCED speed; $0.03 turbo, $0.09 quality",
@@ -2082,12 +2782,35 @@ const EDITING_TOOLS = {
2082
2782
  inputKind: "image",
2083
2783
  name: "Smart Resize",
2084
2784
  outputKeys: ["images", "results"],
2085
- 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
+ },
2086
2792
  pricing: "$0.15 per output image, doubled at 4K, plus a $0.05 vision analysis fee per request",
2087
2793
  queued: true,
2088
2794
  sourceUrl: "https://fal.ai/models/fal-ai/smart-resize",
2089
2795
  task: "multi-size recomposition"
2090
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
+ },
2091
2814
  "text-removal": {
2092
2815
  category: "erase",
2093
2816
  defaultOptions: { output_format: "png" },
@@ -2104,6 +2827,25 @@ const EDITING_TOOLS = {
2104
2827
  pricing: "$0.04/image",
2105
2828
  sourceUrl: "https://fal.ai/models/fal-ai/image-editing/text-removal",
2106
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"
2107
2849
  }
2108
2850
  };
2109
2851
  //#endregion
@@ -2117,7 +2859,10 @@ const STRUCTURE_TOOLS = {
2117
2859
  inputKind: "image",
2118
2860
  name: "Depth Anything v2 Preprocessor",
2119
2861
  outputKeys: ["image"],
2120
- price: { kind: "metered" },
2862
+ price: {
2863
+ kind: "call",
2864
+ usd: 0
2865
+ },
2121
2866
  pricing: "$0/compute-second listed by fal",
2122
2867
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/depth-anything/v2",
2123
2868
  task: "depth preprocessing"
@@ -2146,7 +2891,10 @@ const STRUCTURE_TOOLS = {
2146
2891
  inputKind: "image",
2147
2892
  name: "HED Edge Preprocessor",
2148
2893
  outputKeys: ["image"],
2149
- price: { kind: "metered" },
2894
+ price: {
2895
+ kind: "call",
2896
+ usd: 0
2897
+ },
2150
2898
  pricing: "$0/compute-second listed by fal",
2151
2899
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/hed",
2152
2900
  task: "edge preprocessing"
@@ -2160,7 +2908,10 @@ const STRUCTURE_TOOLS = {
2160
2908
  inputKind: "image",
2161
2909
  name: "Line Art Preprocessor",
2162
2910
  outputKeys: ["image"],
2163
- price: { kind: "metered" },
2911
+ price: {
2912
+ kind: "call",
2913
+ usd: 0
2914
+ },
2164
2915
  pricing: "$0/compute-second listed by fal",
2165
2916
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/lineart",
2166
2917
  task: "image preprocessing"
@@ -2177,7 +2928,10 @@ const STRUCTURE_TOOLS = {
2177
2928
  inputKind: "image",
2178
2929
  name: "Marigold Depth Estimation",
2179
2930
  outputKeys: ["image"],
2180
- price: { kind: "metered" },
2931
+ price: {
2932
+ kind: "call",
2933
+ usd: 0
2934
+ },
2181
2935
  pricing: "$0/compute-second listed by fal",
2182
2936
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/marigold-depth",
2183
2937
  task: "depth map"
@@ -2194,7 +2948,10 @@ const STRUCTURE_TOOLS = {
2194
2948
  inputKind: "image",
2195
2949
  name: "MiDaS Depth Estimation",
2196
2950
  outputKeys: ["image"],
2197
- price: { kind: "metered" },
2951
+ price: {
2952
+ kind: "call",
2953
+ usd: 0
2954
+ },
2198
2955
  pricing: "$0/compute-second listed by fal",
2199
2956
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/depth",
2200
2957
  task: "depth map"
@@ -2207,7 +2964,10 @@ const STRUCTURE_TOOLS = {
2207
2964
  inputKind: "image",
2208
2965
  name: "MiDaS Preprocessor",
2209
2966
  outputKeys: ["depth_map", "normal_map"],
2210
- price: { kind: "metered" },
2967
+ price: {
2968
+ kind: "call",
2969
+ usd: 0
2970
+ },
2211
2971
  pricing: "$0/compute-second listed by fal",
2212
2972
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/midas",
2213
2973
  task: "depth and normal preprocessing"
@@ -2220,7 +2980,10 @@ const STRUCTURE_TOOLS = {
2220
2980
  inputKind: "image",
2221
2981
  name: "M-LSD Line Preprocessor",
2222
2982
  outputKeys: ["image"],
2223
- price: { kind: "metered" },
2983
+ price: {
2984
+ kind: "call",
2985
+ usd: 0
2986
+ },
2224
2987
  pricing: "$0/compute-second listed by fal",
2225
2988
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/mlsd",
2226
2989
  task: "line segment preprocessing"
@@ -2233,7 +2996,10 @@ const STRUCTURE_TOOLS = {
2233
2996
  inputKind: "image",
2234
2997
  name: "PiDiNet Edge Preprocessor",
2235
2998
  outputKeys: ["image"],
2236
- price: { kind: "metered" },
2999
+ price: {
3000
+ kind: "call",
3001
+ usd: 0
3002
+ },
2237
3003
  pricing: "$0/compute-second listed by fal",
2238
3004
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/pidi",
2239
3005
  task: "edge preprocessing"
@@ -2246,7 +3012,10 @@ const STRUCTURE_TOOLS = {
2246
3012
  inputKind: "image",
2247
3013
  name: "SAM Preprocessor",
2248
3014
  outputKeys: ["image"],
2249
- price: { kind: "metered" },
3015
+ price: {
3016
+ kind: "call",
3017
+ usd: 0
3018
+ },
2250
3019
  pricing: "$0/compute-second listed by fal",
2251
3020
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/sam",
2252
3021
  task: "segmentation preprocessing"
@@ -2266,7 +3035,10 @@ const STRUCTURE_TOOLS = {
2266
3035
  inputKind: "image",
2267
3036
  name: "SAM 2 Auto Segment",
2268
3037
  outputKeys: ["combined_mask", "individual_masks"],
2269
- price: { kind: "metered" },
3038
+ price: {
3039
+ kind: "call",
3040
+ usd: 0
3041
+ },
2270
3042
  pricing: "$0/compute-second listed by fal",
2271
3043
  queued: true,
2272
3044
  sourceUrl: "https://fal.ai/models/fal-ai/sam2/auto-segment",
@@ -2311,7 +3083,11 @@ const STRUCTURE_TOOLS = {
2311
3083
  inputKind: "video",
2312
3084
  name: "SAM 3.1 Video",
2313
3085
  outputKeys: ["video", "boundingbox_frames_zip"],
2314
- price: { kind: "metered" },
3086
+ price: {
3087
+ frames: 16,
3088
+ kind: "frames",
3089
+ usd: .01
3090
+ },
2315
3091
  pricing: "$0.01/16 frames of video input",
2316
3092
  queued: true,
2317
3093
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/video",
@@ -2383,7 +3159,11 @@ const STRUCTURE_TOOLS = {
2383
3159
  inputKind: "video",
2384
3160
  name: "SAM 3 Video",
2385
3161
  outputKeys: ["video", "boundingbox_frames_zip"],
2386
- price: { kind: "metered" },
3162
+ price: {
3163
+ frames: 16,
3164
+ kind: "frames",
3165
+ usd: .005
3166
+ },
2387
3167
  pricing: "$0.005/16 frames of video input",
2388
3168
  queued: true,
2389
3169
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video",
@@ -2402,7 +3182,11 @@ const STRUCTURE_TOOLS = {
2402
3182
  inputKind: "video",
2403
3183
  name: "SAM 3 Video RLE",
2404
3184
  outputKeys: ["video", "boundingbox_frames_zip"],
2405
- price: { kind: "metered" },
3185
+ price: {
3186
+ frames: 16,
3187
+ kind: "frames",
3188
+ usd: .005
3189
+ },
2406
3190
  pricing: "$0.005/16 frames of video",
2407
3191
  queued: true,
2408
3192
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video-rle",
@@ -2416,7 +3200,10 @@ const STRUCTURE_TOOLS = {
2416
3200
  inputKind: "image",
2417
3201
  name: "Scribble Preprocessor",
2418
3202
  outputKeys: ["image"],
2419
- price: { kind: "metered" },
3203
+ price: {
3204
+ kind: "call",
3205
+ usd: 0
3206
+ },
2420
3207
  pricing: "$0/compute-second listed by fal",
2421
3208
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/scribble",
2422
3209
  task: "scribble preprocessing"
@@ -2429,7 +3216,10 @@ const STRUCTURE_TOOLS = {
2429
3216
  inputKind: "image",
2430
3217
  name: "TEED Edge Preprocessor",
2431
3218
  outputKeys: ["image"],
2432
- price: { kind: "metered" },
3219
+ price: {
3220
+ kind: "call",
3221
+ usd: 0
3222
+ },
2433
3223
  pricing: "$0/compute-second listed by fal",
2434
3224
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/teed",
2435
3225
  task: "edge preprocessing"
@@ -2442,7 +3232,10 @@ const STRUCTURE_TOOLS = {
2442
3232
  inputKind: "image",
2443
3233
  name: "ZoeDepth Preprocessor",
2444
3234
  outputKeys: ["image"],
2445
- price: { kind: "metered" },
3235
+ price: {
3236
+ kind: "call",
3237
+ usd: 0
3238
+ },
2446
3239
  pricing: "$0/compute-second listed by fal",
2447
3240
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/zoe",
2448
3241
  task: "depth preprocessing"
@@ -2459,6 +3252,17 @@ const FAL_TOOLS = {
2459
3252
  };
2460
3253
  Object.keys(FAL_TOOLS);
2461
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
2462
3266
  //#region src/image/fal.ts
2463
3267
  /**
2464
3268
  * fal provider adapter.
@@ -2476,35 +3280,22 @@ Object.keys(FAL_TOOLS);
2476
3280
  * need a specific size. Example:
2477
3281
  * img.generate({
2478
3282
  * provider: "fal",
2479
- * tier: "balanced",
3283
+ * model: "fal-ai/gpt-image-1.5",
2480
3284
  * prompt: "...",
2481
3285
  * providerOptions: { fal: { image_size: "1024x1024" } },
2482
3286
  * });
2483
3287
  */
2484
3288
  /**
2485
- * fal model ids by tier. `fast` uses FLUX Pro Ultra (fast, cheap); the higher
2486
- * tiers use fal's gpt-image endpoint for its edit quality. Both are proven fal
2487
- * 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).
2488
3291
  */
2489
3292
  const FAL_FLUX_MODEL = "fal-ai/flux-pro/v1.1-ultra";
2490
3293
  const FAL_GPT_IMAGE_MODEL = "fal-ai/gpt-image-1.5";
2491
- /**
2492
- * Tier → fal model id. For fal, explicit `model:` endpoint ids are the primary
2493
- * path (any fal endpoint resolves via passthrough); this tier map is a
2494
- * convenience covering the two most common (FLUX Pro Ultra + gpt-image).
2495
- */
2496
- const FAL_TIER_MODELS = {
2497
- fast: FAL_FLUX_MODEL,
2498
- balanced: FAL_GPT_IMAGE_MODEL,
2499
- quality: FAL_GPT_IMAGE_MODEL,
2500
- hero: FAL_GPT_IMAGE_MODEL
2501
- };
2502
3294
  /** Env var read for the fal key when `apiKey` is not supplied in config. */
2503
3295
  const FAL_API_KEY_ENV = "FAL_KEY";
2504
3296
  /**
2505
- * Static fal USD/image, keyed by the fal ENDPOINT id. Explicit `model:` endpoint
2506
- * ids are fal's primary path (the tier map covers only two), so cost tracking
2507
- * 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
2508
3299
  * `fal-ai/flux/schnell`, `fal-ai/flux-2-pro`, `fal-ai/flux-2-max`. Every price is
2509
3300
  * sourced from the fal registry snapshot in `../models`
2510
3301
  * (`MODELS[...].pricePerImageUsd`) so this table stays in sync rather than
@@ -2531,12 +3322,17 @@ const FAL_IMAGE_PRICE_USD = {
2531
3322
  "fal-ai/bytedance/seedream/v4/text-to-image": MODELS.seedream4?.pricePerImageUsd ?? .03,
2532
3323
  "fal-ai/bytedance/seedream/v4.5/text-to-image": MODELS.seedream45?.pricePerImageUsd ?? .04,
2533
3324
  "bytedance/seedream/v5/pro/text-to-image": MODELS.seedream5?.pricePerImageUsd ?? .0675,
2534
- "fal-ai/bytedance/seedream/v5/lite/text-to-image": MODELS["seedream5-lite"]?.pricePerImageUsd ?? .035,
2535
- "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,
2536
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,
2537
3329
  "fal-ai/ideogram/v3": MODELS.ideogram?.pricePerImageUsd ?? .03,
2538
3330
  "ideogram/v4": MODELS.ideogram4?.pricePerImageUsd ?? .03,
2539
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,
2540
3336
  "fal-ai/qwen-image": MODELS.qwen?.pricePerImageUsd ?? .02
2541
3337
  };
2542
3338
  /**
@@ -2544,15 +3340,17 @@ const FAL_IMAGE_PRICE_USD = {
2544
3340
  * var. Throws `MotifError` when neither is present (callers translate this into
2545
3341
  * a `Result.err`).
2546
3342
  */
2547
- function resolveModel$3(modelId, apiKey) {
3343
+ function resolveModel$3(modelId, apiKey, fetch) {
2548
3344
  const key = apiKey ?? process.env["FAL_KEY"];
2549
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);
2550
- return createFal({ apiKey: key }).image(modelId);
3346
+ return createFal({
3347
+ apiKey: key,
3348
+ ...toProviderFetch(fetch)
3349
+ }).image(modelId);
2551
3350
  }
2552
3351
  /** The fal provider adapter registered in the provider registry. */
2553
3352
  const falAdapter = {
2554
3353
  id: "fal",
2555
- tierModels: FAL_TIER_MODELS,
2556
3354
  apiKeyEnv: FAL_API_KEY_ENV,
2557
3355
  resolveModel: resolveModel$3,
2558
3356
  priceUsdByModel: FAL_IMAGE_PRICE_USD
@@ -2568,19 +3366,6 @@ const falAdapter = {
2568
3366
  * multi-image-in → image-out editing (with an optional mask), which is the core
2569
3367
  * operation this layer normalizes.
2570
3368
  */
2571
- /**
2572
- * Tier → Gemini image model id.
2573
- *
2574
- * Seeded from Material Desk's `RENDER_IMAGE_MODEL_BY_QUALITY` (the driving
2575
- * consumer, see the design doc). `gemini-2.5-flash-image` is the proven-reachable
2576
- * floor; the preview ids may require allowlist/tier access.
2577
- */
2578
- const GOOGLE_TIER_MODELS = {
2579
- fast: "gemini-2.5-flash-image",
2580
- balanced: "gemini-3.1-flash-image-preview",
2581
- quality: "gemini-3-pro-image-preview",
2582
- hero: "gemini-3-pro-image-preview"
2583
- };
2584
3369
  /** Env var read for the Google API key when `apiKey` is not supplied in config. */
2585
3370
  const GOOGLE_API_KEY_ENV = "GOOGLE_GENERATIVE_AI_API_KEY";
2586
3371
  /**
@@ -2610,15 +3395,17 @@ const GOOGLE_IMAGE_PRICE_USD = {
2610
3395
  * `GOOGLE_GENERATIVE_AI_API_KEY` env var. Throws `MotifError` when neither is
2611
3396
  * present (callers translate this into a `Result.err`).
2612
3397
  */
2613
- function resolveModel$2(modelId, apiKey) {
3398
+ function resolveModel$2(modelId, apiKey, fetch) {
2614
3399
  const key = apiKey ?? process.env["GOOGLE_GENERATIVE_AI_API_KEY"];
2615
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);
2616
- return createGoogleGenerativeAI({ apiKey: key }).image(modelId);
3401
+ return createGoogleGenerativeAI({
3402
+ apiKey: key,
3403
+ ...toProviderFetch(fetch)
3404
+ }).image(modelId);
2617
3405
  }
2618
3406
  /** The Google (Gemini) provider adapter registered in the provider registry. */
2619
3407
  const googleAdapter = {
2620
3408
  id: "google",
2621
- tierModels: GOOGLE_TIER_MODELS,
2622
3409
  apiKeyEnv: GOOGLE_API_KEY_ENV,
2623
3410
  resolveModel: resolveModel$2,
2624
3411
  priceUsdByModel: GOOGLE_IMAGE_PRICE_USD
@@ -2632,16 +3419,6 @@ const googleAdapter = {
2632
3419
  * performs no network I/O — the request only happens when `generateImage`
2633
3420
  * invokes `model.doGenerate`.
2634
3421
  */
2635
- /**
2636
- * Flare favors speed for everyday generation; Sunburst favors editing precision.
2637
- * Explicit model ids still override tiers, including older GPT Image models.
2638
- */
2639
- const OPENAI_TIER_MODELS = {
2640
- fast: "gpt-image-2.5-flare",
2641
- balanced: "gpt-image-2.5-flare",
2642
- quality: "gpt-image-2.5-sunburst",
2643
- hero: "gpt-image-2.5-sunburst"
2644
- };
2645
3422
  /** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
2646
3423
  const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
2647
3424
  /**
@@ -2665,15 +3442,17 @@ const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
2665
3442
  * `OPENAI_API_KEY` env var. Throws `MotifError` when neither is present
2666
3443
  * (callers translate this into a `Result.err`).
2667
3444
  */
2668
- function resolveModel$1(modelId, apiKey) {
3445
+ function resolveModel$1(modelId, apiKey, fetch) {
2669
3446
  const key = apiKey ?? process.env["OPENAI_API_KEY"];
2670
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);
2671
- return createOpenAI({ apiKey: key }).image(modelId);
3448
+ return createOpenAI({
3449
+ apiKey: key,
3450
+ ...toProviderFetch(fetch)
3451
+ }).image(modelId);
2672
3452
  }
2673
3453
  /** The OpenAI provider adapter registered in the provider registry. */
2674
3454
  const openaiAdapter = {
2675
3455
  id: "openai",
2676
- tierModels: OPENAI_TIER_MODELS,
2677
3456
  apiKeyEnv: OPENAI_API_KEY_ENV,
2678
3457
  resolveModel: resolveModel$1,
2679
3458
  priceUsdByModel: OPENAI_IMAGE_PRICE_USD
@@ -2690,19 +3469,6 @@ const openaiAdapter = {
2690
3469
  * NOTE: Replicate's SDK names the credential option `apiToken` (not `apiKey`),
2691
3470
  * and reads `REPLICATE_API_TOKEN` from the environment.
2692
3471
  */
2693
- /**
2694
- * Replicate is wired to a single high-quality model for now, so every tier maps
2695
- * to FLUX 1.1 Pro Ultra. (The benchmark found Replicate ~1.45× faster than fal
2696
- * for this model at the same price — see the design doc §10.)
2697
- */
2698
- const REPLICATE_MODEL = "black-forest-labs/flux-1.1-pro-ultra";
2699
- /** Tier → Replicate model id (all tiers → FLUX 1.1 Pro Ultra for now). */
2700
- const REPLICATE_TIER_MODELS = {
2701
- fast: REPLICATE_MODEL,
2702
- balanced: REPLICATE_MODEL,
2703
- quality: REPLICATE_MODEL,
2704
- hero: REPLICATE_MODEL
2705
- };
2706
3472
  /** Env var read for the Replicate API token when `apiToken` is not in config. */
2707
3473
  const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
2708
3474
  /**
@@ -2719,10 +3485,13 @@ const REPLICATE_IMAGE_PRICE_USD = { "black-forest-labs/flux-1.1-pro-ultra": .06
2719
3485
  * token), else the `REPLICATE_API_TOKEN` env var. Throws `MotifError` when
2720
3486
  * neither is present (callers translate this into a `Result.err`).
2721
3487
  */
2722
- function resolveModel(modelId, apiKey) {
3488
+ function resolveModel(modelId, apiKey, fetch) {
2723
3489
  const token = apiKey ?? process.env["REPLICATE_API_TOKEN"];
2724
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);
2725
- return createReplicate({ apiToken: token }).image(modelId);
3491
+ return createReplicate({
3492
+ apiToken: token,
3493
+ ...toProviderFetch(fetch)
3494
+ }).image(modelId);
2726
3495
  }
2727
3496
  //#endregion
2728
3497
  //#region src/image/provider.ts
@@ -2736,7 +3505,6 @@ const PROVIDERS = {
2736
3505
  openai: openaiAdapter,
2737
3506
  replicate: {
2738
3507
  id: "replicate",
2739
- tierModels: REPLICATE_TIER_MODELS,
2740
3508
  apiKeyEnv: REPLICATE_API_KEY_ENV,
2741
3509
  resolveModel,
2742
3510
  priceUsdByModel: REPLICATE_IMAGE_PRICE_USD
@@ -2780,6 +3548,9 @@ function costFromProviderMetadata(providerMetadata) {
2780
3548
  }
2781
3549
  }
2782
3550
  /** Static per-image USD for a (provider, model), or undefined if unknown. */
3551
+ function providerPricePerImageUsd(provider, modelId) {
3552
+ return tablePricePerImage(provider, modelId);
3553
+ }
2783
3554
  function tablePricePerImage(provider, modelId) {
2784
3555
  const adapter = PROVIDERS[provider];
2785
3556
  if (adapter === void 0) return;
@@ -2814,8 +3585,8 @@ function costForImages(provider, modelId, providerMetadata, imageCount) {
2814
3585
  * `@howells/motif-sdk/image` — provider-agnostic image generation + editing.
2815
3586
  *
2816
3587
  * ESM-only subpath export, built on the Vercel AI SDK image interface
2817
- * (`generateImage`, `@ai-sdk/*`). Additive to the fal-specific `FalClient`
2818
- * 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
2819
3590
  * thrown exceptions). Google (Gemini) is the only provider in Phase 1a.
2820
3591
  *
2821
3592
  * @example
@@ -2823,11 +3594,10 @@ function costForImages(provider, modelId, providerMetadata, imageCount) {
2823
3594
  * import { createMotifImage } from "@howells/motif-sdk/image";
2824
3595
  *
2825
3596
  * const img = createMotifImage({ defaultProvider: "google" });
2826
- * 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" });
2827
3598
  * if (r.isOk()) console.log(r.value.images[0].mediaType, r.value.cost);
2828
3599
  * ```
2829
3600
  */
2830
- const DEFAULT_TIER = "balanced";
2831
3601
  const DEFAULT_PROVIDER = "google";
2832
3602
  /**
2833
3603
  * Create a provider-agnostic image client.
@@ -2854,8 +3624,8 @@ function createMotifImage(config = {}, deps = {}) {
2854
3624
  async function generate(opts) {
2855
3625
  const provider = resolveProvider(opts.provider);
2856
3626
  try {
2857
- const modelId = resolveModelId(provider, opts.model, opts.tier);
2858
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider));
3627
+ const modelId = opts.model;
3628
+ const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
2859
3629
  const result = await generateImageFn({
2860
3630
  model,
2861
3631
  prompt: opts.prompt,
@@ -2863,6 +3633,7 @@ function createMotifImage(config = {}, deps = {}) {
2863
3633
  ...opts.size === void 0 ? {} : { size: opts.size },
2864
3634
  ...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
2865
3635
  ...opts.seed === void 0 ? {} : { seed: opts.seed },
3636
+ ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
2866
3637
  ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
2867
3638
  ...opts.headers === void 0 ? {} : { headers: opts.headers },
2868
3639
  ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
@@ -2875,8 +3646,8 @@ function createMotifImage(config = {}, deps = {}) {
2875
3646
  async function edit(opts) {
2876
3647
  const provider = resolveProvider(opts.provider);
2877
3648
  try {
2878
- const modelId = resolveModelId(provider, opts.model, opts.tier);
2879
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider));
3649
+ const modelId = opts.model;
3650
+ const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
2880
3651
  const result = await generateImageFn({
2881
3652
  model,
2882
3653
  prompt: {
@@ -2886,6 +3657,7 @@ function createMotifImage(config = {}, deps = {}) {
2886
3657
  },
2887
3658
  ...opts.n === void 0 ? {} : { n: opts.n },
2888
3659
  ...opts.seed === void 0 ? {} : { seed: opts.seed },
3660
+ ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
2889
3661
  ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
2890
3662
  ...opts.headers === void 0 ? {} : { headers: opts.headers },
2891
3663
  ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
@@ -2971,14 +3743,8 @@ async function runBestOfN(opts, generate, edit) {
2971
3743
  }
2972
3744
  }
2973
3745
  /** Default provider dispatch: resolve the model through the provider registry. */
2974
- function defaultResolveModel(provider, modelId, apiKey) {
2975
- return getProviderAdapter(provider).resolveModel(modelId, apiKey);
2976
- }
2977
- /** Resolve the model id: explicit `model` wins, else the provider's tier map. */
2978
- function resolveModelId(provider, model, tier) {
2979
- if (model !== void 0 && model !== "") return model;
2980
- const resolvedTier = tier ?? DEFAULT_TIER;
2981
- return getProviderAdapter(provider).tierModels[resolvedTier];
3746
+ function defaultResolveModel(provider, modelId, apiKey, fetch) {
3747
+ return getProviderAdapter(provider).resolveModel(modelId, apiKey, fetch);
2982
3748
  }
2983
3749
  /** Map the AI SDK result → normalized MotifImageResult (with cost + requestId). */
2984
3750
  function toMotifImageResult(result, provider, model) {
@@ -3070,7 +3836,10 @@ function toMotifError(error) {
3070
3836
  if (error instanceof MotifError) return error;
3071
3837
  const message = error instanceof Error ? error.message : String(error);
3072
3838
  const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
3073
- 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);
3074
3843
  }
3075
3844
  //#endregion
3076
- 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 };