@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.cjs
CHANGED
|
@@ -50,6 +50,7 @@ __export(index_exports, {
|
|
|
50
50
|
KLING_CAPABILITIES: () => KLING_CAPABILITIES,
|
|
51
51
|
KLING_CATALOG: () => KLING_CATALOG,
|
|
52
52
|
KLING_MODELS: () => KLING_MODELS,
|
|
53
|
+
KieAdapter: () => KieAdapter,
|
|
53
54
|
KlingAdapter: () => KlingAdapter,
|
|
54
55
|
OPENAI_CATALOG: () => OPENAI_CATALOG,
|
|
55
56
|
OPENAI_IMAGE_MODELS: () => OPENAI_IMAGE_MODELS,
|
|
@@ -78,7 +79,9 @@ __export(index_exports, {
|
|
|
78
79
|
getProviders: () => getProviders,
|
|
79
80
|
hasModel: () => hasModel,
|
|
80
81
|
isPendingJob: () => isPendingJob,
|
|
82
|
+
kieSnapshot: () => kieSnapshot,
|
|
81
83
|
ok: () => ok,
|
|
84
|
+
parseKieCallback: () => parseKieCallback,
|
|
82
85
|
persistOutputs: () => persistOutputs,
|
|
83
86
|
registerModels: () => registerModels,
|
|
84
87
|
resetCatalog: () => resetCatalog,
|
|
@@ -1349,6 +1352,7 @@ var PROVIDER_BASE_URLS = {
|
|
|
1349
1352
|
openai: "https://api.openai.com",
|
|
1350
1353
|
qwen: "https://dashscope-intl.aliyuncs.com",
|
|
1351
1354
|
topaz: "https://api.topazlabs.com",
|
|
1355
|
+
kie: "https://api.kie.ai/api/v1",
|
|
1352
1356
|
brotu: "https://api.brotu.app"
|
|
1353
1357
|
};
|
|
1354
1358
|
var DEFAULT_BROTU_API_URL = PROVIDER_BASE_URLS.brotu;
|
|
@@ -1424,6 +1428,14 @@ function resolveProvider(modelId, options) {
|
|
|
1424
1428
|
}
|
|
1425
1429
|
return { id, apiKey: configured.apiKey, baseUrl: baseUrl.replace(/\/$/, "") };
|
|
1426
1430
|
}
|
|
1431
|
+
const kie = vendorProviders(options).kie;
|
|
1432
|
+
if (kie && id !== "kie") {
|
|
1433
|
+
return {
|
|
1434
|
+
id: "kie",
|
|
1435
|
+
apiKey: kie.apiKey,
|
|
1436
|
+
baseUrl: (kie.baseUrl ?? PROVIDER_BASE_URLS.kie).replace(/\/$/, "")
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1427
1439
|
const platformKey = brotuApiKey(options);
|
|
1428
1440
|
if (platformKey) {
|
|
1429
1441
|
return {
|
|
@@ -1521,9 +1533,14 @@ function estimateFor(provider, type, params, defaults) {
|
|
|
1521
1533
|
}
|
|
1522
1534
|
const resolution = params.resolution;
|
|
1523
1535
|
const rate = (resolution ? model?.pricing?.byResolution?.[resolution] : void 0) ?? model?.pricing?.usdPerUnit;
|
|
1536
|
+
const tier = model?.runtimePricingTiers?.find(
|
|
1537
|
+
(candidate) => (candidate.resolution === void 0 || candidate.resolution === resolution) && (candidate.durationSeconds === void 0 || candidate.durationSeconds === units)
|
|
1538
|
+
);
|
|
1539
|
+
const creditsPerUnit = tier?.creditsPerUnit ?? model?.creditsPerUnit;
|
|
1524
1540
|
return {
|
|
1525
1541
|
unit,
|
|
1526
1542
|
units,
|
|
1543
|
+
credits: creditsPerUnit ? creditsPerUnit * units : null,
|
|
1527
1544
|
usd: rate === void 0 || unit === "token" ? null : Number((rate * units).toFixed(4)),
|
|
1528
1545
|
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,
|
|
1529
1546
|
provider,
|
|
@@ -1797,6 +1814,7 @@ var BrotuAdapter = class {
|
|
|
1797
1814
|
return {
|
|
1798
1815
|
unit: type === "video" ? "second" : "image",
|
|
1799
1816
|
units: credits,
|
|
1817
|
+
credits,
|
|
1800
1818
|
usd: null,
|
|
1801
1819
|
note: `${credits} Brotu credit${credits === 1 ? "" : "s"}. A vendor key generates on that provider.`,
|
|
1802
1820
|
provider: this.providerName,
|
|
@@ -2821,10 +2839,383 @@ var GoogleAdapter = class {
|
|
|
2821
2839
|
}
|
|
2822
2840
|
};
|
|
2823
2841
|
|
|
2842
|
+
// src/lib/webhook.ts
|
|
2843
|
+
function resolveWebhook(value) {
|
|
2844
|
+
if (!value) return void 0;
|
|
2845
|
+
if (typeof value === "string") {
|
|
2846
|
+
return isHttpUrl(value) ? { url: value } : void 0;
|
|
2847
|
+
}
|
|
2848
|
+
if (!isHttpUrl(value.url)) return void 0;
|
|
2849
|
+
return {
|
|
2850
|
+
url: value.url,
|
|
2851
|
+
secret: value.secret,
|
|
2852
|
+
headers: value.headers
|
|
2853
|
+
};
|
|
2854
|
+
}
|
|
2855
|
+
function isHttpUrl(value) {
|
|
2856
|
+
try {
|
|
2857
|
+
const parsed = new URL(value);
|
|
2858
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
2859
|
+
} catch {
|
|
2860
|
+
return false;
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
async function deliverWebhook(config, payload) {
|
|
2864
|
+
try {
|
|
2865
|
+
await fetch(config.url, {
|
|
2866
|
+
method: "POST",
|
|
2867
|
+
headers: {
|
|
2868
|
+
"content-type": "application/json",
|
|
2869
|
+
"user-agent": "@brotu/ai",
|
|
2870
|
+
"x-brotu-event": payload.event,
|
|
2871
|
+
...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
|
|
2872
|
+
...config.headers
|
|
2873
|
+
},
|
|
2874
|
+
body: JSON.stringify(payload),
|
|
2875
|
+
signal: AbortSignal.timeout(1e4)
|
|
2876
|
+
});
|
|
2877
|
+
} catch {
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// src/adapters/kie.adapter.ts
|
|
2882
|
+
var DEFAULT_BASE_URL4 = "https://api.kie.ai/api/v1";
|
|
2883
|
+
var POLL_INTERVAL_MS4 = 3e3;
|
|
2884
|
+
var DEFAULT_MAX_POLL_ATTEMPTS4 = 400;
|
|
2885
|
+
var KIE_MODEL_IDS = {
|
|
2886
|
+
"kling/v2-6": "kling-2.6/{mode}-to-video",
|
|
2887
|
+
"kling/v3": "kling-3.0/video",
|
|
2888
|
+
"dreamina-seedance-2-5-260628": "bytedance/seedance-2-5",
|
|
2889
|
+
"dreamina-seedance-2-0-260128": "bytedance/seedance-2",
|
|
2890
|
+
"dreamina-seedance-2-0-fast-260128": "bytedance/seedance-2-fast",
|
|
2891
|
+
"dreamina-seedance-2-0-mini-260615": "bytedance/seedance-2-mini",
|
|
2892
|
+
"wan2.6-t2v": "wan/2-6-text-to-video",
|
|
2893
|
+
"wan2.6-i2v": "wan/2-6-image-to-video",
|
|
2894
|
+
"wan2.7-t2v": "wan/2-7-text-to-video",
|
|
2895
|
+
"wan2.7-i2v": "wan/2-7-image-to-video",
|
|
2896
|
+
"gpt-image-1.5": "gpt-image/1.5-{mode}-to-image",
|
|
2897
|
+
"gpt-image-2": "gpt-image-2-{mode}-to-image",
|
|
2898
|
+
"topaz/video-upscale": "topaz/video-upscale"
|
|
2899
|
+
};
|
|
2900
|
+
var KieAdapter = class {
|
|
2901
|
+
constructor(opts) {
|
|
2902
|
+
this.opts = opts;
|
|
2903
|
+
}
|
|
2904
|
+
opts;
|
|
2905
|
+
providerName = "kie";
|
|
2906
|
+
supportedTypes = [
|
|
2907
|
+
"image",
|
|
2908
|
+
"video",
|
|
2909
|
+
"text",
|
|
2910
|
+
"audio"
|
|
2911
|
+
];
|
|
2912
|
+
get origin() {
|
|
2913
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL4).replace(/\/$/, "");
|
|
2914
|
+
}
|
|
2915
|
+
async request(path, init) {
|
|
2916
|
+
const response = await fetch(`${this.origin}${path}`, {
|
|
2917
|
+
...init,
|
|
2918
|
+
headers: {
|
|
2919
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
2920
|
+
"Content-Type": "application/json",
|
|
2921
|
+
...init?.headers
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
const text = await response.text();
|
|
2925
|
+
let body;
|
|
2926
|
+
try {
|
|
2927
|
+
body = text ? JSON.parse(text) : void 0;
|
|
2928
|
+
} catch {
|
|
2929
|
+
body = void 0;
|
|
2930
|
+
}
|
|
2931
|
+
const code = body?.code;
|
|
2932
|
+
if (!response.ok || typeof code === "number" && code !== 200) {
|
|
2933
|
+
const message = body?.msg || body?.message || text;
|
|
2934
|
+
throw new Error(message || `kie API ${response.status}`);
|
|
2935
|
+
}
|
|
2936
|
+
if (body?.data === void 0) {
|
|
2937
|
+
throw new Error("kie answered without a payload.");
|
|
2938
|
+
}
|
|
2939
|
+
return body.data;
|
|
2940
|
+
}
|
|
2941
|
+
/** Which market model serves this request. */
|
|
2942
|
+
marketModel(modelId, params) {
|
|
2943
|
+
const fromCatalog = getModel(modelId)?.endpoint?.replace(
|
|
2944
|
+
/^\/market\//,
|
|
2945
|
+
""
|
|
2946
|
+
);
|
|
2947
|
+
const mapped = fromCatalog || KIE_MODEL_IDS[modelId] || modelId;
|
|
2948
|
+
if (!mapped.includes("{mode}")) return mapped;
|
|
2949
|
+
return mapped.replace("{mode}", hasInputImage(params) ? "image" : "text");
|
|
2950
|
+
}
|
|
2951
|
+
callbackFor(params) {
|
|
2952
|
+
const perRequest = resolveWebhook(params.webhook);
|
|
2953
|
+
return perRequest?.url ?? this.opts.callbackUrl;
|
|
2954
|
+
}
|
|
2955
|
+
async createTask(kind, params) {
|
|
2956
|
+
const modelId = params.model ?? "";
|
|
2957
|
+
const body = {
|
|
2958
|
+
model: this.marketModel(modelId, params),
|
|
2959
|
+
input: buildInput(kind, params, modelId)
|
|
2960
|
+
};
|
|
2961
|
+
const callBackUrl = this.callbackFor(params);
|
|
2962
|
+
if (callBackUrl) body.callBackUrl = callBackUrl;
|
|
2963
|
+
const data = await this.request("/jobs/createTask", {
|
|
2964
|
+
method: "POST",
|
|
2965
|
+
body: JSON.stringify(body)
|
|
2966
|
+
});
|
|
2967
|
+
const taskId = data.taskId?.trim();
|
|
2968
|
+
if (!taskId) {
|
|
2969
|
+
throw new Error("kie accepted the task but returned no taskId.");
|
|
2970
|
+
}
|
|
2971
|
+
return taskId;
|
|
2972
|
+
}
|
|
2973
|
+
/** Ask kie about one task. The same shape a callback carries. */
|
|
2974
|
+
async task(taskId) {
|
|
2975
|
+
const record = await this.request(
|
|
2976
|
+
`/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`
|
|
2977
|
+
);
|
|
2978
|
+
return readTaskRecord(record, taskId);
|
|
2979
|
+
}
|
|
2980
|
+
async run(kind, params) {
|
|
2981
|
+
const startedAt = Date.now();
|
|
2982
|
+
const modelId = params.model ?? "(none)";
|
|
2983
|
+
const failure = (error) => ({
|
|
2984
|
+
success: false,
|
|
2985
|
+
outputs: [],
|
|
2986
|
+
creditsUsed: 0,
|
|
2987
|
+
provider: this.providerName,
|
|
2988
|
+
model: modelId,
|
|
2989
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2990
|
+
error
|
|
2991
|
+
});
|
|
2992
|
+
let taskId;
|
|
2993
|
+
try {
|
|
2994
|
+
taskId = await this.createTask(kind, params);
|
|
2995
|
+
} catch (error) {
|
|
2996
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2997
|
+
}
|
|
2998
|
+
const pollEndpoint = `/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`;
|
|
2999
|
+
if (isSubmitMode()) {
|
|
3000
|
+
throw new PendingJob(taskId, pollEndpoint);
|
|
3001
|
+
}
|
|
3002
|
+
const job = {
|
|
3003
|
+
id: taskId,
|
|
3004
|
+
provider: this.providerName,
|
|
3005
|
+
model: modelId,
|
|
3006
|
+
kind,
|
|
3007
|
+
pollEndpoint,
|
|
3008
|
+
params,
|
|
3009
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
3010
|
+
};
|
|
3011
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
|
|
3012
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3013
|
+
const snapshot = await this.completeJob(job);
|
|
3014
|
+
if (snapshot.status === "failed") {
|
|
3015
|
+
return failure(snapshot.error ?? "kie task failed.");
|
|
3016
|
+
}
|
|
3017
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
3018
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
3019
|
+
}
|
|
3020
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
|
|
3021
|
+
}
|
|
3022
|
+
return failure(
|
|
3023
|
+
`kie task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
async completeJob(job) {
|
|
3027
|
+
const settled = await this.task(job.id);
|
|
3028
|
+
return snapshotFrom(settled, job.model);
|
|
3029
|
+
}
|
|
3030
|
+
generateImage(params) {
|
|
3031
|
+
return this.run("image", params);
|
|
3032
|
+
}
|
|
3033
|
+
generateVideo(params) {
|
|
3034
|
+
return this.run("video", params);
|
|
3035
|
+
}
|
|
3036
|
+
generateText(params) {
|
|
3037
|
+
return this.run("text", params);
|
|
3038
|
+
}
|
|
3039
|
+
generateAudio(params) {
|
|
3040
|
+
return this.run("audio", params);
|
|
3041
|
+
}
|
|
3042
|
+
async estimateCost(type, params) {
|
|
3043
|
+
return estimateFor(this.providerName, type, params);
|
|
3044
|
+
}
|
|
3045
|
+
supportsModel() {
|
|
3046
|
+
return true;
|
|
3047
|
+
}
|
|
3048
|
+
getAvailableModels() {
|
|
3049
|
+
return [];
|
|
3050
|
+
}
|
|
3051
|
+
};
|
|
3052
|
+
function parseKieCallback(body) {
|
|
3053
|
+
let parsed = body;
|
|
3054
|
+
if (typeof body === "string") {
|
|
3055
|
+
try {
|
|
3056
|
+
parsed = JSON.parse(body);
|
|
3057
|
+
} catch {
|
|
3058
|
+
throw new Error("kie callback body is not JSON.");
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
const root = asRecord(parsed);
|
|
3062
|
+
const record = asRecord(root?.data) ?? root ?? {};
|
|
3063
|
+
const taskId = record.taskId?.trim();
|
|
3064
|
+
if (!taskId) {
|
|
3065
|
+
throw new Error("kie callback carries no taskId.");
|
|
3066
|
+
}
|
|
3067
|
+
return readTaskRecord(record, taskId);
|
|
3068
|
+
}
|
|
3069
|
+
function kieSnapshot(settled, model) {
|
|
3070
|
+
return snapshotFrom(settled, model ?? settled.model ?? "");
|
|
3071
|
+
}
|
|
3072
|
+
function snapshotFrom(settled, model) {
|
|
3073
|
+
if (settled.status === "failed") {
|
|
3074
|
+
return { status: "failed", error: settled.error ?? "kie task failed." };
|
|
3075
|
+
}
|
|
3076
|
+
if (settled.status === "pending") return { status: "pending" };
|
|
3077
|
+
return {
|
|
3078
|
+
status: "succeeded",
|
|
3079
|
+
result: {
|
|
3080
|
+
success: true,
|
|
3081
|
+
outputs: settled.outputs,
|
|
3082
|
+
creditsUsed: settled.creditsUsed,
|
|
3083
|
+
provider: "kie",
|
|
3084
|
+
model,
|
|
3085
|
+
processingTimeMs: 0
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
function readTaskRecord(record, taskId) {
|
|
3090
|
+
const state = (record.state ?? "").toLowerCase();
|
|
3091
|
+
const creditsUsed = record.creditsConsumed ?? 0;
|
|
3092
|
+
if (state === "fail") {
|
|
3093
|
+
return {
|
|
3094
|
+
taskId,
|
|
3095
|
+
model: record.model,
|
|
3096
|
+
status: "failed",
|
|
3097
|
+
outputs: [],
|
|
3098
|
+
creditsUsed,
|
|
3099
|
+
error: record.failMsg || (record.failCode ? `kie failCode ${record.failCode}` : void 0) || "kie task failed."
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
if (state !== "success") {
|
|
3103
|
+
return {
|
|
3104
|
+
taskId,
|
|
3105
|
+
model: record.model,
|
|
3106
|
+
status: "pending",
|
|
3107
|
+
outputs: [],
|
|
3108
|
+
creditsUsed
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
return {
|
|
3112
|
+
taskId,
|
|
3113
|
+
model: record.model,
|
|
3114
|
+
status: "succeeded",
|
|
3115
|
+
outputs: outputsFrom2(record.resultJson),
|
|
3116
|
+
creditsUsed
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
function outputsFrom2(resultJson) {
|
|
3120
|
+
if (!resultJson) return [];
|
|
3121
|
+
let parsed;
|
|
3122
|
+
try {
|
|
3123
|
+
parsed = JSON.parse(resultJson);
|
|
3124
|
+
} catch {
|
|
3125
|
+
return [];
|
|
3126
|
+
}
|
|
3127
|
+
const record = asRecord(parsed);
|
|
3128
|
+
const urls = Array.isArray(record?.resultUrls) ? record.resultUrls.filter(
|
|
3129
|
+
(url) => typeof url === "string" && url.length > 0
|
|
3130
|
+
) : [];
|
|
3131
|
+
if (urls.length > 0) return urls.map((url) => ({ url, mimeType: mimeFor(url) }));
|
|
3132
|
+
const object = record?.resultObject;
|
|
3133
|
+
if (object !== void 0) {
|
|
3134
|
+
const text = typeof object === "string" ? object : JSON.stringify(object);
|
|
3135
|
+
return [
|
|
3136
|
+
{
|
|
3137
|
+
url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
|
|
3138
|
+
mimeType: "text/plain",
|
|
3139
|
+
raw: { text }
|
|
3140
|
+
}
|
|
3141
|
+
];
|
|
3142
|
+
}
|
|
3143
|
+
return [];
|
|
3144
|
+
}
|
|
3145
|
+
function mimeFor(url) {
|
|
3146
|
+
const path = url.split("?")[0]?.toLowerCase() ?? "";
|
|
3147
|
+
if (/\.(mp4|mov|webm|m4v)$/.test(path)) return "video/mp4";
|
|
3148
|
+
if (/\.(mp3|wav|m4a|aac)$/.test(path)) return "audio/mpeg";
|
|
3149
|
+
if (/\.jpe?g$/.test(path)) return "image/jpeg";
|
|
3150
|
+
if (/\.webp$/.test(path)) return "image/webp";
|
|
3151
|
+
return "image/png";
|
|
3152
|
+
}
|
|
3153
|
+
function asRecord(value) {
|
|
3154
|
+
return value && typeof value === "object" ? value : void 0;
|
|
3155
|
+
}
|
|
3156
|
+
function hasInputImage(params) {
|
|
3157
|
+
const p = params;
|
|
3158
|
+
return Boolean(
|
|
3159
|
+
p.imageUrl || p.imageUrls?.length || p.referenceImages?.length
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
function buildInput(kind, params, modelId) {
|
|
3163
|
+
const raw = params.providerOptions?.kie ?? {};
|
|
3164
|
+
const base = modelId.startsWith("kling/") ? klingInput(params) : modelId.includes("seedance") ? seedanceInput(params) : commonInput(kind, params);
|
|
3165
|
+
return prune({ ...base, ...raw });
|
|
3166
|
+
}
|
|
3167
|
+
function klingInput(params) {
|
|
3168
|
+
const p = params;
|
|
3169
|
+
return {
|
|
3170
|
+
prompt: p.prompt,
|
|
3171
|
+
negative_prompt: p.negativePrompt,
|
|
3172
|
+
aspect_ratio: p.aspectRatio,
|
|
3173
|
+
// kie's kling enum is a string, unlike every other family.
|
|
3174
|
+
duration: p.duration === void 0 ? void 0 : String(p.duration),
|
|
3175
|
+
sound: p.withAudio,
|
|
3176
|
+
image_url: p.imageUrl ?? p.referenceImages?.[0]
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
function seedanceInput(params) {
|
|
3180
|
+
const p = params;
|
|
3181
|
+
return {
|
|
3182
|
+
prompt: p.prompt,
|
|
3183
|
+
duration: p.duration,
|
|
3184
|
+
resolution: p.resolution,
|
|
3185
|
+
aspect_ratio: p.aspectRatio,
|
|
3186
|
+
first_frame_url: p.imageUrl ?? p.referenceImages?.[0],
|
|
3187
|
+
last_frame_url: p.referenceImages?.[1],
|
|
3188
|
+
reference_image_urls: p.imageUrls,
|
|
3189
|
+
reference_video_urls: p.videoUrls ?? (p.videoUrl ? [p.videoUrl] : void 0),
|
|
3190
|
+
generate_audio: p.withAudio
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
function commonInput(kind, params) {
|
|
3194
|
+
const p = params;
|
|
3195
|
+
const images = p.imageUrls ?? p.referenceImages;
|
|
3196
|
+
return {
|
|
3197
|
+
prompt: p.prompt,
|
|
3198
|
+
negative_prompt: p.negativePrompt,
|
|
3199
|
+
aspect_ratio: p.aspectRatio,
|
|
3200
|
+
resolution: p.resolution,
|
|
3201
|
+
duration: kind === "video" ? p.duration : void 0,
|
|
3202
|
+
seed: p.seed,
|
|
3203
|
+
output_format: p.outputFormat,
|
|
3204
|
+
image_url: p.imageUrl ?? images?.[0],
|
|
3205
|
+
image_urls: images && images.length > 1 ? images : void 0,
|
|
3206
|
+
video_url: p.videoUrl
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
function prune(input) {
|
|
3210
|
+
return Object.fromEntries(
|
|
3211
|
+
Object.entries(input).filter(([, value]) => value !== void 0)
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
|
|
2824
3215
|
// src/adapters/kling.adapter.ts
|
|
2825
|
-
var
|
|
2826
|
-
var
|
|
2827
|
-
var
|
|
3216
|
+
var DEFAULT_BASE_URL5 = "https://api-singapore.klingai.com";
|
|
3217
|
+
var POLL_INTERVAL_MS5 = 5e3;
|
|
3218
|
+
var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
|
|
2828
3219
|
function stateOf(task) {
|
|
2829
3220
|
const raw = (task.status ?? task.task_status ?? "").toLowerCase();
|
|
2830
3221
|
if (raw === "failed") return "failed";
|
|
@@ -2839,7 +3230,7 @@ var KlingAdapter = class {
|
|
|
2839
3230
|
this.opts = opts;
|
|
2840
3231
|
}
|
|
2841
3232
|
get baseUrl() {
|
|
2842
|
-
return (this.opts.baseUrl ??
|
|
3233
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL5).replace(/\/$/, "");
|
|
2843
3234
|
}
|
|
2844
3235
|
async request(path, init) {
|
|
2845
3236
|
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
@@ -3021,7 +3412,7 @@ var KlingAdapter = class {
|
|
|
3021
3412
|
params,
|
|
3022
3413
|
submittedAt: new Date(startedAt).toISOString()
|
|
3023
3414
|
};
|
|
3024
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
3415
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
|
|
3025
3416
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3026
3417
|
const snapshot = await this.completeJob(job);
|
|
3027
3418
|
if (snapshot.status === "failed") {
|
|
@@ -3030,7 +3421,7 @@ var KlingAdapter = class {
|
|
|
3030
3421
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
3031
3422
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
3032
3423
|
}
|
|
3033
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
3424
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
|
|
3034
3425
|
}
|
|
3035
3426
|
return failure(
|
|
3036
3427
|
`Kling task ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -3167,7 +3558,7 @@ var KlingAdapter = class {
|
|
|
3167
3558
|
};
|
|
3168
3559
|
|
|
3169
3560
|
// src/adapters/openai.adapter.ts
|
|
3170
|
-
var
|
|
3561
|
+
var DEFAULT_BASE_URL6 = "https://api.openai.com";
|
|
3171
3562
|
var IMAGES_PATH2 = "/v1/images/generations";
|
|
3172
3563
|
var OpenAIAdapter = class {
|
|
3173
3564
|
providerName = "openai";
|
|
@@ -3178,7 +3569,7 @@ var OpenAIAdapter = class {
|
|
|
3178
3569
|
this.opts = opts;
|
|
3179
3570
|
}
|
|
3180
3571
|
get baseUrl() {
|
|
3181
|
-
return (this.opts.baseUrl ??
|
|
3572
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL6).replace(/\/$/, "");
|
|
3182
3573
|
}
|
|
3183
3574
|
binding(modelId) {
|
|
3184
3575
|
const id = modelId ?? "";
|
|
@@ -3399,10 +3790,10 @@ var OpenAIAdapter = class {
|
|
|
3399
3790
|
};
|
|
3400
3791
|
|
|
3401
3792
|
// src/adapters/qwen.adapter.ts
|
|
3402
|
-
var
|
|
3793
|
+
var DEFAULT_BASE_URL7 = "https://dashscope-intl.aliyuncs.com";
|
|
3403
3794
|
var TASKS_PATH2 = "/api/v1/tasks";
|
|
3404
|
-
var
|
|
3405
|
-
var
|
|
3795
|
+
var POLL_INTERVAL_MS6 = 5e3;
|
|
3796
|
+
var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
|
|
3406
3797
|
var QwenAdapter = class {
|
|
3407
3798
|
providerName = "qwen";
|
|
3408
3799
|
supportedTypes = ["image", "video"];
|
|
@@ -3411,7 +3802,7 @@ var QwenAdapter = class {
|
|
|
3411
3802
|
this.opts = opts;
|
|
3412
3803
|
}
|
|
3413
3804
|
get baseUrl() {
|
|
3414
|
-
return (this.opts.baseUrl ??
|
|
3805
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
|
|
3415
3806
|
}
|
|
3416
3807
|
async request(path, init) {
|
|
3417
3808
|
const headers = {
|
|
@@ -3609,7 +4000,7 @@ var QwenAdapter = class {
|
|
|
3609
4000
|
params,
|
|
3610
4001
|
submittedAt: new Date(startedAt).toISOString()
|
|
3611
4002
|
};
|
|
3612
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
4003
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
|
|
3613
4004
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3614
4005
|
const snapshot = await this.completeJob(job);
|
|
3615
4006
|
if (snapshot.status === "failed") {
|
|
@@ -3618,7 +4009,7 @@ var QwenAdapter = class {
|
|
|
3618
4009
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
3619
4010
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
3620
4011
|
}
|
|
3621
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4012
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
|
|
3622
4013
|
}
|
|
3623
4014
|
return failure(
|
|
3624
4015
|
`DashScope task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -3863,9 +4254,9 @@ var QwenAdapter = class {
|
|
|
3863
4254
|
};
|
|
3864
4255
|
|
|
3865
4256
|
// src/adapters/topaz.adapter.ts
|
|
3866
|
-
var
|
|
3867
|
-
var
|
|
3868
|
-
var
|
|
4257
|
+
var DEFAULT_BASE_URL8 = "https://api.topazlabs.com";
|
|
4258
|
+
var POLL_INTERVAL_MS7 = 5e3;
|
|
4259
|
+
var DEFAULT_MAX_POLL_ATTEMPTS7 = 240;
|
|
3869
4260
|
var DEFAULT_FPS = 30;
|
|
3870
4261
|
var PIXELS2 = {
|
|
3871
4262
|
"720p": { width: 1280, height: 720 },
|
|
@@ -3885,7 +4276,7 @@ var TopazAdapter = class {
|
|
|
3885
4276
|
this.opts = opts;
|
|
3886
4277
|
}
|
|
3887
4278
|
get baseUrl() {
|
|
3888
|
-
return (this.opts.baseUrl ??
|
|
4279
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL8).replace(/\/$/, "");
|
|
3889
4280
|
}
|
|
3890
4281
|
get headers() {
|
|
3891
4282
|
return {
|
|
@@ -4098,7 +4489,7 @@ var TopazAdapter = class {
|
|
|
4098
4489
|
params,
|
|
4099
4490
|
submittedAt: new Date(startedAt).toISOString()
|
|
4100
4491
|
};
|
|
4101
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
4492
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS7;
|
|
4102
4493
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4103
4494
|
const snapshot = await this.completeJob(job);
|
|
4104
4495
|
if (snapshot.status === "failed") {
|
|
@@ -4107,7 +4498,7 @@ var TopazAdapter = class {
|
|
|
4107
4498
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
4108
4499
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
4109
4500
|
}
|
|
4110
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4501
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS7));
|
|
4111
4502
|
}
|
|
4112
4503
|
return failure(
|
|
4113
4504
|
`Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -4273,7 +4664,7 @@ var TopazAdapter = class {
|
|
|
4273
4664
|
params,
|
|
4274
4665
|
submittedAt: new Date(startedAt).toISOString()
|
|
4275
4666
|
};
|
|
4276
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
4667
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS7;
|
|
4277
4668
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
4278
4669
|
const snapshot = await this.completeJob(job);
|
|
4279
4670
|
if (snapshot.status === "failed") {
|
|
@@ -4282,7 +4673,7 @@ var TopazAdapter = class {
|
|
|
4282
4673
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
4283
4674
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
4284
4675
|
}
|
|
4285
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
4676
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS7));
|
|
4286
4677
|
}
|
|
4287
4678
|
return failure(
|
|
4288
4679
|
`Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -4485,50 +4876,12 @@ async function persistOutputs(storage, outputs) {
|
|
|
4485
4876
|
);
|
|
4486
4877
|
}
|
|
4487
4878
|
|
|
4488
|
-
// src/lib/webhook.ts
|
|
4489
|
-
function resolveWebhook(value) {
|
|
4490
|
-
if (!value) return void 0;
|
|
4491
|
-
if (typeof value === "string") {
|
|
4492
|
-
return isHttpUrl(value) ? { url: value } : void 0;
|
|
4493
|
-
}
|
|
4494
|
-
if (!isHttpUrl(value.url)) return void 0;
|
|
4495
|
-
return {
|
|
4496
|
-
url: value.url,
|
|
4497
|
-
secret: value.secret,
|
|
4498
|
-
headers: value.headers
|
|
4499
|
-
};
|
|
4500
|
-
}
|
|
4501
|
-
function isHttpUrl(value) {
|
|
4502
|
-
try {
|
|
4503
|
-
const parsed = new URL(value);
|
|
4504
|
-
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
4505
|
-
} catch {
|
|
4506
|
-
return false;
|
|
4507
|
-
}
|
|
4508
|
-
}
|
|
4509
|
-
async function deliverWebhook(config, payload) {
|
|
4510
|
-
try {
|
|
4511
|
-
await fetch(config.url, {
|
|
4512
|
-
method: "POST",
|
|
4513
|
-
headers: {
|
|
4514
|
-
"content-type": "application/json",
|
|
4515
|
-
"user-agent": "@brotu/ai",
|
|
4516
|
-
"x-brotu-event": payload.event,
|
|
4517
|
-
...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
|
|
4518
|
-
...config.headers
|
|
4519
|
-
},
|
|
4520
|
-
body: JSON.stringify(payload),
|
|
4521
|
-
signal: AbortSignal.timeout(1e4)
|
|
4522
|
-
});
|
|
4523
|
-
} catch {
|
|
4524
|
-
}
|
|
4525
|
-
}
|
|
4526
|
-
|
|
4527
4879
|
// src/client.ts
|
|
4528
4880
|
var NATIVE_PROVIDERS = [
|
|
4529
4881
|
"byteplus",
|
|
4530
4882
|
"elevenlabs",
|
|
4531
4883
|
"google",
|
|
4884
|
+
"kie",
|
|
4532
4885
|
"kling",
|
|
4533
4886
|
"openai",
|
|
4534
4887
|
"qwen",
|
|
@@ -4594,6 +4947,16 @@ function brotu(options) {
|
|
|
4594
4947
|
workspaceId: optionsWithKey.workspaceId
|
|
4595
4948
|
});
|
|
4596
4949
|
}
|
|
4950
|
+
if (provider.id === "kie") {
|
|
4951
|
+
return new KieAdapter({
|
|
4952
|
+
apiKey: provider.apiKey,
|
|
4953
|
+
baseUrl: provider.baseUrl,
|
|
4954
|
+
// The client-level webhook doubles as kie's callback, so a submit
|
|
4955
|
+
// never has to be polled. kie posts its own payload there — read it
|
|
4956
|
+
// with `parseKieCallback`.
|
|
4957
|
+
callbackUrl: registeredWebhook?.url
|
|
4958
|
+
});
|
|
4959
|
+
}
|
|
4597
4960
|
if (provider.id === "kling") {
|
|
4598
4961
|
return new KlingAdapter({
|
|
4599
4962
|
apiKey: provider.apiKey,
|
|
@@ -4671,6 +5034,7 @@ function brotu(options) {
|
|
|
4671
5034
|
outputs: input.outputs,
|
|
4672
5035
|
error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
|
|
4673
5036
|
metadata: input.metadata ?? input.job?.metadata,
|
|
5037
|
+
creditsUsed: input.params?.credits ?? input.creditsUsed,
|
|
4674
5038
|
processingTimeMs: input.processingTimeMs,
|
|
4675
5039
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4676
5040
|
};
|
|
@@ -4684,6 +5048,7 @@ function brotu(options) {
|
|
|
4684
5048
|
outputs: payload.outputs,
|
|
4685
5049
|
error: payload.error,
|
|
4686
5050
|
metadata: payload.metadata,
|
|
5051
|
+
creditsUsed: payload.creditsUsed,
|
|
4687
5052
|
processingTimeMs: payload.processingTimeMs,
|
|
4688
5053
|
at: payload.completedAt
|
|
4689
5054
|
});
|
|
@@ -4700,7 +5065,7 @@ function brotu(options) {
|
|
|
4700
5065
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
4701
5066
|
});
|
|
4702
5067
|
}
|
|
4703
|
-
async function toGeneration(raw, metadata) {
|
|
5068
|
+
async function toGeneration(raw, metadata, credits) {
|
|
4704
5069
|
if (!raw.success) {
|
|
4705
5070
|
return fail({
|
|
4706
5071
|
code: "provider_error",
|
|
@@ -4715,6 +5080,9 @@ function brotu(options) {
|
|
|
4715
5080
|
provider: raw.provider,
|
|
4716
5081
|
model: raw.model,
|
|
4717
5082
|
processingTimeMs: raw.processingTimeMs,
|
|
5083
|
+
// What you said to charge wins over what the platform reported: the
|
|
5084
|
+
// caller's number is the one their ledger has to match.
|
|
5085
|
+
creditsUsed: credits ?? raw.creditsUsed,
|
|
4718
5086
|
metadata
|
|
4719
5087
|
});
|
|
4720
5088
|
}
|
|
@@ -4725,7 +5093,8 @@ function brotu(options) {
|
|
|
4725
5093
|
try {
|
|
4726
5094
|
const result = await toGeneration(
|
|
4727
5095
|
await generateWith(routed.data.adapter, kind, params),
|
|
4728
|
-
params.metadata
|
|
5096
|
+
params.metadata,
|
|
5097
|
+
params.credits
|
|
4729
5098
|
);
|
|
4730
5099
|
if (result.error) {
|
|
4731
5100
|
await notifySettled({
|
|
@@ -4747,6 +5116,7 @@ function brotu(options) {
|
|
|
4747
5116
|
model: result.data.model,
|
|
4748
5117
|
outputs: result.data.outputs,
|
|
4749
5118
|
metadata: result.data.metadata,
|
|
5119
|
+
creditsUsed: result.data.creditsUsed,
|
|
4750
5120
|
processingTimeMs: result.data.processingTimeMs
|
|
4751
5121
|
});
|
|
4752
5122
|
return result;
|
|
@@ -4843,6 +5213,7 @@ function brotu(options) {
|
|
|
4843
5213
|
outputs: snapshot.result.outputs,
|
|
4844
5214
|
provider: snapshot.result.provider,
|
|
4845
5215
|
model: snapshot.result.model,
|
|
5216
|
+
creditsUsed: snapshot.result.creditsUsed,
|
|
4846
5217
|
processingTimeMs: snapshot.result.processingTimeMs,
|
|
4847
5218
|
metadata: job.metadata
|
|
4848
5219
|
});
|
|
@@ -4865,7 +5236,11 @@ function brotu(options) {
|
|
|
4865
5236
|
}
|
|
4866
5237
|
async function finalizeSnapshot(job, snapshot) {
|
|
4867
5238
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
4868
|
-
const persisted = await toGeneration(
|
|
5239
|
+
const persisted = await toGeneration(
|
|
5240
|
+
snapshot.result,
|
|
5241
|
+
job.metadata,
|
|
5242
|
+
job.params.credits
|
|
5243
|
+
);
|
|
4869
5244
|
if (!persisted.error) {
|
|
4870
5245
|
snapshot = {
|
|
4871
5246
|
status: "succeeded",
|
|
@@ -5052,6 +5427,7 @@ function brotu(options) {
|
|
|
5052
5427
|
provider: snapshot.result.provider,
|
|
5053
5428
|
model: snapshot.result.model,
|
|
5054
5429
|
processingTimeMs: snapshot.result.processingTimeMs,
|
|
5430
|
+
creditsUsed: job.params.credits ?? snapshot.result.creditsUsed,
|
|
5055
5431
|
metadata: job.metadata
|
|
5056
5432
|
});
|
|
5057
5433
|
}
|
|
@@ -5112,6 +5488,7 @@ function brotu(options) {
|
|
|
5112
5488
|
KLING_CAPABILITIES,
|
|
5113
5489
|
KLING_CATALOG,
|
|
5114
5490
|
KLING_MODELS,
|
|
5491
|
+
KieAdapter,
|
|
5115
5492
|
KlingAdapter,
|
|
5116
5493
|
OPENAI_CATALOG,
|
|
5117
5494
|
OPENAI_IMAGE_MODELS,
|
|
@@ -5140,7 +5517,9 @@ function brotu(options) {
|
|
|
5140
5517
|
getProviders,
|
|
5141
5518
|
hasModel,
|
|
5142
5519
|
isPendingJob,
|
|
5520
|
+
kieSnapshot,
|
|
5143
5521
|
ok,
|
|
5522
|
+
parseKieCallback,
|
|
5144
5523
|
persistOutputs,
|
|
5145
5524
|
registerModels,
|
|
5146
5525
|
resetCatalog,
|