@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/README.md +66 -94
- package/dist/image.d.ts +33 -50
- package/dist/image.js +1145 -249
- package/dist/index.cjs +5430 -1278
- package/dist/index.d.cts +1137 -3180
- package/dist/index.d.ts +1137 -3180
- package/dist/index.js +5412 -1249
- package/package.json +16 -14
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
|
-
|
|
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:
|
|
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: "
|
|
506
|
-
editEndpoint: "
|
|
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: "
|
|
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:
|
|
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
|
-
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1295
|
+
customImageSize: {
|
|
1296
|
+
maxPixels: 4194304,
|
|
1297
|
+
minPixels: 262144
|
|
1298
|
+
},
|
|
1299
|
+
endpoint: "alibaba/qwen-image-3/text-to-image",
|
|
1089
1300
|
falPricing: {
|
|
1090
|
-
checkedAt:
|
|
1301
|
+
checkedAt: FAL_PRICING_CHECKED_SEP_2026,
|
|
1091
1302
|
currency: "USD",
|
|
1092
|
-
endpointId: "
|
|
1093
|
-
estimatedCostPerImageUsd: .
|
|
1303
|
+
endpointId: "alibaba/qwen-image-3/text-to-image",
|
|
1304
|
+
estimatedCostPerImageUsd: .04,
|
|
1094
1305
|
source: "fal-pricing-api",
|
|
1095
|
-
unit: "
|
|
1096
|
-
unitPrice: .
|
|
1306
|
+
unit: "images",
|
|
1307
|
+
unitPrice: .04
|
|
1097
1308
|
},
|
|
1098
1309
|
name: "Qwen Image 3",
|
|
1099
|
-
pricePerImageUsd: .
|
|
1100
|
-
pricing: "$0.
|
|
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/
|
|
1194
|
-
/**
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
1420
|
-
|
|
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: {
|
|
1434
|
-
|
|
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: {
|
|
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: .
|
|
2259
|
+
usd: .14
|
|
1644
2260
|
},
|
|
1645
|
-
pricing: "$0.
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
2384
|
+
kind: "megapixel-step",
|
|
2385
|
+
megapixels: 2,
|
|
2386
|
+
usd: .08
|
|
1728
2387
|
},
|
|
1729
|
-
pricing: "$0.
|
|
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
|
-
|
|
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
|
-
|
|
2422
|
+
kind: "megapixel-step",
|
|
2423
|
+
megapixels: 8,
|
|
2424
|
+
usd: .08
|
|
1764
2425
|
},
|
|
1765
|
-
pricing: "$0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1877
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
-
*
|
|
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
|
|
2486
|
-
*
|
|
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.
|
|
2506
|
-
*
|
|
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
|
-
"
|
|
2535
|
-
"fal-ai/recraft-
|
|
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({
|
|
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 —
|
|
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$
|
|
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({
|
|
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$
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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/*`).
|
|
2818
|
-
*
|
|
2819
|
-
* thrown exceptions).
|
|
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: "
|
|
2826
|
-
* const r = await img.generate({
|
|
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
|
|
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 "
|
|
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 =
|
|
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 =
|
|
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
|
-
...
|
|
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 =
|
|
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
|
|
3017
|
-
|
|
3018
|
-
|
|
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
|
-
|
|
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,
|
|
3972
|
+
export { FAL_API_KEY_ENV, OPENAI_API_KEY_ENV, OPENROUTER_API_KEY_ENV, PROVIDERS, REPLICATE_API_KEY_ENV, costForImages, costFromProviderMetadata, createMotifImage, getProviderAdapter, providerPricePerImageUsd };
|