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