@howells/motif-sdk 2.0.0 → 5.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
@@ -1,16 +1,71 @@
1
1
  import { generateImage } from "ai";
2
2
  import { err, ok } from "neverthrow";
3
3
  import { createFal } from "@ai-sdk/fal";
4
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
5
4
  import { createOpenAI } from "@ai-sdk/openai";
6
5
  import { createReplicate } from "@ai-sdk/replicate";
6
+ //#region src/errors.ts
7
+ /**
8
+ * `MotifError` and its coercion helper.
9
+ *
10
+ * Extracted from `server.ts` so `server-tools.ts` can construct errors without
11
+ * importing back into `server.ts` — that import was a dependency cycle. Both
12
+ * `server.ts` and `server-tools.ts` import from here, and `server.ts`
13
+ * re-exports `MotifError` so the package's public surface is unchanged.
14
+ */
15
+ var MotifError = class extends Error {
16
+ status;
17
+ code;
18
+ /** fal's request-correlation id (from the `x-fal-request-id` header or the
19
+ * error body). Ties a failure back to fal's dashboard/support. */
20
+ requestId;
21
+ /** Structured context for the failure, e.g. the refused field. */
22
+ details;
23
+ constructor(message, status, code, requestId, details) {
24
+ super(message);
25
+ this.name = "MotifError";
26
+ this.status = status;
27
+ this.code = code;
28
+ this.requestId = requestId;
29
+ this.details = details;
30
+ }
31
+ };
32
+ /** Error code for a fal account that is locked, usually for lack of credit. */
33
+ const ACCOUNT_LOCKED = "ACCOUNT_LOCKED";
34
+ const ACCOUNT_LOCKED_DETAIL = "User is locked";
35
+ /**
36
+ * Whether a fal HTTP failure means the account is locked.
37
+ *
38
+ * fal answers `403 {"detail":"User is locked. Reason: TOP_UP."}` when the
39
+ * account has run out of credit. Retrying cannot help, and the key is valid.
40
+ */
41
+ function isFalAccountLocked(status, body) {
42
+ return status === 403 && body.includes(ACCOUNT_LOCKED_DETAIL);
43
+ }
44
+ /**
45
+ * Build the `MotifError` for a non-OK fal response, recognising a locked
46
+ * account as `ACCOUNT_LOCKED`.
47
+ */
48
+ function falHttpError(status, body, requestId) {
49
+ if (isFalAccountLocked(status, body)) return new MotifError(`fal account is locked because it is out of credit (fal said: ${body})`, status, ACCOUNT_LOCKED, requestId);
50
+ return new MotifError(`Request failed: ${status} ${body}`, status, void 0, requestId);
51
+ }
52
+ //#endregion
7
53
  //#region src/models.ts
8
54
  const AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-08-23";
9
55
  const AA_IMAGE_SOURCES = ["https://artificialanalysis.ai/image/leaderboard/text-to-image", "https://artificialanalysis.ai/image/leaderboard/editing"];
10
56
  const FAL_PRICING_CHECKED_AT = "2026-05-12";
11
57
  const FAL_PRICING_CHECKED_JUL_2026 = "2026-07-11";
58
+ const FAL_PRICING_CHECKED_AUG_2026 = "2026-08-23";
59
+ const FAL_PRICING_CHECKED_SEP_2026 = "2026-09-16";
12
60
  const MODELS = {
13
61
  flare: {
62
+ customImageSize: {
63
+ maxEdge: 3840,
64
+ maxPixels: 8294400,
65
+ maxRatio: 3,
66
+ minPixels: 655360,
67
+ multipleOf: 16
68
+ },
14
69
  editEndpoint: "openai/gpt-image-2.5/flare/edit",
15
70
  endpoint: "openai/gpt-image-2.5/flare/text-to-image",
16
71
  maxReferenceImages: 16,
@@ -40,6 +95,13 @@ const MODELS = {
40
95
  useQueue: true
41
96
  },
42
97
  sunburst: {
98
+ customImageSize: {
99
+ maxEdge: 3840,
100
+ maxPixels: 8294400,
101
+ maxRatio: 3,
102
+ minPixels: 655360,
103
+ multipleOf: 16
104
+ },
43
105
  editEndpoint: "openai/gpt-image-2.5/sunburst/edit",
44
106
  endpoint: "openai/gpt-image-2.5/sunburst/text-to-image",
45
107
  maxReferenceImages: 16,
@@ -94,7 +156,14 @@ const MODELS = {
94
156
  },
95
157
  useCase: "Highest-ranked text-to-image quality and transparent PNGs"
96
158
  },
97
- editEndpoint: "openai/gpt-image-2/image-to-image",
159
+ customImageSize: {
160
+ maxEdge: 3840,
161
+ maxPixels: 8294400,
162
+ maxRatio: 3,
163
+ minPixels: 655360,
164
+ multipleOf: 16
165
+ },
166
+ editEndpoint: "openai/gpt-image-2/edit",
98
167
  endpoint: "openai/gpt-image-2",
99
168
  falPricing: {
100
169
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -105,19 +174,30 @@ const MODELS = {
105
174
  unit: "units",
106
175
  unitPrice: 1
107
176
  },
108
- maxReferenceImages: 4,
177
+ maxReferenceImages: 16,
109
178
  name: "GPT Image 2",
110
179
  pricePerImageUsd: .211,
111
180
  pricing: "$0.211",
112
181
  sizeMode: "image_size_enum",
113
182
  supportsAspect: true,
183
+ streaming: {
184
+ generation: true,
185
+ edit: true
186
+ },
114
187
  supportsEdit: true,
115
188
  supportsMaskImage: true,
189
+ maskImageField: "mask_url",
116
190
  supportsNumImages: true,
117
191
  supportsOutputFormat: true,
118
192
  supportsQuality: true,
119
193
  supportsResolution: false,
120
194
  supportsSyncMode: true,
195
+ transparencyRoute: {
196
+ apiKeyEnv: "OPENAI_API_KEY",
197
+ model: "gpt-image-2",
198
+ provider: "openai",
199
+ supportsEdit: true
200
+ },
121
201
  type: "generation",
122
202
  useQueue: true
123
203
  },
@@ -165,6 +245,10 @@ const MODELS = {
165
245
  sizeMode: "gpt_size",
166
246
  supportsAspect: false,
167
247
  supportsBackground: true,
248
+ streaming: {
249
+ generation: true,
250
+ edit: true
251
+ },
168
252
  supportsEdit: true,
169
253
  supportsMaskImage: true,
170
254
  supportsNumImages: true,
@@ -267,6 +351,24 @@ const MODELS = {
267
351
  pricePerImageUsd: .15,
268
352
  pricing: "$0.15",
269
353
  sizeMode: "aspect_ratio",
354
+ supportedAspects: [
355
+ "auto",
356
+ "21:9",
357
+ "16:9",
358
+ "3:2",
359
+ "4:3",
360
+ "5:4",
361
+ "1:1",
362
+ "4:5",
363
+ "3:4",
364
+ "2:3",
365
+ "9:16"
366
+ ],
367
+ supportedResolutions: [
368
+ "1K",
369
+ "2K",
370
+ "4K"
371
+ ],
270
372
  supportsAspect: true,
271
373
  supportsEdit: true,
272
374
  supportsGoogleSearch: true,
@@ -305,6 +407,18 @@ const MODELS = {
305
407
  pricePerImageUsd: .0398,
306
408
  pricing: "$0.04",
307
409
  sizeMode: "aspect_ratio",
410
+ supportedAspects: [
411
+ "21:9",
412
+ "16:9",
413
+ "3:2",
414
+ "4:3",
415
+ "5:4",
416
+ "1:1",
417
+ "4:5",
418
+ "3:4",
419
+ "2:3",
420
+ "9:16"
421
+ ],
308
422
  supportsAspect: true,
309
423
  supportsEdit: true,
310
424
  supportsNumImages: true,
@@ -344,6 +458,24 @@ const MODELS = {
344
458
  pricePerImageUsd: .15,
345
459
  pricing: "$0.15",
346
460
  sizeMode: "aspect_ratio",
461
+ supportedAspects: [
462
+ "auto",
463
+ "21:9",
464
+ "16:9",
465
+ "3:2",
466
+ "4:3",
467
+ "5:4",
468
+ "1:1",
469
+ "4:5",
470
+ "3:4",
471
+ "2:3",
472
+ "9:16"
473
+ ],
474
+ supportedResolutions: [
475
+ "1K",
476
+ "2K",
477
+ "4K"
478
+ ],
347
479
  supportsAspect: true,
348
480
  supportsEdit: true,
349
481
  supportsNumImages: true,
@@ -381,6 +513,10 @@ const MODELS = {
381
513
  },
382
514
  useCase: "High-ranked budget generation and large multi-reference edits"
383
515
  },
516
+ customImageSize: {
517
+ maxPixels: 16777216,
518
+ minPixels: 921600
519
+ },
384
520
  editEndpoint: "fal-ai/bytedance/seedream/v4/edit",
385
521
  endpoint: "fal-ai/bytedance/seedream/v4/text-to-image",
386
522
  falPricing: {
@@ -431,6 +567,10 @@ const MODELS = {
431
567
  },
432
568
  useCase: "Cheap current Seedream model with good animation/art scores"
433
569
  },
570
+ customImageSize: {
571
+ maxPixels: 16777216,
572
+ minPixels: 3686400
573
+ },
434
574
  editEndpoint: "fal-ai/bytedance/seedream/v4.5/edit",
435
575
  endpoint: "fal-ai/bytedance/seedream/v4.5/text-to-image",
436
576
  falPricing: {
@@ -468,6 +608,11 @@ const MODELS = {
468
608
  rank: 10
469
609
  }
470
610
  } },
611
+ customImageSize: {
612
+ maxPixels: 4194304,
613
+ maxRatio: 16,
614
+ minPixels: 1048576
615
+ },
471
616
  name: "Seedream 5.0 Pro",
472
617
  endpoint: "bytedance/seedream/v5/pro/text-to-image",
473
618
  editEndpoint: "bytedance/seedream/v5/pro/edit",
@@ -501,16 +646,20 @@ const MODELS = {
501
646
  snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
502
647
  sourceUrls: AA_IMAGE_SOURCES
503
648
  } },
649
+ customImageSize: {
650
+ maxPixels: 16777216,
651
+ minPixels: 3686400
652
+ },
504
653
  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",
654
+ endpoint: "bytedance/seedream/v5/lite/text-to-image",
655
+ editEndpoint: "bytedance/seedream/v5/lite/edit",
507
656
  type: "generation",
508
657
  pricing: "$0.035",
509
658
  pricePerImageUsd: .035,
510
659
  falPricing: {
511
660
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
512
661
  currency: "USD",
513
- endpointId: "fal-ai/bytedance/seedream/v5/lite/text-to-image",
662
+ endpointId: "bytedance/seedream/v5/lite/text-to-image",
514
663
  estimatedCostPerImageUsd: .035,
515
664
  source: "fal-pricing-api",
516
665
  unit: "images",
@@ -551,6 +700,12 @@ const MODELS = {
551
700
  },
552
701
  useCase: "Best FLUX quality and high-end editing"
553
702
  },
703
+ customImageSize: {
704
+ maxEdge: 2560,
705
+ maxPixels: 4194304,
706
+ minEdge: 256,
707
+ multipleOf: 16
708
+ },
554
709
  editEndpoint: "fal-ai/flux-2-max/edit",
555
710
  endpoint: "fal-ai/flux-2-max",
556
711
  falPricing: {
@@ -604,6 +759,12 @@ const MODELS = {
604
759
  },
605
760
  useCase: "Production FLUX quality at a low per-megapixel price"
606
761
  },
762
+ customImageSize: {
763
+ maxEdge: 2560,
764
+ maxPixels: 4194304,
765
+ minEdge: 256,
766
+ multipleOf: 16
767
+ },
607
768
  editEndpoint: "fal-ai/flux-2-pro/edit",
608
769
  endpoint: "fal-ai/flux-2-pro",
609
770
  falPricing: {
@@ -653,6 +814,12 @@ const MODELS = {
653
814
  },
654
815
  useCase: "FLUX quality with guidance and step controls"
655
816
  },
817
+ customImageSize: {
818
+ maxEdge: 2560,
819
+ maxPixels: 4194304,
820
+ minEdge: 256,
821
+ multipleOf: 16
822
+ },
656
823
  editEndpoint: "fal-ai/flux-2-flex/edit",
657
824
  endpoint: "fal-ai/flux-2-flex",
658
825
  falPricing: {
@@ -701,6 +868,10 @@ const MODELS = {
701
868
  },
702
869
  useCase: "Open FLUX.2 quality with low average cost"
703
870
  },
871
+ customImageSize: {
872
+ maxEdge: 2048,
873
+ minEdge: 512
874
+ },
704
875
  editEndpoint: "fal-ai/flux-2/edit",
705
876
  endpoint: "fal-ai/flux-2",
706
877
  falPricing: {
@@ -712,12 +883,16 @@ const MODELS = {
712
883
  unit: "compute seconds",
713
884
  unitPrice: .00167
714
885
  },
715
- maxReferenceImages: 10,
886
+ maxReferenceImages: 4,
716
887
  name: "FLUX.2 [dev]",
717
888
  pricePerImageUsd: .012,
718
889
  pricing: "$0.00167/sec",
719
890
  sizeMode: "image_size_enum",
720
891
  supportsAspect: true,
892
+ streaming: {
893
+ generation: true,
894
+ edit: true
895
+ },
721
896
  supportsEdit: true,
722
897
  supportsGuidanceScale: true,
723
898
  supportsInferenceSteps: true,
@@ -731,6 +906,10 @@ const MODELS = {
731
906
  type: "generation"
732
907
  },
733
908
  "flux2-turbo": {
909
+ customImageSize: {
910
+ maxEdge: 2048,
911
+ minEdge: 512
912
+ },
734
913
  endpoint: "fal-ai/flux-2/turbo",
735
914
  falPricing: {
736
915
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
@@ -786,6 +965,7 @@ const MODELS = {
786
965
  type: "generation"
787
966
  },
788
967
  "flux-fast": {
968
+ customImageSize: {},
789
969
  endpoint: "fal-ai/flux/schnell",
790
970
  falPricing: {
791
971
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -812,11 +992,12 @@ const MODELS = {
812
992
  type: "generation"
813
993
  },
814
994
  recraft: {
815
- endpoint: "fal-ai/recraft-v3",
995
+ customImageSize: {},
996
+ endpoint: "fal-ai/recraft/v3/text-to-image",
816
997
  falPricing: {
817
998
  checkedAt: FAL_PRICING_CHECKED_AT,
818
999
  currency: "USD",
819
- endpointId: "fal-ai/recraft-v3",
1000
+ endpointId: "fal-ai/recraft/v3/text-to-image",
820
1001
  estimatedCostPerImageUsd: .04,
821
1002
  source: "fal-pricing-api",
822
1003
  unit: "images",
@@ -858,7 +1039,7 @@ const MODELS = {
858
1039
  editEndpoint: "fal-ai/reve/edit",
859
1040
  endpoint: "fal-ai/reve/text-to-image",
860
1041
  falPricing: {
861
- checkedAt: "2026-08-23",
1042
+ checkedAt: FAL_PRICING_CHECKED_AUG_2026,
862
1043
  currency: "USD",
863
1044
  endpointId: "fal-ai/reve/text-to-image",
864
1045
  estimatedCostPerImageUsd: .04,
@@ -871,6 +1052,15 @@ const MODELS = {
871
1052
  pricePerImageUsd: .04,
872
1053
  pricing: "$0.04",
873
1054
  sizeMode: "aspect_ratio",
1055
+ supportedAspects: [
1056
+ "16:9",
1057
+ "9:16",
1058
+ "3:2",
1059
+ "2:3",
1060
+ "4:3",
1061
+ "3:4",
1062
+ "1:1"
1063
+ ],
874
1064
  supportsAspect: true,
875
1065
  supportsEdit: true,
876
1066
  supportsNumImages: true,
@@ -889,6 +1079,7 @@ const MODELS = {
889
1079
  rank: 25
890
1080
  }
891
1081
  } },
1082
+ customImageSize: {},
892
1083
  endpoint: "fal-ai/recraft/v4/text-to-image",
893
1084
  falPricing: {
894
1085
  checkedAt: FAL_PRICING_CHECKED_JUL_2026,
@@ -907,11 +1098,11 @@ const MODELS = {
907
1098
  supportsEdit: false,
908
1099
  supportsNumImages: false,
909
1100
  supportsResolution: false,
910
- supportsStyle: true,
911
1101
  supportsSyncMode: true,
912
1102
  type: "generation"
913
1103
  },
914
1104
  ideogram: {
1105
+ customImageSize: {},
915
1106
  endpoint: "fal-ai/ideogram/v3",
916
1107
  falPricing: {
917
1108
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -947,6 +1138,11 @@ const MODELS = {
947
1138
  rank: 19
948
1139
  }
949
1140
  } },
1141
+ customImageSize: {
1142
+ maxEdge: 3840,
1143
+ minEdge: 512,
1144
+ multipleOf: 16
1145
+ },
950
1146
  name: "Ideogram V4",
951
1147
  endpoint: "ideogram/v4",
952
1148
  type: "generation",
@@ -1010,11 +1206,21 @@ const MODELS = {
1010
1206
  unit: "images",
1011
1207
  unitPrice: .02
1012
1208
  },
1013
- maxReferenceImages: 4,
1209
+ maxReferenceImages: 3,
1014
1210
  name: "Grok Imagine Image",
1015
1211
  pricePerImageUsd: .02,
1016
1212
  pricing: "$0.02",
1017
1213
  sizeMode: "aspect_ratio",
1214
+ supportedAspects: [
1215
+ "16:9",
1216
+ "4:3",
1217
+ "3:2",
1218
+ "1:1",
1219
+ "2:3",
1220
+ "3:4",
1221
+ "9:16"
1222
+ ],
1223
+ supportedResolutions: ["1K", "2K"],
1018
1224
  supportsAspect: true,
1019
1225
  supportsEdit: true,
1020
1226
  supportsNumImages: true,
@@ -1041,6 +1247,7 @@ const MODELS = {
1041
1247
  },
1042
1248
  useCase: "Low-cost open-weight text and design generation"
1043
1249
  },
1250
+ customImageSize: {},
1044
1251
  endpoint: "fal-ai/qwen-image",
1045
1252
  falPricing: {
1046
1253
  checkedAt: FAL_PRICING_CHECKED_AT,
@@ -1085,19 +1292,23 @@ const MODELS = {
1085
1292
  },
1086
1293
  useCase: "Strong quality per dollar - 11th on text-to-image at $30/1k"
1087
1294
  },
1088
- endpoint: "fal-ai/qwen-image-3/text-to-image",
1295
+ customImageSize: {
1296
+ maxPixels: 4194304,
1297
+ minPixels: 262144
1298
+ },
1299
+ endpoint: "alibaba/qwen-image-3/text-to-image",
1089
1300
  falPricing: {
1090
- checkedAt: "2026-08-05",
1301
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1091
1302
  currency: "USD",
1092
- endpointId: "fal-ai/qwen-image-3/text-to-image",
1093
- estimatedCostPerImageUsd: .02,
1303
+ endpointId: "alibaba/qwen-image-3/text-to-image",
1304
+ estimatedCostPerImageUsd: .04,
1094
1305
  source: "fal-pricing-api",
1095
- unit: "megapixels",
1096
- unitPrice: .02
1306
+ unit: "images",
1307
+ unitPrice: .04
1097
1308
  },
1098
1309
  name: "Qwen Image 3",
1099
- pricePerImageUsd: .02,
1100
- pricing: "$0.02/MP (assumed from qwen v1; fal pricing page bot-gated at check time)",
1310
+ pricePerImageUsd: .04,
1311
+ pricing: "$0.04 (1K) / $0.075 (2K)",
1101
1312
  sizeMode: "image_size_enum",
1102
1313
  supportsAspect: true,
1103
1314
  supportsEdit: false,
@@ -1109,6 +1320,179 @@ const MODELS = {
1109
1320
  supportsSeed: true,
1110
1321
  type: "generation"
1111
1322
  },
1323
+ "mai-image-2.5-pro": {
1324
+ benchmark: {
1325
+ artificialAnalysis: {
1326
+ editing: {
1327
+ elo: 1272,
1328
+ rank: 1
1329
+ },
1330
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
1331
+ sourceUrls: AA_IMAGE_SOURCES,
1332
+ textToImage: {
1333
+ elo: 1293,
1334
+ rank: 7
1335
+ }
1336
+ },
1337
+ useCase: "Highest-ranked image editor, single reference image"
1338
+ },
1339
+ editEndpoint: "microsoft/mai-image-2.5-pro/edit",
1340
+ editImagesField: "image_url",
1341
+ endpoint: "microsoft/mai-image-2.5-pro",
1342
+ falPricing: {
1343
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1344
+ currency: "USD",
1345
+ endpointId: "microsoft/mai-image-2.5-pro",
1346
+ estimatedCostPerImageUsd: .17,
1347
+ source: "fal-pricing-api",
1348
+ unit: "images",
1349
+ unitPrice: .17
1350
+ },
1351
+ maxReferenceImages: 1,
1352
+ name: "MAI Image 2.5 Pro",
1353
+ pricePerImageUsd: .17,
1354
+ pricing: "~$0.17 (edit ~$0.18-$0.27)",
1355
+ sizeMode: "aspect_ratio",
1356
+ supportedAspects: [
1357
+ "auto",
1358
+ "1:1",
1359
+ "4:3",
1360
+ "3:4",
1361
+ "16:9",
1362
+ "9:16",
1363
+ "3:2",
1364
+ "2:3"
1365
+ ],
1366
+ supportsAspect: true,
1367
+ supportsEdit: true,
1368
+ supportsNumImages: true,
1369
+ supportsOutputFormat: true,
1370
+ supportsResolution: false,
1371
+ supportsSyncMode: true,
1372
+ type: "generation"
1373
+ },
1374
+ "banana2-lite": {
1375
+ benchmark: { artificialAnalysis: {
1376
+ snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
1377
+ sourceUrls: AA_IMAGE_SOURCES,
1378
+ textToImage: {
1379
+ elo: 1289,
1380
+ rank: 8
1381
+ }
1382
+ } },
1383
+ endpoint: "google/nano-banana-2-lite",
1384
+ name: "Nano Banana 2 Lite",
1385
+ 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)",
1386
+ pricePerImageUsd: .048,
1387
+ sizeMode: "aspect_ratio",
1388
+ supportsAspect: true,
1389
+ supportsEdit: false,
1390
+ supportsLimitGenerations: true,
1391
+ supportsNumImages: true,
1392
+ supportsOutputFormat: true,
1393
+ supportsResolution: false,
1394
+ supportsSafetyTolerance: true,
1395
+ supportsSeed: true,
1396
+ supportsSyncMode: true,
1397
+ supportsThinkingLevel: true,
1398
+ type: "generation"
1399
+ },
1400
+ "ideogram3-transparent": {
1401
+ endpoint: "fal-ai/ideogram/v3/generate-transparent",
1402
+ falPricing: {
1403
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1404
+ currency: "USD",
1405
+ endpointId: "fal-ai/ideogram/v3/generate-transparent",
1406
+ estimatedCostPerImageUsd: .06,
1407
+ source: "fal-pricing-api",
1408
+ unit: "images",
1409
+ unitPrice: .06
1410
+ },
1411
+ name: "Ideogram V3 Transparent",
1412
+ pricePerImageUsd: .06,
1413
+ pricing: "$0.03 TURBO / $0.06 BALANCED / $0.09 QUALITY",
1414
+ sizeMode: "aspect_ratio",
1415
+ supportedAspects: [
1416
+ "16:9",
1417
+ "9:16",
1418
+ "3:2",
1419
+ "2:3",
1420
+ "4:3",
1421
+ "3:4",
1422
+ "5:4",
1423
+ "4:5",
1424
+ "1:1"
1425
+ ],
1426
+ supportsAspect: true,
1427
+ supportsEdit: false,
1428
+ supportsExpandPrompt: true,
1429
+ supportsNegativePrompt: true,
1430
+ supportsNumImages: true,
1431
+ supportsRenderingSpeed: true,
1432
+ supportsResolution: false,
1433
+ supportsSeed: true,
1434
+ supportsSyncMode: true,
1435
+ transparentOutput: true,
1436
+ type: "generation"
1437
+ },
1438
+ recraft41: {
1439
+ customImageSize: {},
1440
+ endpoint: "fal-ai/recraft/v4.1/text-to-image",
1441
+ falPricing: {
1442
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1443
+ currency: "USD",
1444
+ endpointId: "fal-ai/recraft/v4.1/text-to-image",
1445
+ estimatedCostPerImageUsd: .035,
1446
+ source: "fal-pricing-api",
1447
+ unit: "images",
1448
+ unitPrice: .035
1449
+ },
1450
+ name: "Recraft V4.1",
1451
+ pricePerImageUsd: .035,
1452
+ pricing: "$0.035",
1453
+ sizeMode: "image_size_enum",
1454
+ supportsAspect: true,
1455
+ supportsEdit: false,
1456
+ supportsNumImages: false,
1457
+ supportsResolution: false,
1458
+ supportsSafetyChecker: true,
1459
+ type: "generation"
1460
+ },
1461
+ "grok-image-2": {
1462
+ editEndpoint: "xai/grok-imagine-image/v2.0/edit",
1463
+ endpoint: "xai/grok-imagine-image/v2.0/text-to-image",
1464
+ falPricing: {
1465
+ checkedAt: FAL_PRICING_CHECKED_SEP_2026,
1466
+ currency: "USD",
1467
+ endpointId: "xai/grok-imagine-image/v2.0/text-to-image",
1468
+ estimatedCostPerImageUsd: .06,
1469
+ source: "fal-pricing-api",
1470
+ unit: "images",
1471
+ unitPrice: .06
1472
+ },
1473
+ maxReferenceImages: 3,
1474
+ name: "Grok Imagine Image 2.0",
1475
+ pricePerImageUsd: .06,
1476
+ pricing: "$0.06 (1K) / $0.08 (2K) at medium quality",
1477
+ sizeMode: "aspect_ratio",
1478
+ supportedAspects: [
1479
+ "16:9",
1480
+ "4:3",
1481
+ "3:2",
1482
+ "1:1",
1483
+ "2:3",
1484
+ "3:4",
1485
+ "9:16"
1486
+ ],
1487
+ supportedResolutions: ["1K", "2K"],
1488
+ supportsAspect: true,
1489
+ supportsEdit: true,
1490
+ supportsNumImages: true,
1491
+ supportsOutputFormat: true,
1492
+ supportsResolution: true,
1493
+ supportsSyncMode: true,
1494
+ type: "generation"
1495
+ },
1112
1496
  kling: {
1113
1497
  endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
1114
1498
  name: "Kling v3 Pro",
@@ -1120,6 +1504,17 @@ const MODELS = {
1120
1504
  supportsResolution: false,
1121
1505
  type: "video"
1122
1506
  },
1507
+ "kling-turbo": {
1508
+ endpoint: "fal-ai/kling-video/v3/turbo/pro/image-to-video",
1509
+ name: "Kling v3 Turbo Pro",
1510
+ pricing: "$0.14/sec",
1511
+ sizeMode: "none",
1512
+ supportsAspect: false,
1513
+ supportsEdit: false,
1514
+ supportsNumImages: false,
1515
+ supportsResolution: false,
1516
+ type: "video"
1517
+ },
1123
1518
  clarity: {
1124
1519
  endpoint: "fal-ai/clarity-upscaler",
1125
1520
  name: "Clarity Upscaler",
@@ -1186,33 +1581,175 @@ const MODELS = {
1186
1581
  "ideogram",
1187
1582
  "ideogram4",
1188
1583
  "grok-image",
1584
+ "grok-image-2",
1189
1585
  "qwen",
1190
- "qwen3"
1586
+ "qwen3",
1587
+ "mai-image-2.5-pro",
1588
+ "banana2-lite",
1589
+ "ideogram3-transparent",
1590
+ "recraft41"
1191
1591
  ].filter((id) => MODELS[id]?.supportsEdit === true);
1192
1592
  //#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
- }
1593
+ //#region src/capabilities.ts
1594
+ /** Generation options a model can refuse, with the registry test for each. */
1595
+ const OPTION_CAPABILITIES = {
1596
+ aspect: (config) => (config.sizeMode ?? "aspect_ratio") !== "none",
1597
+ background: (config) => config.supportsBackground === true,
1598
+ enableGoogleSearch: (config) => config.supportsGoogleSearch === true,
1599
+ enableSafetyChecker: (config) => config.supportsSafetyChecker === true,
1600
+ enableWebSearch: (config) => config.supportsWebSearch === true,
1601
+ enhancePrompt: (config) => config.supportsEnhancePrompt === true,
1602
+ expandPrompt: (config) => config.supportsExpandPrompt === true,
1603
+ guidanceScale: (config) => config.supportsGuidanceScale === true,
1604
+ "image editing": (config) => config.supportsEdit,
1605
+ imagePromptStrength: (config) => config.supportsImagePromptStrength === true,
1606
+ imageSize: (config) => config.sizeMode === "gpt_size" || config.sizeMode === "image_size_enum",
1607
+ inputFidelity: (config) => config.sizeMode === "gpt_size",
1608
+ limitGenerations: (config) => config.supportsLimitGenerations === true,
1609
+ maskImageUrl: (config) => config.supportsMaskImage === true,
1610
+ negativePrompt: (config) => config.supportsNegativePrompt === true,
1611
+ numImages: (config) => config.supportsNumImages,
1612
+ numInferenceSteps: (config) => config.supportsInferenceSteps === true,
1613
+ outputFormat: (config) => config.supportsOutputFormat === true,
1614
+ quality: (config) => config.supportsQuality === true,
1615
+ raw: (config) => config.supportsRaw === true,
1616
+ renderingSpeed: (config) => config.supportsRenderingSpeed === true,
1617
+ resolution: (config) => config.supportsResolution,
1618
+ safetyTolerance: (config) => config.supportsSafetyTolerance === true,
1619
+ seed: (config) => config.supportsSeed === true,
1620
+ style: (config) => config.supportsStyle === true,
1621
+ syncMode: (config) => config.supportsSyncMode === true,
1622
+ thinkingLevel: (config) => config.supportsThinkingLevel === true,
1623
+ "transparent output": (config) => config.supportsBackground === true || config.transparentOutput === true
1215
1624
  };
1625
+ function isModelOption(key) {
1626
+ return Object.hasOwn(OPTION_CAPABILITIES, key);
1627
+ }
1628
+ Object.keys(OPTION_CAPABILITIES).filter(isModelOption);
1629
+ ({
1630
+ look: [
1631
+ {
1632
+ acceptsMood: true,
1633
+ aspect: "1:1",
1634
+ 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",
1635
+ description: "Quiet, materially rich editorial photography for brand and mood imagery.",
1636
+ id: "editorial",
1637
+ label: "Quiet editorial",
1638
+ model: "flux2-pro"
1639
+ },
1640
+ {
1641
+ acceptsMood: true,
1642
+ aspect: "1:1",
1643
+ clause: "Editorial still life in the register of Aesop and Kinfolk, on a warm bone plaster ground, chalky unglazed surfaces in muted mineral colour, a long soft shadow, generous empty space, shot on film with fine grain, restrained and materially rich. No text, no logos, no people",
1644
+ description: "Objects and products on a plaster ground, for product and editorial still life.",
1645
+ id: "still-life",
1646
+ label: "Editorial still life",
1647
+ model: "flux2-pro"
1648
+ },
1649
+ {
1650
+ acceptsMood: true,
1651
+ aspect: "3:2",
1652
+ clause: "Interior photograph in the register of House & Garden and Kinfolk, 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, lived-in rather than showroom-perfect, soft natural daylight, shot on film with fine grain. No text, no logos, no people",
1653
+ description: "Bright, collected rooms that feel lived in, for interior scenes.",
1654
+ id: "interior",
1655
+ label: "Interior",
1656
+ model: "flux2-pro"
1657
+ },
1658
+ {
1659
+ acceptsMood: true,
1660
+ aspect: "4:5",
1661
+ clause: "Architectural photograph in the register of House & Garden and Kinfolk, a considered house seen from outside at editorial distance with its garden and setting, pale render, stone or timber meeting precise detailing, clipped planting, soft warm daylight and long shadow, generous negative space, immaculate and calm, shot on film with fine grain. No text, no logos, no people",
1662
+ description: "Buildings and their settings from outside, for architecture, property and place.",
1663
+ id: "architectural",
1664
+ label: "Architectural exterior",
1665
+ model: "banana"
1666
+ },
1667
+ {
1668
+ acceptsMood: true,
1669
+ aspect: "1:1",
1670
+ clause: "Editorial documentary portrait in the register of Kinfolk, muted warm palette, waist-up and unposed against a plain plaster or linen ground, plain clothing with no logos, soft natural light, shot on film with fine grain. No text",
1671
+ description: "Natural, unposed documentary portraits of people. Pair with a mood for the light.",
1672
+ id: "portrait",
1673
+ label: "Documentary portrait",
1674
+ model: "seedream45"
1675
+ },
1676
+ {
1677
+ acceptsMood: false,
1678
+ aspect: "1:1",
1679
+ clause: "A single matte object centred with generous empty space, soft diffused studio light, minimal and quiet in the register of Aesop, one committed muted mineral colour on a plain ground. No text, no logos, no people",
1680
+ description: "One object in one colour on a clean ground, for icons and simple product shots.",
1681
+ id: "object",
1682
+ label: "Studio object",
1683
+ model: "flux2-pro"
1684
+ },
1685
+ {
1686
+ acceptsMood: false,
1687
+ aspect: "1:1",
1688
+ clause: "Straight-on orthographic photograph of the surface filling the entire frame edge to edge, even shadowless studio light, crisp macro texture, colour-accurate and quietly material. No text, no logos",
1689
+ description: "Flat, edge-to-edge surface photographs, for textures, backgrounds and material swatches.",
1690
+ id: "surface",
1691
+ label: "Flat surface",
1692
+ model: "flux2-pro"
1693
+ },
1694
+ {
1695
+ acceptsMood: false,
1696
+ aspect: "3:2",
1697
+ clause: "Painted abstraction filling the frame edge to edge, mineral pigment and chalk gesso on coarse natural linen, two or three confident gestures, warm ivory, oatmeal, putty and soft charcoal, flat diffuse reproduction light. No text",
1698
+ description: "Painted abstraction edge to edge, for wall art, heroes and calm backgrounds.",
1699
+ id: "abstract",
1700
+ label: "Painted abstract",
1701
+ model: "banana"
1702
+ },
1703
+ {
1704
+ acceptsMood: false,
1705
+ aspect: "1:1",
1706
+ clause: "Stylised editorial illustration in the register of Kinfolk, colour laid as flat planes in a warm muted palette of ivory, putty, sage and charcoal, fine hand-drawn line with a gentle gouache wash, generous empty space, clearly a drawing rather than a photograph. No text, no logos",
1707
+ description: "Line and gouache illustration of any subject, for drawn editorial imagery.",
1708
+ experimental: true,
1709
+ id: "illustration",
1710
+ label: "Editorial illustration",
1711
+ model: "gpt2"
1712
+ }
1713
+ ],
1714
+ mood: [
1715
+ {
1716
+ clause: "soft natural daylight from a window out of frame, gentle falloff into the corners, low contrast, even diffused light with soft, held highlights",
1717
+ description: "Soft, even daylight from a window. The safe default.",
1718
+ id: "window",
1719
+ label: "Window light"
1720
+ },
1721
+ {
1722
+ clause: "early morning light through tall glazing, cool and clear",
1723
+ description: "Cool, clear early morning light.",
1724
+ id: "dawn",
1725
+ label: "Dawn"
1726
+ },
1727
+ {
1728
+ clause: "low raking daylight from the left, long soft shadows that reveal texture",
1729
+ description: "Low side light that brings out surface texture.",
1730
+ id: "raking",
1731
+ label: "Raking light"
1732
+ },
1733
+ {
1734
+ clause: "overcast afternoon with rain on a tall window, soft even grey light",
1735
+ description: "Soft grey light on a rainy afternoon.",
1736
+ id: "overcast",
1737
+ label: "Overcast"
1738
+ },
1739
+ {
1740
+ clause: "evening, warm practical lamps around 2400K, candles and a lit fire, cosy and warm, never gloomy",
1741
+ description: "Warm evening light from lamps, candles and a fire.",
1742
+ id: "lamplit",
1743
+ label: "Lamplit evening"
1744
+ },
1745
+ {
1746
+ clause: "night, one warm low practical light, deep shadow, candlelit",
1747
+ description: "Dark night scene lit by one warm low light.",
1748
+ id: "nocturne",
1749
+ label: "Nocturne"
1750
+ }
1751
+ ]
1752
+ }).look;
1216
1753
  //#endregion
1217
1754
  //#region src/tool-registry/analysis.ts
1218
1755
  const ANALYSIS_TOOLS = {
@@ -1224,7 +1761,11 @@ const ANALYSIS_TOOLS = {
1224
1761
  inputKind: "images",
1225
1762
  name: "GOT-OCR 2.0",
1226
1763
  outputKeys: ["outputs"],
1227
- price: { kind: "metered" },
1764
+ price: {
1765
+ kind: "call",
1766
+ per: ["input_image_urls"],
1767
+ usd: .05
1768
+ },
1228
1769
  pricing: "$0.05/image",
1229
1770
  queued: true,
1230
1771
  sourceUrl: "https://fal.ai/models/fal-ai/got-ocr/v2",
@@ -1238,7 +1779,13 @@ const ANALYSIS_TOOLS = {
1238
1779
  inputKind: "image",
1239
1780
  name: "Moondream 3 Caption",
1240
1781
  outputKeys: ["output"],
1241
- price: { kind: "metered" },
1782
+ price: {
1783
+ inputPerMillion: .4,
1784
+ inputTokens: 737,
1785
+ kind: "token",
1786
+ outputPerMillion: 3.5,
1787
+ outputTokens: 200
1788
+ },
1242
1789
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1243
1790
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/caption",
1244
1791
  task: "image captioning"
@@ -1251,7 +1798,13 @@ const ANALYSIS_TOOLS = {
1251
1798
  inputKind: "image",
1252
1799
  name: "Moondream 3 Detect",
1253
1800
  outputKeys: ["objects", "image"],
1254
- price: { kind: "metered" },
1801
+ price: {
1802
+ inputPerMillion: .4,
1803
+ inputTokens: 737,
1804
+ kind: "token",
1805
+ outputPerMillion: 3.5,
1806
+ outputTokens: 100
1807
+ },
1255
1808
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1256
1809
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/detect",
1257
1810
  task: "object detection"
@@ -1264,7 +1817,13 @@ const ANALYSIS_TOOLS = {
1264
1817
  inputKind: "image",
1265
1818
  name: "Moondream 3 Point",
1266
1819
  outputKeys: ["points", "image"],
1267
- price: { kind: "metered" },
1820
+ price: {
1821
+ inputPerMillion: .4,
1822
+ inputTokens: 737,
1823
+ kind: "token",
1824
+ outputPerMillion: 3.5,
1825
+ outputTokens: 100
1826
+ },
1268
1827
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1269
1828
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/point",
1270
1829
  task: "object pointing"
@@ -1277,7 +1836,13 @@ const ANALYSIS_TOOLS = {
1277
1836
  inputKind: "image",
1278
1837
  name: "Moondream 3 Query",
1279
1838
  outputKeys: ["output", "reasoning"],
1280
- price: { kind: "metered" },
1839
+ price: {
1840
+ inputPerMillion: .4,
1841
+ inputTokens: 737,
1842
+ kind: "token",
1843
+ outputPerMillion: 3.5,
1844
+ outputTokens: 500
1845
+ },
1281
1846
  pricing: "$0.40/M input tokens, $3.50/M output tokens",
1282
1847
  sourceUrl: "https://fal.ai/models/fal-ai/moondream3-preview/query",
1283
1848
  task: "visual question answering"
@@ -1290,7 +1855,11 @@ const ANALYSIS_TOOLS = {
1290
1855
  inputKind: "images",
1291
1856
  name: "NSFW Checker",
1292
1857
  outputKeys: ["has_nsfw_concepts"],
1293
- price: { kind: "metered" },
1858
+ price: {
1859
+ kind: "call",
1860
+ per: ["image_urls"],
1861
+ usd: .001
1862
+ },
1294
1863
  pricing: "$0.001/image",
1295
1864
  sourceUrl: "https://fal.ai/models/fal-ai/x-ailab/nsfw",
1296
1865
  task: "vision moderation"
@@ -1374,6 +1943,34 @@ const ASSET_TOOLS = {
1374
1943
  sourceUrl: "https://fal.ai/models/fal-ai/image2svg",
1375
1944
  task: "raster to vector tracing"
1376
1945
  },
1946
+ "meshy-v7": {
1947
+ category: "3d",
1948
+ description: "Make a textured 3D mesh from one image, optionally rigged for animation.",
1949
+ endpoint: "meshy/v7/image-to-3d",
1950
+ inputField: "image_url",
1951
+ inputKind: "image",
1952
+ name: "Meshy v7 Image to 3D",
1953
+ outputKeys: [
1954
+ "model_glb",
1955
+ "model_urls",
1956
+ "thumbnail",
1957
+ "rigged_character_glb",
1958
+ "rigged_character_fbx"
1959
+ ],
1960
+ price: {
1961
+ extras: {
1962
+ enable_animation: .12,
1963
+ enable_rigging: .2,
1964
+ ultra_mode: .2
1965
+ },
1966
+ kind: "call",
1967
+ usd: 1.2
1968
+ },
1969
+ 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",
1970
+ queued: true,
1971
+ sourceUrl: "https://fal.ai/models/meshy/v7/image-to-3d",
1972
+ task: "single-image 3D reconstruction"
1973
+ },
1377
1974
  patina: {
1378
1975
  category: "material",
1379
1976
  description: "Decompose a surface photograph into PBR maps: basecolor, normal, roughness, metalness, height.",
@@ -1392,7 +1989,12 @@ const ASSET_TOOLS = {
1392
1989
  ],
1393
1990
  fromOption: "maps"
1394
1991
  } },
1395
- price: { kind: "metered" },
1992
+ price: {
1993
+ base: .01,
1994
+ kind: "maps",
1995
+ perMapMegapixel: .01,
1996
+ perMegapixel: 0
1997
+ },
1396
1998
  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
1999
  queued: true,
1398
2000
  sourceUrl: "https://fal.ai/models/fal-ai/patina",
@@ -1416,8 +2018,17 @@ const ASSET_TOOLS = {
1416
2018
  ],
1417
2019
  fromOption: "maps"
1418
2020
  } },
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",
2021
+ price: {
2022
+ base: .1,
2023
+ kind: "maps",
2024
+ perMapMegapixel: .01,
2025
+ perMegapixel: .02,
2026
+ upscale: {
2027
+ "2": .004,
2028
+ "4": .016
2029
+ }
2030
+ },
2031
+ 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
2032
  queued: true,
1422
2033
  sourceUrl: "https://fal.ai/models/fal-ai/patina/material/extract",
1423
2034
  task: "tiling material extraction"
@@ -1430,8 +2041,11 @@ const ASSET_TOOLS = {
1430
2041
  inputKind: "image",
1431
2042
  name: "Qwen Image Layered",
1432
2043
  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",
2044
+ price: {
2045
+ kind: "call",
2046
+ usd: .05
2047
+ },
2048
+ pricing: "$0.05 per image, one input image a call",
1435
2049
  queued: true,
1436
2050
  sourceUrl: "https://fal.ai/models/fal-ai/qwen-image-layered",
1437
2051
  task: "image layer decomposition"
@@ -1498,7 +2112,6 @@ const ASSET_TOOLS = {
1498
2112
  },
1499
2113
  "sam3-3d-objects": {
1500
2114
  category: "3d",
1501
- defaultOptions: { prompt: "car" },
1502
2115
  description: "Reconstruct one or more 3D objects from an image and prompts.",
1503
2116
  endpoint: "fal-ai/sam-3/3d-objects",
1504
2117
  inputField: "image_url",
@@ -1604,7 +2217,10 @@ const BACKGROUND_TOOLS = {
1604
2217
  inputKind: "image",
1605
2218
  name: "BirefNet Background Removal",
1606
2219
  outputKeys: ["image", "mask_image"],
1607
- price: { kind: "metered" },
2220
+ price: {
2221
+ kind: "call",
2222
+ usd: 0
2223
+ },
1608
2224
  pricing: "$0/compute-second listed by fal",
1609
2225
  sourceUrl: "https://fal.ai/models/fal-ai/birefnet/v2",
1610
2226
  task: "image background removal"
@@ -1640,13 +2256,51 @@ const BACKGROUND_TOOLS = {
1640
2256
  outputKeys: ["video"],
1641
2257
  price: {
1642
2258
  kind: "second",
1643
- usd: .00425
2259
+ usd: .14
1644
2260
  },
1645
- pricing: "$0.00425/sec",
2261
+ pricing: "$0.14/sec",
1646
2262
  queued: true,
1647
2263
  sourceUrl: "https://fal.ai/models/bria/video/background-removal",
1648
2264
  task: "video background removal"
1649
2265
  },
2266
+ "bria-video-rmbg-v3": {
2267
+ category: "background",
2268
+ defaultOptions: {
2269
+ background_color: "Black",
2270
+ output_container_and_codec: "webm_vp9",
2271
+ preserve_audio: true
2272
+ },
2273
+ description: "Remove video backgrounds with Bria's VRMBG 3.0, with configurable output container.",
2274
+ endpoint: "bria/video/background-removal/v3",
2275
+ inputField: "video_url",
2276
+ inputKind: "video",
2277
+ name: "Bria Video Background Removal 3.0",
2278
+ outputKeys: ["video"],
2279
+ price: {
2280
+ kind: "second",
2281
+ usd: .05
2282
+ },
2283
+ pricing: "$0.05/sec",
2284
+ queued: true,
2285
+ sourceUrl: "https://fal.ai/models/bria/video/background-removal/v3",
2286
+ task: "video background removal"
2287
+ },
2288
+ "control-light": {
2289
+ category: "restoration",
2290
+ description: "Brighten a dark or underexposed photo, recovering detail while keeping its identity, geometry and colour.",
2291
+ endpoint: "fal-ai/control-light",
2292
+ inputField: "image_url",
2293
+ inputKind: "image",
2294
+ name: "Control Light",
2295
+ outputKeys: ["images"],
2296
+ price: {
2297
+ kind: "megapixel",
2298
+ usd: .03
2299
+ },
2300
+ pricing: "$0.03/megapixel",
2301
+ sourceUrl: "https://fal.ai/models/fal-ai/control-light",
2302
+ task: "low-light enhancement"
2303
+ },
1650
2304
  ddcolor: {
1651
2305
  category: "restoration",
1652
2306
  description: "Colourise black-and-white photographs.",
@@ -1672,7 +2326,10 @@ const BACKGROUND_TOOLS = {
1672
2326
  inputKind: "image",
1673
2327
  name: "Remove Background",
1674
2328
  outputKeys: ["image"],
1675
- price: { kind: "metered" },
2329
+ price: {
2330
+ kind: "call",
2331
+ usd: 0
2332
+ },
1676
2333
  pricing: "$0/compute-second listed by fal",
1677
2334
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/rembg",
1678
2335
  task: "image background removal"
@@ -1705,8 +2362,9 @@ const BACKGROUND_TOOLS = {
1705
2362
  name: "Topaz Adjust",
1706
2363
  outputKeys: ["image"],
1707
2364
  price: {
1708
- kind: "megapixel",
1709
- usd: .08 / 24
2365
+ kind: "megapixel-step",
2366
+ megapixels: 24,
2367
+ usd: .08
1710
2368
  },
1711
2369
  pricing: "$0.08 per 24 output megapixels",
1712
2370
  queued: true,
@@ -1723,10 +2381,11 @@ const BACKGROUND_TOOLS = {
1723
2381
  name: "Topaz Creative Upscale",
1724
2382
  outputKeys: ["image"],
1725
2383
  price: {
1726
- kind: "megapixel",
1727
- usd: .96 / 24
2384
+ kind: "megapixel-step",
2385
+ megapixels: 2,
2386
+ usd: .08
1728
2387
  },
1729
- pricing: "$0.96 per 24 output megapixels",
2388
+ pricing: "$0.08 per started 2 output megapixels, any Bloom model",
1730
2389
  queued: true,
1731
2390
  sourceUrl: "https://fal.ai/models/topaz/upscale/image/creative",
1732
2391
  task: "creative image upscaling"
@@ -1741,8 +2400,9 @@ const BACKGROUND_TOOLS = {
1741
2400
  name: "Topaz Denoise",
1742
2401
  outputKeys: ["image"],
1743
2402
  price: {
1744
- kind: "megapixel",
1745
- usd: .08 / 24
2403
+ kind: "megapixel-step",
2404
+ megapixels: 24,
2405
+ usd: .08
1746
2406
  },
1747
2407
  pricing: "$0.08 per 24 output megapixels at the default Normal model; $0.16 with Denoise Max",
1748
2408
  queued: true,
@@ -1759,10 +2419,11 @@ const BACKGROUND_TOOLS = {
1759
2419
  name: "Topaz Generative Upscale",
1760
2420
  outputKeys: ["image"],
1761
2421
  price: {
1762
- kind: "megapixel",
1763
- usd: .24 / 24
2422
+ kind: "megapixel-step",
2423
+ megapixels: 8,
2424
+ usd: .08
1764
2425
  },
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",
2426
+ 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
2427
  queued: true,
1767
2428
  sourceUrl: "https://fal.ai/models/topaz/upscale/image/generative",
1768
2429
  task: "generative image upscaling"
@@ -1781,8 +2442,9 @@ const BACKGROUND_TOOLS = {
1781
2442
  name: "Topaz Image Upscale",
1782
2443
  outputKeys: ["image"],
1783
2444
  price: {
1784
- kind: "megapixel",
1785
- usd: .08 / 24
2445
+ kind: "megapixel-step",
2446
+ megapixels: 24,
2447
+ usd: .08
1786
2448
  },
1787
2449
  pricing: "$0.08 for output up to 24MP; $0.16 to 48MP, $0.32 to 96MP, up to $1.36 at 512MP",
1788
2450
  queued: true,
@@ -1799,8 +2461,9 @@ const BACKGROUND_TOOLS = {
1799
2461
  name: "Topaz Precision Upscale",
1800
2462
  outputKeys: ["image"],
1801
2463
  price: {
1802
- kind: "megapixel",
1803
- usd: .08 / 24
2464
+ kind: "megapixel-step",
2465
+ megapixels: 24,
2466
+ usd: .08
1804
2467
  },
1805
2468
  pricing: "$0.08 per 24 output megapixels, any precision model",
1806
2469
  queued: true,
@@ -1817,8 +2480,9 @@ const BACKGROUND_TOOLS = {
1817
2480
  name: "Topaz Restore",
1818
2481
  outputKeys: ["image"],
1819
2482
  price: {
1820
- kind: "megapixel",
1821
- usd: .48 / 24
2483
+ kind: "megapixel-step",
2484
+ megapixels: 24,
2485
+ usd: .48
1822
2486
  },
1823
2487
  pricing: "$0.48 per 24 output megapixels at the default Recover 3; $0.08 with Dust-Scratch V2",
1824
2488
  queued: true,
@@ -1835,8 +2499,9 @@ const BACKGROUND_TOOLS = {
1835
2499
  name: "Topaz Sharpen",
1836
2500
  outputKeys: ["image"],
1837
2501
  price: {
1838
- kind: "megapixel",
1839
- usd: .08 / 24
2502
+ kind: "megapixel-step",
2503
+ megapixels: 24,
2504
+ usd: .08
1840
2505
  },
1841
2506
  pricing: "$0.08 per 24 output megapixels at the default Standard model; $0.16 with Super Focus",
1842
2507
  queued: true,
@@ -1852,8 +2517,9 @@ const BACKGROUND_TOOLS = {
1852
2517
  name: "Topaz Transparent Upscale",
1853
2518
  outputKeys: ["image"],
1854
2519
  price: {
1855
- kind: "megapixel",
1856
- usd: .08 / 24
2520
+ kind: "megapixel-step",
2521
+ megapixels: 24,
2522
+ usd: .08
1857
2523
  },
1858
2524
  pricing: "$0.08 per 24 output megapixels",
1859
2525
  queued: true,
@@ -1873,10 +2539,22 @@ const BACKGROUND_TOOLS = {
1873
2539
  name: "Topaz Video Upscale",
1874
2540
  outputKeys: ["video"],
1875
2541
  price: {
1876
- kind: "second",
1877
- usd: .01
2542
+ doubleAtFps: 60,
2543
+ halfWithModel: "Gaia 2",
2544
+ kind: "video-second",
2545
+ tiers: [
2546
+ {
2547
+ upTo: 720,
2548
+ usd: .01
2549
+ },
2550
+ {
2551
+ upTo: 1080,
2552
+ usd: .02
2553
+ },
2554
+ { usd: .08 }
2555
+ ]
1878
2556
  },
1879
- pricing: "$0.01/sec up to 720p; $0.02 to 1080p, $0.08 above, doubled at 60fps, halved with Gaia 2",
2557
+ pricing: "$0.01/sec up to 720p output; $0.02 to 1080p, $0.08 above, doubled at 60fps, halved with Gaia 2",
1880
2558
  queued: true,
1881
2559
  sourceUrl: "https://fal.ai/models/fal-ai/topaz/upscale/video",
1882
2560
  task: "video enhancement"
@@ -1927,6 +2605,7 @@ const EDITING_TOOLS = {
1927
2605
  outputKeys: ["images"],
1928
2606
  price: {
1929
2607
  kind: "call",
2608
+ perImage: true,
1930
2609
  usd: .04
1931
2610
  },
1932
2611
  pricing: "$0.04/generation",
@@ -1957,7 +2636,11 @@ const EDITING_TOOLS = {
1957
2636
  inputKind: "image",
1958
2637
  name: "FLUX.2 Pro Outpaint",
1959
2638
  outputKeys: ["images"],
1960
- price: { kind: "metered" },
2639
+ price: {
2640
+ extra: .015,
2641
+ first: .03,
2642
+ kind: "megapixel-first"
2643
+ },
1961
2644
  pricing: "$0.03 for the first output megapixel, then $0.015 per extra megapixel of input and output, rounded up",
1962
2645
  sourceUrl: "https://fal.ai/models/fal-ai/flux-2-pro/outpaint",
1963
2646
  task: "image outpainting"
@@ -1988,6 +2671,7 @@ const EDITING_TOOLS = {
1988
2671
  outputKeys: ["images"],
1989
2672
  price: {
1990
2673
  kind: "call",
2674
+ perImage: true,
1991
2675
  usd: .06
1992
2676
  },
1993
2677
  pricing: "$0.06/image at the default BALANCED speed; $0.03 turbo, $0.09 quality",
@@ -2082,12 +2766,35 @@ const EDITING_TOOLS = {
2082
2766
  inputKind: "image",
2083
2767
  name: "Smart Resize",
2084
2768
  outputKeys: ["images", "results"],
2085
- price: { kind: "metered" },
2769
+ price: {
2770
+ fee: .05,
2771
+ kind: "call",
2772
+ multipliers: { resolution: { "4K": 2 } },
2773
+ per: ["target_sizes", "num_images_per_size"],
2774
+ usd: .15
2775
+ },
2086
2776
  pricing: "$0.15 per output image, doubled at 4K, plus a $0.05 vision analysis fee per request",
2087
2777
  queued: true,
2088
2778
  sourceUrl: "https://fal.ai/models/fal-ai/smart-resize",
2089
2779
  task: "multi-size recomposition"
2090
2780
  },
2781
+ "telestyle-v2": {
2782
+ category: "restyle",
2783
+ description: "Redraw a content image in the style of a second, style image.",
2784
+ endpoint: "fal-ai/telestyle-v2",
2785
+ inputField: "content_image_url",
2786
+ inputKind: "image",
2787
+ name: "TeleStyle v2",
2788
+ outputKeys: ["images"],
2789
+ price: {
2790
+ kind: "megapixel",
2791
+ usd: .035
2792
+ },
2793
+ pricing: "$0.035/megapixel",
2794
+ referenceField: "style_image_url",
2795
+ sourceUrl: "https://fal.ai/models/fal-ai/telestyle-v2",
2796
+ task: "style transfer from a reference image"
2797
+ },
2091
2798
  "text-removal": {
2092
2799
  category: "erase",
2093
2800
  defaultOptions: { output_format: "png" },
@@ -2104,6 +2811,25 @@ const EDITING_TOOLS = {
2104
2811
  pricing: "$0.04/image",
2105
2812
  sourceUrl: "https://fal.ai/models/fal-ai/image-editing/text-removal",
2106
2813
  task: "text removal"
2814
+ },
2815
+ "virtual-try-on": {
2816
+ category: "try-on",
2817
+ description: "Dress the person in one image in the garment shown in another.",
2818
+ endpoint: "google/virtual-try-on",
2819
+ inputField: "person_image_url",
2820
+ inputKind: "image",
2821
+ name: "Google Virtual Try-On",
2822
+ outputKeys: ["images"],
2823
+ price: {
2824
+ kind: "call",
2825
+ perImage: true,
2826
+ usd: .075
2827
+ },
2828
+ pricing: "$0.075/image",
2829
+ queued: true,
2830
+ referenceField: "product_image_url",
2831
+ sourceUrl: "https://fal.ai/models/google/virtual-try-on",
2832
+ task: "virtual garment try-on"
2107
2833
  }
2108
2834
  };
2109
2835
  //#endregion
@@ -2117,7 +2843,10 @@ const STRUCTURE_TOOLS = {
2117
2843
  inputKind: "image",
2118
2844
  name: "Depth Anything v2 Preprocessor",
2119
2845
  outputKeys: ["image"],
2120
- price: { kind: "metered" },
2846
+ price: {
2847
+ kind: "call",
2848
+ usd: 0
2849
+ },
2121
2850
  pricing: "$0/compute-second listed by fal",
2122
2851
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/depth-anything/v2",
2123
2852
  task: "depth preprocessing"
@@ -2146,7 +2875,10 @@ const STRUCTURE_TOOLS = {
2146
2875
  inputKind: "image",
2147
2876
  name: "HED Edge Preprocessor",
2148
2877
  outputKeys: ["image"],
2149
- price: { kind: "metered" },
2878
+ price: {
2879
+ kind: "call",
2880
+ usd: 0
2881
+ },
2150
2882
  pricing: "$0/compute-second listed by fal",
2151
2883
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/hed",
2152
2884
  task: "edge preprocessing"
@@ -2160,7 +2892,10 @@ const STRUCTURE_TOOLS = {
2160
2892
  inputKind: "image",
2161
2893
  name: "Line Art Preprocessor",
2162
2894
  outputKeys: ["image"],
2163
- price: { kind: "metered" },
2895
+ price: {
2896
+ kind: "call",
2897
+ usd: 0
2898
+ },
2164
2899
  pricing: "$0/compute-second listed by fal",
2165
2900
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/lineart",
2166
2901
  task: "image preprocessing"
@@ -2177,7 +2912,10 @@ const STRUCTURE_TOOLS = {
2177
2912
  inputKind: "image",
2178
2913
  name: "Marigold Depth Estimation",
2179
2914
  outputKeys: ["image"],
2180
- price: { kind: "metered" },
2915
+ price: {
2916
+ kind: "call",
2917
+ usd: 0
2918
+ },
2181
2919
  pricing: "$0/compute-second listed by fal",
2182
2920
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/marigold-depth",
2183
2921
  task: "depth map"
@@ -2194,7 +2932,10 @@ const STRUCTURE_TOOLS = {
2194
2932
  inputKind: "image",
2195
2933
  name: "MiDaS Depth Estimation",
2196
2934
  outputKeys: ["image"],
2197
- price: { kind: "metered" },
2935
+ price: {
2936
+ kind: "call",
2937
+ usd: 0
2938
+ },
2198
2939
  pricing: "$0/compute-second listed by fal",
2199
2940
  sourceUrl: "https://fal.ai/models/fal-ai/imageutils/depth",
2200
2941
  task: "depth map"
@@ -2207,7 +2948,10 @@ const STRUCTURE_TOOLS = {
2207
2948
  inputKind: "image",
2208
2949
  name: "MiDaS Preprocessor",
2209
2950
  outputKeys: ["depth_map", "normal_map"],
2210
- price: { kind: "metered" },
2951
+ price: {
2952
+ kind: "call",
2953
+ usd: 0
2954
+ },
2211
2955
  pricing: "$0/compute-second listed by fal",
2212
2956
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/midas",
2213
2957
  task: "depth and normal preprocessing"
@@ -2220,7 +2964,10 @@ const STRUCTURE_TOOLS = {
2220
2964
  inputKind: "image",
2221
2965
  name: "M-LSD Line Preprocessor",
2222
2966
  outputKeys: ["image"],
2223
- price: { kind: "metered" },
2967
+ price: {
2968
+ kind: "call",
2969
+ usd: 0
2970
+ },
2224
2971
  pricing: "$0/compute-second listed by fal",
2225
2972
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/mlsd",
2226
2973
  task: "line segment preprocessing"
@@ -2233,7 +2980,10 @@ const STRUCTURE_TOOLS = {
2233
2980
  inputKind: "image",
2234
2981
  name: "PiDiNet Edge Preprocessor",
2235
2982
  outputKeys: ["image"],
2236
- price: { kind: "metered" },
2983
+ price: {
2984
+ kind: "call",
2985
+ usd: 0
2986
+ },
2237
2987
  pricing: "$0/compute-second listed by fal",
2238
2988
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/pidi",
2239
2989
  task: "edge preprocessing"
@@ -2246,7 +2996,10 @@ const STRUCTURE_TOOLS = {
2246
2996
  inputKind: "image",
2247
2997
  name: "SAM Preprocessor",
2248
2998
  outputKeys: ["image"],
2249
- price: { kind: "metered" },
2999
+ price: {
3000
+ kind: "call",
3001
+ usd: 0
3002
+ },
2250
3003
  pricing: "$0/compute-second listed by fal",
2251
3004
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/sam",
2252
3005
  task: "segmentation preprocessing"
@@ -2266,7 +3019,10 @@ const STRUCTURE_TOOLS = {
2266
3019
  inputKind: "image",
2267
3020
  name: "SAM 2 Auto Segment",
2268
3021
  outputKeys: ["combined_mask", "individual_masks"],
2269
- price: { kind: "metered" },
3022
+ price: {
3023
+ kind: "call",
3024
+ usd: 0
3025
+ },
2270
3026
  pricing: "$0/compute-second listed by fal",
2271
3027
  queued: true,
2272
3028
  sourceUrl: "https://fal.ai/models/fal-ai/sam2/auto-segment",
@@ -2311,7 +3067,11 @@ const STRUCTURE_TOOLS = {
2311
3067
  inputKind: "video",
2312
3068
  name: "SAM 3.1 Video",
2313
3069
  outputKeys: ["video", "boundingbox_frames_zip"],
2314
- price: { kind: "metered" },
3070
+ price: {
3071
+ frames: 16,
3072
+ kind: "frames",
3073
+ usd: .01
3074
+ },
2315
3075
  pricing: "$0.01/16 frames of video input",
2316
3076
  queued: true,
2317
3077
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3-1/video",
@@ -2383,7 +3143,11 @@ const STRUCTURE_TOOLS = {
2383
3143
  inputKind: "video",
2384
3144
  name: "SAM 3 Video",
2385
3145
  outputKeys: ["video", "boundingbox_frames_zip"],
2386
- price: { kind: "metered" },
3146
+ price: {
3147
+ frames: 16,
3148
+ kind: "frames",
3149
+ usd: .005
3150
+ },
2387
3151
  pricing: "$0.005/16 frames of video input",
2388
3152
  queued: true,
2389
3153
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video",
@@ -2402,7 +3166,11 @@ const STRUCTURE_TOOLS = {
2402
3166
  inputKind: "video",
2403
3167
  name: "SAM 3 Video RLE",
2404
3168
  outputKeys: ["video", "boundingbox_frames_zip"],
2405
- price: { kind: "metered" },
3169
+ price: {
3170
+ frames: 16,
3171
+ kind: "frames",
3172
+ usd: .005
3173
+ },
2406
3174
  pricing: "$0.005/16 frames of video",
2407
3175
  queued: true,
2408
3176
  sourceUrl: "https://fal.ai/models/fal-ai/sam-3/video-rle",
@@ -2416,7 +3184,10 @@ const STRUCTURE_TOOLS = {
2416
3184
  inputKind: "image",
2417
3185
  name: "Scribble Preprocessor",
2418
3186
  outputKeys: ["image"],
2419
- price: { kind: "metered" },
3187
+ price: {
3188
+ kind: "call",
3189
+ usd: 0
3190
+ },
2420
3191
  pricing: "$0/compute-second listed by fal",
2421
3192
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/scribble",
2422
3193
  task: "scribble preprocessing"
@@ -2429,7 +3200,10 @@ const STRUCTURE_TOOLS = {
2429
3200
  inputKind: "image",
2430
3201
  name: "TEED Edge Preprocessor",
2431
3202
  outputKeys: ["image"],
2432
- price: { kind: "metered" },
3203
+ price: {
3204
+ kind: "call",
3205
+ usd: 0
3206
+ },
2433
3207
  pricing: "$0/compute-second listed by fal",
2434
3208
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/teed",
2435
3209
  task: "edge preprocessing"
@@ -2442,7 +3216,10 @@ const STRUCTURE_TOOLS = {
2442
3216
  inputKind: "image",
2443
3217
  name: "ZoeDepth Preprocessor",
2444
3218
  outputKeys: ["image"],
2445
- price: { kind: "metered" },
3219
+ price: {
3220
+ kind: "call",
3221
+ usd: 0
3222
+ },
2446
3223
  pricing: "$0/compute-second listed by fal",
2447
3224
  sourceUrl: "https://fal.ai/models/fal-ai/image-preprocessors/zoe",
2448
3225
  task: "depth preprocessing"
@@ -2459,6 +3236,17 @@ const FAL_TOOLS = {
2459
3236
  };
2460
3237
  Object.keys(FAL_TOOLS);
2461
3238
  //#endregion
3239
+ //#region src/image/fetch.ts
3240
+ function urlOf(input) {
3241
+ if (typeof input === "string") return input;
3242
+ return input instanceof URL ? input.href : input.url;
3243
+ }
3244
+ /** Adapt a configured fetch for a provider factory; undefined keeps global fetch. */
3245
+ function toProviderFetch(fetch) {
3246
+ if (fetch === void 0) return {};
3247
+ return { fetch: async (input, init) => await fetch(urlOf(input), init ?? {}) };
3248
+ }
3249
+ //#endregion
2462
3250
  //#region src/image/fal.ts
2463
3251
  /**
2464
3252
  * fal provider adapter.
@@ -2476,35 +3264,22 @@ Object.keys(FAL_TOOLS);
2476
3264
  * need a specific size. Example:
2477
3265
  * img.generate({
2478
3266
  * provider: "fal",
2479
- * tier: "balanced",
3267
+ * model: "fal-ai/gpt-image-1.5",
2480
3268
  * prompt: "...",
2481
3269
  * providerOptions: { fal: { image_size: "1024x1024" } },
2482
3270
  * });
2483
3271
  */
2484
3272
  /**
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.
3273
+ * fal endpoint ids proven to work with this adapter and priced in
3274
+ * {@link FAL_IMAGE_PRICE_USD} below (FLUX Pro Ultra + gpt-image).
2488
3275
  */
2489
3276
  const FAL_FLUX_MODEL = "fal-ai/flux-pro/v1.1-ultra";
2490
3277
  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
3278
  /** Env var read for the fal key when `apiKey` is not supplied in config. */
2503
3279
  const FAL_API_KEY_ENV = "FAL_KEY";
2504
3280
  /**
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
3281
+ * Static fal USD/image, keyed by the fal ENDPOINT id. Cost tracking must price
3282
+ * the endpoints consumers actually pass — kiln uses
2508
3283
  * `fal-ai/flux/schnell`, `fal-ai/flux-2-pro`, `fal-ai/flux-2-max`. Every price is
2509
3284
  * sourced from the fal registry snapshot in `../models`
2510
3285
  * (`MODELS[...].pricePerImageUsd`) so this table stays in sync rather than
@@ -2531,12 +3306,17 @@ const FAL_IMAGE_PRICE_USD = {
2531
3306
  "fal-ai/bytedance/seedream/v4/text-to-image": MODELS.seedream4?.pricePerImageUsd ?? .03,
2532
3307
  "fal-ai/bytedance/seedream/v4.5/text-to-image": MODELS.seedream45?.pricePerImageUsd ?? .04,
2533
3308
  "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,
3309
+ "bytedance/seedream/v5/lite/text-to-image": MODELS["seedream5-lite"]?.pricePerImageUsd ?? .035,
3310
+ "fal-ai/recraft/v3/text-to-image": MODELS.recraft?.pricePerImageUsd ?? .04,
2536
3311
  "fal-ai/recraft/v4/text-to-image": MODELS.recraft4?.pricePerImageUsd ?? .04,
3312
+ "fal-ai/recraft/v4.1/text-to-image": MODELS.recraft41?.pricePerImageUsd ?? .035,
2537
3313
  "fal-ai/ideogram/v3": MODELS.ideogram?.pricePerImageUsd ?? .03,
2538
3314
  "ideogram/v4": MODELS.ideogram4?.pricePerImageUsd ?? .03,
2539
3315
  "xai/grok-imagine-image": MODELS["grok-image"]?.pricePerImageUsd ?? .02,
3316
+ "xai/grok-imagine-image/v2.0/text-to-image": MODELS["grok-image-2"]?.pricePerImageUsd ?? .06,
3317
+ "microsoft/mai-image-2.5-pro": MODELS["mai-image-2.5-pro"]?.pricePerImageUsd ?? .17,
3318
+ "fal-ai/ideogram/v3/generate-transparent": MODELS["ideogram3-transparent"]?.pricePerImageUsd ?? .06,
3319
+ "alibaba/qwen-image-3/text-to-image": MODELS.qwen3?.pricePerImageUsd ?? .04,
2540
3320
  "fal-ai/qwen-image": MODELS.qwen?.pricePerImageUsd ?? .02
2541
3321
  };
2542
3322
  /**
@@ -2544,86 +3324,33 @@ const FAL_IMAGE_PRICE_USD = {
2544
3324
  * var. Throws `MotifError` when neither is present (callers translate this into
2545
3325
  * a `Result.err`).
2546
3326
  */
2547
- function resolveModel$3(modelId, apiKey) {
3327
+ function resolveModel$3(modelId, apiKey, fetch) {
2548
3328
  const key = apiKey ?? process.env["FAL_KEY"];
2549
3329
  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);
3330
+ return createFal({
3331
+ apiKey: key,
3332
+ ...toProviderFetch(fetch)
3333
+ }).image(modelId);
3334
+ }
3335
+ /**
3336
+ * How a registered fal edit endpoint takes its input images, from the
3337
+ * registry's `editImagesField` (default `image_urls`). `@ai-sdk/fal` sends only
3338
+ * the first image as `image_url` unless told otherwise, which list-only
3339
+ * endpoints reject and which silently drops every reference after the first.
3340
+ * Undefined for an endpoint the registry does not list as an edit route.
3341
+ */
3342
+ function falEditImagesField(modelId) {
3343
+ const config = Object.values(MODELS).find((entry) => entry.editEndpoint === modelId);
3344
+ return config === void 0 ? void 0 : config.editImagesField ?? "image_urls";
2551
3345
  }
2552
3346
  /** The fal provider adapter registered in the provider registry. */
2553
3347
  const falAdapter = {
2554
3348
  id: "fal",
2555
- tierModels: FAL_TIER_MODELS,
2556
3349
  apiKeyEnv: FAL_API_KEY_ENV,
2557
3350
  resolveModel: resolveModel$3,
2558
3351
  priceUsdByModel: FAL_IMAGE_PRICE_USD
2559
3352
  };
2560
3353
  //#endregion
2561
- //#region src/image/google.ts
2562
- /**
2563
- * Google (Gemini) provider adapter.
2564
- *
2565
- * Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/google`. Building a model
2566
- * performs no network I/O — the request only happens when `generateImage`
2567
- * invokes `model.doGenerate`. Gemini supports both text→image generation and
2568
- * multi-image-in → image-out editing (with an optional mask), which is the core
2569
- * operation this layer normalizes.
2570
- */
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
- /** Env var read for the Google API key when `apiKey` is not supplied in config. */
2585
- const GOOGLE_API_KEY_ENV = "GOOGLE_GENERATIVE_AI_API_KEY";
2586
- /**
2587
- * Static Google-direct USD/image, keyed by model id.
2588
- *
2589
- * Sources (Google direct, not fal-hosted):
2590
- * - `gemini-2.5-flash-image` ("nano banana"): image output billed at 1290
2591
- * output tokens/image at $30 / 1M output tokens ≈ $0.039/image.
2592
- * Source: https://ai.google.dev/gemini-api/docs/pricing
2593
- * Sanity anchor: fal-hosted `fal-ai/gemini-25-flash-image` is $0.0398
2594
- * (`MODELS.gemini.pricePerImageUsd` in ../models) — same ballpark.
2595
- * - `gemini-3-pro-image-preview` ("nano banana pro"): standard 1K/2K image
2596
- * output ≈ $0.134/image (higher tiers/4K cost more).
2597
- * Source: https://ai.google.dev/gemini-api/docs/pricing
2598
- * Sanity anchor: fal-hosted `fal-ai/gemini-3-pro-image-preview` is $0.15
2599
- * (`MODELS.gemini3`/`MODELS.banana` in ../models) — fal adds overhead.
2600
- * - `gemini-3.1-flash-image-preview`: flash-tier image output; priced with the
2601
- * 2.5 flash-image line (≈ $0.039/image) pending a distinct published rate.
2602
- */
2603
- const GOOGLE_IMAGE_PRICE_USD = {
2604
- "gemini-2.5-flash-image": .039,
2605
- "gemini-3.1-flash-image-preview": .039,
2606
- "gemini-3-pro-image-preview": .134
2607
- };
2608
- /**
2609
- * Build a Google Gemini `ImageModel`. Prefers the passed `apiKey`, else the
2610
- * `GOOGLE_GENERATIVE_AI_API_KEY` env var. Throws `MotifError` when neither is
2611
- * present (callers translate this into a `Result.err`).
2612
- */
2613
- function resolveModel$2(modelId, apiKey) {
2614
- const key = apiKey ?? process.env["GOOGLE_GENERATIVE_AI_API_KEY"];
2615
- 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);
2617
- }
2618
- /** The Google (Gemini) provider adapter registered in the provider registry. */
2619
- const googleAdapter = {
2620
- id: "google",
2621
- tierModels: GOOGLE_TIER_MODELS,
2622
- apiKeyEnv: GOOGLE_API_KEY_ENV,
2623
- resolveModel: resolveModel$2,
2624
- priceUsdByModel: GOOGLE_IMAGE_PRICE_USD
2625
- };
2626
- //#endregion
2627
3354
  //#region src/image/openai.ts
2628
3355
  /**
2629
3356
  * OpenAI (gpt-image) provider adapter.
@@ -2632,16 +3359,6 @@ const googleAdapter = {
2632
3359
  * performs no network I/O — the request only happens when `generateImage`
2633
3360
  * invokes `model.doGenerate`.
2634
3361
  */
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
3362
  /** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
2646
3363
  const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
2647
3364
  /**
@@ -2652,7 +3369,7 @@ const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
2652
3369
  * and size — this is a documented approximation for the common case.
2653
3370
  * Source: https://platform.openai.com/docs/pricing (image generation)
2654
3371
  * Sanity anchor: the Phase 0 benchmark measured gpt-image direct at $0.042
2655
- * (vs $0.133 via fal — see docs/design/provider-agnostic-image-layer.md §10).
3372
+ * (vs $0.133 via fal — full data and methodology on Linear MOT-23).
2656
3373
  *
2657
3374
  * GPT Image 2.5 is token-priced, with no published per-image estimate. Leave
2658
3375
  * these models absent so cost remains unknown unless supplied by the provider.
@@ -2665,20 +3382,186 @@ const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
2665
3382
  * `OPENAI_API_KEY` env var. Throws `MotifError` when neither is present
2666
3383
  * (callers translate this into a `Result.err`).
2667
3384
  */
2668
- function resolveModel$1(modelId, apiKey) {
3385
+ function resolveModel$2(modelId, apiKey, fetch) {
2669
3386
  const key = apiKey ?? process.env["OPENAI_API_KEY"];
2670
3387
  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);
3388
+ return createOpenAI({
3389
+ apiKey: key,
3390
+ ...toProviderFetch(fetch)
3391
+ }).image(modelId);
2672
3392
  }
2673
3393
  /** The OpenAI provider adapter registered in the provider registry. */
2674
3394
  const openaiAdapter = {
2675
3395
  id: "openai",
2676
- tierModels: OPENAI_TIER_MODELS,
2677
3396
  apiKeyEnv: OPENAI_API_KEY_ENV,
2678
- resolveModel: resolveModel$1,
3397
+ resolveModel: resolveModel$2,
2679
3398
  priceUsdByModel: OPENAI_IMAGE_PRICE_USD
2680
3399
  };
2681
3400
  //#endregion
3401
+ //#region src/image/openrouter.ts
3402
+ /** Env var read for the OpenRouter API key when `apiKey` is not supplied in config. */
3403
+ const OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
3404
+ const OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images";
3405
+ /** Key under which this adapter's metadata sits on `providerMetadata`. */
3406
+ const METADATA_KEY = "openrouter";
3407
+ /**
3408
+ * The Gemini image models Motif exposes, by their bare Google names, mapped to
3409
+ * the OpenRouter slug that serves them. All six are listed by
3410
+ * `GET https://openrouter.ai/api/v1/images/models`.
3411
+ */
3412
+ const OPENROUTER_GEMINI_IMAGE_MODELS = {
3413
+ "gemini-2.5-flash-image": "google/gemini-2.5-flash-image",
3414
+ "gemini-3.1-flash-image-preview": "google/gemini-3.1-flash-image-preview",
3415
+ "gemini-3-pro-image-preview": "google/gemini-3-pro-image-preview",
3416
+ "gemini-3.1-flash-image": "google/gemini-3.1-flash-image",
3417
+ "gemini-3-pro-image": "google/gemini-3-pro-image",
3418
+ "gemini-3.1-flash-lite-image": "google/gemini-3.1-flash-lite-image"
3419
+ };
3420
+ /**
3421
+ * Static USD/image estimate, keyed by the bare model name. Used only when a
3422
+ * response carries no `usage.cost`; OpenRouter normally reports the real figure.
3423
+ *
3424
+ * - `gemini-2.5-flash-image`: 1290 output tokens at $30 / 1M ≈ $0.039.
3425
+ * - `gemini-3-pro-image-preview`, `gemini-3-pro-image`: ≈ $0.134 at 1K/2K.
3426
+ * - `gemini-3.1-flash-image-preview`: priced with the 2.5 flash-image line
3427
+ * pending a distinct published rate.
3428
+ * - `gemini-3.1-flash-image`: $0.067 at 1K.
3429
+ * - `gemini-3.1-flash-lite-image`: ≈ $0.0336 at 1K.
3430
+ * Source: https://ai.google.dev/gemini-api/docs/pricing
3431
+ */
3432
+ const OPENROUTER_IMAGE_PRICE_USD = {
3433
+ "gemini-2.5-flash-image": .039,
3434
+ "gemini-3.1-flash-image-preview": .039,
3435
+ "gemini-3-pro-image-preview": .134,
3436
+ "gemini-3.1-flash-image": .067,
3437
+ "gemini-3-pro-image": .134,
3438
+ "gemini-3.1-flash-lite-image": .0336
3439
+ };
3440
+ /**
3441
+ * Resolve a Motif model name to an OpenRouter slug. A bare Gemini name maps to
3442
+ * its `google/<id>` slug; a name already containing `/` is an OpenRouter slug
3443
+ * and passes through. Anything else is unknown and throws.
3444
+ */
3445
+ function openRouterModelSlug(modelId) {
3446
+ if (modelId.includes("/")) return modelId;
3447
+ const slug = OPENROUTER_GEMINI_IMAGE_MODELS[modelId];
3448
+ if (slug === void 0) throw new MotifError(`No OpenRouter image model for "${modelId}". Known: ${Object.keys(OPENROUTER_GEMINI_IMAGE_MODELS).join(", ")}; or pass an OpenRouter slug such as google/gemini-3.1-flash-image.`, 0);
3449
+ return slug;
3450
+ }
3451
+ function isRecord$2(value) {
3452
+ return typeof value === "object" && value !== null;
3453
+ }
3454
+ function toDataUrl(mediaType, data) {
3455
+ return `data:${mediaType};base64,${typeof data === "string" ? data : Buffer.from(data).toString("base64")}`;
3456
+ }
3457
+ /** One `input_references` entry for an input file: a URL as-is, bytes as a data URL. */
3458
+ function toInputReference(file) {
3459
+ return {
3460
+ type: "image_url",
3461
+ image_url: { url: file.type === "url" ? file.url : toDataUrl(file.mediaType, file.data) }
3462
+ };
3463
+ }
3464
+ function buildBody(slug, options) {
3465
+ if (options.mask !== void 0) throw new MotifError("OpenRouter's Image API takes no mask; describe the region in the instruction instead.", 0);
3466
+ return {
3467
+ ...options.providerOptions[METADATA_KEY] ?? {},
3468
+ model: slug,
3469
+ prompt: options.prompt,
3470
+ n: options.n,
3471
+ ...options.aspectRatio === void 0 ? {} : { aspect_ratio: options.aspectRatio },
3472
+ ...options.files === void 0 || options.files.length === 0 ? {} : { input_references: options.files.map(toInputReference) }
3473
+ };
3474
+ }
3475
+ function errorMessage(body, fallback) {
3476
+ if (isRecord$2(body) && isRecord$2(body.error)) {
3477
+ const { message } = body.error;
3478
+ if (typeof message === "string" && message !== "") return message;
3479
+ }
3480
+ return fallback;
3481
+ }
3482
+ function parseResponse(body) {
3483
+ if (!isRecord$2(body) || !Array.isArray(body.data)) throw new MotifError("OpenRouter image response had no data array", 502);
3484
+ const images = [];
3485
+ for (const entry of body.data) if (isRecord$2(entry) && typeof entry.b64_json === "string") images.push(entry.b64_json);
3486
+ if (images.length === 0) throw new MotifError("OpenRouter returned no images", 502);
3487
+ return {
3488
+ images,
3489
+ cost: isRecord$2(body.usage) && typeof body.usage.cost === "number" ? body.usage.cost : void 0
3490
+ };
3491
+ }
3492
+ function buildModel(modelId, apiKey, doFetch) {
3493
+ const slug = openRouterModelSlug(modelId);
3494
+ return {
3495
+ specificationVersion: "v4",
3496
+ provider: METADATA_KEY,
3497
+ modelId: slug,
3498
+ maxImagesPerCall: 10,
3499
+ async doGenerate(options) {
3500
+ const warnings = [];
3501
+ if (options.seed !== void 0) warnings.push({
3502
+ type: "unsupported",
3503
+ feature: "seed"
3504
+ });
3505
+ if (options.size !== void 0) warnings.push({
3506
+ type: "unsupported",
3507
+ feature: "size",
3508
+ details: "Gemini on OpenRouter takes aspectRatio, not pixel sizes."
3509
+ });
3510
+ const timestamp = /* @__PURE__ */ new Date();
3511
+ const response = await doFetch(OPENROUTER_IMAGES_URL, {
3512
+ method: "POST",
3513
+ headers: {
3514
+ ...options.headers,
3515
+ Authorization: `Bearer ${apiKey}`,
3516
+ "Content-Type": "application/json"
3517
+ },
3518
+ body: JSON.stringify(buildBody(slug, options)),
3519
+ ...options.abortSignal === void 0 ? {} : { signal: options.abortSignal }
3520
+ });
3521
+ const text = await response.text();
3522
+ let body;
3523
+ try {
3524
+ body = JSON.parse(text);
3525
+ } catch {
3526
+ body = void 0;
3527
+ }
3528
+ if (!response.ok) throw new MotifError(errorMessage(body, `OpenRouter ${response.status}: ${text}`), response.status);
3529
+ const { images, cost } = parseResponse(body);
3530
+ return {
3531
+ images,
3532
+ warnings,
3533
+ providerMetadata: { [METADATA_KEY]: {
3534
+ images: images.map(() => ({})),
3535
+ ...cost === void 0 ? {} : { cost }
3536
+ } },
3537
+ response: {
3538
+ timestamp,
3539
+ modelId: slug,
3540
+ headers: Object.fromEntries(response.headers.entries())
3541
+ }
3542
+ };
3543
+ }
3544
+ };
3545
+ }
3546
+ /**
3547
+ * Build an OpenRouter `ImageModel`. Prefers the passed `apiKey`, else the
3548
+ * `OPENROUTER_API_KEY` env var. Throws `MotifError` when neither is present or
3549
+ * the model name is unknown (callers translate this into a `Result.err`).
3550
+ */
3551
+ function resolveModel$1(modelId, apiKey, fetch) {
3552
+ const key = apiKey ?? process.env["OPENROUTER_API_KEY"];
3553
+ if (key === void 0 || key === "") throw new MotifError(`OpenRouter image generation requires an API key (config.openrouter.apiKey or ${OPENROUTER_API_KEY_ENV})`, 0);
3554
+ const configured = toProviderFetch(fetch);
3555
+ return buildModel(modelId, key, "fetch" in configured ? configured.fetch : globalThis.fetch);
3556
+ }
3557
+ /** The OpenRouter provider adapter registered in the provider registry. */
3558
+ const openrouterAdapter = {
3559
+ id: "openrouter",
3560
+ apiKeyEnv: OPENROUTER_API_KEY_ENV,
3561
+ resolveModel: resolveModel$1,
3562
+ priceUsdByModel: OPENROUTER_IMAGE_PRICE_USD
3563
+ };
3564
+ //#endregion
2682
3565
  //#region src/image/replicate.ts
2683
3566
  /**
2684
3567
  * Replicate provider adapter.
@@ -2690,19 +3573,6 @@ const openaiAdapter = {
2690
3573
  * NOTE: Replicate's SDK names the credential option `apiToken` (not `apiKey`),
2691
3574
  * and reads `REPLICATE_API_TOKEN` from the environment.
2692
3575
  */
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
3576
  /** Env var read for the Replicate API token when `apiToken` is not in config. */
2707
3577
  const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
2708
3578
  /**
@@ -2719,10 +3589,13 @@ const REPLICATE_IMAGE_PRICE_USD = { "black-forest-labs/flux-1.1-pro-ultra": .06
2719
3589
  * token), else the `REPLICATE_API_TOKEN` env var. Throws `MotifError` when
2720
3590
  * neither is present (callers translate this into a `Result.err`).
2721
3591
  */
2722
- function resolveModel(modelId, apiKey) {
3592
+ function resolveModel(modelId, apiKey, fetch) {
2723
3593
  const token = apiKey ?? process.env["REPLICATE_API_TOKEN"];
2724
3594
  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);
3595
+ return createReplicate({
3596
+ apiToken: token,
3597
+ ...toProviderFetch(fetch)
3598
+ }).image(modelId);
2726
3599
  }
2727
3600
  //#endregion
2728
3601
  //#region src/image/provider.ts
@@ -2732,11 +3605,10 @@ function resolveModel(modelId, apiKey) {
2732
3605
  * goes through {@link getProviderAdapter}.
2733
3606
  */
2734
3607
  const PROVIDERS = {
2735
- google: googleAdapter,
3608
+ openrouter: openrouterAdapter,
2736
3609
  openai: openaiAdapter,
2737
3610
  replicate: {
2738
3611
  id: "replicate",
2739
- tierModels: REPLICATE_TIER_MODELS,
2740
3612
  apiKeyEnv: REPLICATE_API_KEY_ENV,
2741
3613
  resolveModel,
2742
3614
  priceUsdByModel: REPLICATE_IMAGE_PRICE_USD
@@ -2780,6 +3652,9 @@ function costFromProviderMetadata(providerMetadata) {
2780
3652
  }
2781
3653
  }
2782
3654
  /** Static per-image USD for a (provider, model), or undefined if unknown. */
3655
+ function providerPricePerImageUsd(provider, modelId) {
3656
+ return tablePricePerImage(provider, modelId);
3657
+ }
2783
3658
  function tablePricePerImage(provider, modelId) {
2784
3659
  const adapter = PROVIDERS[provider];
2785
3660
  if (adapter === void 0) return;
@@ -2793,7 +3668,19 @@ function roundUsd(value) {
2793
3668
  * then the static table (× image count), then unknown.
2794
3669
  */
2795
3670
  function costForImages(provider, modelId, providerMetadata, imageCount) {
2796
- const metaCost = costFromProviderMetadata(providerMetadata);
3671
+ return costForCalls(provider, modelId, [providerMetadata], imageCount);
3672
+ }
3673
+ /**
3674
+ * Cost across every underlying model call of one generation. Provider-metadata
3675
+ * costs from the calls that report one are summed; otherwise the static table
3676
+ * (× image count), then unknown.
3677
+ */
3678
+ function costForCalls(provider, modelId, callMetadata, imageCount) {
3679
+ let metaCost;
3680
+ for (const providerMetadata of callMetadata) {
3681
+ const callCost = costFromProviderMetadata(providerMetadata);
3682
+ if (callCost !== void 0) metaCost = (metaCost ?? 0) + callCost;
3683
+ }
2797
3684
  if (metaCost !== void 0) return {
2798
3685
  usd: roundUsd(metaCost),
2799
3686
  source: "provider-metadata"
@@ -2814,21 +3701,20 @@ function costForImages(provider, modelId, providerMetadata, imageCount) {
2814
3701
  * `@howells/motif-sdk/image` — provider-agnostic image generation + editing.
2815
3702
  *
2816
3703
  * 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
2819
- * thrown exceptions). Google (Gemini) is the only provider in Phase 1a.
3704
+ * (`generateImage`, `@ai-sdk/*`). The caller names the provider and model;
3705
+ * reuses the SDK's Result convention (`Result<T, MotifError>` — no
3706
+ * thrown exceptions). Gemini is reached through OpenRouter.
2820
3707
  *
2821
3708
  * @example
2822
3709
  * ```ts
2823
3710
  * import { createMotifImage } from "@howells/motif-sdk/image";
2824
3711
  *
2825
- * const img = createMotifImage({ defaultProvider: "google" });
2826
- * const r = await img.generate({ tier: "fast", prompt: "a bare concrete wall" });
3712
+ * const img = createMotifImage({ defaultProvider: "openrouter" });
3713
+ * const r = await img.generate({ model: "gemini-3.1-flash-image", prompt: "a bare concrete wall" });
2827
3714
  * if (r.isOk()) console.log(r.value.images[0].mediaType, r.value.cost);
2828
3715
  * ```
2829
3716
  */
2830
- const DEFAULT_TIER = "balanced";
2831
- const DEFAULT_PROVIDER = "google";
3717
+ const DEFAULT_PROVIDER = "openrouter";
2832
3718
  /**
2833
3719
  * Create a provider-agnostic image client.
2834
3720
  *
@@ -2844,7 +3730,7 @@ function createMotifImage(config = {}, deps = {}) {
2844
3730
  }
2845
3731
  function apiKeyFor(provider) {
2846
3732
  switch (provider) {
2847
- case "google": return config.google?.apiKey;
3733
+ case "openrouter": return config.openrouter?.apiKey;
2848
3734
  case "openai": return config.openai?.apiKey;
2849
3735
  case "replicate": return config.replicate?.apiToken;
2850
3736
  case "fal": return config.fal?.apiKey;
@@ -2854,8 +3740,8 @@ function createMotifImage(config = {}, deps = {}) {
2854
3740
  async function generate(opts) {
2855
3741
  const provider = resolveProvider(opts.provider);
2856
3742
  try {
2857
- const modelId = resolveModelId(provider, opts.model, opts.tier);
2858
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider));
3743
+ const modelId = opts.model;
3744
+ const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
2859
3745
  const result = await generateImageFn({
2860
3746
  model,
2861
3747
  prompt: opts.prompt,
@@ -2863,6 +3749,7 @@ function createMotifImage(config = {}, deps = {}) {
2863
3749
  ...opts.size === void 0 ? {} : { size: opts.size },
2864
3750
  ...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
2865
3751
  ...opts.seed === void 0 ? {} : { seed: opts.seed },
3752
+ ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
2866
3753
  ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
2867
3754
  ...opts.headers === void 0 ? {} : { headers: opts.headers },
2868
3755
  ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
@@ -2874,9 +3761,18 @@ function createMotifImage(config = {}, deps = {}) {
2874
3761
  }
2875
3762
  async function edit(opts) {
2876
3763
  const provider = resolveProvider(opts.provider);
3764
+ const imagesField = provider === "fal" ? falEditImagesField(opts.model) : void 0;
3765
+ if (imagesField === "image_url" && opts.images.length > 1) return err(new MotifError(`${opts.model} takes one input image; ${opts.images.length} were given`, 0));
3766
+ const providerOptions = imagesField === "image_urls" && opts.providerOptions?.fal?.useMultipleImages === void 0 ? {
3767
+ ...opts.providerOptions,
3768
+ fal: {
3769
+ ...opts.providerOptions?.fal,
3770
+ useMultipleImages: true
3771
+ }
3772
+ } : opts.providerOptions;
2877
3773
  try {
2878
- const modelId = resolveModelId(provider, opts.model, opts.tier);
2879
- const model = resolveModelFn(provider, modelId, apiKeyFor(provider));
3774
+ const modelId = opts.model;
3775
+ const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
2880
3776
  const result = await generateImageFn({
2881
3777
  model,
2882
3778
  prompt: {
@@ -2886,9 +3782,10 @@ function createMotifImage(config = {}, deps = {}) {
2886
3782
  },
2887
3783
  ...opts.n === void 0 ? {} : { n: opts.n },
2888
3784
  ...opts.seed === void 0 ? {} : { seed: opts.seed },
3785
+ ...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
2889
3786
  ...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
2890
3787
  ...opts.headers === void 0 ? {} : { headers: opts.headers },
2891
- ...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
3788
+ ...providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(providerOptions) }
2892
3789
  });
2893
3790
  return ok(toMotifImageResult(result, provider, modelId));
2894
3791
  } catch (error) {
@@ -2971,14 +3868,8 @@ async function runBestOfN(opts, generate, edit) {
2971
3868
  }
2972
3869
  }
2973
3870
  /** 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];
3871
+ function defaultResolveModel(provider, modelId, apiKey, fetch) {
3872
+ return getProviderAdapter(provider).resolveModel(modelId, apiKey, fetch);
2982
3873
  }
2983
3874
  /** Map the AI SDK result → normalized MotifImageResult (with cost + requestId). */
2984
3875
  function toMotifImageResult(result, provider, model) {
@@ -2987,7 +3878,7 @@ function toMotifImageResult(result, provider, model) {
2987
3878
  base64: file.base64,
2988
3879
  mediaType: file.mediaType
2989
3880
  }));
2990
- const cost = costForImages(provider, model, result.providerMetadata, images.length);
3881
+ const cost = costForCalls(provider, model, result.calls.map((call) => call.providerMetadata), images.length);
2991
3882
  const requestId = extractRequestId(result);
2992
3883
  const warnings = result.warnings.map(renderWarning);
2993
3884
  return {
@@ -3013,9 +3904,11 @@ function isRecord(value) {
3013
3904
  }
3014
3905
  /** Look for a provider correlation id in providerMetadata, then response headers. */
3015
3906
  function extractRequestId(result) {
3016
- const fromMetadata = requestIdFromMetadata(result.providerMetadata);
3017
- if (fromMetadata !== void 0) return fromMetadata;
3018
- for (const response of result.responses) {
3907
+ for (const call of result.calls) {
3908
+ const fromMetadata = requestIdFromMetadata(call.providerMetadata);
3909
+ if (fromMetadata !== void 0) return fromMetadata;
3910
+ }
3911
+ for (const { response } of result.calls) {
3019
3912
  const { headers } = response;
3020
3913
  if (headers) {
3021
3914
  const id = headers["x-request-id"] ?? headers["x-goog-request-id"] ?? headers["x-fal-request-id"];
@@ -3070,7 +3963,10 @@ function toMotifError(error) {
3070
3963
  if (error instanceof MotifError) return error;
3071
3964
  const message = error instanceof Error ? error.message : String(error);
3072
3965
  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);
3966
+ const status = error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : 0;
3967
+ const body = error instanceof Error && "responseBody" in error && typeof error.responseBody === "string" ? error.responseBody : message;
3968
+ if (isFalAccountLocked(status, body)) return falHttpError(status, body);
3969
+ return new MotifError(message, status, code);
3074
3970
  }
3075
3971
  //#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 };
3972
+ export { FAL_API_KEY_ENV, OPENAI_API_KEY_ENV, OPENROUTER_API_KEY_ENV, PROVIDERS, REPLICATE_API_KEY_ENV, costForImages, costFromProviderMetadata, createMotifImage, getProviderAdapter, providerPricePerImageUsd };