@brotu/ai 0.7.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 +44 -0
- 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/index.cjs +421 -61
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +410 -62
- 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";
|
|
@@ -1394,10 +1394,383 @@ var GoogleAdapter = class {
|
|
|
1394
1394
|
}
|
|
1395
1395
|
};
|
|
1396
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
|
+
|
|
1397
1770
|
// src/adapters/kling.adapter.ts
|
|
1398
|
-
var
|
|
1399
|
-
var
|
|
1400
|
-
var
|
|
1771
|
+
var DEFAULT_BASE_URL5 = "https://api-singapore.klingai.com";
|
|
1772
|
+
var POLL_INTERVAL_MS5 = 5e3;
|
|
1773
|
+
var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
|
|
1401
1774
|
function stateOf(task) {
|
|
1402
1775
|
const raw = (task.status ?? task.task_status ?? "").toLowerCase();
|
|
1403
1776
|
if (raw === "failed") return "failed";
|
|
@@ -1412,7 +1785,7 @@ var KlingAdapter = class {
|
|
|
1412
1785
|
this.opts = opts;
|
|
1413
1786
|
}
|
|
1414
1787
|
get baseUrl() {
|
|
1415
|
-
return (this.opts.baseUrl ??
|
|
1788
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL5).replace(/\/$/, "");
|
|
1416
1789
|
}
|
|
1417
1790
|
async request(path, init) {
|
|
1418
1791
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
@@ -1594,7 +1967,7 @@ var KlingAdapter = class {
|
|
|
1594
1967
|
params,
|
|
1595
1968
|
submittedAt: new Date(startedAt).toISOString()
|
|
1596
1969
|
};
|
|
1597
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
1970
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
|
|
1598
1971
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1599
1972
|
const snapshot = await this.completeJob(job);
|
|
1600
1973
|
if (snapshot.status === "failed") {
|
|
@@ -1603,7 +1976,7 @@ var KlingAdapter = class {
|
|
|
1603
1976
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
1604
1977
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
1605
1978
|
}
|
|
1606
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1979
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
|
|
1607
1980
|
}
|
|
1608
1981
|
return failure(
|
|
1609
1982
|
`Kling task ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -1740,7 +2113,7 @@ var KlingAdapter = class {
|
|
|
1740
2113
|
};
|
|
1741
2114
|
|
|
1742
2115
|
// src/adapters/openai.adapter.ts
|
|
1743
|
-
var
|
|
2116
|
+
var DEFAULT_BASE_URL6 = "https://api.openai.com";
|
|
1744
2117
|
var IMAGES_PATH2 = "/v1/images/generations";
|
|
1745
2118
|
var OpenAIAdapter = class {
|
|
1746
2119
|
providerName = "openai";
|
|
@@ -1751,7 +2124,7 @@ var OpenAIAdapter = class {
|
|
|
1751
2124
|
this.opts = opts;
|
|
1752
2125
|
}
|
|
1753
2126
|
get baseUrl() {
|
|
1754
|
-
return (this.opts.baseUrl ??
|
|
2127
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL6).replace(/\/$/, "");
|
|
1755
2128
|
}
|
|
1756
2129
|
binding(modelId) {
|
|
1757
2130
|
const id = modelId ?? "";
|
|
@@ -1972,10 +2345,10 @@ var OpenAIAdapter = class {
|
|
|
1972
2345
|
};
|
|
1973
2346
|
|
|
1974
2347
|
// src/adapters/qwen.adapter.ts
|
|
1975
|
-
var
|
|
2348
|
+
var DEFAULT_BASE_URL7 = "https://dashscope-intl.aliyuncs.com";
|
|
1976
2349
|
var TASKS_PATH2 = "/api/v1/tasks";
|
|
1977
|
-
var
|
|
1978
|
-
var
|
|
2350
|
+
var POLL_INTERVAL_MS6 = 5e3;
|
|
2351
|
+
var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
|
|
1979
2352
|
var QwenAdapter = class {
|
|
1980
2353
|
providerName = "qwen";
|
|
1981
2354
|
supportedTypes = ["image", "video"];
|
|
@@ -1984,7 +2357,7 @@ var QwenAdapter = class {
|
|
|
1984
2357
|
this.opts = opts;
|
|
1985
2358
|
}
|
|
1986
2359
|
get baseUrl() {
|
|
1987
|
-
return (this.opts.baseUrl ??
|
|
2360
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
|
|
1988
2361
|
}
|
|
1989
2362
|
async request(path, init) {
|
|
1990
2363
|
const headers = {
|
|
@@ -2182,7 +2555,7 @@ var QwenAdapter = class {
|
|
|
2182
2555
|
params,
|
|
2183
2556
|
submittedAt: new Date(startedAt).toISOString()
|
|
2184
2557
|
};
|
|
2185
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
2558
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
|
|
2186
2559
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2187
2560
|
const snapshot = await this.completeJob(job);
|
|
2188
2561
|
if (snapshot.status === "failed") {
|
|
@@ -2191,7 +2564,7 @@ var QwenAdapter = class {
|
|
|
2191
2564
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2192
2565
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2193
2566
|
}
|
|
2194
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
2567
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
|
|
2195
2568
|
}
|
|
2196
2569
|
return failure(
|
|
2197
2570
|
`DashScope task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -2436,9 +2809,9 @@ var QwenAdapter = class {
|
|
|
2436
2809
|
};
|
|
2437
2810
|
|
|
2438
2811
|
// src/adapters/topaz.adapter.ts
|
|
2439
|
-
var
|
|
2440
|
-
var
|
|
2441
|
-
var
|
|
2812
|
+
var DEFAULT_BASE_URL8 = "https://api.topazlabs.com";
|
|
2813
|
+
var POLL_INTERVAL_MS7 = 5e3;
|
|
2814
|
+
var DEFAULT_MAX_POLL_ATTEMPTS7 = 240;
|
|
2442
2815
|
var DEFAULT_FPS = 30;
|
|
2443
2816
|
var PIXELS = {
|
|
2444
2817
|
"720p": { width: 1280, height: 720 },
|
|
@@ -2458,7 +2831,7 @@ var TopazAdapter = class {
|
|
|
2458
2831
|
this.opts = opts;
|
|
2459
2832
|
}
|
|
2460
2833
|
get baseUrl() {
|
|
2461
|
-
return (this.opts.baseUrl ??
|
|
2834
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL8).replace(/\/$/, "");
|
|
2462
2835
|
}
|
|
2463
2836
|
get headers() {
|
|
2464
2837
|
return {
|
|
@@ -2671,7 +3044,7 @@ var TopazAdapter = class {
|
|
|
2671
3044
|
params,
|
|
2672
3045
|
submittedAt: new Date(startedAt).toISOString()
|
|
2673
3046
|
};
|
|
2674
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
3047
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS7;
|
|
2675
3048
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2676
3049
|
const snapshot = await this.completeJob(job);
|
|
2677
3050
|
if (snapshot.status === "failed") {
|
|
@@ -2680,7 +3053,7 @@ var TopazAdapter = class {
|
|
|
2680
3053
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2681
3054
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2682
3055
|
}
|
|
2683
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
3056
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS7));
|
|
2684
3057
|
}
|
|
2685
3058
|
return failure(
|
|
2686
3059
|
`Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -2846,7 +3219,7 @@ var TopazAdapter = class {
|
|
|
2846
3219
|
params,
|
|
2847
3220
|
submittedAt: new Date(startedAt).toISOString()
|
|
2848
3221
|
};
|
|
2849
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
3222
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS7;
|
|
2850
3223
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2851
3224
|
const snapshot = await this.completeJob(job);
|
|
2852
3225
|
if (snapshot.status === "failed") {
|
|
@@ -2855,7 +3228,7 @@ var TopazAdapter = class {
|
|
|
2855
3228
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2856
3229
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2857
3230
|
}
|
|
2858
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
3231
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS7));
|
|
2859
3232
|
}
|
|
2860
3233
|
return failure(
|
|
2861
3234
|
`Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -3058,50 +3431,12 @@ async function persistOutputs(storage, outputs) {
|
|
|
3058
3431
|
);
|
|
3059
3432
|
}
|
|
3060
3433
|
|
|
3061
|
-
// src/lib/webhook.ts
|
|
3062
|
-
function resolveWebhook(value) {
|
|
3063
|
-
if (!value) return void 0;
|
|
3064
|
-
if (typeof value === "string") {
|
|
3065
|
-
return isHttpUrl(value) ? { url: value } : void 0;
|
|
3066
|
-
}
|
|
3067
|
-
if (!isHttpUrl(value.url)) return void 0;
|
|
3068
|
-
return {
|
|
3069
|
-
url: value.url,
|
|
3070
|
-
secret: value.secret,
|
|
3071
|
-
headers: value.headers
|
|
3072
|
-
};
|
|
3073
|
-
}
|
|
3074
|
-
function isHttpUrl(value) {
|
|
3075
|
-
try {
|
|
3076
|
-
const parsed = new URL(value);
|
|
3077
|
-
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
3078
|
-
} catch {
|
|
3079
|
-
return false;
|
|
3080
|
-
}
|
|
3081
|
-
}
|
|
3082
|
-
async function deliverWebhook(config, payload) {
|
|
3083
|
-
try {
|
|
3084
|
-
await fetch(config.url, {
|
|
3085
|
-
method: "POST",
|
|
3086
|
-
headers: {
|
|
3087
|
-
"content-type": "application/json",
|
|
3088
|
-
"user-agent": "@brotu/ai",
|
|
3089
|
-
"x-brotu-event": payload.event,
|
|
3090
|
-
...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
|
|
3091
|
-
...config.headers
|
|
3092
|
-
},
|
|
3093
|
-
body: JSON.stringify(payload),
|
|
3094
|
-
signal: AbortSignal.timeout(1e4)
|
|
3095
|
-
});
|
|
3096
|
-
} catch {
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
3434
|
// src/client.ts
|
|
3101
3435
|
var NATIVE_PROVIDERS = [
|
|
3102
3436
|
"byteplus",
|
|
3103
3437
|
"elevenlabs",
|
|
3104
3438
|
"google",
|
|
3439
|
+
"kie",
|
|
3105
3440
|
"kling",
|
|
3106
3441
|
"openai",
|
|
3107
3442
|
"qwen",
|
|
@@ -3167,6 +3502,16 @@ function brotu(options) {
|
|
|
3167
3502
|
workspaceId: optionsWithKey.workspaceId
|
|
3168
3503
|
});
|
|
3169
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
|
+
}
|
|
3170
3515
|
if (provider.id === "kling") {
|
|
3171
3516
|
return new KlingAdapter({
|
|
3172
3517
|
apiKey: provider.apiKey,
|
|
@@ -3697,6 +4042,7 @@ export {
|
|
|
3697
4042
|
KLING_CAPABILITIES,
|
|
3698
4043
|
KLING_CATALOG,
|
|
3699
4044
|
KLING_MODELS,
|
|
4045
|
+
KieAdapter,
|
|
3700
4046
|
KlingAdapter,
|
|
3701
4047
|
OPENAI_CATALOG,
|
|
3702
4048
|
OPENAI_IMAGE_MODELS,
|
|
@@ -3725,7 +4071,9 @@ export {
|
|
|
3725
4071
|
getProviders,
|
|
3726
4072
|
hasModel,
|
|
3727
4073
|
isPendingJob,
|
|
4074
|
+
kieSnapshot,
|
|
3728
4075
|
ok,
|
|
4076
|
+
parseKieCallback,
|
|
3729
4077
|
persistOutputs,
|
|
3730
4078
|
registerModels,
|
|
3731
4079
|
resetCatalog,
|