@brotu/ai 0.3.0 → 0.5.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
@@ -30,9 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BROTU_SUPPORTED_CATEGORIES: () => BROTU_SUPPORTED_CATEGORIES,
33
34
  BYTEPLUS_CATALOG: () => BYTEPLUS_CATALOG,
34
35
  BYTEPLUS_MODELS: () => BYTEPLUS_MODELS,
36
+ BrotuAdapter: () => BrotuAdapter,
35
37
  BytePlusAdapter: () => BytePlusAdapter,
38
+ DEFAULT_BROTU_API_URL: () => DEFAULT_BROTU_API_URL,
36
39
  ELEVENLABS_CATALOG: () => ELEVENLABS_CATALOG,
37
40
  ELEVENLABS_MODELS: () => ELEVENLABS_MODELS,
38
41
  ELEVENLABS_OUTPUT_FORMATS: () => ELEVENLABS_OUTPUT_FORMATS,
@@ -59,9 +62,10 @@ __export(index_exports, {
59
62
  QWEN_TEXT_MODELS: () => QWEN_TEXT_MODELS,
60
63
  QWEN_VIDEO_MODELS: () => QWEN_VIDEO_MODELS,
61
64
  QwenAdapter: () => QwenAdapter,
62
- brotuClient: () => brotuClient,
65
+ brotu: () => brotu,
63
66
  createS3Storage: () => createS3Storage,
64
67
  deliverWebhook: () => deliverWebhook,
68
+ describeModels: () => describeModels,
65
69
  fail: () => fail,
66
70
  getAvailableModels: () => getAvailableModels,
67
71
  getModel: () => getModel,
@@ -78,34 +82,6 @@ __export(index_exports, {
78
82
  });
79
83
  module.exports = __toCommonJS(index_exports);
80
84
 
81
- // src/lib/jobs.ts
82
- var import_node_async_hooks = require("async_hooks");
83
- var PendingJob = class extends Error {
84
- taskId;
85
- pollEndpoint;
86
- constructor(taskId, pollEndpoint) {
87
- super(`Job ${taskId} was submitted and is still running.`);
88
- this.name = "PendingJob";
89
- this.taskId = taskId;
90
- this.pollEndpoint = pollEndpoint;
91
- }
92
- };
93
- function isPendingJob(error) {
94
- return error instanceof PendingJob;
95
- }
96
- var submitMode = new import_node_async_hooks.AsyncLocalStorage();
97
- function runInSubmitMode(fn) {
98
- return submitMode.run(true, fn);
99
- }
100
- function isSubmitMode() {
101
- return submitMode.getStore() === true;
102
- }
103
-
104
- // src/ports/content-generator.port.ts
105
- function expiresInHours(hours) {
106
- return new Date(Date.now() + hours * 36e5).toISOString();
107
- }
108
-
109
85
  // src/providers/byteplus.models.ts
110
86
  var FPS = 24;
111
87
  var PIXELS = {
@@ -1267,8 +1243,16 @@ var PROVIDER_BASE_URLS = {
1267
1243
  elevenlabs: "https://api.elevenlabs.io",
1268
1244
  google: "https://generativelanguage.googleapis.com",
1269
1245
  openai: "https://api.openai.com",
1270
- qwen: "https://dashscope-intl.aliyuncs.com"
1246
+ qwen: "https://dashscope-intl.aliyuncs.com",
1247
+ brotu: "https://api.brotu.app"
1271
1248
  };
1249
+ var DEFAULT_BROTU_API_URL = PROVIDER_BASE_URLS.brotu;
1250
+ function vendorProviders(options) {
1251
+ return options.providers ?? {};
1252
+ }
1253
+ function brotuApiKey(options) {
1254
+ return options.apiKey?.trim() || void 0;
1255
+ }
1272
1256
  var BUILT_IN = [
1273
1257
  ...KLING_CATALOG,
1274
1258
  ...KLING_AUDIO_CATALOG,
@@ -1324,25 +1308,95 @@ function resolveProvider(modelId, options) {
1324
1308
  `Model "${modelId}" declares no provider, so nothing knows how to run it.`
1325
1309
  );
1326
1310
  }
1327
- const configured = options.providers[id];
1328
- if (!configured) {
1329
- const owned = Object.keys(options.providers).join(", ") || "none";
1330
- throw new Error(
1331
- `Model "${modelId}" runs on "${id}", but no API key was given for it (configured: ${owned}).`
1332
- );
1311
+ const configured = vendorProviders(options)[id];
1312
+ if (configured) {
1313
+ const baseUrl = configured.baseUrl ?? PROVIDER_BASE_URLS[id];
1314
+ if (!baseUrl) {
1315
+ throw new Error(
1316
+ `Provider "${id}" has no known base URL \u2014 pass providers.${id}.baseUrl.`
1317
+ );
1318
+ }
1319
+ return { id, apiKey: configured.apiKey, baseUrl: baseUrl.replace(/\/$/, "") };
1333
1320
  }
1334
- const baseUrl = configured.baseUrl ?? PROVIDER_BASE_URLS[id];
1335
- if (!baseUrl) {
1336
- throw new Error(
1337
- `Provider "${id}" has no known base URL \u2014 pass providers.${id}.baseUrl.`
1338
- );
1321
+ const platformKey = brotuApiKey(options);
1322
+ if (platformKey) {
1323
+ return {
1324
+ id: "brotu",
1325
+ apiKey: platformKey,
1326
+ baseUrl: (options.apiUrl ?? DEFAULT_BROTU_API_URL).replace(/\/$/, "")
1327
+ };
1339
1328
  }
1340
- return { id, apiKey: configured.apiKey, baseUrl: baseUrl.replace(/\/$/, "") };
1329
+ const owned = Object.keys(vendorProviders(options)).join(", ") || "none";
1330
+ throw new Error(
1331
+ `Model "${modelId}" runs on "${id}", but no API key was given for it (configured: ${owned}). Pass a Brotu key to generate on the platform.`
1332
+ );
1333
+ }
1334
+ var BROTU_SUPPORTED_CATEGORIES = ["image", "video"];
1335
+ function describeModels(options) {
1336
+ const vendors = vendorProviders(options);
1337
+ const platformKey = brotuApiKey(options);
1338
+ return catalog.map((model) => {
1339
+ const provider = model.provider;
1340
+ if (!provider) {
1341
+ return {
1342
+ model,
1343
+ runsOn: "(none)",
1344
+ runnable: false,
1345
+ reason: "The model declares no provider."
1346
+ };
1347
+ }
1348
+ if (provider in vendors) {
1349
+ return { model, runsOn: provider, runnable: true };
1350
+ }
1351
+ if (platformKey) {
1352
+ if (BROTU_SUPPORTED_CATEGORIES.includes(model.category)) {
1353
+ return { model, runsOn: "brotu", runnable: true };
1354
+ }
1355
+ return {
1356
+ model,
1357
+ runsOn: provider,
1358
+ runnable: false,
1359
+ reason: `Brotu generates ${BROTU_SUPPORTED_CATEGORIES.join(" and ")} only. Pass providers.${provider} to run this one.`
1360
+ };
1361
+ }
1362
+ return {
1363
+ model,
1364
+ runsOn: provider,
1365
+ runnable: false,
1366
+ reason: `No key for "${provider}". Pass a Brotu key or providers.${provider}.`
1367
+ };
1368
+ });
1341
1369
  }
1342
1370
  function getAvailableModels(options) {
1343
- return catalog.filter(
1344
- (model) => model.provider && model.provider in options.providers
1345
- );
1371
+ return describeModels(options).filter((entry) => entry.runnable).map((entry) => entry.model);
1372
+ }
1373
+
1374
+ // src/lib/jobs.ts
1375
+ var import_node_async_hooks = require("async_hooks");
1376
+ var PendingJob = class extends Error {
1377
+ taskId;
1378
+ pollEndpoint;
1379
+ constructor(taskId, pollEndpoint) {
1380
+ super(`Job ${taskId} was submitted and is still running.`);
1381
+ this.name = "PendingJob";
1382
+ this.taskId = taskId;
1383
+ this.pollEndpoint = pollEndpoint;
1384
+ }
1385
+ };
1386
+ function isPendingJob(error) {
1387
+ return error instanceof PendingJob;
1388
+ }
1389
+ var submitMode = new import_node_async_hooks.AsyncLocalStorage();
1390
+ function runInSubmitMode(fn) {
1391
+ return submitMode.run(true, fn);
1392
+ }
1393
+ function isSubmitMode() {
1394
+ return submitMode.getStore() === true;
1395
+ }
1396
+
1397
+ // src/ports/content-generator.port.ts
1398
+ function expiresInHours(hours) {
1399
+ return new Date(Date.now() + hours * 36e5).toISOString();
1346
1400
  }
1347
1401
 
1348
1402
  // src/adapters/estimate.ts
@@ -1371,12 +1425,316 @@ function estimateFor(provider, type, params, defaults) {
1371
1425
  };
1372
1426
  }
1373
1427
 
1428
+ // src/adapters/brotu.adapter.ts
1429
+ var DEFAULT_API_URL = "https://api.brotu.app";
1430
+ var POLL_INTERVAL_MS = 3e3;
1431
+ var DEFAULT_MAX_POLL_ATTEMPTS = 400;
1432
+ var PLATFORM_MODEL_IDS = {
1433
+ "kling/v2-6": "kling-2.6",
1434
+ "kling/v3": "kling-3.0/video",
1435
+ "dreamina-seedance-2-5-260628": "bytedance/seedance-2-5",
1436
+ "dreamina-seedance-2-0-260128": "bytedance/seedance-2",
1437
+ "dreamina-seedance-2-0-fast-260128": "bytedance/seedance-2-fast",
1438
+ "gpt-image-1.5": "gpt-image/1.5"
1439
+ };
1440
+ var UNSUPPORTED_VIA_CREDITS = "Speech and text generate on the vendor. Pass that provider's key.";
1441
+ var BrotuAdapter = class {
1442
+ constructor(opts) {
1443
+ this.opts = opts;
1444
+ }
1445
+ opts;
1446
+ providerName = "brotu";
1447
+ supportedTypes = BROTU_SUPPORTED_CATEGORIES;
1448
+ workspaceIdCache;
1449
+ get origin() {
1450
+ return (this.opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
1451
+ }
1452
+ headers() {
1453
+ return {
1454
+ Authorization: `Bearer ${this.opts.apiKey}`,
1455
+ "Content-Type": "application/json"
1456
+ };
1457
+ }
1458
+ async workspaceId() {
1459
+ if (this.opts.workspaceId) return this.opts.workspaceId;
1460
+ if (this.workspaceIdCache) return this.workspaceIdCache;
1461
+ const payload = await this.request("/api/v1/studio/default-workspace");
1462
+ const id = payload.workspaceId?.trim();
1463
+ if (!id) {
1464
+ throw new Error(
1465
+ "This Brotu account has no workspace. Open https://brotu.app and create one."
1466
+ );
1467
+ }
1468
+ this.workspaceIdCache = id;
1469
+ return id;
1470
+ }
1471
+ studioPath(path, workspaceId) {
1472
+ const joiner = path.includes("?") ? "&" : "?";
1473
+ return `${path}${joiner}workspaceId=${encodeURIComponent(workspaceId)}`;
1474
+ }
1475
+ async request(path, init) {
1476
+ const response = await fetch(`${this.origin}${path}`, {
1477
+ ...init,
1478
+ headers: { ...this.headers(), ...init?.headers }
1479
+ });
1480
+ const text = await response.text();
1481
+ let body = void 0;
1482
+ if (text) {
1483
+ try {
1484
+ body = JSON.parse(text);
1485
+ } catch {
1486
+ body = text;
1487
+ }
1488
+ }
1489
+ if (!response.ok) {
1490
+ const parsed = body;
1491
+ const message = parsed && typeof parsed === "object" ? parsed.error?.message || parsed.message || text : text;
1492
+ throw new Error(message || `Brotu API ${response.status}`);
1493
+ }
1494
+ return body;
1495
+ }
1496
+ platformModel(modelId) {
1497
+ return PLATFORM_MODEL_IDS[modelId] ?? modelId;
1498
+ }
1499
+ async startGeneration(kind, params) {
1500
+ const modelId = params.model ?? "";
1501
+ const workspaceId = await this.workspaceId();
1502
+ const path = kind === "video" ? "/api/v1/studio/videos" : "/api/v1/studio/images";
1503
+ const body = kind === "video" ? this.videoBody(params, modelId) : this.imageBody(params, modelId);
1504
+ const result = await this.request(
1505
+ this.studioPath(path, workspaceId),
1506
+ { method: "POST", body: JSON.stringify(body) }
1507
+ );
1508
+ const generationId = result.generationId?.trim();
1509
+ if (!generationId) {
1510
+ throw new Error("Brotu accepted the request but returned no generation id.");
1511
+ }
1512
+ return { generationId, workspaceId };
1513
+ }
1514
+ imageBody(params, modelId) {
1515
+ return {
1516
+ prompt: params.prompt,
1517
+ model: this.platformModel(modelId),
1518
+ aspectRatio: params.aspectRatio,
1519
+ resolution: params.resolution,
1520
+ negativePrompt: params.negativePrompt,
1521
+ outputFormat: params.outputFormat,
1522
+ seed: params.seed,
1523
+ referenceImages: params.referenceImages
1524
+ };
1525
+ }
1526
+ videoBody(params, modelId) {
1527
+ return {
1528
+ prompt: params.prompt,
1529
+ model: this.platformModel(modelId),
1530
+ duration: params.duration,
1531
+ resolution: params.resolution,
1532
+ aspectRatio: params.aspectRatio,
1533
+ mode: params.mode,
1534
+ withAudio: params.withAudio,
1535
+ seed: params.seed,
1536
+ imageUrl: params.imageUrl,
1537
+ imageUrls: params.imageUrls,
1538
+ videoUrl: params.videoUrl,
1539
+ videoUrls: params.videoUrls,
1540
+ referenceImages: params.referenceImages
1541
+ };
1542
+ }
1543
+ async run(kind, params) {
1544
+ const startedAt = Date.now();
1545
+ const modelId = params.model ?? "(none)";
1546
+ const failure = (error) => ({
1547
+ success: false,
1548
+ outputs: [],
1549
+ creditsUsed: 0,
1550
+ provider: this.providerName,
1551
+ model: modelId,
1552
+ processingTimeMs: Date.now() - startedAt,
1553
+ error
1554
+ });
1555
+ let submitted;
1556
+ try {
1557
+ submitted = await this.startGeneration(kind, params);
1558
+ } catch (error) {
1559
+ return failure(error instanceof Error ? error.message : String(error));
1560
+ }
1561
+ const pollEndpoint = this.studioPath(
1562
+ `/api/v1/studio/generations/${submitted.generationId}`,
1563
+ submitted.workspaceId
1564
+ );
1565
+ if (isSubmitMode()) {
1566
+ throw new PendingJob(submitted.generationId, pollEndpoint);
1567
+ }
1568
+ const job = {
1569
+ id: submitted.generationId,
1570
+ provider: this.providerName,
1571
+ model: modelId,
1572
+ kind,
1573
+ pollEndpoint,
1574
+ params,
1575
+ submittedAt: new Date(startedAt).toISOString()
1576
+ };
1577
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS;
1578
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1579
+ const snapshot = await this.completeJob(job);
1580
+ if (snapshot.status === "failed") {
1581
+ return failure(snapshot.error ?? "Brotu generation failed.");
1582
+ }
1583
+ if (snapshot.status === "succeeded" && snapshot.result) {
1584
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
1585
+ }
1586
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1587
+ }
1588
+ return failure(
1589
+ `Brotu generation ${submitted.generationId} did not finish after ${maxAttempts} checks.`
1590
+ );
1591
+ }
1592
+ async completeJob(job) {
1593
+ const workspaceId = this.workspaceFromPoll(job.pollEndpoint) ?? await this.workspaceId();
1594
+ const path = this.studioPath(
1595
+ `/api/v1/studio/generations/${job.id}`,
1596
+ workspaceId
1597
+ );
1598
+ const generation = await this.request(path);
1599
+ const status = (generation.status ?? "").toLowerCase();
1600
+ if (status === "failed") {
1601
+ return {
1602
+ status: "failed",
1603
+ error: generation.errorMessage ?? "Brotu generation failed."
1604
+ };
1605
+ }
1606
+ if (status !== "completed") {
1607
+ return { status: "pending" };
1608
+ }
1609
+ const outputs = outputsFrom(generation, job.kind);
1610
+ return {
1611
+ status: "succeeded",
1612
+ result: {
1613
+ success: true,
1614
+ outputs,
1615
+ creditsUsed: generation.creditsUsed ?? 0,
1616
+ provider: this.providerName,
1617
+ model: job.model,
1618
+ processingTimeMs: 0
1619
+ }
1620
+ };
1621
+ }
1622
+ workspaceFromPoll(pollEndpoint) {
1623
+ if (!pollEndpoint) return void 0;
1624
+ try {
1625
+ const url = new URL(pollEndpoint, "https://brotu.invalid");
1626
+ return url.searchParams.get("workspaceId") ?? void 0;
1627
+ } catch {
1628
+ return void 0;
1629
+ }
1630
+ }
1631
+ generateImage(params) {
1632
+ return this.run("image", params);
1633
+ }
1634
+ generateVideo(params) {
1635
+ return this.run("video", params);
1636
+ }
1637
+ async generateText(params) {
1638
+ return {
1639
+ success: false,
1640
+ outputs: [],
1641
+ creditsUsed: 0,
1642
+ provider: this.providerName,
1643
+ model: params.model ?? "(none)",
1644
+ processingTimeMs: 0,
1645
+ error: UNSUPPORTED_VIA_CREDITS
1646
+ };
1647
+ }
1648
+ async generateAudio(params) {
1649
+ return {
1650
+ success: false,
1651
+ outputs: [],
1652
+ creditsUsed: 0,
1653
+ provider: this.providerName,
1654
+ model: params.model ?? "(none)",
1655
+ processingTimeMs: 0,
1656
+ error: UNSUPPORTED_VIA_CREDITS
1657
+ };
1658
+ }
1659
+ async estimateCost(type, params) {
1660
+ if (type === "audio" || type === "text") {
1661
+ const estimate = estimateFor(this.providerName, type, params);
1662
+ return {
1663
+ ...estimate,
1664
+ usd: null,
1665
+ note: UNSUPPORTED_VIA_CREDITS
1666
+ };
1667
+ }
1668
+ try {
1669
+ const workspaceId = await this.workspaceId();
1670
+ const video = params;
1671
+ const image = params;
1672
+ const payload = await this.request(
1673
+ this.studioPath("/api/v1/studio/estimate", workspaceId),
1674
+ {
1675
+ method: "POST",
1676
+ body: JSON.stringify({
1677
+ model: this.platformModel(params.model ?? ""),
1678
+ duration: video.duration,
1679
+ resolution: video.resolution ?? image.resolution,
1680
+ aspectRatio: video.aspectRatio ?? image.aspectRatio,
1681
+ withAudio: video.withAudio,
1682
+ hasReferenceImage: Boolean(
1683
+ video.imageUrl || video.imageUrls?.length || video.referenceImages?.length || image.referenceImages?.length
1684
+ ),
1685
+ hasReferenceVideo: Boolean(video.videoUrl || video.videoUrls?.length),
1686
+ quality: image.quality
1687
+ })
1688
+ }
1689
+ );
1690
+ const credits = payload.estimatedCredits ?? 0;
1691
+ return {
1692
+ unit: type === "video" ? "second" : "image",
1693
+ units: credits,
1694
+ usd: null,
1695
+ note: `${credits} Brotu credit${credits === 1 ? "" : "s"}. A vendor key generates on that provider.`,
1696
+ provider: this.providerName,
1697
+ model: params.model ?? ""
1698
+ };
1699
+ } catch (error) {
1700
+ const estimate = estimateFor(this.providerName, type, params);
1701
+ return {
1702
+ ...estimate,
1703
+ usd: null,
1704
+ note: error instanceof Error ? error.message : "Could not estimate Brotu credits."
1705
+ };
1706
+ }
1707
+ }
1708
+ supportsModel() {
1709
+ return true;
1710
+ }
1711
+ getAvailableModels() {
1712
+ return [];
1713
+ }
1714
+ };
1715
+ function outputsFrom(generation, kind) {
1716
+ const bucket = generation.outputs;
1717
+ const urls = kind === "video" ? bucket?.videos ?? bucket?.images ?? [] : bucket?.images ?? bucket?.videos ?? [];
1718
+ const copies = bucket?.copies ?? [];
1719
+ const mimeType = kind === "video" ? "video/mp4" : "image/png";
1720
+ const fromUrls = urls.filter(Boolean).map((url) => ({
1721
+ url,
1722
+ mimeType
1723
+ }));
1724
+ if (fromUrls.length > 0) return fromUrls;
1725
+ return copies.filter(Boolean).map((url) => ({
1726
+ url,
1727
+ mimeType: "text/plain",
1728
+ raw: { text: url }
1729
+ }));
1730
+ }
1731
+
1374
1732
  // src/adapters/byteplus.adapter.ts
1375
1733
  var DEFAULT_BASE_URL = "https://ark.ap-southeast.bytepluses.com";
1376
1734
  var TASKS_PATH = "/api/v3/contents/generations/tasks";
1377
1735
  var IMAGES_PATH = "/api/v3/images/generations";
1378
- var POLL_INTERVAL_MS = 5e3;
1379
- var DEFAULT_MAX_POLL_ATTEMPTS = 240;
1736
+ var POLL_INTERVAL_MS2 = 5e3;
1737
+ var DEFAULT_MAX_POLL_ATTEMPTS2 = 240;
1380
1738
  var BytePlusAdapter = class {
1381
1739
  providerName = "byteplus";
1382
1740
  supportedTypes = ["image", "video"];
@@ -1576,7 +1934,7 @@ var BytePlusAdapter = class {
1576
1934
  params,
1577
1935
  submittedAt: new Date(startedAt).toISOString()
1578
1936
  };
1579
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS;
1937
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS2;
1580
1938
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1581
1939
  const snapshot = await this.completeJob(job);
1582
1940
  if (snapshot.status === "failed") {
@@ -1585,7 +1943,7 @@ var BytePlusAdapter = class {
1585
1943
  if (snapshot.status === "succeeded" && snapshot.result) {
1586
1944
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
1587
1945
  }
1588
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
1946
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS2));
1589
1947
  }
1590
1948
  return failure(
1591
1949
  `Ark task ${taskId} did not finish after ${maxAttempts} checks.`
@@ -1862,8 +2220,8 @@ var ElevenLabsAdapter = class {
1862
2220
  // src/adapters/google.adapter.ts
1863
2221
  var DEFAULT_BASE_URL3 = "https://generativelanguage.googleapis.com";
1864
2222
  var INTERACTIONS_PATH = "/v1beta/interactions";
1865
- var POLL_INTERVAL_MS2 = 1e4;
1866
- var DEFAULT_MAX_POLL_ATTEMPTS2 = 120;
2223
+ var POLL_INTERVAL_MS3 = 1e4;
2224
+ var DEFAULT_MAX_POLL_ATTEMPTS3 = 120;
1867
2225
  var GoogleAdapter = class {
1868
2226
  providerName = "google";
1869
2227
  supportedTypes = [
@@ -2135,7 +2493,7 @@ var GoogleAdapter = class {
2135
2493
  params,
2136
2494
  submittedAt: new Date(startedAt).toISOString()
2137
2495
  };
2138
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS2;
2496
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS3;
2139
2497
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2140
2498
  const snapshot = await this.completeJob(job);
2141
2499
  if (snapshot.status === "failed") {
@@ -2144,7 +2502,7 @@ var GoogleAdapter = class {
2144
2502
  if (snapshot.status === "succeeded" && snapshot.result) {
2145
2503
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
2146
2504
  }
2147
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS2));
2505
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS3));
2148
2506
  }
2149
2507
  return failure(`Veo operation ${operation} did not finish in time.`);
2150
2508
  }
@@ -2359,8 +2717,8 @@ var GoogleAdapter = class {
2359
2717
 
2360
2718
  // src/adapters/kling.adapter.ts
2361
2719
  var DEFAULT_BASE_URL4 = "https://api-singapore.klingai.com";
2362
- var POLL_INTERVAL_MS3 = 5e3;
2363
- var DEFAULT_MAX_POLL_ATTEMPTS3 = 240;
2720
+ var POLL_INTERVAL_MS4 = 5e3;
2721
+ var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
2364
2722
  function stateOf(task) {
2365
2723
  const raw = (task.status ?? task.task_status ?? "").toLowerCase();
2366
2724
  if (raw === "failed") return "failed";
@@ -2557,7 +2915,7 @@ var KlingAdapter = class {
2557
2915
  params,
2558
2916
  submittedAt: new Date(startedAt).toISOString()
2559
2917
  };
2560
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS3;
2918
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
2561
2919
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2562
2920
  const snapshot = await this.completeJob(job);
2563
2921
  if (snapshot.status === "failed") {
@@ -2566,7 +2924,7 @@ var KlingAdapter = class {
2566
2924
  if (snapshot.status === "succeeded" && snapshot.result) {
2567
2925
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
2568
2926
  }
2569
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS3));
2927
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
2570
2928
  }
2571
2929
  return failure(
2572
2930
  `Kling task ${submitted.taskId} did not finish after ${maxAttempts} checks.`
@@ -2937,8 +3295,8 @@ var OpenAIAdapter = class {
2937
3295
  // src/adapters/qwen.adapter.ts
2938
3296
  var DEFAULT_BASE_URL6 = "https://dashscope-intl.aliyuncs.com";
2939
3297
  var TASKS_PATH2 = "/api/v1/tasks";
2940
- var POLL_INTERVAL_MS4 = 5e3;
2941
- var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
3298
+ var POLL_INTERVAL_MS5 = 5e3;
3299
+ var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
2942
3300
  var QwenAdapter = class {
2943
3301
  providerName = "qwen";
2944
3302
  supportedTypes = ["image", "video"];
@@ -3145,7 +3503,7 @@ var QwenAdapter = class {
3145
3503
  params,
3146
3504
  submittedAt: new Date(startedAt).toISOString()
3147
3505
  };
3148
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
3506
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
3149
3507
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
3150
3508
  const snapshot = await this.completeJob(job);
3151
3509
  if (snapshot.status === "failed") {
@@ -3154,7 +3512,7 @@ var QwenAdapter = class {
3154
3512
  if (snapshot.status === "succeeded" && snapshot.result) {
3155
3513
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
3156
3514
  }
3157
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
3515
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
3158
3516
  }
3159
3517
  return failure(
3160
3518
  `DashScope task ${taskId} did not finish after ${maxAttempts} checks.`
@@ -3414,6 +3772,22 @@ function failFrom(code, cause, context) {
3414
3772
  });
3415
3773
  }
3416
3774
 
3775
+ // src/lib/hooks.ts
3776
+ function hookName(kind, stage) {
3777
+ return `on${kind[0].toUpperCase()}${kind.slice(1)}${stage}`;
3778
+ }
3779
+ function findHook(hooks, kind, stage) {
3780
+ if (!hooks || !kind) return void 0;
3781
+ return hooks[hookName(kind, stage)];
3782
+ }
3783
+ async function runHook(hook, event) {
3784
+ if (!hook) return;
3785
+ try {
3786
+ await hook(event);
3787
+ } catch {
3788
+ }
3789
+ }
3790
+
3417
3791
  // src/lib/storage.ts
3418
3792
  async function loadS3(config) {
3419
3793
  let s3;
@@ -3584,10 +3958,15 @@ var NATIVE_PROVIDERS = [
3584
3958
  "openai",
3585
3959
  "qwen"
3586
3960
  ];
3587
- function brotuClient(options) {
3588
- if (Object.keys(options.providers).length === 0) {
3589
- throw new Error("brotuClient requires at least one provider API key.");
3961
+ function brotu(options) {
3962
+ const apiKey = options.apiKey?.trim();
3963
+ if (!apiKey) {
3964
+ throw new Error(
3965
+ "Pass a Brotu API key (brotu_sk_\u2026). Get one at https://brotu.app."
3966
+ );
3590
3967
  }
3968
+ const optionsWithKey = { ...options, apiKey };
3969
+ const vendors = optionsWithKey.providers ?? {};
3591
3970
  registerModels(options.models);
3592
3971
  let registeredWebhook = resolveWebhook(options.webhook);
3593
3972
  const notifiedJobIds = /* @__PURE__ */ new Set();
@@ -3611,7 +3990,7 @@ function brotuClient(options) {
3611
3990
  }
3612
3991
  let provider;
3613
3992
  try {
3614
- provider = resolveProvider(modelId, options);
3993
+ provider = resolveProvider(modelId, optionsWithKey);
3615
3994
  } catch (error) {
3616
3995
  return failFrom("missing_key", error, { model: modelId });
3617
3996
  }
@@ -3632,6 +4011,13 @@ function brotuClient(options) {
3632
4011
  return ok({ adapter, provider: provider.id, model: modelId });
3633
4012
  }
3634
4013
  function buildAdapter(provider) {
4014
+ if (provider.id === "brotu") {
4015
+ return new BrotuAdapter({
4016
+ apiKey,
4017
+ apiUrl: optionsWithKey.apiUrl,
4018
+ workspaceId: optionsWithKey.workspaceId
4019
+ });
4020
+ }
3635
4021
  if (provider.id === "kling") {
3636
4022
  return new KlingAdapter({
3637
4023
  apiKey: provider.apiKey,
@@ -3685,7 +4071,10 @@ function brotuClient(options) {
3685
4071
  }
3686
4072
  async function notifySettled(input) {
3687
4073
  const config = webhookFor(input.params);
3688
- if (!config) return;
4074
+ const kind = input.kind ?? input.job?.kind;
4075
+ const stage = input.event === "generation.succeeded" ? "Success" : "Error";
4076
+ const hook = findHook(options.hooks, kind, stage);
4077
+ if (!config && !hook) return;
3689
4078
  const jobId = input.job?.id;
3690
4079
  if (jobId) {
3691
4080
  if (notifiedJobIds.has(jobId)) return;
@@ -3696,14 +4085,38 @@ function brotuClient(options) {
3696
4085
  jobId,
3697
4086
  provider: input.provider ?? input.job?.provider,
3698
4087
  model: input.model ?? input.job?.model,
3699
- kind: input.kind ?? input.job?.kind,
4088
+ kind,
3700
4089
  outputs: input.outputs,
3701
4090
  error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
3702
4091
  metadata: input.metadata ?? input.job?.metadata,
3703
4092
  processingTimeMs: input.processingTimeMs,
3704
4093
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3705
4094
  };
3706
- await deliverWebhook(config, payload);
4095
+ if (hook && kind) {
4096
+ await runHook(hook, {
4097
+ kind,
4098
+ stage,
4099
+ provider: payload.provider ?? "",
4100
+ model: payload.model ?? "",
4101
+ jobId: payload.jobId,
4102
+ outputs: payload.outputs,
4103
+ error: payload.error,
4104
+ metadata: payload.metadata,
4105
+ processingTimeMs: payload.processingTimeMs,
4106
+ at: payload.completedAt
4107
+ });
4108
+ }
4109
+ if (config) await deliverWebhook(config, payload);
4110
+ }
4111
+ function notifyLoading(kind, routed, params) {
4112
+ return runHook(findHook(options.hooks, kind, "Loading"), {
4113
+ kind,
4114
+ stage: "Loading",
4115
+ provider: routed.provider,
4116
+ model: routed.model,
4117
+ metadata: params.metadata,
4118
+ at: (/* @__PURE__ */ new Date()).toISOString()
4119
+ });
3707
4120
  }
3708
4121
  async function toGeneration(raw, metadata) {
3709
4122
  if (!raw.success) {
@@ -3726,6 +4139,7 @@ function brotuClient(options) {
3726
4139
  async function generate(kind, params) {
3727
4140
  const routed = route(params.model);
3728
4141
  if (routed.error) return fail(routed.error);
4142
+ await notifyLoading(kind, routed.data, params);
3729
4143
  try {
3730
4144
  const result = await toGeneration(
3731
4145
  await generateWith(routed.data.adapter, kind, params),
@@ -3776,6 +4190,7 @@ function brotuClient(options) {
3776
4190
  async function submit(kind, params) {
3777
4191
  const routed = route(params.model);
3778
4192
  if (routed.error) return fail(routed.error);
4193
+ await notifyLoading(kind, routed.data, params);
3779
4194
  const base = {
3780
4195
  provider: routed.data.provider,
3781
4196
  model: routed.data.model,
@@ -3906,7 +4321,7 @@ function brotuClient(options) {
3906
4321
  }
3907
4322
  }
3908
4323
  function klingNamespace() {
3909
- const configured = options.providers.kling;
4324
+ const configured = vendors.kling;
3910
4325
  if (!configured) return void 0;
3911
4326
  const adapter = new KlingAdapter({
3912
4327
  apiKey: configured.apiKey,
@@ -3931,7 +4346,7 @@ function brotuClient(options) {
3931
4346
  };
3932
4347
  }
3933
4348
  function googleNamespace() {
3934
- const configured = options.providers.google;
4349
+ const configured = vendors.google;
3935
4350
  if (!configured) return void 0;
3936
4351
  const adapter = new GoogleAdapter({
3937
4352
  apiKey: configured.apiKey,
@@ -3947,24 +4362,33 @@ function brotuClient(options) {
3947
4362
  }
3948
4363
  };
3949
4364
  }
4365
+ function listFor(category) {
4366
+ return describeModels(optionsWithKey).filter(
4367
+ (entry) => entry.model.category === category
4368
+ );
4369
+ }
3950
4370
  return {
3951
4371
  google: googleNamespace(),
3952
4372
  kling: klingNamespace(),
3953
4373
  image: {
3954
4374
  submit: (params) => submit("image", params),
3955
- generate: (params) => generate("image", params)
4375
+ generate: (params) => generate("image", params),
4376
+ list: () => listFor("image")
3956
4377
  },
3957
4378
  video: {
3958
4379
  submit: (params) => submit("video", params),
3959
- generate: (params) => generate("video", params)
4380
+ generate: (params) => generate("video", params),
4381
+ list: () => listFor("video")
3960
4382
  },
3961
4383
  text: {
3962
4384
  submit: (params) => submit("text", params),
3963
- generate: (params) => generate("text", params)
4385
+ generate: (params) => generate("text", params),
4386
+ list: () => listFor("text")
3964
4387
  },
3965
4388
  audio: {
3966
4389
  submit: (params) => submit("audio", params),
3967
- generate: (params) => generate("audio", params)
4390
+ generate: (params) => generate("audio", params),
4391
+ list: () => listFor("audio")
3968
4392
  },
3969
4393
  webhook: {
3970
4394
  set(value) {
@@ -4014,7 +4438,7 @@ function brotuClient(options) {
4014
4438
  }
4015
4439
  }
4016
4440
  },
4017
- models: () => getAvailableModels(options),
4441
+ models: () => getAvailableModels(optionsWithKey),
4018
4442
  estimateCost: async (type, params) => {
4019
4443
  const routed = route(params.model);
4020
4444
  if (routed.error) return fail(routed.error);
@@ -4031,9 +4455,12 @@ function brotuClient(options) {
4031
4455
  }
4032
4456
  // Annotate the CommonJS export names for ESM import in node:
4033
4457
  0 && (module.exports = {
4458
+ BROTU_SUPPORTED_CATEGORIES,
4034
4459
  BYTEPLUS_CATALOG,
4035
4460
  BYTEPLUS_MODELS,
4461
+ BrotuAdapter,
4036
4462
  BytePlusAdapter,
4463
+ DEFAULT_BROTU_API_URL,
4037
4464
  ELEVENLABS_CATALOG,
4038
4465
  ELEVENLABS_MODELS,
4039
4466
  ELEVENLABS_OUTPUT_FORMATS,
@@ -4060,9 +4487,10 @@ function brotuClient(options) {
4060
4487
  QWEN_TEXT_MODELS,
4061
4488
  QWEN_VIDEO_MODELS,
4062
4489
  QwenAdapter,
4063
- brotuClient,
4490
+ brotu,
4064
4491
  createS3Storage,
4065
4492
  deliverWebhook,
4493
+ describeModels,
4066
4494
  fail,
4067
4495
  getAvailableModels,
4068
4496
  getModel,