@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/README.md +62 -2
- package/dist/adapters/brotu.adapter.d.ts.map +1 -1
- package/dist/adapters/estimate.d.ts.map +1 -1
- package/dist/adapters/kie.adapter.d.ts +62 -0
- package/dist/adapters/kie.adapter.d.ts.map +1 -0
- package/dist/catalog.cjs +9 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +1 -1
- package/dist/{chunk-QQIHMDJC.js → chunk-OISTBZ64.js} +9 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/helpers/result.d.ts +6 -0
- package/dist/helpers/result.d.ts.map +1 -1
- package/dist/index.cjs +443 -64
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +432 -65
- package/dist/lib/hooks.d.ts +2 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/webhook.d.ts +2 -0
- package/dist/lib/webhook.d.ts.map +1 -1
- package/dist/ports/content-generator.port.d.ts +14 -0
- package/dist/ports/content-generator.port.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
resetCatalog,
|
|
43
43
|
resolveProvider,
|
|
44
44
|
videoPathFor
|
|
45
|
-
} from "./chunk-
|
|
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
|
|
1393
|
-
var
|
|
1394
|
-
var
|
|
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 ??
|
|
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 ??
|
|
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,
|
|
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
|
|
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 ??
|
|
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
|
|
2348
|
+
var DEFAULT_BASE_URL7 = "https://dashscope-intl.aliyuncs.com";
|
|
1970
2349
|
var TASKS_PATH2 = "/api/v1/tasks";
|
|
1971
|
-
var
|
|
1972
|
-
var
|
|
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 ??
|
|
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 ??
|
|
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,
|
|
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
|
|
2434
|
-
var
|
|
2435
|
-
var
|
|
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 ??
|
|
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 ??
|
|
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,
|
|
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 ??
|
|
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,
|
|
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(
|
|
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,
|