@brotu/ai 0.6.0 → 0.8.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.js CHANGED
@@ -42,7 +42,7 @@ import {
42
42
  resetCatalog,
43
43
  resolveProvider,
44
44
  videoPathFor
45
- } from "./chunk-QQIHMDJC.js";
45
+ } from "./chunk-OISTBZ64.js";
46
46
 
47
47
  // src/lib/jobs.ts
48
48
  import { AsyncLocalStorage } from "async_hooks";
@@ -88,9 +88,14 @@ function estimateFor(provider, type, params, defaults) {
88
88
  }
89
89
  const resolution = params.resolution;
90
90
  const rate = (resolution ? model?.pricing?.byResolution?.[resolution] : void 0) ?? model?.pricing?.usdPerUnit;
91
+ const tier = model?.runtimePricingTiers?.find(
92
+ (candidate) => (candidate.resolution === void 0 || candidate.resolution === resolution) && (candidate.durationSeconds === void 0 || candidate.durationSeconds === units)
93
+ );
94
+ const creditsPerUnit = tier?.creditsPerUnit ?? model?.creditsPerUnit;
91
95
  return {
92
96
  unit,
93
97
  units,
98
+ credits: creditsPerUnit ? creditsPerUnit * units : null,
94
99
  usd: rate === void 0 || unit === "token" ? null : Number((rate * units).toFixed(4)),
95
100
  note: unit === "token" && rate !== void 0 ? `Billed per token at $${(rate * 1e6).toFixed(2)} per million output tokens. The total depends on how much the model writes, so it is only known after generating.` : rate === void 0 ? `No verified rate for "${modelId}". It bills to your own ${provider} account; ${units} ${unit}${units === 1 ? "" : "s"} will be charged.` : void 0,
96
101
  provider,
@@ -364,6 +369,7 @@ var BrotuAdapter = class {
364
369
  return {
365
370
  unit: type === "video" ? "second" : "image",
366
371
  units: credits,
372
+ credits,
367
373
  usd: null,
368
374
  note: `${credits} Brotu credit${credits === 1 ? "" : "s"}. A vendor key generates on that provider.`,
369
375
  provider: this.providerName,
@@ -1388,10 +1394,383 @@ var GoogleAdapter = class {
1388
1394
  }
1389
1395
  };
1390
1396
 
1397
+ // src/lib/webhook.ts
1398
+ function resolveWebhook(value) {
1399
+ if (!value) return void 0;
1400
+ if (typeof value === "string") {
1401
+ return isHttpUrl(value) ? { url: value } : void 0;
1402
+ }
1403
+ if (!isHttpUrl(value.url)) return void 0;
1404
+ return {
1405
+ url: value.url,
1406
+ secret: value.secret,
1407
+ headers: value.headers
1408
+ };
1409
+ }
1410
+ function isHttpUrl(value) {
1411
+ try {
1412
+ const parsed = new URL(value);
1413
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
1414
+ } catch {
1415
+ return false;
1416
+ }
1417
+ }
1418
+ async function deliverWebhook(config, payload) {
1419
+ try {
1420
+ await fetch(config.url, {
1421
+ method: "POST",
1422
+ headers: {
1423
+ "content-type": "application/json",
1424
+ "user-agent": "@brotu/ai",
1425
+ "x-brotu-event": payload.event,
1426
+ ...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
1427
+ ...config.headers
1428
+ },
1429
+ body: JSON.stringify(payload),
1430
+ signal: AbortSignal.timeout(1e4)
1431
+ });
1432
+ } catch {
1433
+ }
1434
+ }
1435
+
1436
+ // src/adapters/kie.adapter.ts
1437
+ var DEFAULT_BASE_URL4 = "https://api.kie.ai/api/v1";
1438
+ var POLL_INTERVAL_MS4 = 3e3;
1439
+ var DEFAULT_MAX_POLL_ATTEMPTS4 = 400;
1440
+ var KIE_MODEL_IDS = {
1441
+ "kling/v2-6": "kling-2.6/{mode}-to-video",
1442
+ "kling/v3": "kling-3.0/video",
1443
+ "dreamina-seedance-2-5-260628": "bytedance/seedance-2-5",
1444
+ "dreamina-seedance-2-0-260128": "bytedance/seedance-2",
1445
+ "dreamina-seedance-2-0-fast-260128": "bytedance/seedance-2-fast",
1446
+ "dreamina-seedance-2-0-mini-260615": "bytedance/seedance-2-mini",
1447
+ "wan2.6-t2v": "wan/2-6-text-to-video",
1448
+ "wan2.6-i2v": "wan/2-6-image-to-video",
1449
+ "wan2.7-t2v": "wan/2-7-text-to-video",
1450
+ "wan2.7-i2v": "wan/2-7-image-to-video",
1451
+ "gpt-image-1.5": "gpt-image/1.5-{mode}-to-image",
1452
+ "gpt-image-2": "gpt-image-2-{mode}-to-image",
1453
+ "topaz/video-upscale": "topaz/video-upscale"
1454
+ };
1455
+ var KieAdapter = class {
1456
+ constructor(opts) {
1457
+ this.opts = opts;
1458
+ }
1459
+ opts;
1460
+ providerName = "kie";
1461
+ supportedTypes = [
1462
+ "image",
1463
+ "video",
1464
+ "text",
1465
+ "audio"
1466
+ ];
1467
+ get origin() {
1468
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL4).replace(/\/$/, "");
1469
+ }
1470
+ async request(path, init) {
1471
+ const response = await fetch(`${this.origin}${path}`, {
1472
+ ...init,
1473
+ headers: {
1474
+ Authorization: `Bearer ${this.opts.apiKey}`,
1475
+ "Content-Type": "application/json",
1476
+ ...init?.headers
1477
+ }
1478
+ });
1479
+ const text = await response.text();
1480
+ let body;
1481
+ try {
1482
+ body = text ? JSON.parse(text) : void 0;
1483
+ } catch {
1484
+ body = void 0;
1485
+ }
1486
+ const code = body?.code;
1487
+ if (!response.ok || typeof code === "number" && code !== 200) {
1488
+ const message = body?.msg || body?.message || text;
1489
+ throw new Error(message || `kie API ${response.status}`);
1490
+ }
1491
+ if (body?.data === void 0) {
1492
+ throw new Error("kie answered without a payload.");
1493
+ }
1494
+ return body.data;
1495
+ }
1496
+ /** Which market model serves this request. */
1497
+ marketModel(modelId, params) {
1498
+ const fromCatalog = getModel(modelId)?.endpoint?.replace(
1499
+ /^\/market\//,
1500
+ ""
1501
+ );
1502
+ const mapped = fromCatalog || KIE_MODEL_IDS[modelId] || modelId;
1503
+ if (!mapped.includes("{mode}")) return mapped;
1504
+ return mapped.replace("{mode}", hasInputImage(params) ? "image" : "text");
1505
+ }
1506
+ callbackFor(params) {
1507
+ const perRequest = resolveWebhook(params.webhook);
1508
+ return perRequest?.url ?? this.opts.callbackUrl;
1509
+ }
1510
+ async createTask(kind, params) {
1511
+ const modelId = params.model ?? "";
1512
+ const body = {
1513
+ model: this.marketModel(modelId, params),
1514
+ input: buildInput(kind, params, modelId)
1515
+ };
1516
+ const callBackUrl = this.callbackFor(params);
1517
+ if (callBackUrl) body.callBackUrl = callBackUrl;
1518
+ const data = await this.request("/jobs/createTask", {
1519
+ method: "POST",
1520
+ body: JSON.stringify(body)
1521
+ });
1522
+ const taskId = data.taskId?.trim();
1523
+ if (!taskId) {
1524
+ throw new Error("kie accepted the task but returned no taskId.");
1525
+ }
1526
+ return taskId;
1527
+ }
1528
+ /** Ask kie about one task. The same shape a callback carries. */
1529
+ async task(taskId) {
1530
+ const record = await this.request(
1531
+ `/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`
1532
+ );
1533
+ return readTaskRecord(record, taskId);
1534
+ }
1535
+ async run(kind, params) {
1536
+ const startedAt = Date.now();
1537
+ const modelId = params.model ?? "(none)";
1538
+ const failure = (error) => ({
1539
+ success: false,
1540
+ outputs: [],
1541
+ creditsUsed: 0,
1542
+ provider: this.providerName,
1543
+ model: modelId,
1544
+ processingTimeMs: Date.now() - startedAt,
1545
+ error
1546
+ });
1547
+ let taskId;
1548
+ try {
1549
+ taskId = await this.createTask(kind, params);
1550
+ } catch (error) {
1551
+ return failure(error instanceof Error ? error.message : String(error));
1552
+ }
1553
+ const pollEndpoint = `/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`;
1554
+ if (isSubmitMode()) {
1555
+ throw new PendingJob(taskId, pollEndpoint);
1556
+ }
1557
+ const job = {
1558
+ id: taskId,
1559
+ provider: this.providerName,
1560
+ model: modelId,
1561
+ kind,
1562
+ pollEndpoint,
1563
+ params,
1564
+ submittedAt: new Date(startedAt).toISOString()
1565
+ };
1566
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
1567
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1568
+ const snapshot = await this.completeJob(job);
1569
+ if (snapshot.status === "failed") {
1570
+ return failure(snapshot.error ?? "kie task failed.");
1571
+ }
1572
+ if (snapshot.status === "succeeded" && snapshot.result) {
1573
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
1574
+ }
1575
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
1576
+ }
1577
+ return failure(
1578
+ `kie task ${taskId} did not finish after ${maxAttempts} checks.`
1579
+ );
1580
+ }
1581
+ async completeJob(job) {
1582
+ const settled = await this.task(job.id);
1583
+ return snapshotFrom(settled, job.model);
1584
+ }
1585
+ generateImage(params) {
1586
+ return this.run("image", params);
1587
+ }
1588
+ generateVideo(params) {
1589
+ return this.run("video", params);
1590
+ }
1591
+ generateText(params) {
1592
+ return this.run("text", params);
1593
+ }
1594
+ generateAudio(params) {
1595
+ return this.run("audio", params);
1596
+ }
1597
+ async estimateCost(type, params) {
1598
+ return estimateFor(this.providerName, type, params);
1599
+ }
1600
+ supportsModel() {
1601
+ return true;
1602
+ }
1603
+ getAvailableModels() {
1604
+ return [];
1605
+ }
1606
+ };
1607
+ function parseKieCallback(body) {
1608
+ let parsed = body;
1609
+ if (typeof body === "string") {
1610
+ try {
1611
+ parsed = JSON.parse(body);
1612
+ } catch {
1613
+ throw new Error("kie callback body is not JSON.");
1614
+ }
1615
+ }
1616
+ const root = asRecord(parsed);
1617
+ const record = asRecord(root?.data) ?? root ?? {};
1618
+ const taskId = record.taskId?.trim();
1619
+ if (!taskId) {
1620
+ throw new Error("kie callback carries no taskId.");
1621
+ }
1622
+ return readTaskRecord(record, taskId);
1623
+ }
1624
+ function kieSnapshot(settled, model) {
1625
+ return snapshotFrom(settled, model ?? settled.model ?? "");
1626
+ }
1627
+ function snapshotFrom(settled, model) {
1628
+ if (settled.status === "failed") {
1629
+ return { status: "failed", error: settled.error ?? "kie task failed." };
1630
+ }
1631
+ if (settled.status === "pending") return { status: "pending" };
1632
+ return {
1633
+ status: "succeeded",
1634
+ result: {
1635
+ success: true,
1636
+ outputs: settled.outputs,
1637
+ creditsUsed: settled.creditsUsed,
1638
+ provider: "kie",
1639
+ model,
1640
+ processingTimeMs: 0
1641
+ }
1642
+ };
1643
+ }
1644
+ function readTaskRecord(record, taskId) {
1645
+ const state = (record.state ?? "").toLowerCase();
1646
+ const creditsUsed = record.creditsConsumed ?? 0;
1647
+ if (state === "fail") {
1648
+ return {
1649
+ taskId,
1650
+ model: record.model,
1651
+ status: "failed",
1652
+ outputs: [],
1653
+ creditsUsed,
1654
+ error: record.failMsg || (record.failCode ? `kie failCode ${record.failCode}` : void 0) || "kie task failed."
1655
+ };
1656
+ }
1657
+ if (state !== "success") {
1658
+ return {
1659
+ taskId,
1660
+ model: record.model,
1661
+ status: "pending",
1662
+ outputs: [],
1663
+ creditsUsed
1664
+ };
1665
+ }
1666
+ return {
1667
+ taskId,
1668
+ model: record.model,
1669
+ status: "succeeded",
1670
+ outputs: outputsFrom2(record.resultJson),
1671
+ creditsUsed
1672
+ };
1673
+ }
1674
+ function outputsFrom2(resultJson) {
1675
+ if (!resultJson) return [];
1676
+ let parsed;
1677
+ try {
1678
+ parsed = JSON.parse(resultJson);
1679
+ } catch {
1680
+ return [];
1681
+ }
1682
+ const record = asRecord(parsed);
1683
+ const urls = Array.isArray(record?.resultUrls) ? record.resultUrls.filter(
1684
+ (url) => typeof url === "string" && url.length > 0
1685
+ ) : [];
1686
+ if (urls.length > 0) return urls.map((url) => ({ url, mimeType: mimeFor(url) }));
1687
+ const object = record?.resultObject;
1688
+ if (object !== void 0) {
1689
+ const text = typeof object === "string" ? object : JSON.stringify(object);
1690
+ return [
1691
+ {
1692
+ url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
1693
+ mimeType: "text/plain",
1694
+ raw: { text }
1695
+ }
1696
+ ];
1697
+ }
1698
+ return [];
1699
+ }
1700
+ function mimeFor(url) {
1701
+ const path = url.split("?")[0]?.toLowerCase() ?? "";
1702
+ if (/\.(mp4|mov|webm|m4v)$/.test(path)) return "video/mp4";
1703
+ if (/\.(mp3|wav|m4a|aac)$/.test(path)) return "audio/mpeg";
1704
+ if (/\.jpe?g$/.test(path)) return "image/jpeg";
1705
+ if (/\.webp$/.test(path)) return "image/webp";
1706
+ return "image/png";
1707
+ }
1708
+ function asRecord(value) {
1709
+ return value && typeof value === "object" ? value : void 0;
1710
+ }
1711
+ function hasInputImage(params) {
1712
+ const p = params;
1713
+ return Boolean(
1714
+ p.imageUrl || p.imageUrls?.length || p.referenceImages?.length
1715
+ );
1716
+ }
1717
+ function buildInput(kind, params, modelId) {
1718
+ const raw = params.providerOptions?.kie ?? {};
1719
+ const base = modelId.startsWith("kling/") ? klingInput(params) : modelId.includes("seedance") ? seedanceInput(params) : commonInput(kind, params);
1720
+ return prune({ ...base, ...raw });
1721
+ }
1722
+ function klingInput(params) {
1723
+ const p = params;
1724
+ return {
1725
+ prompt: p.prompt,
1726
+ negative_prompt: p.negativePrompt,
1727
+ aspect_ratio: p.aspectRatio,
1728
+ // kie's kling enum is a string, unlike every other family.
1729
+ duration: p.duration === void 0 ? void 0 : String(p.duration),
1730
+ sound: p.withAudio,
1731
+ image_url: p.imageUrl ?? p.referenceImages?.[0]
1732
+ };
1733
+ }
1734
+ function seedanceInput(params) {
1735
+ const p = params;
1736
+ return {
1737
+ prompt: p.prompt,
1738
+ duration: p.duration,
1739
+ resolution: p.resolution,
1740
+ aspect_ratio: p.aspectRatio,
1741
+ first_frame_url: p.imageUrl ?? p.referenceImages?.[0],
1742
+ last_frame_url: p.referenceImages?.[1],
1743
+ reference_image_urls: p.imageUrls,
1744
+ reference_video_urls: p.videoUrls ?? (p.videoUrl ? [p.videoUrl] : void 0),
1745
+ generate_audio: p.withAudio
1746
+ };
1747
+ }
1748
+ function commonInput(kind, params) {
1749
+ const p = params;
1750
+ const images = p.imageUrls ?? p.referenceImages;
1751
+ return {
1752
+ prompt: p.prompt,
1753
+ negative_prompt: p.negativePrompt,
1754
+ aspect_ratio: p.aspectRatio,
1755
+ resolution: p.resolution,
1756
+ duration: kind === "video" ? p.duration : void 0,
1757
+ seed: p.seed,
1758
+ output_format: p.outputFormat,
1759
+ image_url: p.imageUrl ?? images?.[0],
1760
+ image_urls: images && images.length > 1 ? images : void 0,
1761
+ video_url: p.videoUrl
1762
+ };
1763
+ }
1764
+ function prune(input) {
1765
+ return Object.fromEntries(
1766
+ Object.entries(input).filter(([, value]) => value !== void 0)
1767
+ );
1768
+ }
1769
+
1391
1770
  // src/adapters/kling.adapter.ts
1392
- var DEFAULT_BASE_URL4 = "https://api-singapore.klingai.com";
1393
- var POLL_INTERVAL_MS4 = 5e3;
1394
- var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
1771
+ var DEFAULT_BASE_URL5 = "https://api-singapore.klingai.com";
1772
+ var POLL_INTERVAL_MS5 = 5e3;
1773
+ var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
1395
1774
  function stateOf(task) {
1396
1775
  const raw = (task.status ?? task.task_status ?? "").toLowerCase();
1397
1776
  if (raw === "failed") return "failed";
@@ -1406,7 +1785,7 @@ var KlingAdapter = class {
1406
1785
  this.opts = opts;
1407
1786
  }
1408
1787
  get baseUrl() {
1409
- return (this.opts.baseUrl ?? DEFAULT_BASE_URL4).replace(/\/$/, "");
1788
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL5).replace(/\/$/, "");
1410
1789
  }
1411
1790
  async request(path, init) {
1412
1791
  const response = await fetch(`${this.baseUrl}${path}`, {
@@ -1588,7 +1967,7 @@ var KlingAdapter = class {
1588
1967
  params,
1589
1968
  submittedAt: new Date(startedAt).toISOString()
1590
1969
  };
1591
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
1970
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
1592
1971
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1593
1972
  const snapshot = await this.completeJob(job);
1594
1973
  if (snapshot.status === "failed") {
@@ -1597,7 +1976,7 @@ var KlingAdapter = class {
1597
1976
  if (snapshot.status === "succeeded" && snapshot.result) {
1598
1977
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
1599
1978
  }
1600
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
1979
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
1601
1980
  }
1602
1981
  return failure(
1603
1982
  `Kling task ${submitted.taskId} did not finish after ${maxAttempts} checks.`
@@ -1734,7 +2113,7 @@ var KlingAdapter = class {
1734
2113
  };
1735
2114
 
1736
2115
  // src/adapters/openai.adapter.ts
1737
- var DEFAULT_BASE_URL5 = "https://api.openai.com";
2116
+ var DEFAULT_BASE_URL6 = "https://api.openai.com";
1738
2117
  var IMAGES_PATH2 = "/v1/images/generations";
1739
2118
  var OpenAIAdapter = class {
1740
2119
  providerName = "openai";
@@ -1745,7 +2124,7 @@ var OpenAIAdapter = class {
1745
2124
  this.opts = opts;
1746
2125
  }
1747
2126
  get baseUrl() {
1748
- return (this.opts.baseUrl ?? DEFAULT_BASE_URL5).replace(/\/$/, "");
2127
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL6).replace(/\/$/, "");
1749
2128
  }
1750
2129
  binding(modelId) {
1751
2130
  const id = modelId ?? "";
@@ -1966,10 +2345,10 @@ var OpenAIAdapter = class {
1966
2345
  };
1967
2346
 
1968
2347
  // src/adapters/qwen.adapter.ts
1969
- var DEFAULT_BASE_URL6 = "https://dashscope-intl.aliyuncs.com";
2348
+ var DEFAULT_BASE_URL7 = "https://dashscope-intl.aliyuncs.com";
1970
2349
  var TASKS_PATH2 = "/api/v1/tasks";
1971
- var POLL_INTERVAL_MS5 = 5e3;
1972
- var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
2350
+ var POLL_INTERVAL_MS6 = 5e3;
2351
+ var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
1973
2352
  var QwenAdapter = class {
1974
2353
  providerName = "qwen";
1975
2354
  supportedTypes = ["image", "video"];
@@ -1978,7 +2357,7 @@ var QwenAdapter = class {
1978
2357
  this.opts = opts;
1979
2358
  }
1980
2359
  get baseUrl() {
1981
- return (this.opts.baseUrl ?? DEFAULT_BASE_URL6).replace(/\/$/, "");
2360
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
1982
2361
  }
1983
2362
  async request(path, init) {
1984
2363
  const headers = {
@@ -2176,7 +2555,7 @@ var QwenAdapter = class {
2176
2555
  params,
2177
2556
  submittedAt: new Date(startedAt).toISOString()
2178
2557
  };
2179
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
2558
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
2180
2559
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2181
2560
  const snapshot = await this.completeJob(job);
2182
2561
  if (snapshot.status === "failed") {
@@ -2185,7 +2564,7 @@ var QwenAdapter = class {
2185
2564
  if (snapshot.status === "succeeded" && snapshot.result) {
2186
2565
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
2187
2566
  }
2188
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
2567
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
2189
2568
  }
2190
2569
  return failure(
2191
2570
  `DashScope task ${taskId} did not finish after ${maxAttempts} checks.`
@@ -2430,9 +2809,9 @@ var QwenAdapter = class {
2430
2809
  };
2431
2810
 
2432
2811
  // src/adapters/topaz.adapter.ts
2433
- var DEFAULT_BASE_URL7 = "https://api.topazlabs.com";
2434
- var POLL_INTERVAL_MS6 = 5e3;
2435
- var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
2812
+ var DEFAULT_BASE_URL8 = "https://api.topazlabs.com";
2813
+ var POLL_INTERVAL_MS7 = 5e3;
2814
+ var DEFAULT_MAX_POLL_ATTEMPTS7 = 240;
2436
2815
  var DEFAULT_FPS = 30;
2437
2816
  var PIXELS = {
2438
2817
  "720p": { width: 1280, height: 720 },
@@ -2452,7 +2831,7 @@ var TopazAdapter = class {
2452
2831
  this.opts = opts;
2453
2832
  }
2454
2833
  get baseUrl() {
2455
- return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
2834
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL8).replace(/\/$/, "");
2456
2835
  }
2457
2836
  get headers() {
2458
2837
  return {
@@ -2665,7 +3044,7 @@ var TopazAdapter = class {
2665
3044
  params,
2666
3045
  submittedAt: new Date(startedAt).toISOString()
2667
3046
  };
2668
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
3047
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS7;
2669
3048
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2670
3049
  const snapshot = await this.completeJob(job);
2671
3050
  if (snapshot.status === "failed") {
@@ -2674,7 +3053,7 @@ var TopazAdapter = class {
2674
3053
  if (snapshot.status === "succeeded" && snapshot.result) {
2675
3054
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
2676
3055
  }
2677
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
3056
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS7));
2678
3057
  }
2679
3058
  return failure(
2680
3059
  `Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
@@ -2840,7 +3219,7 @@ var TopazAdapter = class {
2840
3219
  params,
2841
3220
  submittedAt: new Date(startedAt).toISOString()
2842
3221
  };
2843
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
3222
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS7;
2844
3223
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
2845
3224
  const snapshot = await this.completeJob(job);
2846
3225
  if (snapshot.status === "failed") {
@@ -2849,7 +3228,7 @@ var TopazAdapter = class {
2849
3228
  if (snapshot.status === "succeeded" && snapshot.result) {
2850
3229
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
2851
3230
  }
2852
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
3231
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS7));
2853
3232
  }
2854
3233
  return failure(
2855
3234
  `Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
@@ -3052,50 +3431,12 @@ async function persistOutputs(storage, outputs) {
3052
3431
  );
3053
3432
  }
3054
3433
 
3055
- // src/lib/webhook.ts
3056
- function resolveWebhook(value) {
3057
- if (!value) return void 0;
3058
- if (typeof value === "string") {
3059
- return isHttpUrl(value) ? { url: value } : void 0;
3060
- }
3061
- if (!isHttpUrl(value.url)) return void 0;
3062
- return {
3063
- url: value.url,
3064
- secret: value.secret,
3065
- headers: value.headers
3066
- };
3067
- }
3068
- function isHttpUrl(value) {
3069
- try {
3070
- const parsed = new URL(value);
3071
- return parsed.protocol === "https:" || parsed.protocol === "http:";
3072
- } catch {
3073
- return false;
3074
- }
3075
- }
3076
- async function deliverWebhook(config, payload) {
3077
- try {
3078
- await fetch(config.url, {
3079
- method: "POST",
3080
- headers: {
3081
- "content-type": "application/json",
3082
- "user-agent": "@brotu/ai",
3083
- "x-brotu-event": payload.event,
3084
- ...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
3085
- ...config.headers
3086
- },
3087
- body: JSON.stringify(payload),
3088
- signal: AbortSignal.timeout(1e4)
3089
- });
3090
- } catch {
3091
- }
3092
- }
3093
-
3094
3434
  // src/client.ts
3095
3435
  var NATIVE_PROVIDERS = [
3096
3436
  "byteplus",
3097
3437
  "elevenlabs",
3098
3438
  "google",
3439
+ "kie",
3099
3440
  "kling",
3100
3441
  "openai",
3101
3442
  "qwen",
@@ -3161,6 +3502,16 @@ function brotu(options) {
3161
3502
  workspaceId: optionsWithKey.workspaceId
3162
3503
  });
3163
3504
  }
3505
+ if (provider.id === "kie") {
3506
+ return new KieAdapter({
3507
+ apiKey: provider.apiKey,
3508
+ baseUrl: provider.baseUrl,
3509
+ // The client-level webhook doubles as kie's callback, so a submit
3510
+ // never has to be polled. kie posts its own payload there — read it
3511
+ // with `parseKieCallback`.
3512
+ callbackUrl: registeredWebhook?.url
3513
+ });
3514
+ }
3164
3515
  if (provider.id === "kling") {
3165
3516
  return new KlingAdapter({
3166
3517
  apiKey: provider.apiKey,
@@ -3238,6 +3589,7 @@ function brotu(options) {
3238
3589
  outputs: input.outputs,
3239
3590
  error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
3240
3591
  metadata: input.metadata ?? input.job?.metadata,
3592
+ creditsUsed: input.params?.credits ?? input.creditsUsed,
3241
3593
  processingTimeMs: input.processingTimeMs,
3242
3594
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3243
3595
  };
@@ -3251,6 +3603,7 @@ function brotu(options) {
3251
3603
  outputs: payload.outputs,
3252
3604
  error: payload.error,
3253
3605
  metadata: payload.metadata,
3606
+ creditsUsed: payload.creditsUsed,
3254
3607
  processingTimeMs: payload.processingTimeMs,
3255
3608
  at: payload.completedAt
3256
3609
  });
@@ -3267,7 +3620,7 @@ function brotu(options) {
3267
3620
  at: (/* @__PURE__ */ new Date()).toISOString()
3268
3621
  });
3269
3622
  }
3270
- async function toGeneration(raw, metadata) {
3623
+ async function toGeneration(raw, metadata, credits) {
3271
3624
  if (!raw.success) {
3272
3625
  return fail({
3273
3626
  code: "provider_error",
@@ -3282,6 +3635,9 @@ function brotu(options) {
3282
3635
  provider: raw.provider,
3283
3636
  model: raw.model,
3284
3637
  processingTimeMs: raw.processingTimeMs,
3638
+ // What you said to charge wins over what the platform reported: the
3639
+ // caller's number is the one their ledger has to match.
3640
+ creditsUsed: credits ?? raw.creditsUsed,
3285
3641
  metadata
3286
3642
  });
3287
3643
  }
@@ -3292,7 +3648,8 @@ function brotu(options) {
3292
3648
  try {
3293
3649
  const result = await toGeneration(
3294
3650
  await generateWith(routed.data.adapter, kind, params),
3295
- params.metadata
3651
+ params.metadata,
3652
+ params.credits
3296
3653
  );
3297
3654
  if (result.error) {
3298
3655
  await notifySettled({
@@ -3314,6 +3671,7 @@ function brotu(options) {
3314
3671
  model: result.data.model,
3315
3672
  outputs: result.data.outputs,
3316
3673
  metadata: result.data.metadata,
3674
+ creditsUsed: result.data.creditsUsed,
3317
3675
  processingTimeMs: result.data.processingTimeMs
3318
3676
  });
3319
3677
  return result;
@@ -3410,6 +3768,7 @@ function brotu(options) {
3410
3768
  outputs: snapshot.result.outputs,
3411
3769
  provider: snapshot.result.provider,
3412
3770
  model: snapshot.result.model,
3771
+ creditsUsed: snapshot.result.creditsUsed,
3413
3772
  processingTimeMs: snapshot.result.processingTimeMs,
3414
3773
  metadata: job.metadata
3415
3774
  });
@@ -3432,7 +3791,11 @@ function brotu(options) {
3432
3791
  }
3433
3792
  async function finalizeSnapshot(job, snapshot) {
3434
3793
  if (snapshot.status === "succeeded" && snapshot.result) {
3435
- const persisted = await toGeneration(snapshot.result, job.metadata);
3794
+ const persisted = await toGeneration(
3795
+ snapshot.result,
3796
+ job.metadata,
3797
+ job.params.credits
3798
+ );
3436
3799
  if (!persisted.error) {
3437
3800
  snapshot = {
3438
3801
  status: "succeeded",
@@ -3619,6 +3982,7 @@ function brotu(options) {
3619
3982
  provider: snapshot.result.provider,
3620
3983
  model: snapshot.result.model,
3621
3984
  processingTimeMs: snapshot.result.processingTimeMs,
3985
+ creditsUsed: job.params.credits ?? snapshot.result.creditsUsed,
3622
3986
  metadata: job.metadata
3623
3987
  });
3624
3988
  }
@@ -3678,6 +4042,7 @@ export {
3678
4042
  KLING_CAPABILITIES,
3679
4043
  KLING_CATALOG,
3680
4044
  KLING_MODELS,
4045
+ KieAdapter,
3681
4046
  KlingAdapter,
3682
4047
  OPENAI_CATALOG,
3683
4048
  OPENAI_IMAGE_MODELS,
@@ -3706,7 +4071,9 @@ export {
3706
4071
  getProviders,
3707
4072
  hasModel,
3708
4073
  isPendingJob,
4074
+ kieSnapshot,
3709
4075
  ok,
4076
+ parseKieCallback,
3710
4077
  persistOutputs,
3711
4078
  registerModels,
3712
4079
  resetCatalog,