@howells/motif-sdk 5.4.0 → 7.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 +7 -7
- package/dist/image.js +8 -679
- package/dist/index.cjs +45 -1792
- package/dist/index.d.cts +7 -14
- package/dist/index.d.ts +7 -14
- package/dist/index.js +46 -1792
- package/dist/registry.d.ts +1757 -0
- package/dist/registry.js +2714 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { err, err as err$1, ok, ok as ok$1 } from "neverthrow";
|
|
2
2
|
import { defineEnv } from "@howells/envy";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { generateImage } from "ai";
|
|
5
|
-
import { createFal } from "@ai-sdk/fal";
|
|
6
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
7
|
-
import { createReplicate } from "@ai-sdk/replicate";
|
|
8
4
|
//#region src/aspects.ts
|
|
9
5
|
/** Ordered by popularity: square first, then common ratios */
|
|
10
6
|
const ASPECT_RATIOS = [
|
|
@@ -317,7 +313,7 @@ const CREATIVE_TAXONOMY = {
|
|
|
317
313
|
description: "Natural, unposed documentary portraits of people. Pair with a mood for the light.",
|
|
318
314
|
id: "portrait",
|
|
319
315
|
label: "Documentary portrait",
|
|
320
|
-
model: "
|
|
316
|
+
model: "seedream5"
|
|
321
317
|
},
|
|
322
318
|
{
|
|
323
319
|
acceptsMood: false,
|
|
@@ -354,7 +350,7 @@ const CREATIVE_TAXONOMY = {
|
|
|
354
350
|
experimental: true,
|
|
355
351
|
id: "illustration",
|
|
356
352
|
label: "Editorial illustration",
|
|
357
|
-
model: "
|
|
353
|
+
model: "flare"
|
|
358
354
|
}
|
|
359
355
|
],
|
|
360
356
|
mood: [
|
|
@@ -537,7 +533,7 @@ function falHttpError(status, body, requestId) {
|
|
|
537
533
|
* `CreativeOptionError`'s `"INVALID_OPTION"`) onto the returned error so callers
|
|
538
534
|
* keep structured error metadata. Status `0` marks a non-HTTP local error.
|
|
539
535
|
*/
|
|
540
|
-
function toMotifError
|
|
536
|
+
function toMotifError(error) {
|
|
541
537
|
if (error instanceof MotifError) return error;
|
|
542
538
|
return new MotifError(error instanceof Error ? error.message : String(error), 0, error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0);
|
|
543
539
|
}
|
|
@@ -691,27 +687,19 @@ function promptWarnings(prompt, context = {}) {
|
|
|
691
687
|
}
|
|
692
688
|
//#endregion
|
|
693
689
|
//#region src/env.ts
|
|
694
|
-
const motifEnvSchema = defineEnv({ optional: {
|
|
695
|
-
FAL_KEY: z.string().min(1),
|
|
696
|
-
OPENAI_API_KEY: z.string().min(1)
|
|
697
|
-
} });
|
|
690
|
+
const motifEnvSchema = defineEnv({ optional: { FAL_KEY: z.string().min(1) } });
|
|
698
691
|
function parseMotifEnv(input = process.env) {
|
|
699
692
|
return motifEnvSchema.parse(input);
|
|
700
693
|
}
|
|
701
694
|
function getFalKeyFromEnv(input = process.env) {
|
|
702
695
|
return parseMotifEnv(input).FAL_KEY;
|
|
703
696
|
}
|
|
704
|
-
/** The OpenAI key, used by direct OpenAI routes such as gpt2 transparency. */
|
|
705
|
-
function getOpenAiKeyFromEnv(input = process.env) {
|
|
706
|
-
return parseMotifEnv(input).OPENAI_API_KEY;
|
|
707
|
-
}
|
|
708
697
|
//#endregion
|
|
709
698
|
//#region src/models.ts
|
|
710
699
|
const AA_IMAGE_LEADERBOARD_SNAPSHOT = "2026-10-07";
|
|
711
700
|
const AA_IMAGE_SOURCES = ["https://artificialanalysis.ai/image/leaderboard/text-to-image", "https://artificialanalysis.ai/image/leaderboard/editing"];
|
|
712
701
|
const FAL_PRICING_CHECKED_AT = "2026-05-12";
|
|
713
702
|
const FAL_PRICING_CHECKED_JUL_2026 = "2026-07-11";
|
|
714
|
-
const FAL_PRICING_CHECKED_AUG_2026 = "2026-08-23";
|
|
715
703
|
const FAL_PRICING_CHECKED_SEP_2026 = "2026-09-16";
|
|
716
704
|
const FAL_PRICING_CHECKED_OCT_2026 = "2026-10-29";
|
|
717
705
|
const MODELS = {
|
|
@@ -849,77 +837,6 @@ const MODELS = {
|
|
|
849
837
|
type: "generation",
|
|
850
838
|
useQueue: true
|
|
851
839
|
},
|
|
852
|
-
gpt2: {
|
|
853
|
-
benchmark: {
|
|
854
|
-
artificialAnalysis: {
|
|
855
|
-
editing: {
|
|
856
|
-
elo: 1125,
|
|
857
|
-
rank: 5
|
|
858
|
-
},
|
|
859
|
-
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
860
|
-
sourceUrls: AA_IMAGE_SOURCES,
|
|
861
|
-
textToImage: {
|
|
862
|
-
elo: 1172,
|
|
863
|
-
rank: 3
|
|
864
|
-
}
|
|
865
|
-
},
|
|
866
|
-
speed: {
|
|
867
|
-
medianSeconds: 200.7,
|
|
868
|
-
p95Seconds: 232.6,
|
|
869
|
-
source: "artificial-analysis-models"
|
|
870
|
-
},
|
|
871
|
-
tiers: {
|
|
872
|
-
price: "ultra",
|
|
873
|
-
quality: "frontier",
|
|
874
|
-
speed: "very_slow"
|
|
875
|
-
},
|
|
876
|
-
useCase: "Established frontier generation and transparent PNGs"
|
|
877
|
-
},
|
|
878
|
-
customImageSize: {
|
|
879
|
-
maxEdge: 3840,
|
|
880
|
-
maxPixels: 8294400,
|
|
881
|
-
maxRatio: 3,
|
|
882
|
-
minPixels: 655360,
|
|
883
|
-
multipleOf: 16
|
|
884
|
-
},
|
|
885
|
-
editEndpoint: "openai/gpt-image-2/edit",
|
|
886
|
-
endpoint: "openai/gpt-image-2",
|
|
887
|
-
falPricing: {
|
|
888
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
889
|
-
currency: "USD",
|
|
890
|
-
endpointId: "openai/gpt-image-2",
|
|
891
|
-
estimatedCostPerImageUsd: .211,
|
|
892
|
-
source: "fal-pricing-api",
|
|
893
|
-
unit: "units",
|
|
894
|
-
unitPrice: 1
|
|
895
|
-
},
|
|
896
|
-
maxReferenceImages: 16,
|
|
897
|
-
name: "GPT Image 2",
|
|
898
|
-
pricePerImageUsd: .211,
|
|
899
|
-
pricing: "$0.211",
|
|
900
|
-
sizeMode: "image_size_enum",
|
|
901
|
-
supportsAspect: true,
|
|
902
|
-
streaming: {
|
|
903
|
-
generation: true,
|
|
904
|
-
edit: true
|
|
905
|
-
},
|
|
906
|
-
supportsEdit: true,
|
|
907
|
-
supportsMaskImage: true,
|
|
908
|
-
maskImageField: "mask_url",
|
|
909
|
-
supportsNumImages: true,
|
|
910
|
-
supportsOutputFormat: true,
|
|
911
|
-
supportsQuality: true,
|
|
912
|
-
supportsResolution: false,
|
|
913
|
-
supportsSyncMode: true,
|
|
914
|
-
transparencyRoute: {
|
|
915
|
-
apiKeyEnv: "OPENAI_API_KEY",
|
|
916
|
-
model: "gpt-image-2",
|
|
917
|
-
provider: "openai",
|
|
918
|
-
supportsEdit: true
|
|
919
|
-
},
|
|
920
|
-
type: "generation",
|
|
921
|
-
useQueue: true
|
|
922
|
-
},
|
|
923
840
|
gpt: {
|
|
924
841
|
benchmark: {
|
|
925
842
|
artificialAnalysis: {
|
|
@@ -1081,7 +998,7 @@ const MODELS = {
|
|
|
1081
998
|
quality: "frontier",
|
|
1082
999
|
speed: "unknown"
|
|
1083
1000
|
},
|
|
1084
|
-
useCase: "
|
|
1001
|
+
useCase: "Gemini workhorse with 0.5K output"
|
|
1085
1002
|
},
|
|
1086
1003
|
editEndpoint: "fal-ai/nano-banana-2/edit",
|
|
1087
1004
|
endpoint: "fal-ai/nano-banana-2",
|
|
@@ -1186,211 +1103,6 @@ const MODELS = {
|
|
|
1186
1103
|
supportsWebSearch: true,
|
|
1187
1104
|
type: "generation"
|
|
1188
1105
|
},
|
|
1189
|
-
gemini: {
|
|
1190
|
-
benchmark: { artificialAnalysis: {
|
|
1191
|
-
editing: {
|
|
1192
|
-
elo: 987,
|
|
1193
|
-
rank: 50
|
|
1194
|
-
},
|
|
1195
|
-
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
1196
|
-
sourceUrls: AA_IMAGE_SOURCES,
|
|
1197
|
-
textToImage: {
|
|
1198
|
-
elo: 985,
|
|
1199
|
-
rank: 60
|
|
1200
|
-
}
|
|
1201
|
-
} },
|
|
1202
|
-
editEndpoint: "fal-ai/gemini-25-flash-image/edit",
|
|
1203
|
-
endpoint: "fal-ai/gemini-25-flash-image",
|
|
1204
|
-
falPricing: {
|
|
1205
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1206
|
-
currency: "USD",
|
|
1207
|
-
endpointId: "fal-ai/gemini-25-flash-image",
|
|
1208
|
-
estimatedCostPerImageUsd: .0398,
|
|
1209
|
-
source: "fal-pricing-api",
|
|
1210
|
-
unit: "images",
|
|
1211
|
-
unitPrice: .0398
|
|
1212
|
-
},
|
|
1213
|
-
maxReferenceImages: 4,
|
|
1214
|
-
name: "Gemini 2.5 Flash",
|
|
1215
|
-
pricePerImageUsd: .0398,
|
|
1216
|
-
pricing: "$0.04",
|
|
1217
|
-
sizeMode: "aspect_ratio",
|
|
1218
|
-
supportedAspects: [
|
|
1219
|
-
"21:9",
|
|
1220
|
-
"16:9",
|
|
1221
|
-
"3:2",
|
|
1222
|
-
"4:3",
|
|
1223
|
-
"5:4",
|
|
1224
|
-
"1:1",
|
|
1225
|
-
"4:5",
|
|
1226
|
-
"3:4",
|
|
1227
|
-
"2:3",
|
|
1228
|
-
"9:16"
|
|
1229
|
-
],
|
|
1230
|
-
supportsAspect: true,
|
|
1231
|
-
supportsEdit: true,
|
|
1232
|
-
supportsNumImages: true,
|
|
1233
|
-
supportsOutputFormat: true,
|
|
1234
|
-
supportsResolution: false,
|
|
1235
|
-
supportsSafetyTolerance: true,
|
|
1236
|
-
supportsSeed: true,
|
|
1237
|
-
supportsSyncMode: true,
|
|
1238
|
-
type: "generation"
|
|
1239
|
-
},
|
|
1240
|
-
gemini3: {
|
|
1241
|
-
editEndpoint: "fal-ai/gemini-3-pro-image-preview/edit",
|
|
1242
|
-
endpoint: "fal-ai/gemini-3-pro-image-preview",
|
|
1243
|
-
falPricing: {
|
|
1244
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1245
|
-
currency: "USD",
|
|
1246
|
-
endpointId: "fal-ai/gemini-3-pro-image-preview",
|
|
1247
|
-
estimatedCostPerImageUsd: .15,
|
|
1248
|
-
source: "fal-pricing-api",
|
|
1249
|
-
unit: "images",
|
|
1250
|
-
unitPrice: .15
|
|
1251
|
-
},
|
|
1252
|
-
maxReferenceImages: 4,
|
|
1253
|
-
name: "Gemini 3 Pro",
|
|
1254
|
-
pricePerImageUsd: .15,
|
|
1255
|
-
pricing: "$0.15",
|
|
1256
|
-
sizeMode: "aspect_ratio",
|
|
1257
|
-
supportedAspects: [
|
|
1258
|
-
"auto",
|
|
1259
|
-
"21:9",
|
|
1260
|
-
"16:9",
|
|
1261
|
-
"3:2",
|
|
1262
|
-
"4:3",
|
|
1263
|
-
"5:4",
|
|
1264
|
-
"1:1",
|
|
1265
|
-
"4:5",
|
|
1266
|
-
"3:4",
|
|
1267
|
-
"2:3",
|
|
1268
|
-
"9:16"
|
|
1269
|
-
],
|
|
1270
|
-
supportedResolutions: [
|
|
1271
|
-
"1K",
|
|
1272
|
-
"2K",
|
|
1273
|
-
"4K"
|
|
1274
|
-
],
|
|
1275
|
-
supportsAspect: true,
|
|
1276
|
-
supportsEdit: true,
|
|
1277
|
-
supportsNumImages: true,
|
|
1278
|
-
supportsOutputFormat: true,
|
|
1279
|
-
supportsResolution: true,
|
|
1280
|
-
supportsSafetyTolerance: true,
|
|
1281
|
-
supportsSeed: true,
|
|
1282
|
-
supportsSyncMode: true,
|
|
1283
|
-
supportsWebSearch: true,
|
|
1284
|
-
type: "generation"
|
|
1285
|
-
},
|
|
1286
|
-
seedream4: {
|
|
1287
|
-
benchmark: {
|
|
1288
|
-
artificialAnalysis: {
|
|
1289
|
-
editing: {
|
|
1290
|
-
elo: 1015,
|
|
1291
|
-
rank: 36
|
|
1292
|
-
},
|
|
1293
|
-
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
1294
|
-
sourceUrls: AA_IMAGE_SOURCES,
|
|
1295
|
-
textToImage: {
|
|
1296
|
-
elo: 1030,
|
|
1297
|
-
rank: 21
|
|
1298
|
-
}
|
|
1299
|
-
},
|
|
1300
|
-
speed: {
|
|
1301
|
-
medianSeconds: 14.3,
|
|
1302
|
-
p95Seconds: 21,
|
|
1303
|
-
source: "artificial-analysis-models"
|
|
1304
|
-
},
|
|
1305
|
-
tiers: {
|
|
1306
|
-
price: "budget",
|
|
1307
|
-
quality: "best",
|
|
1308
|
-
speed: "balanced"
|
|
1309
|
-
},
|
|
1310
|
-
useCase: "Legacy budget generation and large multi-reference edits"
|
|
1311
|
-
},
|
|
1312
|
-
customImageSize: {
|
|
1313
|
-
maxPixels: 16777216,
|
|
1314
|
-
minPixels: 921600
|
|
1315
|
-
},
|
|
1316
|
-
editEndpoint: "fal-ai/bytedance/seedream/v4/edit",
|
|
1317
|
-
endpoint: "fal-ai/bytedance/seedream/v4/text-to-image",
|
|
1318
|
-
falPricing: {
|
|
1319
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1320
|
-
currency: "USD",
|
|
1321
|
-
endpointId: "fal-ai/bytedance/seedream/v4/text-to-image",
|
|
1322
|
-
estimatedCostPerImageUsd: .03,
|
|
1323
|
-
source: "fal-pricing-api",
|
|
1324
|
-
unit: "images",
|
|
1325
|
-
unitPrice: .03
|
|
1326
|
-
},
|
|
1327
|
-
maxReferenceImages: 10,
|
|
1328
|
-
name: "Seedream 4.0",
|
|
1329
|
-
pricePerImageUsd: .03,
|
|
1330
|
-
pricing: "$0.03",
|
|
1331
|
-
sizeMode: "image_size_enum",
|
|
1332
|
-
supportsAspect: true,
|
|
1333
|
-
supportsEdit: true,
|
|
1334
|
-
supportsNumImages: true,
|
|
1335
|
-
supportsResolution: false,
|
|
1336
|
-
supportsSeed: true,
|
|
1337
|
-
supportsSyncMode: true,
|
|
1338
|
-
type: "generation"
|
|
1339
|
-
},
|
|
1340
|
-
seedream45: {
|
|
1341
|
-
benchmark: {
|
|
1342
|
-
artificialAnalysis: {
|
|
1343
|
-
editing: {
|
|
1344
|
-
elo: 1037,
|
|
1345
|
-
rank: 32
|
|
1346
|
-
},
|
|
1347
|
-
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
1348
|
-
sourceUrls: AA_IMAGE_SOURCES,
|
|
1349
|
-
textToImage: {
|
|
1350
|
-
elo: 1022,
|
|
1351
|
-
rank: 26
|
|
1352
|
-
}
|
|
1353
|
-
},
|
|
1354
|
-
speed: {
|
|
1355
|
-
medianSeconds: 18.2,
|
|
1356
|
-
p95Seconds: 43.3,
|
|
1357
|
-
source: "artificial-analysis-models"
|
|
1358
|
-
},
|
|
1359
|
-
tiers: {
|
|
1360
|
-
price: "standard",
|
|
1361
|
-
quality: "better",
|
|
1362
|
-
speed: "balanced"
|
|
1363
|
-
},
|
|
1364
|
-
useCase: "Cheap current Seedream model with good animation/art scores"
|
|
1365
|
-
},
|
|
1366
|
-
customImageSize: {
|
|
1367
|
-
maxPixels: 16777216,
|
|
1368
|
-
minPixels: 3686400
|
|
1369
|
-
},
|
|
1370
|
-
editEndpoint: "fal-ai/bytedance/seedream/v4.5/edit",
|
|
1371
|
-
endpoint: "fal-ai/bytedance/seedream/v4.5/text-to-image",
|
|
1372
|
-
falPricing: {
|
|
1373
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1374
|
-
currency: "USD",
|
|
1375
|
-
endpointId: "fal-ai/bytedance/seedream/v4.5/text-to-image",
|
|
1376
|
-
estimatedCostPerImageUsd: .04,
|
|
1377
|
-
source: "fal-pricing-api",
|
|
1378
|
-
unit: "images",
|
|
1379
|
-
unitPrice: .04
|
|
1380
|
-
},
|
|
1381
|
-
maxReferenceImages: 10,
|
|
1382
|
-
name: "Seedream 4.5",
|
|
1383
|
-
pricePerImageUsd: .04,
|
|
1384
|
-
pricing: "$0.04",
|
|
1385
|
-
sizeMode: "image_size_enum",
|
|
1386
|
-
supportsAspect: true,
|
|
1387
|
-
supportsEdit: true,
|
|
1388
|
-
supportsNumImages: true,
|
|
1389
|
-
supportsResolution: false,
|
|
1390
|
-
supportsSeed: true,
|
|
1391
|
-
supportsSyncMode: true,
|
|
1392
|
-
type: "generation"
|
|
1393
|
-
},
|
|
1394
1106
|
seedream5: {
|
|
1395
1107
|
benchmark: { artificialAnalysis: {
|
|
1396
1108
|
editing: {
|
|
@@ -1474,65 +1186,6 @@ const MODELS = {
|
|
|
1474
1186
|
supportsSyncMode: true,
|
|
1475
1187
|
maxReferenceImages: 10
|
|
1476
1188
|
},
|
|
1477
|
-
"flux2-max": {
|
|
1478
|
-
benchmark: {
|
|
1479
|
-
artificialAnalysis: {
|
|
1480
|
-
editing: {
|
|
1481
|
-
elo: 997,
|
|
1482
|
-
rank: 49
|
|
1483
|
-
},
|
|
1484
|
-
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
1485
|
-
sourceUrls: AA_IMAGE_SOURCES,
|
|
1486
|
-
textToImage: {
|
|
1487
|
-
elo: 1020,
|
|
1488
|
-
rank: 28
|
|
1489
|
-
}
|
|
1490
|
-
},
|
|
1491
|
-
speed: {
|
|
1492
|
-
medianSeconds: 31.6,
|
|
1493
|
-
p95Seconds: 41,
|
|
1494
|
-
source: "artificial-analysis-models"
|
|
1495
|
-
},
|
|
1496
|
-
tiers: {
|
|
1497
|
-
price: "premium",
|
|
1498
|
-
quality: "best",
|
|
1499
|
-
speed: "slow"
|
|
1500
|
-
},
|
|
1501
|
-
useCase: "Best FLUX quality and high-end editing"
|
|
1502
|
-
},
|
|
1503
|
-
customImageSize: {
|
|
1504
|
-
maxEdge: 2560,
|
|
1505
|
-
maxPixels: 4194304,
|
|
1506
|
-
minEdge: 256,
|
|
1507
|
-
multipleOf: 16
|
|
1508
|
-
},
|
|
1509
|
-
editEndpoint: "fal-ai/flux-2-max/edit",
|
|
1510
|
-
endpoint: "fal-ai/flux-2-max",
|
|
1511
|
-
falPricing: {
|
|
1512
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1513
|
-
currency: "USD",
|
|
1514
|
-
endpointId: "fal-ai/flux-2-max",
|
|
1515
|
-
estimatedCostPerImageUsd: .07,
|
|
1516
|
-
source: "fal-pricing-api",
|
|
1517
|
-
unit: "megapixels",
|
|
1518
|
-
unitPrice: .07
|
|
1519
|
-
},
|
|
1520
|
-
maxReferenceImages: 10,
|
|
1521
|
-
name: "FLUX.2 Max",
|
|
1522
|
-
pricePerImageUsd: .07,
|
|
1523
|
-
pricing: "$0.07/MP",
|
|
1524
|
-
sizeMode: "image_size_enum",
|
|
1525
|
-
supportsAspect: true,
|
|
1526
|
-
supportsEdit: true,
|
|
1527
|
-
supportsNumImages: false,
|
|
1528
|
-
supportsOutputFormat: true,
|
|
1529
|
-
supportsResolution: false,
|
|
1530
|
-
supportsSafetyChecker: true,
|
|
1531
|
-
supportsSafetyTolerance: true,
|
|
1532
|
-
supportsSeed: true,
|
|
1533
|
-
supportsSyncMode: true,
|
|
1534
|
-
type: "generation"
|
|
1535
|
-
},
|
|
1536
1189
|
"flux2-pro": {
|
|
1537
1190
|
benchmark: {
|
|
1538
1191
|
artificialAnalysis: {
|
|
@@ -1638,77 +1291,14 @@ const MODELS = {
|
|
|
1638
1291
|
maxReferenceImages: 10,
|
|
1639
1292
|
name: "FLUX.2 Flex",
|
|
1640
1293
|
pricePerImageUsd: .05,
|
|
1641
|
-
pricing: "$0.05/MP",
|
|
1642
|
-
sizeMode: "image_size_enum",
|
|
1643
|
-
supportedOutputFormats: ["jpeg", "png"],
|
|
1644
|
-
supportsAspect: true,
|
|
1645
|
-
supportsEdit: true,
|
|
1646
|
-
supportsGuidanceScale: true,
|
|
1647
|
-
supportsInferenceSteps: true,
|
|
1648
|
-
supportsNumImages: false,
|
|
1649
|
-
supportsOutputFormat: true,
|
|
1650
|
-
supportsResolution: false,
|
|
1651
|
-
supportsSafetyChecker: true,
|
|
1652
|
-
supportsSafetyTolerance: true,
|
|
1653
|
-
supportsSeed: true,
|
|
1654
|
-
supportsSyncMode: true,
|
|
1655
|
-
type: "generation"
|
|
1656
|
-
},
|
|
1657
|
-
"flux2-dev": {
|
|
1658
|
-
benchmark: {
|
|
1659
|
-
artificialAnalysis: {
|
|
1660
|
-
editing: {
|
|
1661
|
-
elo: 1e3,
|
|
1662
|
-
rank: 45
|
|
1663
|
-
},
|
|
1664
|
-
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
1665
|
-
sourceUrls: AA_IMAGE_SOURCES,
|
|
1666
|
-
textToImage: {
|
|
1667
|
-
elo: 1e3,
|
|
1668
|
-
rank: 42
|
|
1669
|
-
}
|
|
1670
|
-
},
|
|
1671
|
-
speed: {
|
|
1672
|
-
medianSeconds: 4.6,
|
|
1673
|
-
p95Seconds: 69.6,
|
|
1674
|
-
source: "artificial-analysis-models"
|
|
1675
|
-
},
|
|
1676
|
-
tiers: {
|
|
1677
|
-
price: "variable",
|
|
1678
|
-
quality: "better",
|
|
1679
|
-
speed: "fast"
|
|
1680
|
-
},
|
|
1681
|
-
useCase: "Open FLUX.2 quality with low average cost"
|
|
1682
|
-
},
|
|
1683
|
-
customImageSize: {
|
|
1684
|
-
maxEdge: 2048,
|
|
1685
|
-
minEdge: 512
|
|
1686
|
-
},
|
|
1687
|
-
editEndpoint: "fal-ai/flux-2/edit",
|
|
1688
|
-
endpoint: "fal-ai/flux-2",
|
|
1689
|
-
falPricing: {
|
|
1690
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1691
|
-
currency: "USD",
|
|
1692
|
-
endpointId: "fal-ai/flux-2",
|
|
1693
|
-
estimatedCostPerImageUsd: .012,
|
|
1694
|
-
source: "fal-pricing-api",
|
|
1695
|
-
unit: "compute seconds",
|
|
1696
|
-
unitPrice: .00167
|
|
1697
|
-
},
|
|
1698
|
-
maxReferenceImages: 4,
|
|
1699
|
-
name: "FLUX.2 [dev]",
|
|
1700
|
-
pricePerImageUsd: .012,
|
|
1701
|
-
pricing: "$0.00167/sec",
|
|
1294
|
+
pricing: "$0.05/MP",
|
|
1702
1295
|
sizeMode: "image_size_enum",
|
|
1296
|
+
supportedOutputFormats: ["jpeg", "png"],
|
|
1703
1297
|
supportsAspect: true,
|
|
1704
|
-
streaming: {
|
|
1705
|
-
generation: true,
|
|
1706
|
-
edit: true
|
|
1707
|
-
},
|
|
1708
1298
|
supportsEdit: true,
|
|
1709
1299
|
supportsGuidanceScale: true,
|
|
1710
1300
|
supportsInferenceSteps: true,
|
|
1711
|
-
supportsNumImages:
|
|
1301
|
+
supportsNumImages: false,
|
|
1712
1302
|
supportsOutputFormat: true,
|
|
1713
1303
|
supportsResolution: false,
|
|
1714
1304
|
supportsSafetyChecker: true,
|
|
@@ -1747,188 +1337,6 @@ const MODELS = {
|
|
|
1747
1337
|
supportsSyncMode: true,
|
|
1748
1338
|
type: "generation"
|
|
1749
1339
|
},
|
|
1750
|
-
flux: {
|
|
1751
|
-
endpoint: "fal-ai/flux-pro/v1.1-ultra",
|
|
1752
|
-
falPricing: {
|
|
1753
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1754
|
-
currency: "USD",
|
|
1755
|
-
endpointId: "fal-ai/flux-pro/v1.1-ultra",
|
|
1756
|
-
estimatedCostPerImageUsd: .06,
|
|
1757
|
-
source: "fal-pricing-api",
|
|
1758
|
-
unit: "images",
|
|
1759
|
-
unitPrice: .06
|
|
1760
|
-
},
|
|
1761
|
-
maxReferenceImages: 1,
|
|
1762
|
-
name: "FLUX Pro Ultra",
|
|
1763
|
-
pricePerImageUsd: .06,
|
|
1764
|
-
pricing: "$0.06",
|
|
1765
|
-
sizeMode: "aspect_ratio",
|
|
1766
|
-
supportsAspect: true,
|
|
1767
|
-
supportsEdit: true,
|
|
1768
|
-
supportsEnhancePrompt: true,
|
|
1769
|
-
supportsImagePromptStrength: true,
|
|
1770
|
-
supportsNumImages: true,
|
|
1771
|
-
supportsOutputFormat: true,
|
|
1772
|
-
supportsRaw: true,
|
|
1773
|
-
supportsResolution: false,
|
|
1774
|
-
supportsSafetyTolerance: true,
|
|
1775
|
-
supportsSeed: true,
|
|
1776
|
-
supportsSyncMode: true,
|
|
1777
|
-
type: "generation"
|
|
1778
|
-
},
|
|
1779
|
-
"flux-fast": {
|
|
1780
|
-
customImageSize: {},
|
|
1781
|
-
endpoint: "fal-ai/flux/schnell",
|
|
1782
|
-
falPricing: {
|
|
1783
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1784
|
-
currency: "USD",
|
|
1785
|
-
endpointId: "fal-ai/flux/schnell",
|
|
1786
|
-
estimatedCostPerImageUsd: .003,
|
|
1787
|
-
source: "fal-pricing-api",
|
|
1788
|
-
unit: "megapixels",
|
|
1789
|
-
unitPrice: .003
|
|
1790
|
-
},
|
|
1791
|
-
name: "FLUX Schnell",
|
|
1792
|
-
pricePerImageUsd: .003,
|
|
1793
|
-
pricing: "$0.003",
|
|
1794
|
-
sizeMode: "image_size_enum",
|
|
1795
|
-
supportsAspect: true,
|
|
1796
|
-
supportsEdit: false,
|
|
1797
|
-
supportsGuidanceScale: true,
|
|
1798
|
-
supportsInferenceSteps: true,
|
|
1799
|
-
supportsNumImages: true,
|
|
1800
|
-
supportsOutputFormat: true,
|
|
1801
|
-
supportsResolution: false,
|
|
1802
|
-
supportsSeed: true,
|
|
1803
|
-
supportsSyncMode: true,
|
|
1804
|
-
type: "generation"
|
|
1805
|
-
},
|
|
1806
|
-
recraft: {
|
|
1807
|
-
customImageSize: {},
|
|
1808
|
-
endpoint: "fal-ai/recraft/v3/text-to-image",
|
|
1809
|
-
falPricing: {
|
|
1810
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1811
|
-
currency: "USD",
|
|
1812
|
-
endpointId: "fal-ai/recraft/v3/text-to-image",
|
|
1813
|
-
estimatedCostPerImageUsd: .04,
|
|
1814
|
-
source: "fal-pricing-api",
|
|
1815
|
-
unit: "images",
|
|
1816
|
-
unitPrice: .04
|
|
1817
|
-
},
|
|
1818
|
-
name: "Recraft V3",
|
|
1819
|
-
pricePerImageUsd: .04,
|
|
1820
|
-
pricing: "$0.04",
|
|
1821
|
-
sizeMode: "image_size_enum",
|
|
1822
|
-
supportsAspect: true,
|
|
1823
|
-
supportsEdit: false,
|
|
1824
|
-
supportsNumImages: false,
|
|
1825
|
-
supportsResolution: false,
|
|
1826
|
-
supportsStyle: true,
|
|
1827
|
-
supportsSyncMode: true,
|
|
1828
|
-
type: "generation"
|
|
1829
|
-
},
|
|
1830
|
-
reve: {
|
|
1831
|
-
benchmark: {
|
|
1832
|
-
tiers: {
|
|
1833
|
-
price: "premium",
|
|
1834
|
-
quality: "frontier",
|
|
1835
|
-
speed: "unknown"
|
|
1836
|
-
},
|
|
1837
|
-
useCase: "Historical Reve route; not currently verified on fal"
|
|
1838
|
-
},
|
|
1839
|
-
editEndpoint: "fal-ai/reve/edit",
|
|
1840
|
-
endpoint: "fal-ai/reve/text-to-image",
|
|
1841
|
-
falPricing: {
|
|
1842
|
-
checkedAt: FAL_PRICING_CHECKED_AUG_2026,
|
|
1843
|
-
currency: "USD",
|
|
1844
|
-
endpointId: "fal-ai/reve/text-to-image",
|
|
1845
|
-
estimatedCostPerImageUsd: .04,
|
|
1846
|
-
source: "fal-pricing-api",
|
|
1847
|
-
unit: "images",
|
|
1848
|
-
unitPrice: .04
|
|
1849
|
-
},
|
|
1850
|
-
maxReferenceImages: 1,
|
|
1851
|
-
name: "Reve 2.1",
|
|
1852
|
-
pricePerImageUsd: .04,
|
|
1853
|
-
pricing: "$0.04",
|
|
1854
|
-
sizeMode: "aspect_ratio",
|
|
1855
|
-
supportedAspects: [
|
|
1856
|
-
"16:9",
|
|
1857
|
-
"9:16",
|
|
1858
|
-
"3:2",
|
|
1859
|
-
"2:3",
|
|
1860
|
-
"4:3",
|
|
1861
|
-
"3:4",
|
|
1862
|
-
"1:1"
|
|
1863
|
-
],
|
|
1864
|
-
supportsAspect: true,
|
|
1865
|
-
supportsEdit: true,
|
|
1866
|
-
supportsNumImages: true,
|
|
1867
|
-
supportsOutputFormat: true,
|
|
1868
|
-
supportsResolution: false,
|
|
1869
|
-
supportsSeed: false,
|
|
1870
|
-
supportsSyncMode: true,
|
|
1871
|
-
type: "generation"
|
|
1872
|
-
},
|
|
1873
|
-
recraft4: {
|
|
1874
|
-
benchmark: { artificialAnalysis: {
|
|
1875
|
-
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
1876
|
-
sourceUrls: AA_IMAGE_SOURCES,
|
|
1877
|
-
textToImage: {
|
|
1878
|
-
elo: 984,
|
|
1879
|
-
rank: 62
|
|
1880
|
-
}
|
|
1881
|
-
} },
|
|
1882
|
-
customImageSize: {},
|
|
1883
|
-
endpoint: "fal-ai/recraft/v4/text-to-image",
|
|
1884
|
-
falPricing: {
|
|
1885
|
-
checkedAt: FAL_PRICING_CHECKED_JUL_2026,
|
|
1886
|
-
currency: "USD",
|
|
1887
|
-
endpointId: "fal-ai/recraft/v4/text-to-image",
|
|
1888
|
-
estimatedCostPerImageUsd: .04,
|
|
1889
|
-
source: "fal-pricing-api",
|
|
1890
|
-
unit: "images",
|
|
1891
|
-
unitPrice: .04
|
|
1892
|
-
},
|
|
1893
|
-
name: "Recraft V4",
|
|
1894
|
-
pricePerImageUsd: .04,
|
|
1895
|
-
pricing: "$0.04",
|
|
1896
|
-
sizeMode: "image_size_enum",
|
|
1897
|
-
supportsAspect: true,
|
|
1898
|
-
supportsEdit: false,
|
|
1899
|
-
supportsNumImages: false,
|
|
1900
|
-
supportsResolution: false,
|
|
1901
|
-
supportsSyncMode: true,
|
|
1902
|
-
type: "generation"
|
|
1903
|
-
},
|
|
1904
|
-
ideogram: {
|
|
1905
|
-
customImageSize: {},
|
|
1906
|
-
endpoint: "fal-ai/ideogram/v3",
|
|
1907
|
-
falPricing: {
|
|
1908
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
1909
|
-
currency: "USD",
|
|
1910
|
-
endpointId: "fal-ai/ideogram/v3",
|
|
1911
|
-
estimatedCostPerImageUsd: .03,
|
|
1912
|
-
source: "fal-pricing-api",
|
|
1913
|
-
unit: "images",
|
|
1914
|
-
unitPrice: .03
|
|
1915
|
-
},
|
|
1916
|
-
name: "Ideogram V3",
|
|
1917
|
-
pricePerImageUsd: .03,
|
|
1918
|
-
pricing: "$0.03",
|
|
1919
|
-
sizeMode: "image_size_enum",
|
|
1920
|
-
supportsAspect: true,
|
|
1921
|
-
supportsEdit: false,
|
|
1922
|
-
supportsExpandPrompt: true,
|
|
1923
|
-
supportsNegativePrompt: true,
|
|
1924
|
-
supportsNumImages: true,
|
|
1925
|
-
supportsRenderingSpeed: true,
|
|
1926
|
-
supportsResolution: false,
|
|
1927
|
-
supportsSeed: true,
|
|
1928
|
-
supportsStyle: true,
|
|
1929
|
-
supportsSyncMode: true,
|
|
1930
|
-
type: "generation"
|
|
1931
|
-
},
|
|
1932
1340
|
ideogram4: {
|
|
1933
1341
|
benchmark: { artificialAnalysis: {
|
|
1934
1342
|
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
@@ -2029,52 +1437,6 @@ const MODELS = {
|
|
|
2029
1437
|
supportsSyncMode: true,
|
|
2030
1438
|
type: "generation"
|
|
2031
1439
|
},
|
|
2032
|
-
qwen: {
|
|
2033
|
-
benchmark: {
|
|
2034
|
-
artificialAnalysis: {
|
|
2035
|
-
snapshotDate: AA_IMAGE_LEADERBOARD_SNAPSHOT,
|
|
2036
|
-
sourceUrls: AA_IMAGE_SOURCES,
|
|
2037
|
-
textToImage: {
|
|
2038
|
-
elo: 890,
|
|
2039
|
-
rank: 106
|
|
2040
|
-
}
|
|
2041
|
-
},
|
|
2042
|
-
speed: {
|
|
2043
|
-
medianSeconds: 23.3,
|
|
2044
|
-
p95Seconds: 491,
|
|
2045
|
-
source: "artificial-analysis-models"
|
|
2046
|
-
},
|
|
2047
|
-
tiers: {
|
|
2048
|
-
price: "budget",
|
|
2049
|
-
quality: "better",
|
|
2050
|
-
speed: "slow"
|
|
2051
|
-
},
|
|
2052
|
-
useCase: "Low-cost open-weight text and design generation"
|
|
2053
|
-
},
|
|
2054
|
-
customImageSize: {},
|
|
2055
|
-
endpoint: "fal-ai/qwen-image",
|
|
2056
|
-
falPricing: {
|
|
2057
|
-
checkedAt: FAL_PRICING_CHECKED_AT,
|
|
2058
|
-
currency: "USD",
|
|
2059
|
-
endpointId: "fal-ai/qwen-image",
|
|
2060
|
-
estimatedCostPerImageUsd: .02,
|
|
2061
|
-
source: "fal-pricing-api",
|
|
2062
|
-
unit: "megapixels",
|
|
2063
|
-
unitPrice: .02
|
|
2064
|
-
},
|
|
2065
|
-
name: "Qwen Image",
|
|
2066
|
-
pricePerImageUsd: .02,
|
|
2067
|
-
pricing: "$0.02/MP",
|
|
2068
|
-
sizeMode: "image_size_enum",
|
|
2069
|
-
supportsAspect: true,
|
|
2070
|
-
supportsEdit: false,
|
|
2071
|
-
supportsNumImages: true,
|
|
2072
|
-
supportsOutputFormat: true,
|
|
2073
|
-
supportsResolution: false,
|
|
2074
|
-
supportsSeed: true,
|
|
2075
|
-
supportsSyncMode: true,
|
|
2076
|
-
type: "generation"
|
|
2077
|
-
},
|
|
2078
1440
|
qwen3: {
|
|
2079
1441
|
benchmark: {
|
|
2080
1442
|
artificialAnalysis: {
|
|
@@ -2475,33 +1837,12 @@ const MODELS = {
|
|
|
2475
1837
|
supportsNumImages: false,
|
|
2476
1838
|
supportsResolution: false,
|
|
2477
1839
|
type: "utility"
|
|
2478
|
-
},
|
|
2479
|
-
rmbg: {
|
|
2480
|
-
endpoint: "fal-ai/birefnet",
|
|
2481
|
-
name: "BiRefNet (Background Removal)",
|
|
2482
|
-
pricing: "$0.02",
|
|
2483
|
-
supportsAspect: false,
|
|
2484
|
-
supportsEdit: false,
|
|
2485
|
-
supportsNumImages: false,
|
|
2486
|
-
supportsResolution: false,
|
|
2487
|
-
type: "utility"
|
|
2488
|
-
},
|
|
2489
|
-
bria: {
|
|
2490
|
-
endpoint: "fal-ai/bria/background/remove",
|
|
2491
|
-
name: "Bria RMBG 2.0",
|
|
2492
|
-
pricing: "$0.02",
|
|
2493
|
-
supportsAspect: false,
|
|
2494
|
-
supportsEdit: false,
|
|
2495
|
-
supportsNumImages: false,
|
|
2496
|
-
supportsResolution: false,
|
|
2497
|
-
type: "utility"
|
|
2498
1840
|
}
|
|
2499
1841
|
};
|
|
2500
|
-
/** Curated generation
|
|
1842
|
+
/** Curated generation Models, the only ones Motif recommends. */
|
|
2501
1843
|
const GENERATION_MODELS = [
|
|
2502
1844
|
"sunburst",
|
|
2503
1845
|
"flare",
|
|
2504
|
-
"gpt2",
|
|
2505
1846
|
"gpt",
|
|
2506
1847
|
"banana21",
|
|
2507
1848
|
"banana2",
|
|
@@ -2524,28 +1865,7 @@ const GENERATION_MODELS = [
|
|
|
2524
1865
|
"ideogram3-transparent",
|
|
2525
1866
|
"recraft41"
|
|
2526
1867
|
];
|
|
2527
|
-
/** Retained for old pins, explicit overrides and tuned Looks, never auto-selected. */
|
|
2528
|
-
const LEGACY_GENERATION_MODELS = [
|
|
2529
|
-
"gemini",
|
|
2530
|
-
"gemini3",
|
|
2531
|
-
"seedream4",
|
|
2532
|
-
"seedream45",
|
|
2533
|
-
"flux2-max",
|
|
2534
|
-
"flux2-dev",
|
|
2535
|
-
"flux",
|
|
2536
|
-
"flux-fast",
|
|
2537
|
-
"recraft",
|
|
2538
|
-
"recraft4",
|
|
2539
|
-
"ideogram",
|
|
2540
|
-
"qwen"
|
|
2541
|
-
];
|
|
2542
1868
|
GENERATION_MODELS.filter((id) => MODELS[id]?.supportsEdit === true);
|
|
2543
|
-
const UTILITY_MODELS = [
|
|
2544
|
-
"clarity",
|
|
2545
|
-
"crystal",
|
|
2546
|
-
"rmbg",
|
|
2547
|
-
"bria"
|
|
2548
|
-
];
|
|
2549
1869
|
//#endregion
|
|
2550
1870
|
//#region src/cost.ts
|
|
2551
1871
|
/**
|
|
@@ -2559,26 +1879,16 @@ function estimateCost(model, resolution, numImages = 1, editImages = 0) {
|
|
|
2559
1879
|
if (model === "grok-image-2" && editImages > 0) return (resolution === "2K" || resolution === "4K" ? .08 : .06) * numImages + .01 * editImages;
|
|
2560
1880
|
if (model === "grok-image-quality") return (resolution === "2K" || resolution === "4K" ? .07 : .05) * numImages + .01 * editImages;
|
|
2561
1881
|
if (configuredPrice !== void 0) {
|
|
2562
|
-
if (
|
|
1882
|
+
if (model === "banana" && resolution === "4K") return configuredPrice * 2 * numImages;
|
|
2563
1883
|
if (model === "seedream5" && (resolution === "2K" || resolution === "4K")) return configuredPrice * 2 * numImages;
|
|
2564
1884
|
if (model === "grok-image-2" && (resolution === "2K" || resolution === "4K")) return .08 * numImages;
|
|
2565
1885
|
if (model === "banana2") return configuredPrice * (resolution === "4K" ? 2 : resolution === "2K" ? 1.5 : resolution === "0.5K" ? .75 : 1) * numImages;
|
|
2566
1886
|
return configuredPrice * numImages;
|
|
2567
1887
|
}
|
|
2568
1888
|
switch (model) {
|
|
2569
|
-
case "gpt2": return .211 * numImages;
|
|
2570
1889
|
case "gpt": return .133 * numImages;
|
|
2571
|
-
case "banana":
|
|
2572
|
-
case "gemini3": return (resolution === "4K" ? .3 : .15) * numImages;
|
|
2573
|
-
case "gemini": return .039 * numImages;
|
|
2574
|
-
case "flux":
|
|
2575
|
-
case "ideogram": return .06 * numImages;
|
|
2576
|
-
case "recraft": return .04 * numImages;
|
|
2577
|
-
case "flux-fast": return .003 * numImages;
|
|
2578
1890
|
case "clarity": return .03 * numImages;
|
|
2579
|
-
case "crystal":
|
|
2580
|
-
case "rmbg":
|
|
2581
|
-
case "bria": return .02 * numImages;
|
|
1891
|
+
case "crystal": return .02 * numImages;
|
|
2582
1892
|
default: return .1 * numImages;
|
|
2583
1893
|
}
|
|
2584
1894
|
}
|
|
@@ -2595,7 +1905,7 @@ function estimateVideoCost(durationSeconds = 5, generateAudio = true, model = "k
|
|
|
2595
1905
|
* `Response.json()` yields `any`; these helpers narrow that untyped payload into
|
|
2596
1906
|
* the SDK's response types with runtime guards instead of unchecked assertions.
|
|
2597
1907
|
*/
|
|
2598
|
-
function isRecord$
|
|
1908
|
+
function isRecord$1(value) {
|
|
2599
1909
|
return typeof value === "object" && value !== null;
|
|
2600
1910
|
}
|
|
2601
1911
|
function asString(value) {
|
|
@@ -2605,7 +1915,7 @@ function asNumber(value) {
|
|
|
2605
1915
|
return typeof value === "number" ? value : void 0;
|
|
2606
1916
|
}
|
|
2607
1917
|
function toMotifImage(value) {
|
|
2608
|
-
if (isRecord$
|
|
1918
|
+
if (isRecord$1(value)) return {
|
|
2609
1919
|
content_type: asString(value.content_type),
|
|
2610
1920
|
height: asNumber(value.height),
|
|
2611
1921
|
url: asString(value.url) ?? "",
|
|
@@ -2620,7 +1930,7 @@ function parseImages(value) {
|
|
|
2620
1930
|
function parseLogs(value) {
|
|
2621
1931
|
if (!Array.isArray(value)) return;
|
|
2622
1932
|
const entries = [];
|
|
2623
|
-
for (const entry of value) if (isRecord$
|
|
1933
|
+
for (const entry of value) if (isRecord$1(entry)) entries.push({
|
|
2624
1934
|
message: asString(entry.message) ?? "",
|
|
2625
1935
|
timestamp: asString(entry.timestamp) ?? ""
|
|
2626
1936
|
});
|
|
@@ -2650,7 +1960,7 @@ function queueAppPath(endpoint) {
|
|
|
2650
1960
|
return endpoint.split("/").slice(0, 2).join("/");
|
|
2651
1961
|
}
|
|
2652
1962
|
function parseQueueSubmission(data) {
|
|
2653
|
-
if (!isRecord$
|
|
1963
|
+
if (!isRecord$1(data)) return { requestId: "" };
|
|
2654
1964
|
return {
|
|
2655
1965
|
requestId: asString(data.request_id) ?? "",
|
|
2656
1966
|
responseUrl: asString(data.response_url)
|
|
@@ -2690,7 +2000,7 @@ function requestIdFromBody(text) {
|
|
|
2690
2000
|
} catch {
|
|
2691
2001
|
return;
|
|
2692
2002
|
}
|
|
2693
|
-
if (!isRecord$
|
|
2003
|
+
if (!isRecord$1(parsed)) return;
|
|
2694
2004
|
return asString(parsed.request_id) ?? asString(parsed.requestId) ?? asString(parsed.trace_id);
|
|
2695
2005
|
}
|
|
2696
2006
|
//#endregion
|
|
@@ -2722,11 +2032,9 @@ const OPTION_CAPABILITIES = {
|
|
|
2722
2032
|
enableGoogleSearch: (config) => config.supportsGoogleSearch === true,
|
|
2723
2033
|
enableSafetyChecker: (config) => config.supportsSafetyChecker === true,
|
|
2724
2034
|
enableWebSearch: (config) => config.supportsWebSearch === true,
|
|
2725
|
-
enhancePrompt: (config) => config.supportsEnhancePrompt === true,
|
|
2726
2035
|
expandPrompt: (config) => config.supportsExpandPrompt === true,
|
|
2727
2036
|
guidanceScale: (config) => config.supportsGuidanceScale === true,
|
|
2728
2037
|
"image editing": (config) => config.supportsEdit,
|
|
2729
|
-
imagePromptStrength: (config) => config.supportsImagePromptStrength === true,
|
|
2730
2038
|
imageSize: (config) => config.sizeMode === "gpt_size" || config.sizeMode === "image_size_enum",
|
|
2731
2039
|
inputFidelity: (config) => config.sizeMode === "gpt_size",
|
|
2732
2040
|
limitGenerations: (config) => config.supportsLimitGenerations === true,
|
|
@@ -2737,12 +2045,10 @@ const OPTION_CAPABILITIES = {
|
|
|
2737
2045
|
numInferenceSteps: (config) => config.supportsInferenceSteps === true,
|
|
2738
2046
|
outputFormat: (config) => config.supportsOutputFormat === true,
|
|
2739
2047
|
quality: (config) => config.supportsQuality === true,
|
|
2740
|
-
raw: (config) => config.supportsRaw === true,
|
|
2741
2048
|
renderingSpeed: (config) => config.supportsRenderingSpeed === true,
|
|
2742
2049
|
resolution: (config) => config.supportsResolution,
|
|
2743
2050
|
safetyTolerance: (config) => config.supportsSafetyTolerance === true,
|
|
2744
2051
|
seed: (config) => config.supportsSeed === true,
|
|
2745
|
-
style: (config) => config.supportsStyle === true,
|
|
2746
2052
|
syncMode: (config) => config.supportsSyncMode === true,
|
|
2747
2053
|
thinkingLevel: (config) => config.supportsThinkingLevel === true,
|
|
2748
2054
|
"transparent output": (config) => config.supportsBackground === true || config.transparentOutput === true
|
|
@@ -2770,7 +2076,7 @@ function modelsSupporting(option) {
|
|
|
2770
2076
|
*/
|
|
2771
2077
|
var UnsupportedOptionError = class extends Error {
|
|
2772
2078
|
code = "INVALID_OPTION";
|
|
2773
|
-
/** Display name of the model that refused, e.g. `"Seedream
|
|
2079
|
+
/** Display name of the model that refused, e.g. `"Seedream 5 Pro"`. */
|
|
2774
2080
|
model;
|
|
2775
2081
|
/** Generation model ids that accept `option`. */
|
|
2776
2082
|
modelsSupporting;
|
|
@@ -2780,8 +2086,7 @@ var UnsupportedOptionError = class extends Error {
|
|
|
2780
2086
|
constructor(config, option) {
|
|
2781
2087
|
const models = modelsSupporting(option);
|
|
2782
2088
|
const supported = supportedOptions(config);
|
|
2783
|
-
|
|
2784
|
-
super(`${config.name} does not support ${option}. Models that do: ${models.length > 0 ? models.join(", ") : "none"}. ${config.name} supports: ${supported.length > 0 ? supported.join(", ") : "none of the optional settings"}.${route}`);
|
|
2089
|
+
super(`${config.name} does not support ${option}. Models that do: ${models.length > 0 ? models.join(", ") : "none"}. ${config.name} supports: ${supported.length > 0 ? supported.join(", ") : "none of the optional settings"}.`);
|
|
2785
2090
|
this.name = "UnsupportedOptionError";
|
|
2786
2091
|
this.model = config.name;
|
|
2787
2092
|
this.modelsSupporting = models;
|
|
@@ -2860,8 +2165,6 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
|
|
|
2860
2165
|
if (config.sizeMode !== "gpt_size") unsupported(config, "inputFidelity");
|
|
2861
2166
|
}
|
|
2862
2167
|
if (options.imageSize !== void 0 && sizeMode !== "gpt_size" && sizeMode !== "image_size_enum") unsupported(config, "imageSize");
|
|
2863
|
-
if (options.imagePromptStrength !== void 0 && config.supportsImagePromptStrength !== true) unsupported(config, "imagePromptStrength");
|
|
2864
|
-
if (options.imagePromptStrength !== void 0 && !hasEditImages) throw new Error("imagePromptStrength requires editImageUrls");
|
|
2865
2168
|
if (options.maskImageUrl !== void 0 && config.supportsMaskImage !== true) unsupported(config, "maskImageUrl");
|
|
2866
2169
|
if (options.maskImageUrl !== void 0 && !hasEditImages) throw new Error("maskImageUrl requires editImageUrls");
|
|
2867
2170
|
if (options.markupImageUrl !== void 0 && config.supportsMarkupImage !== true) unsupported(config, "markupImageUrl");
|
|
@@ -2880,13 +2183,10 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
|
|
|
2880
2183
|
if (!qualities.includes(options.quality)) throw new Error(`${config.name} quality must be one of ${qualities.join(", ")}`);
|
|
2881
2184
|
}
|
|
2882
2185
|
if (options.negativePrompt !== void 0 && config.supportsNegativePrompt !== true) unsupported(config, "negativePrompt");
|
|
2883
|
-
if (options.style !== void 0 && config.supportsStyle !== true) unsupported(config, "style");
|
|
2884
2186
|
if (options.syncMode !== void 0 && config.supportsSyncMode !== true) unsupported(config, "syncMode");
|
|
2885
2187
|
if (options.renderingSpeed !== void 0 && config.supportsRenderingSpeed !== true) unsupported(config, "renderingSpeed");
|
|
2886
2188
|
if (options.guidanceScale !== void 0 && config.supportsGuidanceScale !== true) unsupported(config, "guidanceScale");
|
|
2887
2189
|
if (options.numInferenceSteps !== void 0 && config.supportsInferenceSteps !== true) unsupported(config, "numInferenceSteps");
|
|
2888
|
-
if (options.raw !== void 0 && config.supportsRaw !== true) unsupported(config, "raw");
|
|
2889
|
-
if (options.enhancePrompt !== void 0 && config.supportsEnhancePrompt !== true) unsupported(config, "enhancePrompt");
|
|
2890
2190
|
if (options.safetyTolerance !== void 0 && config.supportsSafetyTolerance !== true) unsupported(config, "safetyTolerance");
|
|
2891
2191
|
if (options.enableWebSearch !== void 0 && config.supportsWebSearch !== true) unsupported(config, "enableWebSearch");
|
|
2892
2192
|
if (options.enableGoogleSearch !== void 0 && config.supportsGoogleSearch !== true) unsupported(config, "enableGoogleSearch");
|
|
@@ -2902,11 +2202,11 @@ function validateGenerateOptions(options, config, sizeMode, hasEditImages) {
|
|
|
2902
2202
|
* Models use different sizing APIs:
|
|
2903
2203
|
* - `gpt_size`: GPT's fixed dimensions (1024x1024, 1536x1024, etc.)
|
|
2904
2204
|
* - `aspect_ratio`: string like "16:9" (Gemini, Grok, FLUX Pro Ultra)
|
|
2905
|
-
* - `image_size_enum`: named presets like "landscape_4_3" (FLUX.2, Seedream, Recraft, Ideogram, GPT Image
|
|
2205
|
+
* - `image_size_enum`: named presets like "landscape_4_3" (FLUX.2, Seedream, Recraft, Ideogram, GPT Image)
|
|
2906
2206
|
* - `none`: no size control (video models)
|
|
2907
2207
|
*/
|
|
2908
2208
|
function buildGenerateBody(options) {
|
|
2909
|
-
const { prompt, model, creative, aspect = "1:1", resolution = "2K", numImages = 1, background, editImageUrls, enableGoogleSearch, enableSafetyChecker, transparent, inputFidelity, imageSize,
|
|
2209
|
+
const { prompt, model, creative, aspect = "1:1", resolution = "2K", numImages = 1, background, editImageUrls, enableGoogleSearch, enableSafetyChecker, transparent, inputFidelity, imageSize, limitGenerations, maskImageUrl, markupImageUrl, seed, outputFormat, quality, negativePrompt, syncMode, thinkingLevel, renderingSpeed, guidanceScale, numInferenceSteps, safetyTolerance, enableWebSearch, expandPrompt } = options;
|
|
2910
2210
|
const config = MODELS[model];
|
|
2911
2211
|
if (!config) throw new Error(`Unknown model: ${model}`);
|
|
2912
2212
|
let { endpoint } = config;
|
|
@@ -2942,12 +2242,6 @@ function buildGenerateBody(options) {
|
|
|
2942
2242
|
if (!formats.includes(outputFormat)) throw new Error(`${config.name} ${hasEditImages ? "edits support" : "supports"} output formats: ${formats.join(", ")}`);
|
|
2943
2243
|
body.output_format = outputFormat;
|
|
2944
2244
|
}
|
|
2945
|
-
if (config.supportsRaw === true && raw !== void 0) body.raw = raw;
|
|
2946
|
-
if (config.supportsEnhancePrompt === true && enhancePrompt !== void 0) body.enhance_prompt = enhancePrompt;
|
|
2947
|
-
if (config.supportsImagePromptStrength === true && imagePromptStrength !== void 0) body.image_prompt_strength = imagePromptStrength;
|
|
2948
|
-
if (model === "flux") {
|
|
2949
|
-
if (safetyTolerance !== void 0 && safetyTolerance !== "") body.safety_tolerance = safetyTolerance;
|
|
2950
|
-
}
|
|
2951
2245
|
if (config.supportsGuidanceScale === true && guidanceScale !== void 0) body.guidance_scale = guidanceScale;
|
|
2952
2246
|
if (config.supportsInferenceSteps === true && numInferenceSteps !== void 0) body.num_inference_steps = numInferenceSteps;
|
|
2953
2247
|
if (config.supportsSafetyTolerance === true) {
|
|
@@ -2968,25 +2262,17 @@ function buildGenerateBody(options) {
|
|
|
2968
2262
|
if (config.supportsThinkingLevel === true) {
|
|
2969
2263
|
if (thinkingLevel) body.thinking_level = thinkingLevel;
|
|
2970
2264
|
}
|
|
2971
|
-
if (config.supportsStyle === true && style !== void 0 && style !== "") body.style = style;
|
|
2972
2265
|
if (config.supportsNegativePrompt === true) {
|
|
2973
2266
|
if (negativePrompt !== void 0 && negativePrompt !== "") body.negative_prompt = negativePrompt;
|
|
2974
2267
|
}
|
|
2975
|
-
if (model === "ideogram") {
|
|
2976
|
-
if (style !== void 0 && style !== "") body.style = style;
|
|
2977
|
-
}
|
|
2978
2268
|
if (config.supportsRenderingSpeed === true) {
|
|
2979
2269
|
if (renderingSpeed) body.rendering_speed = renderingSpeed;
|
|
2980
2270
|
}
|
|
2981
2271
|
if (config.supportsExpandPrompt === true) {
|
|
2982
2272
|
if (expandPrompt !== void 0) body.expand_prompt = expandPrompt;
|
|
2983
2273
|
}
|
|
2984
|
-
if (model === "flux" && hasEditImages) {
|
|
2985
|
-
body.image_url = editImages[0];
|
|
2986
|
-
body.image_prompt_strength = imagePromptStrength ?? .5;
|
|
2987
|
-
}
|
|
2988
2274
|
if (config.supportsNumImages) body.num_images = numImages;
|
|
2989
|
-
if (hasEditImages && sizeMode !== "gpt_size"
|
|
2275
|
+
if (hasEditImages && sizeMode !== "gpt_size") {
|
|
2990
2276
|
if (!config.supportsEdit) throw new Error(`Model ${model} does not support image editing`);
|
|
2991
2277
|
endpoint = config.editEndpoint ?? `${endpoint}/edit`;
|
|
2992
2278
|
const editImagesField = config.editImagesField ?? "image_urls";
|
|
@@ -4556,7 +3842,7 @@ async function runRequest(exec, prepared) {
|
|
|
4556
3842
|
const response = await exec.request(`${FAL_BASE_URL$1}/${prepared.endpoint}`, requestInit(prepared));
|
|
4557
3843
|
if (response.isErr()) return err$1(response.error);
|
|
4558
3844
|
const data = await response.value.json();
|
|
4559
|
-
const record = isRecord$
|
|
3845
|
+
const record = isRecord$1(data) ? data : {};
|
|
4560
3846
|
const requestId = response.value.headers.get("x-fal-request-id") ?? asString(record.request_id);
|
|
4561
3847
|
return ok$1({
|
|
4562
3848
|
data: record,
|
|
@@ -4583,7 +3869,7 @@ async function getToolResult(exec, job) {
|
|
|
4583
3869
|
const response = await exec.request(url);
|
|
4584
3870
|
if (response.isErr()) return err$1(response.error);
|
|
4585
3871
|
const data = await response.value.json();
|
|
4586
|
-
return ok$1(isRecord$
|
|
3872
|
+
return ok$1(isRecord$1(data) ? data : {});
|
|
4587
3873
|
}
|
|
4588
3874
|
/** Poll a submitted job to completion and fetch its result. Submits nothing. */
|
|
4589
3875
|
async function awaitQueuedRequest(exec, job, onProgress) {
|
|
@@ -4692,7 +3978,7 @@ var FalClient = class {
|
|
|
4692
3978
|
try {
|
|
4693
3979
|
built = buildGenerateBody(options);
|
|
4694
3980
|
} catch (error) {
|
|
4695
|
-
return err$1(toMotifError
|
|
3981
|
+
return err$1(toMotifError(error));
|
|
4696
3982
|
}
|
|
4697
3983
|
const { endpoint, body } = built;
|
|
4698
3984
|
const response = await this.request(`${FAL_BASE_URL}/${endpoint}`, {
|
|
@@ -4730,7 +4016,7 @@ var FalClient = class {
|
|
|
4730
4016
|
try {
|
|
4731
4017
|
built = buildGenerateBody(options);
|
|
4732
4018
|
} catch (error) {
|
|
4733
|
-
return err$1(toMotifError
|
|
4019
|
+
return err$1(toMotifError(error));
|
|
4734
4020
|
}
|
|
4735
4021
|
const { endpoint, body } = built;
|
|
4736
4022
|
const response = await this.request(`${FAL_QUEUE_URL}/${endpoint}`, {
|
|
@@ -4752,7 +4038,7 @@ var FalClient = class {
|
|
|
4752
4038
|
const response = await this.request(url);
|
|
4753
4039
|
if (response.isErr()) return err$1(response.error);
|
|
4754
4040
|
const data = await response.value.json();
|
|
4755
|
-
const record = isRecord$
|
|
4041
|
+
const record = isRecord$1(data) ? data : {};
|
|
4756
4042
|
const rawStatus = asString(record.status);
|
|
4757
4043
|
let status;
|
|
4758
4044
|
if (rawStatus === "IN_QUEUE") status = "queued";
|
|
@@ -4775,100 +4061,6 @@ var FalClient = class {
|
|
|
4775
4061
|
const data = await response.value.json();
|
|
4776
4062
|
return this.normalizeResponse(data, requestId);
|
|
4777
4063
|
}
|
|
4778
|
-
/** ─── Processing ──────────────────────────────────────────── */
|
|
4779
|
-
/** Upscale an image using clarity or crystal upscaler. */
|
|
4780
|
-
async upscale(options) {
|
|
4781
|
-
const { imageUrl, model = "clarity", scaleFactor, creativity, resemblance, prompt: upscalePrompt, negativePrompt, numInferenceSteps, guidanceScale } = options;
|
|
4782
|
-
const config = MODELS[model];
|
|
4783
|
-
if (!config || config.type !== "utility") return err$1(new MotifError(`Invalid upscale model: ${model}`, 0));
|
|
4784
|
-
const body = { image_url: imageUrl };
|
|
4785
|
-
if (model === "crystal") {
|
|
4786
|
-
if (scaleFactor !== void 0) body.scale_factor = scaleFactor;
|
|
4787
|
-
if (creativity !== void 0) body.creativity = creativity;
|
|
4788
|
-
} else {
|
|
4789
|
-
if (scaleFactor !== void 0) body.upscale_factor = scaleFactor;
|
|
4790
|
-
if (creativity !== void 0) body.creativity = creativity;
|
|
4791
|
-
if (resemblance !== void 0) body.resemblance = resemblance;
|
|
4792
|
-
if (upscalePrompt !== void 0 && upscalePrompt !== "") body.prompt = upscalePrompt;
|
|
4793
|
-
if (negativePrompt !== void 0 && negativePrompt !== "") body.negative_prompt = negativePrompt;
|
|
4794
|
-
if (numInferenceSteps !== void 0) body.num_inference_steps = numInferenceSteps;
|
|
4795
|
-
if (guidanceScale !== void 0) body.guidance_scale = guidanceScale;
|
|
4796
|
-
}
|
|
4797
|
-
const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
|
|
4798
|
-
body: JSON.stringify(body),
|
|
4799
|
-
method: "POST"
|
|
4800
|
-
});
|
|
4801
|
-
if (response.isErr()) return err$1(response.error);
|
|
4802
|
-
const data = await response.value.json();
|
|
4803
|
-
return this.normalizeResponse(data);
|
|
4804
|
-
}
|
|
4805
|
-
/** Remove the background from an image. */
|
|
4806
|
-
async removeBackground(options) {
|
|
4807
|
-
const { imageUrl, model = "rmbg", variant, operatingResolution, outputFormat, refineForeground, outputMask } = options;
|
|
4808
|
-
const config = MODELS[model];
|
|
4809
|
-
if (!config) return err$1(new MotifError(`Invalid background removal model: ${model}`, 0));
|
|
4810
|
-
const rbBody = { image_url: imageUrl };
|
|
4811
|
-
if (model === "rmbg") {
|
|
4812
|
-
if (variant) rbBody.model = variant;
|
|
4813
|
-
if (operatingResolution) rbBody.operating_resolution = operatingResolution;
|
|
4814
|
-
if (outputFormat) rbBody.output_format = outputFormat;
|
|
4815
|
-
if (refineForeground !== void 0) rbBody.refine_foreground = refineForeground;
|
|
4816
|
-
if (outputMask !== void 0) rbBody.output_mask = outputMask;
|
|
4817
|
-
}
|
|
4818
|
-
const response = await this.request(`${FAL_BASE_URL}/${config.endpoint}`, {
|
|
4819
|
-
body: JSON.stringify(rbBody),
|
|
4820
|
-
method: "POST"
|
|
4821
|
-
});
|
|
4822
|
-
if (response.isErr()) return err$1(response.error);
|
|
4823
|
-
const data = await response.value.json();
|
|
4824
|
-
return this.normalizeResponse(data);
|
|
4825
|
-
}
|
|
4826
|
-
/** ─── Video Generation ───────────────────────────────────── */
|
|
4827
|
-
/**
|
|
4828
|
-
* Generate a video from an image using Kling v3 Pro.
|
|
4829
|
-
* This uses the queue API since video generation takes 30-120s.
|
|
4830
|
-
* Returns immediately with a job — poll with getJobStatus/getVideoResult.
|
|
4831
|
-
*/
|
|
4832
|
-
async submitVideo(options) {
|
|
4833
|
-
const { imageUrl, prompt, duration = 5, generateAudio = true, endImageUrl, negativePrompt, cfgScale } = options;
|
|
4834
|
-
const config = MODELS.kling;
|
|
4835
|
-
if (!config) return err$1(new MotifError("Kling video model not found", 0));
|
|
4836
|
-
const body = {
|
|
4837
|
-
duration: String(duration),
|
|
4838
|
-
generate_audio: generateAudio,
|
|
4839
|
-
prompt,
|
|
4840
|
-
start_image_url: imageUrl
|
|
4841
|
-
};
|
|
4842
|
-
if (endImageUrl !== void 0 && endImageUrl !== "") body.end_image_url = endImageUrl;
|
|
4843
|
-
if (negativePrompt !== void 0 && negativePrompt !== "") body.negative_prompt = negativePrompt;
|
|
4844
|
-
if (cfgScale !== void 0) body.cfg_scale = cfgScale;
|
|
4845
|
-
const response = await this.request(`${FAL_QUEUE_URL}/${config.endpoint}`, {
|
|
4846
|
-
body: JSON.stringify(body),
|
|
4847
|
-
method: "POST"
|
|
4848
|
-
});
|
|
4849
|
-
if (response.isErr()) return err$1(response.error);
|
|
4850
|
-
const submission = parseQueueSubmission(await response.value.json());
|
|
4851
|
-
return ok$1({
|
|
4852
|
-
endpoint: endpointFromQueueUrl(submission.responseUrl, config.endpoint),
|
|
4853
|
-
estimatedCost: estimateVideoCost(duration, generateAudio),
|
|
4854
|
-
requestId: submission.requestId
|
|
4855
|
-
});
|
|
4856
|
-
}
|
|
4857
|
-
/** Fetch the completed video result from the queue. */
|
|
4858
|
-
async getVideoResult(endpoint, requestId) {
|
|
4859
|
-
const url = `${FAL_QUEUE_URL}/${endpoint}/requests/${requestId}`;
|
|
4860
|
-
const response = await this.request(url);
|
|
4861
|
-
if (response.isErr()) return err$1(response.error);
|
|
4862
|
-
const data = await response.value.json();
|
|
4863
|
-
const video = isRecord$4(data) && isRecord$4(data.video) ? data.video : void 0;
|
|
4864
|
-
if (video === void 0) return err$1(new MotifError("No video in response", 0));
|
|
4865
|
-
return ok$1({
|
|
4866
|
-
contentType: asString(video.content_type) ?? "",
|
|
4867
|
-
fileName: asString(video.file_name) ?? "",
|
|
4868
|
-
fileSize: asNumber(video.file_size) ?? 0,
|
|
4869
|
-
url: asString(video.url) ?? ""
|
|
4870
|
-
});
|
|
4871
|
-
}
|
|
4872
4064
|
/** ─── File Upload ─────────────────────────────────────────── */
|
|
4873
4065
|
/**
|
|
4874
4066
|
* Upload a file to fal.ai CDN storage and return the public URL.
|
|
@@ -4884,8 +4076,8 @@ var FalClient = class {
|
|
|
4884
4076
|
});
|
|
4885
4077
|
if (initiateResponse.isErr()) return err$1(initiateResponse.error);
|
|
4886
4078
|
const initiateData = await initiateResponse.value.json();
|
|
4887
|
-
const fileUrl = isRecord$
|
|
4888
|
-
const uploadUrl = isRecord$
|
|
4079
|
+
const fileUrl = isRecord$1(initiateData) ? asString(initiateData.file_url) ?? "" : "";
|
|
4080
|
+
const uploadUrl = isRecord$1(initiateData) ? asString(initiateData.upload_url) : void 0;
|
|
4889
4081
|
if (uploadUrl === void 0 || uploadUrl === "") return err$1(new MotifError("Upload initiate response missing upload_url", 0));
|
|
4890
4082
|
let putResponse;
|
|
4891
4083
|
try {
|
|
@@ -4964,10 +4156,6 @@ var FalClient = class {
|
|
|
4964
4156
|
get generationModels() {
|
|
4965
4157
|
return GENERATION_MODELS;
|
|
4966
4158
|
}
|
|
4967
|
-
/** Utility model keys. */
|
|
4968
|
-
get utilityModels() {
|
|
4969
|
-
return UTILITY_MODELS;
|
|
4970
|
-
}
|
|
4971
4159
|
/** Registered fal utility/tool endpoints. */
|
|
4972
4160
|
get tools() {
|
|
4973
4161
|
return FAL_TOOLS;
|
|
@@ -5037,7 +4225,7 @@ var FalClient = class {
|
|
|
5037
4225
|
* Some APIs return `{ image: {...} }` instead of `{ images: [...] }`.
|
|
5038
4226
|
*/
|
|
5039
4227
|
normalizeResponse(data, fallbackRequestId) {
|
|
5040
|
-
if (!isRecord$
|
|
4228
|
+
if (!isRecord$1(data)) return err$1(new MotifError("Unexpected fal response shape", 0, "FAL_ERROR"));
|
|
5041
4229
|
const obj = data;
|
|
5042
4230
|
const requestId = asString(obj.request_id) ?? asString(obj.requestId) ?? fallbackRequestId;
|
|
5043
4231
|
if ("detail" in obj) return err$1(new MotifError(asString(obj.detail) ?? "", 0, "FAL_ERROR"));
|
|
@@ -5359,13 +4547,6 @@ const BOARDS = {
|
|
|
5359
4547
|
preliminary: true,
|
|
5360
4548
|
votes: 39218
|
|
5361
4549
|
},
|
|
5362
|
-
gpt2: {
|
|
5363
|
-
elo: 1381,
|
|
5364
|
-
rank: 3,
|
|
5365
|
-
uncertainty: 4,
|
|
5366
|
-
evaluatedModel: "GPT-Image-2 (Medium)",
|
|
5367
|
-
votes: 103385
|
|
5368
|
-
},
|
|
5369
4550
|
banana21: {
|
|
5370
4551
|
elo: 1325,
|
|
5371
4552
|
rank: 5,
|
|
@@ -5461,12 +4642,6 @@ const BOARDS = {
|
|
|
5461
4642
|
evaluatedModel: "GPT-Image-2.5 Flare",
|
|
5462
4643
|
preliminary: true
|
|
5463
4644
|
},
|
|
5464
|
-
gpt2: {
|
|
5465
|
-
elo: 1460,
|
|
5466
|
-
rank: 4,
|
|
5467
|
-
uncertainty: 3,
|
|
5468
|
-
evaluatedModel: "GPT-Image-2 (Medium)"
|
|
5469
|
-
},
|
|
5470
4645
|
"grok-image-2": {
|
|
5471
4646
|
elo: 1439,
|
|
5472
4647
|
rank: 5,
|
|
@@ -5577,10 +4752,6 @@ const GENERATION_MODELS_RANKED = withImageBenchmarks([
|
|
|
5577
4752
|
model: "flare",
|
|
5578
4753
|
tier: "quality"
|
|
5579
4754
|
},
|
|
5580
|
-
{
|
|
5581
|
-
model: "gpt2",
|
|
5582
|
-
tier: "quality"
|
|
5583
|
-
},
|
|
5584
4755
|
{
|
|
5585
4756
|
model: "seedream5",
|
|
5586
4757
|
tier: "quality"
|
|
@@ -5670,15 +4841,11 @@ const GENERATION_MODELS_RANKED = withImageBenchmarks([
|
|
|
5670
4841
|
], "text-to-image");
|
|
5671
4842
|
const EDITING_MODELS_RANKED = withImageBenchmarks([
|
|
5672
4843
|
{
|
|
5673
|
-
model: "sunburst",
|
|
5674
|
-
tier: "quality"
|
|
5675
|
-
},
|
|
5676
|
-
{
|
|
5677
|
-
model: "flare",
|
|
4844
|
+
model: "sunburst",
|
|
5678
4845
|
tier: "quality"
|
|
5679
4846
|
},
|
|
5680
4847
|
{
|
|
5681
|
-
model: "
|
|
4848
|
+
model: "flare",
|
|
5682
4849
|
tier: "quality"
|
|
5683
4850
|
},
|
|
5684
4851
|
{
|
|
@@ -5920,7 +5087,7 @@ const TASKS = {
|
|
|
5920
5087
|
summary: "Remove an object, person, text or clutter from an image and fill the gap."
|
|
5921
5088
|
},
|
|
5922
5089
|
generate: {
|
|
5923
|
-
basis: "Order follows Arena text-to-image (https://arena.ai/leaderboard/image/text-to-image), read 2026-10-29
|
|
5090
|
+
basis: "Order follows Arena text-to-image (https://arena.ai/leaderboard/image/text-to-image), read 2026-10-29. Sunburst and Flare lead quality at the benchmarked max setting. Balanced starts with Nano Banana 2.1 on preliminary Arena evidence, then Grok Image 2 and Muse Image. Models not yet on the board follow the ranked ones in their Tier, in their previous order. The Artificial Analysis v2 snapshot of 2026-10-07 stays as recorded benchmark data. Fast keeps Nano Banana 2 Lite, budget Grok and Seedream 5 Lite, and FLUX.2 Turbo. Design, transparency and tuned Look routes are capability exceptions. Generation with References uses the separate editing ranking. Older routes are explicit/pin/Look-only.",
|
|
5924
5091
|
models: GENERATION_MODELS_RANKED,
|
|
5925
5092
|
notFor: "Variations of an image you already have (vary), or a consistent set of images (series run).",
|
|
5926
5093
|
rankedAt: IMAGE_RANKED_AT,
|
|
@@ -6408,7 +5575,7 @@ const TASKS = {
|
|
|
6408
5575
|
summary: "Trace a raster image to a clean SVG."
|
|
6409
5576
|
},
|
|
6410
5577
|
vary: {
|
|
6411
|
-
basis: "Order follows Arena image edit, single image (https://arena.ai/leaderboard/image/image-edit/single-image), read 2026-10-29
|
|
5578
|
+
basis: "Order follows Arena image edit, single image (https://arena.ai/leaderboard/image/image-edit/single-image), read 2026-10-29. Editing order differs from generation: Grok Image 2 leads balanced, ahead of Nano Banana 2.1, and Seedream 5 Pro outranks GPT Image 1.5. Arena scores Krea 3 only at its Ultra setting, which costs $0.27 to $0.35 an edit and ignores the requested size, so krea3, which runs fal's default setting, is ranked as unscored. Models not yet on the board follow the ranked ones in their Tier, in their previous order. The Artificial Analysis v2 snapshot of 2026-10-07 stays as recorded benchmark data. Sunburst and Flare use max quality. vary reuses a previously chosen Model when explicitly supplied, including retained legacy routes.",
|
|
6412
5579
|
models: EDITING_MODELS_RANKED,
|
|
6413
5580
|
notFor: "A specific change to an image described in words (generate with a reference), or a set of different scenes in one style (series run).",
|
|
6414
5581
|
rankedAt: IMAGE_RANKED_AT,
|
|
@@ -8603,9 +7770,7 @@ const NO_MODEL_AVAILABLE = "NO_MODEL_AVAILABLE";
|
|
|
8603
7770
|
const FAL_KEY = "FAL_KEY";
|
|
8604
7771
|
function generationProfile(config) {
|
|
8605
7772
|
const capabilities = /* @__PURE__ */ new Set(["image"]);
|
|
8606
|
-
const keyed = /* @__PURE__ */ new Map();
|
|
8607
7773
|
if (config.supportsBackground === true || config.transparentOutput === true) capabilities.add("transparency");
|
|
8608
|
-
else if (config.transparencyRoute !== void 0) keyed.set("transparency", config.transparencyRoute.apiKeyEnv);
|
|
8609
7774
|
if (config.supportsEdit) capabilities.add("references");
|
|
8610
7775
|
if (config.supportsMaskImage === true) capabilities.add("mask");
|
|
8611
7776
|
if (config.supportsMarkupImage === true) capabilities.add("markup");
|
|
@@ -8618,7 +7783,6 @@ function generationProfile(config) {
|
|
|
8618
7783
|
return {
|
|
8619
7784
|
aspects: config.supportedAspects,
|
|
8620
7785
|
capabilities,
|
|
8621
|
-
keyedCapabilities: keyed,
|
|
8622
7786
|
keys: [FAL_KEY],
|
|
8623
7787
|
maxReferences: config.supportsEdit ? config.maxReferenceImages ?? 1 : 0,
|
|
8624
7788
|
outputFormats: {
|
|
@@ -8651,7 +7815,6 @@ function toolProfile(tool) {
|
|
|
8651
7815
|
if (takesReference) capabilities.add("references");
|
|
8652
7816
|
return {
|
|
8653
7817
|
capabilities,
|
|
8654
|
-
keyedCapabilities: /* @__PURE__ */ new Map(),
|
|
8655
7818
|
keys: [FAL_KEY],
|
|
8656
7819
|
maxReferences: takesReference ? 1 : 0
|
|
8657
7820
|
};
|
|
@@ -8707,25 +7870,10 @@ function check(profile, needed, request, keys, requires = []) {
|
|
|
8707
7870
|
missingInput: true,
|
|
8708
7871
|
ok: false
|
|
8709
7872
|
};
|
|
8710
|
-
for (const capability of needed) {
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
ok: false
|
|
8715
|
-
};
|
|
8716
|
-
continue;
|
|
8717
|
-
}
|
|
8718
|
-
const key = profile.keyedCapabilities.get(capability);
|
|
8719
|
-
if (key === void 0) return {
|
|
8720
|
-
blockedBy: capability,
|
|
8721
|
-
ok: false
|
|
8722
|
-
};
|
|
8723
|
-
if (!keys.includes(key)) return {
|
|
8724
|
-
blockedBy: "key",
|
|
8725
|
-
missingKey: key,
|
|
8726
|
-
ok: false
|
|
8727
|
-
};
|
|
8728
|
-
}
|
|
7873
|
+
for (const capability of needed) if (!profile.capabilities.has(capability) || !withinLimits(profile, capability, request)) return {
|
|
7874
|
+
blockedBy: capability,
|
|
7875
|
+
ok: false
|
|
7876
|
+
};
|
|
8729
7877
|
return { ok: true };
|
|
8730
7878
|
}
|
|
8731
7879
|
const TIER_ORDER = {
|
|
@@ -8807,7 +7955,7 @@ function resolveFixed(task, fixed, request, needed, keys) {
|
|
|
8807
7955
|
const { chosenBy, model } = fixed;
|
|
8808
7956
|
const tier = request.tier ?? "balanced";
|
|
8809
7957
|
const entries = TASKS[task].models;
|
|
8810
|
-
const entry = entries.find((candidate) => candidate.model === model && candidate.mode === request.mode)
|
|
7958
|
+
const entry = entries.find((candidate) => candidate.model === model && candidate.mode === request.mode);
|
|
8811
7959
|
const profile = modelProfile(model);
|
|
8812
7960
|
if (entry === void 0 || profile === void 0) {
|
|
8813
7961
|
const servesTask = entries.some((candidate) => candidate.model === model);
|
|
@@ -8820,14 +7968,6 @@ function resolveFixed(task, fixed, request, needed, keys) {
|
|
|
8820
7968
|
unblockers.push(unblockerFor(result));
|
|
8821
7969
|
return unresolved(task, result.blockedBy, unblockers, `${model} (${chosenBy}) ${describeBlock(result.blockedBy, request, result.missingKey, result.missingInput)}.`, result.missingKey);
|
|
8822
7970
|
}
|
|
8823
|
-
/** Old pins and tuned Looks remain usable without recommending predecessors. */
|
|
8824
|
-
function legacyEntry(task, model, mode, tier) {
|
|
8825
|
-
if (mode !== void 0 || task !== "generate" && task !== "vary" || !LEGACY_GENERATION_MODELS.some((id) => id === model) || task === "vary" && MODELS[model]?.supportsEdit !== true) return;
|
|
8826
|
-
return {
|
|
8827
|
-
model,
|
|
8828
|
-
tier
|
|
8829
|
-
};
|
|
8830
|
-
}
|
|
8831
7971
|
function unknownForMode(task, model, mode, entries) {
|
|
8832
7972
|
const otherModes = entries.filter((candidate) => candidate.model === model).map((candidate) => candidate.mode ?? "no mode");
|
|
8833
7973
|
const inMode = [...new Set(entries.filter((candidate) => candidate.mode === mode).map((candidate) => candidate.model))];
|
|
@@ -8915,730 +8055,6 @@ function tierChangesChoice(task, request, environment) {
|
|
|
8915
8055
|
return chosen.size > 1;
|
|
8916
8056
|
}
|
|
8917
8057
|
//#endregion
|
|
8918
|
-
//#region src/image/fetch.ts
|
|
8919
|
-
function urlOf$1(input) {
|
|
8920
|
-
if (typeof input === "string") return input;
|
|
8921
|
-
return input instanceof URL ? input.href : input.url;
|
|
8922
|
-
}
|
|
8923
|
-
/** Adapt a configured fetch for a provider factory; undefined keeps global fetch. */
|
|
8924
|
-
function toProviderFetch(fetch) {
|
|
8925
|
-
if (fetch === void 0) return {};
|
|
8926
|
-
return { fetch: async (input, init) => await fetch(urlOf$1(input), init ?? {}) };
|
|
8927
|
-
}
|
|
8928
|
-
//#endregion
|
|
8929
|
-
//#region src/image/fal.ts
|
|
8930
|
-
/**
|
|
8931
|
-
* fal provider adapter.
|
|
8932
|
-
*
|
|
8933
|
-
* Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/fal`. Building a model
|
|
8934
|
-
* performs no network I/O — the request only happens when `generateImage`
|
|
8935
|
-
* invokes `model.doGenerate`. This is the lightweight fal *image* adapter; the
|
|
8936
|
-
* richer `FalClient` fal client (queue, upload, upscale, rmbg, video, tools)
|
|
8937
|
-
* is a separate, later fold (design doc §8, phase 1d).
|
|
8938
|
-
*
|
|
8939
|
-
* ENDPOINT QUIRK (Phase 0): fal's gpt-image endpoint wants `image_size` as a
|
|
8940
|
-
* STRING enum (e.g. "1024x1024"), passed at generate time via
|
|
8941
|
-
* `providerOptions.fal.image_size` — NOT the AI SDK's generic `size` object.
|
|
8942
|
-
* This adapter does NOT auto-inject it; callers pass `providerOptions` when they
|
|
8943
|
-
* need a specific size. Example:
|
|
8944
|
-
* img.generate({
|
|
8945
|
-
* provider: "fal",
|
|
8946
|
-
* model: "fal-ai/gpt-image-1.5",
|
|
8947
|
-
* prompt: "...",
|
|
8948
|
-
* providerOptions: { fal: { image_size: "1024x1024" } },
|
|
8949
|
-
* });
|
|
8950
|
-
*/
|
|
8951
|
-
/**
|
|
8952
|
-
* fal endpoint ids proven to work with this adapter and priced in
|
|
8953
|
-
* {@link FAL_IMAGE_PRICE_USD} below (FLUX Pro Ultra + gpt-image).
|
|
8954
|
-
*/
|
|
8955
|
-
const FAL_FLUX_MODEL = "fal-ai/flux-pro/v1.1-ultra";
|
|
8956
|
-
const FAL_GPT_IMAGE_MODEL = "fal-ai/gpt-image-1.5";
|
|
8957
|
-
/** Env var read for the fal key when `apiKey` is not supplied in config. */
|
|
8958
|
-
const FAL_API_KEY_ENV = "FAL_KEY";
|
|
8959
|
-
/**
|
|
8960
|
-
* Static fal USD/image, keyed by the fal ENDPOINT id. Cost tracking must price
|
|
8961
|
-
* the endpoints consumers actually pass — kiln uses
|
|
8962
|
-
* `fal-ai/flux/schnell`, `fal-ai/flux-2-pro`, `fal-ai/flux-2-max`. Every price is
|
|
8963
|
-
* sourced from the fal registry snapshot in `../models`
|
|
8964
|
-
* (`MODELS[...].pricePerImageUsd`) so this table stays in sync rather than
|
|
8965
|
-
* drifting to a second hand-maintained copy; the `??` fallbacks are the registry
|
|
8966
|
-
* values at time of writing, used only if a registry entry loses its optional
|
|
8967
|
-
* price. Each key is `MODELS[x].endpoint`; the trailing comment cites the
|
|
8968
|
-
* `MODELS` key it comes from. Covers the flux/flux2 family, banana, seedream, and
|
|
8969
|
-
* the other priced fal generation endpoints.
|
|
8970
|
-
*/
|
|
8971
|
-
const FAL_IMAGE_PRICE_USD = {
|
|
8972
|
-
[FAL_FLUX_MODEL]: MODELS.flux?.pricePerImageUsd ?? .06,
|
|
8973
|
-
"fal-ai/flux/schnell": MODELS["flux-fast"]?.pricePerImageUsd ?? .003,
|
|
8974
|
-
"fal-ai/flux-2-max": MODELS["flux2-max"]?.pricePerImageUsd ?? .07,
|
|
8975
|
-
"fal-ai/flux-2-pro": MODELS["flux2-pro"]?.pricePerImageUsd ?? .03,
|
|
8976
|
-
"fal-ai/flux-2-flex": MODELS["flux2-flex"]?.pricePerImageUsd ?? .05,
|
|
8977
|
-
"fal-ai/flux-2": MODELS["flux2-dev"]?.pricePerImageUsd ?? .012,
|
|
8978
|
-
"fal-ai/flux-2/turbo": MODELS["flux2-turbo"]?.pricePerImageUsd ?? .008,
|
|
8979
|
-
[FAL_GPT_IMAGE_MODEL]: MODELS.gpt?.pricePerImageUsd ?? .133,
|
|
8980
|
-
"openai/gpt-image-2": MODELS.gpt2?.pricePerImageUsd ?? .211,
|
|
8981
|
-
"fal-ai/nano-banana-2": MODELS.banana2?.pricePerImageUsd ?? .08,
|
|
8982
|
-
"fal-ai/nano-banana-pro": MODELS.banana?.pricePerImageUsd ?? .15,
|
|
8983
|
-
"fal-ai/gemini-25-flash-image": MODELS.gemini?.pricePerImageUsd ?? .0398,
|
|
8984
|
-
"fal-ai/gemini-3-pro-image-preview": MODELS.gemini3?.pricePerImageUsd ?? .15,
|
|
8985
|
-
"fal-ai/bytedance/seedream/v4/text-to-image": MODELS.seedream4?.pricePerImageUsd ?? .03,
|
|
8986
|
-
"fal-ai/bytedance/seedream/v4.5/text-to-image": MODELS.seedream45?.pricePerImageUsd ?? .04,
|
|
8987
|
-
"bytedance/seedream/v5/pro/text-to-image": MODELS.seedream5?.pricePerImageUsd ?? .0675,
|
|
8988
|
-
"bytedance/seedream/v5/lite/text-to-image": MODELS["seedream5-lite"]?.pricePerImageUsd ?? .035,
|
|
8989
|
-
"fal-ai/recraft/v3/text-to-image": MODELS.recraft?.pricePerImageUsd ?? .04,
|
|
8990
|
-
"fal-ai/recraft/v4/text-to-image": MODELS.recraft4?.pricePerImageUsd ?? .04,
|
|
8991
|
-
"fal-ai/recraft/v4.1/text-to-image": MODELS.recraft41?.pricePerImageUsd ?? .035,
|
|
8992
|
-
"fal-ai/ideogram/v3": MODELS.ideogram?.pricePerImageUsd ?? .03,
|
|
8993
|
-
"ideogram/v4": MODELS.ideogram4?.pricePerImageUsd ?? .03,
|
|
8994
|
-
"xai/grok-imagine-image": MODELS["grok-image"]?.pricePerImageUsd ?? .02,
|
|
8995
|
-
"xai/grok-imagine-image/v2.0/text-to-image": MODELS["grok-image-2"]?.pricePerImageUsd ?? .06,
|
|
8996
|
-
"xai/grok-imagine-image/quality/text-to-image": MODELS["grok-image-quality"]?.pricePerImageUsd ?? .05,
|
|
8997
|
-
"meta/muse-image/text-to-image": MODELS["muse-image"]?.pricePerImageUsd ?? .01,
|
|
8998
|
-
"microsoft/mai-image-2.5-pro": MODELS["mai-image-2.5-pro"]?.pricePerImageUsd ?? .17,
|
|
8999
|
-
"fal-ai/ideogram/v3/generate-transparent": MODELS["ideogram3-transparent"]?.pricePerImageUsd ?? .06,
|
|
9000
|
-
"alibaba/qwen-image-3/text-to-image": MODELS.qwen3?.pricePerImageUsd ?? .04,
|
|
9001
|
-
"fal-ai/qwen-image": MODELS.qwen?.pricePerImageUsd ?? .02
|
|
9002
|
-
};
|
|
9003
|
-
/**
|
|
9004
|
-
* Build a fal `ImageModel`. Prefers the passed `apiKey`, else the `FAL_KEY` env
|
|
9005
|
-
* var. Throws `MotifError` when neither is present (callers translate this into
|
|
9006
|
-
* a `Result.err`).
|
|
9007
|
-
*/
|
|
9008
|
-
function resolveModel$3(modelId, apiKey, fetch) {
|
|
9009
|
-
const key = apiKey ?? process.env["FAL_KEY"];
|
|
9010
|
-
if (key === void 0 || key === "") throw new MotifError(`fal image generation requires an API key (config.fal.apiKey or ${FAL_API_KEY_ENV})`, 0);
|
|
9011
|
-
return createFal({
|
|
9012
|
-
apiKey: key,
|
|
9013
|
-
...toProviderFetch(fetch)
|
|
9014
|
-
}).image(modelId);
|
|
9015
|
-
}
|
|
9016
|
-
/**
|
|
9017
|
-
* How a registered fal edit endpoint takes its input images, from the
|
|
9018
|
-
* registry's `editImagesField` (default `image_urls`). `@ai-sdk/fal` sends only
|
|
9019
|
-
* the first image as `image_url` unless told otherwise, which list-only
|
|
9020
|
-
* endpoints reject and which silently drops every reference after the first.
|
|
9021
|
-
* Undefined for an endpoint the registry does not list as an edit route.
|
|
9022
|
-
*/
|
|
9023
|
-
function falEditImagesField(modelId) {
|
|
9024
|
-
const config = Object.values(MODELS).find((entry) => entry.editEndpoint === modelId);
|
|
9025
|
-
return config === void 0 ? void 0 : config.editImagesField ?? "image_urls";
|
|
9026
|
-
}
|
|
9027
|
-
/** The fal provider adapter registered in the provider registry. */
|
|
9028
|
-
const falAdapter = {
|
|
9029
|
-
id: "fal",
|
|
9030
|
-
apiKeyEnv: FAL_API_KEY_ENV,
|
|
9031
|
-
resolveModel: resolveModel$3,
|
|
9032
|
-
priceUsdByModel: FAL_IMAGE_PRICE_USD
|
|
9033
|
-
};
|
|
9034
|
-
//#endregion
|
|
9035
|
-
//#region src/image/openai.ts
|
|
9036
|
-
/**
|
|
9037
|
-
* OpenAI (gpt-image) provider adapter.
|
|
9038
|
-
*
|
|
9039
|
-
* Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/openai`. Building a model
|
|
9040
|
-
* performs no network I/O — the request only happens when `generateImage`
|
|
9041
|
-
* invokes `model.doGenerate`.
|
|
9042
|
-
*/
|
|
9043
|
-
/** Env var read for the OpenAI API key when `apiKey` is not supplied in config. */
|
|
9044
|
-
const OPENAI_API_KEY_ENV = "OPENAI_API_KEY";
|
|
9045
|
-
/**
|
|
9046
|
-
* Static OpenAI-direct USD/image, keyed by model id.
|
|
9047
|
-
*
|
|
9048
|
-
* `gpt-image-1` ≈ $0.042/image at medium quality, 1024×1024. OpenAI bills image
|
|
9049
|
-
* output by tokens, so the real figure varies with quality (low/medium/high)
|
|
9050
|
-
* and size — this is a documented approximation for the common case.
|
|
9051
|
-
* Source: https://platform.openai.com/docs/pricing (image generation)
|
|
9052
|
-
* Sanity anchor: the Phase 0 benchmark measured gpt-image direct at $0.042
|
|
9053
|
-
* (vs $0.133 via fal — full data and methodology on Linear MOT-23).
|
|
9054
|
-
*
|
|
9055
|
-
* GPT Image 2.6 is token-priced, with no published per-image estimate. Leave
|
|
9056
|
-
* these models absent so cost remains unknown unless supplied by the provider.
|
|
9057
|
-
* https://developers.openai.com/api/docs/models/gpt-image-2.6-flare
|
|
9058
|
-
* https://developers.openai.com/api/docs/models/gpt-image-2.6-sunburst
|
|
9059
|
-
*/
|
|
9060
|
-
const OPENAI_IMAGE_PRICE_USD = { "gpt-image-1": .042 };
|
|
9061
|
-
/**
|
|
9062
|
-
* Build an OpenAI `ImageModel`. Prefers the passed `apiKey`, else the
|
|
9063
|
-
* `OPENAI_API_KEY` env var. Throws `MotifError` when neither is present
|
|
9064
|
-
* (callers translate this into a `Result.err`).
|
|
9065
|
-
*/
|
|
9066
|
-
function resolveModel$2(modelId, apiKey, fetch) {
|
|
9067
|
-
const key = apiKey ?? process.env["OPENAI_API_KEY"];
|
|
9068
|
-
if (key === void 0 || key === "") throw new MotifError(`OpenAI image generation requires an API key (config.openai.apiKey or ${OPENAI_API_KEY_ENV})`, 0);
|
|
9069
|
-
return createOpenAI({
|
|
9070
|
-
apiKey: key,
|
|
9071
|
-
...toProviderFetch(fetch)
|
|
9072
|
-
}).image(modelId);
|
|
9073
|
-
}
|
|
9074
|
-
/** The OpenAI provider adapter registered in the provider registry. */
|
|
9075
|
-
const openaiAdapter = {
|
|
9076
|
-
id: "openai",
|
|
9077
|
-
apiKeyEnv: OPENAI_API_KEY_ENV,
|
|
9078
|
-
resolveModel: resolveModel$2,
|
|
9079
|
-
priceUsdByModel: OPENAI_IMAGE_PRICE_USD
|
|
9080
|
-
};
|
|
9081
|
-
//#endregion
|
|
9082
|
-
//#region src/image/openrouter.ts
|
|
9083
|
-
/** Env var read for the OpenRouter API key when `apiKey` is not supplied in config. */
|
|
9084
|
-
const OPENROUTER_API_KEY_ENV = "OPENROUTER_API_KEY";
|
|
9085
|
-
const OPENROUTER_IMAGES_URL = "https://openrouter.ai/api/v1/images";
|
|
9086
|
-
/** Key under which this adapter's metadata sits on `providerMetadata`. */
|
|
9087
|
-
const METADATA_KEY = "openrouter";
|
|
9088
|
-
/**
|
|
9089
|
-
* The Gemini image models Motif exposes, by their bare Google names, mapped to
|
|
9090
|
-
* the OpenRouter slug that serves them. All six are listed by
|
|
9091
|
-
* `GET https://openrouter.ai/api/v1/images/models`.
|
|
9092
|
-
*/
|
|
9093
|
-
const OPENROUTER_GEMINI_IMAGE_MODELS = {
|
|
9094
|
-
"gemini-2.5-flash-image": "google/gemini-2.5-flash-image",
|
|
9095
|
-
"gemini-3.1-flash-image-preview": "google/gemini-3.1-flash-image-preview",
|
|
9096
|
-
"gemini-3-pro-image-preview": "google/gemini-3-pro-image-preview",
|
|
9097
|
-
"gemini-3.1-flash-image": "google/gemini-3.1-flash-image",
|
|
9098
|
-
"gemini-3-pro-image": "google/gemini-3-pro-image",
|
|
9099
|
-
"gemini-3.1-flash-lite-image": "google/gemini-3.1-flash-lite-image"
|
|
9100
|
-
};
|
|
9101
|
-
/**
|
|
9102
|
-
* Static USD/image estimate, keyed by the bare model name. Used only when a
|
|
9103
|
-
* response carries no `usage.cost`; OpenRouter normally reports the real figure.
|
|
9104
|
-
*
|
|
9105
|
-
* - `gemini-2.5-flash-image`: 1290 output tokens at $30 / 1M ≈ $0.039.
|
|
9106
|
-
* - `gemini-3-pro-image-preview`, `gemini-3-pro-image`: ≈ $0.134 at 1K/2K.
|
|
9107
|
-
* - `gemini-3.1-flash-image-preview`: priced with the 2.5 flash-image line
|
|
9108
|
-
* pending a distinct published rate.
|
|
9109
|
-
* - `gemini-3.1-flash-image`: $0.067 at 1K.
|
|
9110
|
-
* - `gemini-3.1-flash-lite-image`: ≈ $0.0336 at 1K.
|
|
9111
|
-
* Source: https://ai.google.dev/gemini-api/docs/pricing
|
|
9112
|
-
*/
|
|
9113
|
-
const OPENROUTER_IMAGE_PRICE_USD = {
|
|
9114
|
-
"gemini-2.5-flash-image": .039,
|
|
9115
|
-
"gemini-3.1-flash-image-preview": .039,
|
|
9116
|
-
"gemini-3-pro-image-preview": .134,
|
|
9117
|
-
"gemini-3.1-flash-image": .067,
|
|
9118
|
-
"gemini-3-pro-image": .134,
|
|
9119
|
-
"gemini-3.1-flash-lite-image": .0336
|
|
9120
|
-
};
|
|
9121
|
-
/**
|
|
9122
|
-
* Resolve a Motif model name to an OpenRouter slug. A bare Gemini name maps to
|
|
9123
|
-
* its `google/<id>` slug; a name already containing `/` is an OpenRouter slug
|
|
9124
|
-
* and passes through. Anything else is unknown and throws.
|
|
9125
|
-
*/
|
|
9126
|
-
function openRouterModelSlug(modelId) {
|
|
9127
|
-
if (modelId.includes("/")) return modelId;
|
|
9128
|
-
const slug = OPENROUTER_GEMINI_IMAGE_MODELS[modelId];
|
|
9129
|
-
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);
|
|
9130
|
-
return slug;
|
|
9131
|
-
}
|
|
9132
|
-
function isRecord$3(value) {
|
|
9133
|
-
return typeof value === "object" && value !== null;
|
|
9134
|
-
}
|
|
9135
|
-
function toDataUrl(mediaType, data) {
|
|
9136
|
-
return `data:${mediaType};base64,${typeof data === "string" ? data : Buffer.from(data).toString("base64")}`;
|
|
9137
|
-
}
|
|
9138
|
-
/** One `input_references` entry for an input file: a URL as-is, bytes as a data URL. */
|
|
9139
|
-
function toInputReference(file) {
|
|
9140
|
-
return {
|
|
9141
|
-
type: "image_url",
|
|
9142
|
-
image_url: { url: file.type === "url" ? file.url : toDataUrl(file.mediaType, file.data) }
|
|
9143
|
-
};
|
|
9144
|
-
}
|
|
9145
|
-
function buildBody(slug, options) {
|
|
9146
|
-
if (options.mask !== void 0) throw new MotifError("OpenRouter's Image API takes no mask; describe the region in the instruction instead.", 0);
|
|
9147
|
-
return {
|
|
9148
|
-
...options.providerOptions[METADATA_KEY] ?? {},
|
|
9149
|
-
model: slug,
|
|
9150
|
-
prompt: options.prompt,
|
|
9151
|
-
n: options.n,
|
|
9152
|
-
...options.aspectRatio === void 0 ? {} : { aspect_ratio: options.aspectRatio },
|
|
9153
|
-
...options.files === void 0 || options.files.length === 0 ? {} : { input_references: options.files.map(toInputReference) }
|
|
9154
|
-
};
|
|
9155
|
-
}
|
|
9156
|
-
function errorMessage(body, fallback) {
|
|
9157
|
-
if (isRecord$3(body) && isRecord$3(body.error)) {
|
|
9158
|
-
const { message } = body.error;
|
|
9159
|
-
if (typeof message === "string" && message !== "") return message;
|
|
9160
|
-
}
|
|
9161
|
-
return fallback;
|
|
9162
|
-
}
|
|
9163
|
-
function parseResponse(body) {
|
|
9164
|
-
if (!isRecord$3(body) || !Array.isArray(body.data)) throw new MotifError("OpenRouter image response had no data array", 502);
|
|
9165
|
-
const images = [];
|
|
9166
|
-
for (const entry of body.data) if (isRecord$3(entry) && typeof entry.b64_json === "string") images.push(entry.b64_json);
|
|
9167
|
-
if (images.length === 0) throw new MotifError("OpenRouter returned no images", 502);
|
|
9168
|
-
return {
|
|
9169
|
-
images,
|
|
9170
|
-
cost: isRecord$3(body.usage) && typeof body.usage.cost === "number" ? body.usage.cost : void 0
|
|
9171
|
-
};
|
|
9172
|
-
}
|
|
9173
|
-
function buildModel(modelId, apiKey, doFetch) {
|
|
9174
|
-
const slug = openRouterModelSlug(modelId);
|
|
9175
|
-
return {
|
|
9176
|
-
specificationVersion: "v4",
|
|
9177
|
-
provider: METADATA_KEY,
|
|
9178
|
-
modelId: slug,
|
|
9179
|
-
maxImagesPerCall: 10,
|
|
9180
|
-
async doGenerate(options) {
|
|
9181
|
-
const warnings = [];
|
|
9182
|
-
if (options.seed !== void 0) warnings.push({
|
|
9183
|
-
type: "unsupported",
|
|
9184
|
-
feature: "seed"
|
|
9185
|
-
});
|
|
9186
|
-
if (options.size !== void 0) warnings.push({
|
|
9187
|
-
type: "unsupported",
|
|
9188
|
-
feature: "size",
|
|
9189
|
-
details: "Gemini on OpenRouter takes aspectRatio, not pixel sizes."
|
|
9190
|
-
});
|
|
9191
|
-
const timestamp = /* @__PURE__ */ new Date();
|
|
9192
|
-
const response = await doFetch(OPENROUTER_IMAGES_URL, {
|
|
9193
|
-
method: "POST",
|
|
9194
|
-
headers: {
|
|
9195
|
-
...options.headers,
|
|
9196
|
-
Authorization: `Bearer ${apiKey}`,
|
|
9197
|
-
"Content-Type": "application/json"
|
|
9198
|
-
},
|
|
9199
|
-
body: JSON.stringify(buildBody(slug, options)),
|
|
9200
|
-
...options.abortSignal === void 0 ? {} : { signal: options.abortSignal }
|
|
9201
|
-
});
|
|
9202
|
-
const text = await response.text();
|
|
9203
|
-
let body;
|
|
9204
|
-
try {
|
|
9205
|
-
body = JSON.parse(text);
|
|
9206
|
-
} catch {
|
|
9207
|
-
body = void 0;
|
|
9208
|
-
}
|
|
9209
|
-
if (!response.ok) throw new MotifError(errorMessage(body, `OpenRouter ${response.status}: ${text}`), response.status);
|
|
9210
|
-
const { images, cost } = parseResponse(body);
|
|
9211
|
-
return {
|
|
9212
|
-
images,
|
|
9213
|
-
warnings,
|
|
9214
|
-
providerMetadata: { [METADATA_KEY]: {
|
|
9215
|
-
images: images.map(() => ({})),
|
|
9216
|
-
...cost === void 0 ? {} : { cost }
|
|
9217
|
-
} },
|
|
9218
|
-
response: {
|
|
9219
|
-
timestamp,
|
|
9220
|
-
modelId: slug,
|
|
9221
|
-
headers: Object.fromEntries(response.headers.entries())
|
|
9222
|
-
}
|
|
9223
|
-
};
|
|
9224
|
-
}
|
|
9225
|
-
};
|
|
9226
|
-
}
|
|
9227
|
-
/**
|
|
9228
|
-
* Build an OpenRouter `ImageModel`. Prefers the passed `apiKey`, else the
|
|
9229
|
-
* `OPENROUTER_API_KEY` env var. Throws `MotifError` when neither is present or
|
|
9230
|
-
* the model name is unknown (callers translate this into a `Result.err`).
|
|
9231
|
-
*/
|
|
9232
|
-
function resolveModel$1(modelId, apiKey, fetch) {
|
|
9233
|
-
const key = apiKey ?? process.env["OPENROUTER_API_KEY"];
|
|
9234
|
-
if (key === void 0 || key === "") throw new MotifError(`OpenRouter image generation requires an API key (config.openrouter.apiKey or ${OPENROUTER_API_KEY_ENV})`, 0);
|
|
9235
|
-
const configured = toProviderFetch(fetch);
|
|
9236
|
-
return buildModel(modelId, key, "fetch" in configured ? configured.fetch : globalThis.fetch);
|
|
9237
|
-
}
|
|
9238
|
-
/** The OpenRouter provider adapter registered in the provider registry. */
|
|
9239
|
-
const openrouterAdapter = {
|
|
9240
|
-
id: "openrouter",
|
|
9241
|
-
apiKeyEnv: OPENROUTER_API_KEY_ENV,
|
|
9242
|
-
resolveModel: resolveModel$1,
|
|
9243
|
-
priceUsdByModel: OPENROUTER_IMAGE_PRICE_USD
|
|
9244
|
-
};
|
|
9245
|
-
//#endregion
|
|
9246
|
-
//#region src/image/replicate.ts
|
|
9247
|
-
/**
|
|
9248
|
-
* Replicate provider adapter.
|
|
9249
|
-
*
|
|
9250
|
-
* Builds a Vercel AI SDK `ImageModel` from `@ai-sdk/replicate`. Building a model
|
|
9251
|
-
* performs no network I/O — the request only happens when `generateImage`
|
|
9252
|
-
* invokes `model.doGenerate`.
|
|
9253
|
-
*
|
|
9254
|
-
* NOTE: Replicate's SDK names the credential option `apiToken` (not `apiKey`),
|
|
9255
|
-
* and reads `REPLICATE_API_TOKEN` from the environment.
|
|
9256
|
-
*/
|
|
9257
|
-
/** Env var read for the Replicate API token when `apiToken` is not in config. */
|
|
9258
|
-
const REPLICATE_API_KEY_ENV = "REPLICATE_API_TOKEN";
|
|
9259
|
-
/**
|
|
9260
|
-
* Static Replicate USD/image, keyed by model id.
|
|
9261
|
-
*
|
|
9262
|
-
* `black-forest-labs/flux-1.1-pro-ultra` = $0.06/image (Replicate list price).
|
|
9263
|
-
* Source: https://replicate.com/black-forest-labs/flux-1.1-pro-ultra
|
|
9264
|
-
* Sanity anchor: fal's `fal-ai/flux-pro/v1.1-ultra` is also $0.06
|
|
9265
|
-
* (`MODELS.flux.pricePerImageUsd` in ../models) — same price, both routes.
|
|
9266
|
-
*/
|
|
9267
|
-
const REPLICATE_IMAGE_PRICE_USD = { "black-forest-labs/flux-1.1-pro-ultra": .06 };
|
|
9268
|
-
/**
|
|
9269
|
-
* Build a Replicate `ImageModel`. Prefers the passed `apiKey` (the normalized
|
|
9270
|
-
* token), else the `REPLICATE_API_TOKEN` env var. Throws `MotifError` when
|
|
9271
|
-
* neither is present (callers translate this into a `Result.err`).
|
|
9272
|
-
*/
|
|
9273
|
-
function resolveModel(modelId, apiKey, fetch) {
|
|
9274
|
-
const token = apiKey ?? process.env["REPLICATE_API_TOKEN"];
|
|
9275
|
-
if (token === void 0 || token === "") throw new MotifError(`Replicate image generation requires an API token (config.replicate.apiToken or ${REPLICATE_API_KEY_ENV})`, 0);
|
|
9276
|
-
return createReplicate({
|
|
9277
|
-
apiToken: token,
|
|
9278
|
-
...toProviderFetch(fetch)
|
|
9279
|
-
}).image(modelId);
|
|
9280
|
-
}
|
|
9281
|
-
//#endregion
|
|
9282
|
-
//#region src/image/provider.ts
|
|
9283
|
-
/**
|
|
9284
|
-
* The provider registry. Every id in {@link ImageProviderId}'s closed part maps
|
|
9285
|
-
* to its adapter; the open union tail means a lookup can still miss, so access
|
|
9286
|
-
* goes through {@link getProviderAdapter}.
|
|
9287
|
-
*/
|
|
9288
|
-
const PROVIDERS = {
|
|
9289
|
-
openrouter: openrouterAdapter,
|
|
9290
|
-
openai: openaiAdapter,
|
|
9291
|
-
replicate: {
|
|
9292
|
-
id: "replicate",
|
|
9293
|
-
apiKeyEnv: REPLICATE_API_KEY_ENV,
|
|
9294
|
-
resolveModel,
|
|
9295
|
-
priceUsdByModel: REPLICATE_IMAGE_PRICE_USD
|
|
9296
|
-
},
|
|
9297
|
-
fal: falAdapter
|
|
9298
|
-
};
|
|
9299
|
-
/**
|
|
9300
|
-
* Look up an adapter by provider id, throwing a `MotifError` for an unknown
|
|
9301
|
-
* provider (callers catch this into a `Result.err`).
|
|
9302
|
-
*/
|
|
9303
|
-
function getProviderAdapter(provider) {
|
|
9304
|
-
const adapter = PROVIDERS[provider];
|
|
9305
|
-
if (adapter === void 0) throw new MotifError(`Unsupported image provider: ${provider}`, 0);
|
|
9306
|
-
return adapter;
|
|
9307
|
-
}
|
|
9308
|
-
//#endregion
|
|
9309
|
-
//#region src/image/cost.ts
|
|
9310
|
-
/**
|
|
9311
|
-
* Per-call cost tracking for the image layer.
|
|
9312
|
-
*
|
|
9313
|
-
* Preference order:
|
|
9314
|
-
* 1. A cost surfaced by the provider on `result.providerMetadata` (most image
|
|
9315
|
-
* providers do NOT surface one today, so this is usually absent).
|
|
9316
|
-
* 2. A static per-model table, owned per-adapter (`priceUsdByModel`) and read
|
|
9317
|
-
* from the provider registry (see sources in each adapter).
|
|
9318
|
-
* 3. Unknown → `{ usd: 0, source: "unknown" }`.
|
|
9319
|
-
*/
|
|
9320
|
-
function isRecord$2(value) {
|
|
9321
|
-
return typeof value === "object" && value !== null;
|
|
9322
|
-
}
|
|
9323
|
-
/**
|
|
9324
|
-
* Extract a provider-reported total cost from a `generateImage` result's
|
|
9325
|
-
* `providerMetadata`, if the provider surfaces a numeric `cost`. The shape is
|
|
9326
|
-
* `{ [provider]: { ...; cost?: number } }`. Returns undefined otherwise.
|
|
9327
|
-
*/
|
|
9328
|
-
function costFromProviderMetadata(providerMetadata) {
|
|
9329
|
-
if (!isRecord$2(providerMetadata)) return;
|
|
9330
|
-
for (const value of Object.values(providerMetadata)) if (isRecord$2(value)) {
|
|
9331
|
-
const cost = value.cost;
|
|
9332
|
-
if (typeof cost === "number" && Number.isFinite(cost)) return cost;
|
|
9333
|
-
}
|
|
9334
|
-
}
|
|
9335
|
-
function tablePricePerImage(provider, modelId) {
|
|
9336
|
-
const adapter = PROVIDERS[provider];
|
|
9337
|
-
if (adapter === void 0) return;
|
|
9338
|
-
return adapter.priceUsdByModel[modelId];
|
|
9339
|
-
}
|
|
9340
|
-
function roundUsd(value) {
|
|
9341
|
-
return Number(value.toFixed(6));
|
|
9342
|
-
}
|
|
9343
|
-
/**
|
|
9344
|
-
* Cost across every underlying model call of one generation. Provider-metadata
|
|
9345
|
-
* costs from the calls that report one are summed; otherwise the static table
|
|
9346
|
-
* (× image count), then unknown.
|
|
9347
|
-
*/
|
|
9348
|
-
function costForCalls(provider, modelId, callMetadata, imageCount) {
|
|
9349
|
-
let metaCost;
|
|
9350
|
-
for (const providerMetadata of callMetadata) {
|
|
9351
|
-
const callCost = costFromProviderMetadata(providerMetadata);
|
|
9352
|
-
if (callCost !== void 0) metaCost = (metaCost ?? 0) + callCost;
|
|
9353
|
-
}
|
|
9354
|
-
if (metaCost !== void 0) return {
|
|
9355
|
-
usd: roundUsd(metaCost),
|
|
9356
|
-
source: "provider-metadata"
|
|
9357
|
-
};
|
|
9358
|
-
const perImage = tablePricePerImage(provider, modelId);
|
|
9359
|
-
if (perImage !== void 0) return {
|
|
9360
|
-
usd: roundUsd(perImage * Math.max(imageCount, 1)),
|
|
9361
|
-
source: "table"
|
|
9362
|
-
};
|
|
9363
|
-
return {
|
|
9364
|
-
usd: 0,
|
|
9365
|
-
source: "unknown"
|
|
9366
|
-
};
|
|
9367
|
-
}
|
|
9368
|
-
//#endregion
|
|
9369
|
-
//#region src/image/index.ts
|
|
9370
|
-
/**
|
|
9371
|
-
* `@howells/motif-sdk/image` — provider-agnostic image generation + editing.
|
|
9372
|
-
*
|
|
9373
|
-
* ESM-only subpath export, built on the Vercel AI SDK image interface
|
|
9374
|
-
* (`generateImage`, `@ai-sdk/*`). The caller names the provider and model;
|
|
9375
|
-
* reuses the SDK's Result convention (`Result<T, MotifError>` — no
|
|
9376
|
-
* thrown exceptions). Gemini is reached through OpenRouter.
|
|
9377
|
-
*
|
|
9378
|
-
* @example
|
|
9379
|
-
* ```ts
|
|
9380
|
-
* import { createMotifImage } from "@howells/motif-sdk/image";
|
|
9381
|
-
*
|
|
9382
|
-
* const img = createMotifImage({ defaultProvider: "openrouter" });
|
|
9383
|
-
* const r = await img.generate({ model: "gemini-3.1-flash-image", prompt: "a bare concrete wall" });
|
|
9384
|
-
* if (r.isOk()) console.log(r.value.images[0].mediaType, r.value.cost);
|
|
9385
|
-
* ```
|
|
9386
|
-
*/
|
|
9387
|
-
const DEFAULT_PROVIDER = "openrouter";
|
|
9388
|
-
/**
|
|
9389
|
-
* Create a provider-agnostic image client.
|
|
9390
|
-
*
|
|
9391
|
-
* @param config - provider selection + keys (keys fall back to env).
|
|
9392
|
-
* @param deps - INTERNAL testing seam. Defaults to the real AI SDK `generateImage`
|
|
9393
|
-
* and the built-in provider adapters; tests inject fakes here to run offline.
|
|
9394
|
-
*/
|
|
9395
|
-
function createMotifImage(config = {}, deps = {}) {
|
|
9396
|
-
const generateImageFn = deps.generateImage ?? generateImage;
|
|
9397
|
-
const resolveModelFn = deps.resolveModel ?? defaultResolveModel;
|
|
9398
|
-
function resolveProvider(provider) {
|
|
9399
|
-
return provider ?? config.defaultProvider ?? DEFAULT_PROVIDER;
|
|
9400
|
-
}
|
|
9401
|
-
function apiKeyFor(provider) {
|
|
9402
|
-
switch (provider) {
|
|
9403
|
-
case "openrouter": return config.openrouter?.apiKey;
|
|
9404
|
-
case "openai": return config.openai?.apiKey;
|
|
9405
|
-
case "replicate": return config.replicate?.apiToken;
|
|
9406
|
-
case "fal": return config.fal?.apiKey;
|
|
9407
|
-
default: return;
|
|
9408
|
-
}
|
|
9409
|
-
}
|
|
9410
|
-
async function generate(opts) {
|
|
9411
|
-
const provider = resolveProvider(opts.provider);
|
|
9412
|
-
try {
|
|
9413
|
-
const modelId = opts.model;
|
|
9414
|
-
const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
|
|
9415
|
-
const result = await generateImageFn({
|
|
9416
|
-
model,
|
|
9417
|
-
prompt: opts.prompt,
|
|
9418
|
-
...opts.n === void 0 ? {} : { n: opts.n },
|
|
9419
|
-
...opts.size === void 0 ? {} : { size: opts.size },
|
|
9420
|
-
...opts.aspectRatio === void 0 ? {} : { aspectRatio: opts.aspectRatio },
|
|
9421
|
-
...opts.seed === void 0 ? {} : { seed: opts.seed },
|
|
9422
|
-
...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
|
|
9423
|
-
...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
|
|
9424
|
-
...opts.headers === void 0 ? {} : { headers: opts.headers },
|
|
9425
|
-
...opts.providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(opts.providerOptions) }
|
|
9426
|
-
});
|
|
9427
|
-
return ok$1(toMotifImageResult(result, provider, modelId));
|
|
9428
|
-
} catch (error) {
|
|
9429
|
-
return err$1(toMotifError(error));
|
|
9430
|
-
}
|
|
9431
|
-
}
|
|
9432
|
-
async function edit(opts) {
|
|
9433
|
-
const provider = resolveProvider(opts.provider);
|
|
9434
|
-
const imagesField = provider === "fal" ? falEditImagesField(opts.model) : void 0;
|
|
9435
|
-
if (imagesField === "image_url" && opts.images.length > 1) return err$1(new MotifError(`${opts.model} takes one input image; ${opts.images.length} were given`, 0));
|
|
9436
|
-
const providerOptions = imagesField === "image_urls" && opts.providerOptions?.fal?.useMultipleImages === void 0 ? {
|
|
9437
|
-
...opts.providerOptions,
|
|
9438
|
-
fal: {
|
|
9439
|
-
...opts.providerOptions?.fal,
|
|
9440
|
-
useMultipleImages: true
|
|
9441
|
-
}
|
|
9442
|
-
} : opts.providerOptions;
|
|
9443
|
-
try {
|
|
9444
|
-
const modelId = opts.model;
|
|
9445
|
-
const model = resolveModelFn(provider, modelId, apiKeyFor(provider), config.fetch);
|
|
9446
|
-
const result = await generateImageFn({
|
|
9447
|
-
model,
|
|
9448
|
-
prompt: {
|
|
9449
|
-
images: opts.images,
|
|
9450
|
-
text: opts.instruction,
|
|
9451
|
-
...opts.mask === void 0 ? {} : { mask: opts.mask }
|
|
9452
|
-
},
|
|
9453
|
-
...opts.n === void 0 ? {} : { n: opts.n },
|
|
9454
|
-
...opts.seed === void 0 ? {} : { seed: opts.seed },
|
|
9455
|
-
...config.maxRetries === void 0 ? {} : { maxRetries: config.maxRetries },
|
|
9456
|
-
...opts.signal === void 0 ? {} : { abortSignal: opts.signal },
|
|
9457
|
-
...opts.headers === void 0 ? {} : { headers: opts.headers },
|
|
9458
|
-
...providerOptions === void 0 ? {} : { providerOptions: toProviderOptions(providerOptions) }
|
|
9459
|
-
});
|
|
9460
|
-
return ok$1(toMotifImageResult(result, provider, modelId));
|
|
9461
|
-
} catch (error) {
|
|
9462
|
-
return err$1(toMotifError(error));
|
|
9463
|
-
}
|
|
9464
|
-
}
|
|
9465
|
-
async function bestOfN(opts) {
|
|
9466
|
-
return await runBestOfN(opts, generate, edit);
|
|
9467
|
-
}
|
|
9468
|
-
return {
|
|
9469
|
-
generate,
|
|
9470
|
-
edit,
|
|
9471
|
-
bestOfN
|
|
9472
|
-
};
|
|
9473
|
-
}
|
|
9474
|
-
/**
|
|
9475
|
-
* Fire one best-of-N candidate: reuse `generate`/`edit` (discriminated by the
|
|
9476
|
-
* presence of `images`, narrowed inline so TS refines the union). Each candidate
|
|
9477
|
-
* makes a single image (`n: 1`) — the outer `n` is the candidate count, not the
|
|
9478
|
-
* per-call image count — and takes a distinct `seed` (`seed + index`) when a seed
|
|
9479
|
-
* was given, so the N vary. `opts.signal` rides along via the spread, so one
|
|
9480
|
-
* cancel aborts every candidate.
|
|
9481
|
-
*/
|
|
9482
|
-
async function runCandidate(opts, index, generate, edit) {
|
|
9483
|
-
const candidateSeed = opts.seed === void 0 ? void 0 : opts.seed + index;
|
|
9484
|
-
const overrides = {
|
|
9485
|
-
n: 1,
|
|
9486
|
-
...candidateSeed === void 0 ? {} : { seed: candidateSeed }
|
|
9487
|
-
};
|
|
9488
|
-
if ("images" in opts) return await edit({
|
|
9489
|
-
...opts,
|
|
9490
|
-
...overrides
|
|
9491
|
-
});
|
|
9492
|
-
return await generate({
|
|
9493
|
-
...opts,
|
|
9494
|
-
...overrides
|
|
9495
|
-
});
|
|
9496
|
-
}
|
|
9497
|
-
/** Ask the judge to pick a winner and validate its index; no judge → index 0. */
|
|
9498
|
-
async function selectWinner(opts, candidates) {
|
|
9499
|
-
const { judge } = opts;
|
|
9500
|
-
if (judge === void 0) return ok$1({ chosenIndex: 0 });
|
|
9501
|
-
const decision = await judge(candidates, "images" in opts ? { instruction: opts.instruction } : { prompt: opts.prompt });
|
|
9502
|
-
if (!Number.isInteger(decision.index) || decision.index < 0 || decision.index >= candidates.length) return err$1(new MotifError(`bestOfN judge returned an out-of-range index ${decision.index} (expected 0..${candidates.length - 1})`, 0));
|
|
9503
|
-
return ok$1({
|
|
9504
|
-
chosenIndex: decision.index,
|
|
9505
|
-
...decision.reason === void 0 ? {} : { reason: decision.reason }
|
|
9506
|
-
});
|
|
9507
|
-
}
|
|
9508
|
-
/**
|
|
9509
|
-
* Best-of-N: generate `n` candidates in parallel, keep the successes, and pick a
|
|
9510
|
-
* winner via the (optional, injectable) judge. Wraps everything so no throw
|
|
9511
|
-
* escapes — any failure becomes a `Result.err`.
|
|
9512
|
-
*/
|
|
9513
|
-
async function runBestOfN(opts, generate, edit) {
|
|
9514
|
-
try {
|
|
9515
|
-
const { n } = opts;
|
|
9516
|
-
if (!Number.isInteger(n) || n < 1) return err$1(new MotifError(`bestOfN requires an integer n >= 1 (got ${n})`, 0));
|
|
9517
|
-
const results = await Promise.all(Array.from({ length: n }, (_unused, index) => index).map(async (index) => await runCandidate(opts, index, generate, edit)));
|
|
9518
|
-
const candidates = [];
|
|
9519
|
-
let firstError;
|
|
9520
|
-
for (const result of results) if (result.isOk()) candidates.push(result.value);
|
|
9521
|
-
else firstError ??= result.error;
|
|
9522
|
-
if (candidates.length === 0) return err$1(firstError ?? new MotifError(`all ${n} bestOfN candidates failed`, 0));
|
|
9523
|
-
const winner = await selectWinner(opts, candidates);
|
|
9524
|
-
if (winner.isErr()) return err$1(winner.error);
|
|
9525
|
-
const { chosenIndex, reason } = winner.value;
|
|
9526
|
-
const best = candidates[chosenIndex];
|
|
9527
|
-
if (best === void 0) return err$1(new MotifError("bestOfN failed to resolve a winner", 0));
|
|
9528
|
-
const totalCostUsd = Number(candidates.reduce((sum, candidate) => sum + candidate.cost.usd, 0).toFixed(6));
|
|
9529
|
-
return ok$1({
|
|
9530
|
-
best,
|
|
9531
|
-
chosenIndex,
|
|
9532
|
-
...reason === void 0 ? {} : { reason },
|
|
9533
|
-
candidates,
|
|
9534
|
-
totalCostUsd
|
|
9535
|
-
});
|
|
9536
|
-
} catch (error) {
|
|
9537
|
-
return err$1(toMotifError(error));
|
|
9538
|
-
}
|
|
9539
|
-
}
|
|
9540
|
-
/** Default provider dispatch: resolve the model through the provider registry. */
|
|
9541
|
-
function defaultResolveModel(provider, modelId, apiKey, fetch) {
|
|
9542
|
-
return getProviderAdapter(provider).resolveModel(modelId, apiKey, fetch);
|
|
9543
|
-
}
|
|
9544
|
-
/** Map the AI SDK result → normalized MotifImageResult (with cost + requestId). */
|
|
9545
|
-
function toMotifImageResult(result, provider, model) {
|
|
9546
|
-
const images = result.images.map((file) => ({
|
|
9547
|
-
uint8Array: file.uint8Array,
|
|
9548
|
-
base64: file.base64,
|
|
9549
|
-
mediaType: file.mediaType
|
|
9550
|
-
}));
|
|
9551
|
-
const cost = costForCalls(provider, model, result.calls.map((call) => call.providerMetadata), images.length);
|
|
9552
|
-
const requestId = extractRequestId(result);
|
|
9553
|
-
const warnings = result.warnings.map(renderWarning);
|
|
9554
|
-
return {
|
|
9555
|
-
images,
|
|
9556
|
-
cost,
|
|
9557
|
-
provider,
|
|
9558
|
-
model,
|
|
9559
|
-
...requestId === void 0 ? {} : { requestId },
|
|
9560
|
-
...warnings.length === 0 ? {} : { warnings }
|
|
9561
|
-
};
|
|
9562
|
-
}
|
|
9563
|
-
/** Render an AI SDK `Warning` object into a readable one-line string. */
|
|
9564
|
-
function renderWarning(warning) {
|
|
9565
|
-
if (warning.type === "unsupported" || warning.type === "compatibility") {
|
|
9566
|
-
const label = warning.type === "unsupported" ? "unsupported feature" : "compatibility mode for feature";
|
|
9567
|
-
return warning.details === void 0 ? `${label} "${warning.feature}"` : `${label} "${warning.feature}": ${warning.details}`;
|
|
9568
|
-
}
|
|
9569
|
-
if (warning.type === "deprecated") return `deprecated setting "${warning.setting}": ${warning.message}`;
|
|
9570
|
-
return warning.message;
|
|
9571
|
-
}
|
|
9572
|
-
function isRecord$1(value) {
|
|
9573
|
-
return typeof value === "object" && value !== null;
|
|
9574
|
-
}
|
|
9575
|
-
/** Look for a provider correlation id in providerMetadata, then response headers. */
|
|
9576
|
-
function extractRequestId(result) {
|
|
9577
|
-
for (const call of result.calls) {
|
|
9578
|
-
const fromMetadata = requestIdFromMetadata(call.providerMetadata);
|
|
9579
|
-
if (fromMetadata !== void 0) return fromMetadata;
|
|
9580
|
-
}
|
|
9581
|
-
for (const { response } of result.calls) {
|
|
9582
|
-
const { headers } = response;
|
|
9583
|
-
if (headers) {
|
|
9584
|
-
const id = headers["x-request-id"] ?? headers["x-goog-request-id"] ?? headers["x-fal-request-id"];
|
|
9585
|
-
if (typeof id === "string" && id !== "") return id;
|
|
9586
|
-
}
|
|
9587
|
-
}
|
|
9588
|
-
}
|
|
9589
|
-
function requestIdFromMetadata(providerMetadata) {
|
|
9590
|
-
if (!isRecord$1(providerMetadata)) return;
|
|
9591
|
-
for (const value of Object.values(providerMetadata)) if (isRecord$1(value)) {
|
|
9592
|
-
const id = value.requestId ?? value.request_id;
|
|
9593
|
-
if (typeof id === "string" && id !== "") return id;
|
|
9594
|
-
}
|
|
9595
|
-
}
|
|
9596
|
-
/**
|
|
9597
|
-
* Coerce a public `providerOptions` (values typed `unknown`) into the AI SDK's
|
|
9598
|
-
* JSON-only `Record<string, Record<string, JSONValue>>`. Fails fast: a
|
|
9599
|
-
* non-JSON-representable value (undefined/function/bigint/symbol) throws a
|
|
9600
|
-
* `MotifError` naming the offending path rather than silently forwarding a value
|
|
9601
|
-
* the caller did not intend.
|
|
9602
|
-
*/
|
|
9603
|
-
function toProviderOptions(input) {
|
|
9604
|
-
const out = {};
|
|
9605
|
-
for (const [namespace, options] of Object.entries(input)) {
|
|
9606
|
-
const inner = {};
|
|
9607
|
-
for (const [key, value] of Object.entries(options)) inner[key] = toJsonValue(value, `providerOptions.${namespace}.${key}`);
|
|
9608
|
-
out[namespace] = inner;
|
|
9609
|
-
}
|
|
9610
|
-
return out;
|
|
9611
|
-
}
|
|
9612
|
-
function toJsonValue(value, path) {
|
|
9613
|
-
if (value === null) return null;
|
|
9614
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
9615
|
-
if (typeof value === "object") {
|
|
9616
|
-
if (Array.isArray(value)) {
|
|
9617
|
-
const arr = [];
|
|
9618
|
-
for (const [index, item] of value.entries()) arr.push(toJsonValue(item, `${path}[${index}]`));
|
|
9619
|
-
return arr;
|
|
9620
|
-
}
|
|
9621
|
-
const obj = {};
|
|
9622
|
-
for (const [key, entry] of Object.entries(value)) obj[key] = toJsonValue(entry, `${path}.${key}`);
|
|
9623
|
-
return obj;
|
|
9624
|
-
}
|
|
9625
|
-
throw new MotifError(`providerOptions value at ${path} is not JSON-representable (type: ${typeof value})`, 0);
|
|
9626
|
-
}
|
|
9627
|
-
/**
|
|
9628
|
-
* Coerce an unknown thrown value into a `MotifError`, mirroring server.ts's
|
|
9629
|
-
* mapping: preserve an existing `MotifError`, lift a string `code`, and mark
|
|
9630
|
-
* non-HTTP local failures with status `0`.
|
|
9631
|
-
*/
|
|
9632
|
-
function toMotifError(error) {
|
|
9633
|
-
if (error instanceof MotifError) return error;
|
|
9634
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9635
|
-
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
9636
|
-
const status = error instanceof Error && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : 0;
|
|
9637
|
-
const body = error instanceof Error && "responseBody" in error && typeof error.responseBody === "string" ? error.responseBody : message;
|
|
9638
|
-
if (isFalAccountLocked(status, body)) return falHttpError(status, body);
|
|
9639
|
-
return new MotifError(message, status, code);
|
|
9640
|
-
}
|
|
9641
|
-
//#endregion
|
|
9642
8058
|
//#region src/task-output.ts
|
|
9643
8059
|
function isPlainRecord(value) {
|
|
9644
8060
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -9932,14 +8348,14 @@ function projectedOutputs(model, input, body) {
|
|
|
9932
8348
|
}
|
|
9933
8349
|
function outputSize(parameters, input, imageSize) {
|
|
9934
8350
|
if (typeof imageSize === "string") return FAL_PRESET_SIZES[imageSize];
|
|
9935
|
-
if (isPixelSize
|
|
8351
|
+
if (isPixelSize(imageSize)) return imageSize;
|
|
9936
8352
|
if (imageSize === void 0) {
|
|
9937
8353
|
const fallback = parameters.find((parameter) => parameter.key === "image_size")?.fallback;
|
|
9938
8354
|
if (fallback === void 0) return sourceSizeOf(input);
|
|
9939
8355
|
return typeof fallback === "string" ? FAL_PRESET_SIZES[fallback] : void 0;
|
|
9940
8356
|
}
|
|
9941
8357
|
}
|
|
9942
|
-
function isPixelSize
|
|
8358
|
+
function isPixelSize(value) {
|
|
9943
8359
|
return typeof value === "object" && value !== null && "width" in value && "height" in value && typeof value.width === "number" && typeof value.height === "number";
|
|
9944
8360
|
}
|
|
9945
8361
|
/** What the plan knows of the source, for prices that read it. */
|
|
@@ -10325,7 +8741,6 @@ function taskRequest(task, input) {
|
|
|
10325
8741
|
function resolve(task, input, context, options) {
|
|
10326
8742
|
const keys = [];
|
|
10327
8743
|
if (options.dryRun === true || context.falKey !== void 0) keys.push("FAL_KEY");
|
|
10328
|
-
if (options.dryRun === true || context.openAiKey !== void 0) keys.push("OPENAI_API_KEY");
|
|
10329
8744
|
const resolution = resolveTask(task, taskRequest(task, input), {
|
|
10330
8745
|
keys,
|
|
10331
8746
|
pins: context.pins
|
|
@@ -10400,7 +8815,7 @@ function withCode(error, model, task) {
|
|
|
10400
8815
|
field: error.field,
|
|
10401
8816
|
task
|
|
10402
8817
|
});
|
|
10403
|
-
const motif = toMotifError
|
|
8818
|
+
const motif = toMotifError(error);
|
|
10404
8819
|
if (motif.code !== void 0) return motif;
|
|
10405
8820
|
return new MotifError(motif.message, 0, INVALID_OPTION, void 0, {
|
|
10406
8821
|
model,
|
|
@@ -10498,7 +8913,6 @@ function generationPlan(task, model, config, input) {
|
|
|
10498
8913
|
field: "mask",
|
|
10499
8914
|
requires: "references"
|
|
10500
8915
|
}));
|
|
10501
|
-
if (input.transparent === true && config.supportsBackground !== true && config.transparencyRoute !== void 0) return openAiPlan(task, model, config.transparencyRoute, input, editImageUrls);
|
|
10502
8916
|
if (input.outputFormat !== void 0 && !outputFormatsOf(config, editImageUrls.length > 0).includes(input.outputFormat)) return err$1(cannotCarry("outputFormat", model, task));
|
|
10503
8917
|
const { quality, ...params } = input.params ?? {};
|
|
10504
8918
|
const liftQuality = config.supportsQuality === true && quality !== void 0;
|
|
@@ -10541,55 +8955,6 @@ function generationPlan(task, model, config, input) {
|
|
|
10541
8955
|
queued: config.useQueue === true || input.onProgress !== void 0 || input.onSubmitted !== void 0
|
|
10542
8956
|
});
|
|
10543
8957
|
}
|
|
10544
|
-
/**
|
|
10545
|
-
* The OpenAI transparency route. Ported from the CLI's generate-openai: the
|
|
10546
|
-
* route carries only what gpt-image takes, so everything else is refused.
|
|
10547
|
-
*/
|
|
10548
|
-
function openAiPlan(task, model, route, input, editImageUrls) {
|
|
10549
|
-
const allowed = /* @__PURE__ */ new Set([
|
|
10550
|
-
"aspect",
|
|
10551
|
-
"count",
|
|
10552
|
-
"look",
|
|
10553
|
-
"mask",
|
|
10554
|
-
"mood",
|
|
10555
|
-
"prompt",
|
|
10556
|
-
"references",
|
|
10557
|
-
"transparent"
|
|
10558
|
-
]);
|
|
10559
|
-
if (task === "vary") allowed.add("image");
|
|
10560
|
-
if (input.outputFormat === "png") allowed.add("outputFormat");
|
|
10561
|
-
const refused = refuseOthers(input, allowed, model, task);
|
|
10562
|
-
if (refused !== void 0) return err$1(refused);
|
|
10563
|
-
if (input.params !== void 0) return err$1(cannotCarry("params", model, task));
|
|
10564
|
-
const editing = editImageUrls.length > 0;
|
|
10565
|
-
if (editing && !route.supportsEdit) return err$1(cannotCarry("references", model, task));
|
|
10566
|
-
let prompt;
|
|
10567
|
-
try {
|
|
10568
|
-
const creative = creativeOf(input);
|
|
10569
|
-
prompt = creative === void 0 ? input.prompt ?? "" : enrichPrompt({
|
|
10570
|
-
creative,
|
|
10571
|
-
prompt: input.prompt ?? ""
|
|
10572
|
-
}).prompt;
|
|
10573
|
-
} catch (error) {
|
|
10574
|
-
return err$1(withCode(error, model, task));
|
|
10575
|
-
}
|
|
10576
|
-
return ok$1({
|
|
10577
|
-
body: {
|
|
10578
|
-
n: input.count ?? 1,
|
|
10579
|
-
prompt,
|
|
10580
|
-
size: aspectToGptSize(input.aspect ?? "1:1"),
|
|
10581
|
-
...editing && { images: [...editImageUrls] },
|
|
10582
|
-
...input.mask !== void 0 && { mask: input.mask },
|
|
10583
|
-
background: "transparent",
|
|
10584
|
-
outputFormat: "png"
|
|
10585
|
-
},
|
|
10586
|
-
cost: UNKNOWN_COST,
|
|
10587
|
-
endpoint: `${route.provider}:${route.model}`,
|
|
10588
|
-
prompt,
|
|
10589
|
-
provider: "openai",
|
|
10590
|
-
queued: false
|
|
10591
|
-
});
|
|
10592
|
-
}
|
|
10593
8958
|
function flatPrice(pricing) {
|
|
10594
8959
|
const match = FLAT_PRICE_REGEX.exec(pricing.trim());
|
|
10595
8960
|
return match === null ? null : Number(match[1]);
|
|
@@ -10641,7 +9006,7 @@ function parsePayload(text) {
|
|
|
10641
9006
|
if (JSON_START.test(text.trim())) throw error;
|
|
10642
9007
|
raw = text;
|
|
10643
9008
|
}
|
|
10644
|
-
if (!isRecord$
|
|
9009
|
+
if (!isRecord$1(raw) || Array.isArray(raw)) return {
|
|
10645
9010
|
raw,
|
|
10646
9011
|
files: []
|
|
10647
9012
|
};
|
|
@@ -10914,8 +9279,8 @@ function enforceBufferLimit(length, requestId) {
|
|
|
10914
9279
|
/**
|
|
10915
9280
|
* The Task client: one function per Task. `createMotif()` returns a client
|
|
10916
9281
|
* whose `generate`, `erase`, `upscale` and the rest resolve the Model through
|
|
10917
|
-
* `resolveTask`, build the request that Model needs, run it on fal
|
|
10918
|
-
*
|
|
9282
|
+
* `resolveTask`, build the request that Model needs, run it on fal and return
|
|
9283
|
+
* the files and fields it produced.
|
|
10919
9284
|
*
|
|
10920
9285
|
* @example
|
|
10921
9286
|
* ```ts
|
|
@@ -10925,7 +9290,6 @@ function enforceBufferLimit(length, requestId) {
|
|
|
10925
9290
|
* ```
|
|
10926
9291
|
*/
|
|
10927
9292
|
const MISSING_API_KEY = "MISSING_API_KEY";
|
|
10928
|
-
const PIXEL_SIZE_REGEX = /^\d+x\d+$/;
|
|
10929
9293
|
function presentKey(value) {
|
|
10930
9294
|
return value === void 0 || value === "" ? void 0 : value;
|
|
10931
9295
|
}
|
|
@@ -10943,9 +9307,6 @@ function outputKeysFor(model, body) {
|
|
|
10943
9307
|
if (model === "clarity" || model === "crystal") return ["image"];
|
|
10944
9308
|
return ["images"];
|
|
10945
9309
|
}
|
|
10946
|
-
function isPixelSize(size) {
|
|
10947
|
-
return PIXEL_SIZE_REGEX.test(size);
|
|
10948
|
-
}
|
|
10949
9310
|
function isRecord(value) {
|
|
10950
9311
|
return typeof value === "object" && value !== null;
|
|
10951
9312
|
}
|
|
@@ -10996,106 +9357,6 @@ function falOutput(plan, result) {
|
|
|
10996
9357
|
tier: plan.tier
|
|
10997
9358
|
};
|
|
10998
9359
|
}
|
|
10999
|
-
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
11000
|
-
/**
|
|
11001
|
-
* Remote images as bytes, fetched through the configured seam: the AI SDK
|
|
11002
|
-
* would otherwise download them with global fetch. Data URLs pass through.
|
|
11003
|
-
*/
|
|
11004
|
-
async function localImages(sources, fetch) {
|
|
11005
|
-
const images = [];
|
|
11006
|
-
for (const source of sources) {
|
|
11007
|
-
if (!source.startsWith("https://") && !source.startsWith("http://")) {
|
|
11008
|
-
images.push(source);
|
|
11009
|
-
continue;
|
|
11010
|
-
}
|
|
11011
|
-
try {
|
|
11012
|
-
const response = await fetch(source, { method: "GET" });
|
|
11013
|
-
if (!response.ok) return err$1(new MotifError(`Could not download ${source}: ${response.status}`, response.status, "DOWNLOAD_FAILED"));
|
|
11014
|
-
images.push(new Uint8Array(await response.arrayBuffer()));
|
|
11015
|
-
} catch (error) {
|
|
11016
|
-
return err$1(new MotifError(`Could not download ${source}: ${error instanceof Error ? error.message : String(error)}`, 0, "DOWNLOAD_FAILED"));
|
|
11017
|
-
}
|
|
11018
|
-
}
|
|
11019
|
-
return ok$1(images);
|
|
11020
|
-
}
|
|
11021
|
-
async function openAiOutput(plan, openAiKey, config) {
|
|
11022
|
-
const fetch = config.fetch ?? (async (url, init) => await globalThis.fetch(url, init));
|
|
11023
|
-
const client = createMotifImage({
|
|
11024
|
-
defaultProvider: "openai",
|
|
11025
|
-
fetch,
|
|
11026
|
-
openai: { apiKey: openAiKey },
|
|
11027
|
-
...config.retries !== void 0 && { maxRetries: config.retries }
|
|
11028
|
-
});
|
|
11029
|
-
const signal = AbortSignal.timeout(config.timeout ?? DEFAULT_TIMEOUT_MS);
|
|
11030
|
-
const { body } = plan;
|
|
11031
|
-
const [provider, model = ""] = plan.endpoint.split(":");
|
|
11032
|
-
const n = typeof body.n === "number" ? body.n : 1;
|
|
11033
|
-
const prompt = typeof body.prompt === "string" ? body.prompt : "";
|
|
11034
|
-
const providerOptions = { openai: {
|
|
11035
|
-
background: "transparent",
|
|
11036
|
-
outputFormat: "png"
|
|
11037
|
-
} };
|
|
11038
|
-
const sources = Array.isArray(body.images) ? body.images.filter((item) => typeof item === "string") : [];
|
|
11039
|
-
const mask = typeof body.mask === "string" ? [body.mask] : [];
|
|
11040
|
-
const downloaded = await localImages([...sources, ...mask], fetch);
|
|
11041
|
-
if (downloaded.isErr()) return err$1(downloaded.error);
|
|
11042
|
-
const images = downloaded.value.slice(0, sources.length);
|
|
11043
|
-
const maskImage = downloaded.value[sources.length];
|
|
11044
|
-
const size = typeof body.size === "string" ? body.size : "auto";
|
|
11045
|
-
const common = {
|
|
11046
|
-
model,
|
|
11047
|
-
n,
|
|
11048
|
-
provider,
|
|
11049
|
-
providerOptions,
|
|
11050
|
-
signal
|
|
11051
|
-
};
|
|
11052
|
-
const result = images.length === 0 ? await client.generate({
|
|
11053
|
-
...common,
|
|
11054
|
-
prompt,
|
|
11055
|
-
...isPixelSize(size) && { size }
|
|
11056
|
-
}) : await client.edit({
|
|
11057
|
-
...common,
|
|
11058
|
-
images,
|
|
11059
|
-
instruction: prompt,
|
|
11060
|
-
...maskImage !== void 0 && { mask: maskImage }
|
|
11061
|
-
});
|
|
11062
|
-
if (result.isErr()) return err$1(result.error);
|
|
11063
|
-
const value = result.value;
|
|
11064
|
-
let cost = {
|
|
11065
|
-
basis: "unknown",
|
|
11066
|
-
usd: null
|
|
11067
|
-
};
|
|
11068
|
-
if (value.cost.source === "provider-metadata") cost = {
|
|
11069
|
-
basis: "measured",
|
|
11070
|
-
usd: value.cost.usd
|
|
11071
|
-
};
|
|
11072
|
-
else if (value.cost.source === "table") cost = {
|
|
11073
|
-
basis: "projected",
|
|
11074
|
-
usd: value.cost.usd
|
|
11075
|
-
};
|
|
11076
|
-
return ok$1({
|
|
11077
|
-
chosenBy: plan.chosenBy,
|
|
11078
|
-
cost,
|
|
11079
|
-
data: {},
|
|
11080
|
-
endpoint: plan.endpoint,
|
|
11081
|
-
files: value.images.map((image) => ({
|
|
11082
|
-
key: "images",
|
|
11083
|
-
url: `data:${image.mediaType};base64,${image.base64}`
|
|
11084
|
-
})),
|
|
11085
|
-
model: plan.model,
|
|
11086
|
-
...plan.mode !== void 0 && { mode: plan.mode },
|
|
11087
|
-
...plan.prompt !== void 0 && { prompt: plan.prompt },
|
|
11088
|
-
provider: plan.provider,
|
|
11089
|
-
raw: {
|
|
11090
|
-
model: value.model,
|
|
11091
|
-
provider: value.provider,
|
|
11092
|
-
...value.warnings !== void 0 && { warnings: value.warnings }
|
|
11093
|
-
},
|
|
11094
|
-
...value.requestId !== void 0 && { requestId: value.requestId },
|
|
11095
|
-
task: plan.task,
|
|
11096
|
-
tier: plan.tier
|
|
11097
|
-
});
|
|
11098
|
-
}
|
|
11099
9360
|
function taskStreamer(config, plan, context) {
|
|
11100
9361
|
return async (task, input, options = {}) => {
|
|
11101
9362
|
const planned = plan(task, input);
|
|
@@ -11106,12 +9367,11 @@ function taskStreamer(config, plan, context) {
|
|
|
11106
9367
|
});
|
|
11107
9368
|
};
|
|
11108
9369
|
}
|
|
11109
|
-
/** Create the Task client.
|
|
9370
|
+
/** Create the Task client. The key falls back to FAL_KEY. */
|
|
11110
9371
|
function createMotif(config = {}) {
|
|
11111
9372
|
function context() {
|
|
11112
9373
|
return {
|
|
11113
9374
|
falKey: presentKey(config.falKey ?? getFalKeyFromEnv()),
|
|
11114
|
-
openAiKey: presentKey(config.openAiKey ?? getOpenAiKeyFromEnv()),
|
|
11115
9375
|
pins: config.pins
|
|
11116
9376
|
};
|
|
11117
9377
|
}
|
|
@@ -11134,11 +9394,6 @@ function createMotif(config = {}) {
|
|
|
11134
9394
|
const planned = plan(task, input);
|
|
11135
9395
|
if (planned.isErr()) return err$1(planned.error);
|
|
11136
9396
|
const chosen = planned.value;
|
|
11137
|
-
if (chosen.provider === "openai") {
|
|
11138
|
-
const openAiKey = context().openAiKey;
|
|
11139
|
-
if (openAiKey === void 0) return err$1(new MotifError("OPENAI_API_KEY is not set.", 0, MISSING_API_KEY, void 0, { envVar: "OPENAI_API_KEY" }));
|
|
11140
|
-
return await openAiOutput(chosen, openAiKey, config);
|
|
11141
|
-
}
|
|
11142
9397
|
const headers = input.ephemeral === true ? { "X-Fal-Store-IO": "0" } : {};
|
|
11143
9398
|
const prepared = {
|
|
11144
9399
|
body: chosen.body,
|
|
@@ -11152,7 +9407,6 @@ function createMotif(config = {}) {
|
|
|
11152
9407
|
const planned = plan(task, input);
|
|
11153
9408
|
if (planned.isErr()) return err$1(planned.error);
|
|
11154
9409
|
const chosen = planned.value;
|
|
11155
|
-
if (chosen.provider !== "fal") return err$1(new MotifError(`Only a fal run can be resumed; ${chosen.model} runs on ${chosen.provider}.`, 0));
|
|
11156
9410
|
const client = falClient();
|
|
11157
9411
|
if (client.isErr()) return err$1(client.error);
|
|
11158
9412
|
return (await awaitQueuedRequest(falRequestExecutor(client.value), {
|
|
@@ -11198,4 +9452,4 @@ function createMotif(config = {}) {
|
|
|
11198
9452
|
};
|
|
11199
9453
|
}
|
|
11200
9454
|
//#endregion
|
|
11201
|
-
export { ACCOUNT_LOCKED, ASPECT_RATIOS, CREATIVE_FIELDS, CREATIVE_TAXONOMY, CreativeOptionError, DEFAULT_TIER, FORMAT_PRESETS, INVALID_OPTION, ImageSizeBoundsError, LOOKS, MotifError, NO_MODEL_AVAILABLE, RESOLUTIONS, TASKS, TASK_IDS, TIERS, createMotif, enrichPrompt, err, formatCost, getFalKeyFromEnv, getLook,
|
|
9455
|
+
export { ACCOUNT_LOCKED, ASPECT_RATIOS, CREATIVE_FIELDS, CREATIVE_TAXONOMY, CreativeOptionError, DEFAULT_TIER, FORMAT_PRESETS, INVALID_OPTION, ImageSizeBoundsError, LOOKS, MotifError, NO_MODEL_AVAILABLE, RESOLUTIONS, TASKS, TASK_IDS, TIERS, createMotif, enrichPrompt, err, formatCost, getFalKeyFromEnv, getLook, isFalAccountLocked, isTaskId, modelProfile, motifEnvSchema, ok, parseMotifEnv, promptWarnings, resolveTask, sanitizePrompt, sumCosts, tierChangesChoice, validateCreativeDirection };
|