@brotu/ai 0.2.0 → 0.4.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 +53 -6
- package/dist/adapters/brotu.adapter.d.ts +46 -0
- package/dist/adapters/brotu.adapter.d.ts.map +1 -0
- package/dist/catalog.cjs +36 -14
- package/dist/catalog.d.ts +1 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +3 -1
- package/dist/{chunk-PL534BFN.js → chunk-QOWHHKSI.js} +35 -14
- package/dist/client.d.ts +11 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/helpers/result.d.ts +1 -1
- package/dist/helpers/result.d.ts.map +1 -1
- package/dist/index.cjs +574 -47
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +537 -33
- package/dist/lib/webhook.d.ts +28 -0
- package/dist/lib/webhook.d.ts.map +1 -0
- package/dist/ports/content-generator.port.d.ts +6 -0
- package/dist/ports/content-generator.port.d.ts.map +1 -1
- package/dist/types.d.ts +21 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
BYTEPLUS_CATALOG,
|
|
3
3
|
BYTEPLUS_IMAGE_MODELS,
|
|
4
4
|
BYTEPLUS_MODELS,
|
|
5
|
+
DEFAULT_BROTU_API_URL,
|
|
5
6
|
ELEVENLABS_CATALOG,
|
|
6
7
|
ELEVENLABS_MODELS,
|
|
7
8
|
ELEVENLABS_OUTPUT_FORMATS,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
resetCatalog,
|
|
36
37
|
resolveProvider,
|
|
37
38
|
videoPathFor
|
|
38
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-QOWHHKSI.js";
|
|
39
40
|
|
|
40
41
|
// src/lib/jobs.ts
|
|
41
42
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -91,12 +92,316 @@ function estimateFor(provider, type, params, defaults) {
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// src/adapters/brotu.adapter.ts
|
|
96
|
+
var DEFAULT_API_URL = "https://api.brotu.app";
|
|
97
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
98
|
+
var DEFAULT_MAX_POLL_ATTEMPTS = 400;
|
|
99
|
+
var PLATFORM_MODEL_IDS = {
|
|
100
|
+
"kling/v2-6": "kling-2.6",
|
|
101
|
+
"kling/v3": "kling-3.0/video",
|
|
102
|
+
"dreamina-seedance-2-5-260628": "bytedance/seedance-2-5",
|
|
103
|
+
"dreamina-seedance-2-0-260128": "bytedance/seedance-2",
|
|
104
|
+
"dreamina-seedance-2-0-fast-260128": "bytedance/seedance-2-fast",
|
|
105
|
+
"gpt-image-1.5": "gpt-image/1.5"
|
|
106
|
+
};
|
|
107
|
+
var UNSUPPORTED_VIA_CREDITS = "Speech and text generate on the vendor. Pass that provider's key.";
|
|
108
|
+
var BrotuAdapter = class {
|
|
109
|
+
constructor(opts) {
|
|
110
|
+
this.opts = opts;
|
|
111
|
+
}
|
|
112
|
+
opts;
|
|
113
|
+
providerName = "brotu";
|
|
114
|
+
supportedTypes = ["image", "video"];
|
|
115
|
+
workspaceIdCache;
|
|
116
|
+
get origin() {
|
|
117
|
+
return (this.opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
118
|
+
}
|
|
119
|
+
headers() {
|
|
120
|
+
return {
|
|
121
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
122
|
+
"Content-Type": "application/json"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async workspaceId() {
|
|
126
|
+
if (this.opts.workspaceId) return this.opts.workspaceId;
|
|
127
|
+
if (this.workspaceIdCache) return this.workspaceIdCache;
|
|
128
|
+
const payload = await this.request("/api/v1/studio/default-workspace");
|
|
129
|
+
const id = payload.workspaceId?.trim();
|
|
130
|
+
if (!id) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"This Brotu account has no workspace. Open https://brotu.app and create one."
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
this.workspaceIdCache = id;
|
|
136
|
+
return id;
|
|
137
|
+
}
|
|
138
|
+
studioPath(path, workspaceId) {
|
|
139
|
+
const joiner = path.includes("?") ? "&" : "?";
|
|
140
|
+
return `${path}${joiner}workspaceId=${encodeURIComponent(workspaceId)}`;
|
|
141
|
+
}
|
|
142
|
+
async request(path, init) {
|
|
143
|
+
const response = await fetch(`${this.origin}${path}`, {
|
|
144
|
+
...init,
|
|
145
|
+
headers: { ...this.headers(), ...init?.headers }
|
|
146
|
+
});
|
|
147
|
+
const text = await response.text();
|
|
148
|
+
let body = void 0;
|
|
149
|
+
if (text) {
|
|
150
|
+
try {
|
|
151
|
+
body = JSON.parse(text);
|
|
152
|
+
} catch {
|
|
153
|
+
body = text;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const parsed = body;
|
|
158
|
+
const message = parsed && typeof parsed === "object" ? parsed.error?.message || parsed.message || text : text;
|
|
159
|
+
throw new Error(message || `Brotu API ${response.status}`);
|
|
160
|
+
}
|
|
161
|
+
return body;
|
|
162
|
+
}
|
|
163
|
+
platformModel(modelId) {
|
|
164
|
+
return PLATFORM_MODEL_IDS[modelId] ?? modelId;
|
|
165
|
+
}
|
|
166
|
+
async startGeneration(kind, params) {
|
|
167
|
+
const modelId = params.model ?? "";
|
|
168
|
+
const workspaceId = await this.workspaceId();
|
|
169
|
+
const path = kind === "video" ? "/api/v1/studio/videos" : "/api/v1/studio/images";
|
|
170
|
+
const body = kind === "video" ? this.videoBody(params, modelId) : this.imageBody(params, modelId);
|
|
171
|
+
const result = await this.request(
|
|
172
|
+
this.studioPath(path, workspaceId),
|
|
173
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
174
|
+
);
|
|
175
|
+
const generationId = result.generationId?.trim();
|
|
176
|
+
if (!generationId) {
|
|
177
|
+
throw new Error("Brotu accepted the request but returned no generation id.");
|
|
178
|
+
}
|
|
179
|
+
return { generationId, workspaceId };
|
|
180
|
+
}
|
|
181
|
+
imageBody(params, modelId) {
|
|
182
|
+
return {
|
|
183
|
+
prompt: params.prompt,
|
|
184
|
+
model: this.platformModel(modelId),
|
|
185
|
+
aspectRatio: params.aspectRatio,
|
|
186
|
+
resolution: params.resolution,
|
|
187
|
+
negativePrompt: params.negativePrompt,
|
|
188
|
+
outputFormat: params.outputFormat,
|
|
189
|
+
seed: params.seed,
|
|
190
|
+
referenceImages: params.referenceImages
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
videoBody(params, modelId) {
|
|
194
|
+
return {
|
|
195
|
+
prompt: params.prompt,
|
|
196
|
+
model: this.platformModel(modelId),
|
|
197
|
+
duration: params.duration,
|
|
198
|
+
resolution: params.resolution,
|
|
199
|
+
aspectRatio: params.aspectRatio,
|
|
200
|
+
mode: params.mode,
|
|
201
|
+
withAudio: params.withAudio,
|
|
202
|
+
seed: params.seed,
|
|
203
|
+
imageUrl: params.imageUrl,
|
|
204
|
+
imageUrls: params.imageUrls,
|
|
205
|
+
videoUrl: params.videoUrl,
|
|
206
|
+
videoUrls: params.videoUrls,
|
|
207
|
+
referenceImages: params.referenceImages
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async run(kind, params) {
|
|
211
|
+
const startedAt = Date.now();
|
|
212
|
+
const modelId = params.model ?? "(none)";
|
|
213
|
+
const failure = (error) => ({
|
|
214
|
+
success: false,
|
|
215
|
+
outputs: [],
|
|
216
|
+
creditsUsed: 0,
|
|
217
|
+
provider: this.providerName,
|
|
218
|
+
model: modelId,
|
|
219
|
+
processingTimeMs: Date.now() - startedAt,
|
|
220
|
+
error
|
|
221
|
+
});
|
|
222
|
+
let submitted;
|
|
223
|
+
try {
|
|
224
|
+
submitted = await this.startGeneration(kind, params);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
227
|
+
}
|
|
228
|
+
const pollEndpoint = this.studioPath(
|
|
229
|
+
`/api/v1/studio/generations/${submitted.generationId}`,
|
|
230
|
+
submitted.workspaceId
|
|
231
|
+
);
|
|
232
|
+
if (isSubmitMode()) {
|
|
233
|
+
throw new PendingJob(submitted.generationId, pollEndpoint);
|
|
234
|
+
}
|
|
235
|
+
const job = {
|
|
236
|
+
id: submitted.generationId,
|
|
237
|
+
provider: this.providerName,
|
|
238
|
+
model: modelId,
|
|
239
|
+
kind,
|
|
240
|
+
pollEndpoint,
|
|
241
|
+
params,
|
|
242
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
243
|
+
};
|
|
244
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS;
|
|
245
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
246
|
+
const snapshot = await this.completeJob(job);
|
|
247
|
+
if (snapshot.status === "failed") {
|
|
248
|
+
return failure(snapshot.error ?? "Brotu generation failed.");
|
|
249
|
+
}
|
|
250
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
251
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
252
|
+
}
|
|
253
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
254
|
+
}
|
|
255
|
+
return failure(
|
|
256
|
+
`Brotu generation ${submitted.generationId} did not finish after ${maxAttempts} checks.`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
async completeJob(job) {
|
|
260
|
+
const workspaceId = this.workspaceFromPoll(job.pollEndpoint) ?? await this.workspaceId();
|
|
261
|
+
const path = this.studioPath(
|
|
262
|
+
`/api/v1/studio/generations/${job.id}`,
|
|
263
|
+
workspaceId
|
|
264
|
+
);
|
|
265
|
+
const generation = await this.request(path);
|
|
266
|
+
const status = (generation.status ?? "").toLowerCase();
|
|
267
|
+
if (status === "failed") {
|
|
268
|
+
return {
|
|
269
|
+
status: "failed",
|
|
270
|
+
error: generation.errorMessage ?? "Brotu generation failed."
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (status !== "completed") {
|
|
274
|
+
return { status: "pending" };
|
|
275
|
+
}
|
|
276
|
+
const outputs = outputsFrom(generation, job.kind);
|
|
277
|
+
return {
|
|
278
|
+
status: "succeeded",
|
|
279
|
+
result: {
|
|
280
|
+
success: true,
|
|
281
|
+
outputs,
|
|
282
|
+
creditsUsed: generation.creditsUsed ?? 0,
|
|
283
|
+
provider: this.providerName,
|
|
284
|
+
model: job.model,
|
|
285
|
+
processingTimeMs: 0
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
workspaceFromPoll(pollEndpoint) {
|
|
290
|
+
if (!pollEndpoint) return void 0;
|
|
291
|
+
try {
|
|
292
|
+
const url = new URL(pollEndpoint, "https://brotu.invalid");
|
|
293
|
+
return url.searchParams.get("workspaceId") ?? void 0;
|
|
294
|
+
} catch {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
generateImage(params) {
|
|
299
|
+
return this.run("image", params);
|
|
300
|
+
}
|
|
301
|
+
generateVideo(params) {
|
|
302
|
+
return this.run("video", params);
|
|
303
|
+
}
|
|
304
|
+
async generateText(params) {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
outputs: [],
|
|
308
|
+
creditsUsed: 0,
|
|
309
|
+
provider: this.providerName,
|
|
310
|
+
model: params.model ?? "(none)",
|
|
311
|
+
processingTimeMs: 0,
|
|
312
|
+
error: UNSUPPORTED_VIA_CREDITS
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
async generateAudio(params) {
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
outputs: [],
|
|
319
|
+
creditsUsed: 0,
|
|
320
|
+
provider: this.providerName,
|
|
321
|
+
model: params.model ?? "(none)",
|
|
322
|
+
processingTimeMs: 0,
|
|
323
|
+
error: UNSUPPORTED_VIA_CREDITS
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async estimateCost(type, params) {
|
|
327
|
+
if (type === "audio" || type === "text") {
|
|
328
|
+
const estimate = estimateFor(this.providerName, type, params);
|
|
329
|
+
return {
|
|
330
|
+
...estimate,
|
|
331
|
+
usd: null,
|
|
332
|
+
note: UNSUPPORTED_VIA_CREDITS
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const workspaceId = await this.workspaceId();
|
|
337
|
+
const video = params;
|
|
338
|
+
const image = params;
|
|
339
|
+
const payload = await this.request(
|
|
340
|
+
this.studioPath("/api/v1/studio/estimate", workspaceId),
|
|
341
|
+
{
|
|
342
|
+
method: "POST",
|
|
343
|
+
body: JSON.stringify({
|
|
344
|
+
model: this.platformModel(params.model ?? ""),
|
|
345
|
+
duration: video.duration,
|
|
346
|
+
resolution: video.resolution ?? image.resolution,
|
|
347
|
+
aspectRatio: video.aspectRatio ?? image.aspectRatio,
|
|
348
|
+
withAudio: video.withAudio,
|
|
349
|
+
hasReferenceImage: Boolean(
|
|
350
|
+
video.imageUrl || video.imageUrls?.length || video.referenceImages?.length || image.referenceImages?.length
|
|
351
|
+
),
|
|
352
|
+
hasReferenceVideo: Boolean(video.videoUrl || video.videoUrls?.length),
|
|
353
|
+
quality: image.quality
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
const credits = payload.estimatedCredits ?? 0;
|
|
358
|
+
return {
|
|
359
|
+
unit: type === "video" ? "second" : "image",
|
|
360
|
+
units: credits,
|
|
361
|
+
usd: null,
|
|
362
|
+
note: `${credits} Brotu credit${credits === 1 ? "" : "s"}. A vendor key generates on that provider.`,
|
|
363
|
+
provider: this.providerName,
|
|
364
|
+
model: params.model ?? ""
|
|
365
|
+
};
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const estimate = estimateFor(this.providerName, type, params);
|
|
368
|
+
return {
|
|
369
|
+
...estimate,
|
|
370
|
+
usd: null,
|
|
371
|
+
note: error instanceof Error ? error.message : "Could not estimate Brotu credits."
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
supportsModel() {
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
getAvailableModels() {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
function outputsFrom(generation, kind) {
|
|
383
|
+
const bucket = generation.outputs;
|
|
384
|
+
const urls = kind === "video" ? bucket?.videos ?? bucket?.images ?? [] : bucket?.images ?? bucket?.videos ?? [];
|
|
385
|
+
const copies = bucket?.copies ?? [];
|
|
386
|
+
const mimeType = kind === "video" ? "video/mp4" : "image/png";
|
|
387
|
+
const fromUrls = urls.filter(Boolean).map((url) => ({
|
|
388
|
+
url,
|
|
389
|
+
mimeType
|
|
390
|
+
}));
|
|
391
|
+
if (fromUrls.length > 0) return fromUrls;
|
|
392
|
+
return copies.filter(Boolean).map((url) => ({
|
|
393
|
+
url,
|
|
394
|
+
mimeType: "text/plain",
|
|
395
|
+
raw: { text: url }
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
|
|
94
399
|
// src/adapters/byteplus.adapter.ts
|
|
95
400
|
var DEFAULT_BASE_URL = "https://ark.ap-southeast.bytepluses.com";
|
|
96
401
|
var TASKS_PATH = "/api/v3/contents/generations/tasks";
|
|
97
402
|
var IMAGES_PATH = "/api/v3/images/generations";
|
|
98
|
-
var
|
|
99
|
-
var
|
|
403
|
+
var POLL_INTERVAL_MS2 = 5e3;
|
|
404
|
+
var DEFAULT_MAX_POLL_ATTEMPTS2 = 240;
|
|
100
405
|
var BytePlusAdapter = class {
|
|
101
406
|
providerName = "byteplus";
|
|
102
407
|
supportedTypes = ["image", "video"];
|
|
@@ -296,7 +601,7 @@ var BytePlusAdapter = class {
|
|
|
296
601
|
params,
|
|
297
602
|
submittedAt: new Date(startedAt).toISOString()
|
|
298
603
|
};
|
|
299
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
604
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS2;
|
|
300
605
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
301
606
|
const snapshot = await this.completeJob(job);
|
|
302
607
|
if (snapshot.status === "failed") {
|
|
@@ -305,7 +610,7 @@ var BytePlusAdapter = class {
|
|
|
305
610
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
306
611
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
307
612
|
}
|
|
308
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
613
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS2));
|
|
309
614
|
}
|
|
310
615
|
return failure(
|
|
311
616
|
`Ark task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -582,8 +887,8 @@ var ElevenLabsAdapter = class {
|
|
|
582
887
|
// src/adapters/google.adapter.ts
|
|
583
888
|
var DEFAULT_BASE_URL3 = "https://generativelanguage.googleapis.com";
|
|
584
889
|
var INTERACTIONS_PATH = "/v1beta/interactions";
|
|
585
|
-
var
|
|
586
|
-
var
|
|
890
|
+
var POLL_INTERVAL_MS3 = 1e4;
|
|
891
|
+
var DEFAULT_MAX_POLL_ATTEMPTS3 = 120;
|
|
587
892
|
var GoogleAdapter = class {
|
|
588
893
|
providerName = "google";
|
|
589
894
|
supportedTypes = [
|
|
@@ -855,7 +1160,7 @@ var GoogleAdapter = class {
|
|
|
855
1160
|
params,
|
|
856
1161
|
submittedAt: new Date(startedAt).toISOString()
|
|
857
1162
|
};
|
|
858
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
1163
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS3;
|
|
859
1164
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
860
1165
|
const snapshot = await this.completeJob(job);
|
|
861
1166
|
if (snapshot.status === "failed") {
|
|
@@ -864,7 +1169,7 @@ var GoogleAdapter = class {
|
|
|
864
1169
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
865
1170
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
866
1171
|
}
|
|
867
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1172
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS3));
|
|
868
1173
|
}
|
|
869
1174
|
return failure(`Veo operation ${operation} did not finish in time.`);
|
|
870
1175
|
}
|
|
@@ -1079,8 +1384,8 @@ var GoogleAdapter = class {
|
|
|
1079
1384
|
|
|
1080
1385
|
// src/adapters/kling.adapter.ts
|
|
1081
1386
|
var DEFAULT_BASE_URL4 = "https://api-singapore.klingai.com";
|
|
1082
|
-
var
|
|
1083
|
-
var
|
|
1387
|
+
var POLL_INTERVAL_MS4 = 5e3;
|
|
1388
|
+
var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
|
|
1084
1389
|
function stateOf(task) {
|
|
1085
1390
|
const raw = (task.status ?? task.task_status ?? "").toLowerCase();
|
|
1086
1391
|
if (raw === "failed") return "failed";
|
|
@@ -1277,7 +1582,7 @@ var KlingAdapter = class {
|
|
|
1277
1582
|
params,
|
|
1278
1583
|
submittedAt: new Date(startedAt).toISOString()
|
|
1279
1584
|
};
|
|
1280
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
1585
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
|
|
1281
1586
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1282
1587
|
const snapshot = await this.completeJob(job);
|
|
1283
1588
|
if (snapshot.status === "failed") {
|
|
@@ -1286,7 +1591,7 @@ var KlingAdapter = class {
|
|
|
1286
1591
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
1287
1592
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
1288
1593
|
}
|
|
1289
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1594
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
|
|
1290
1595
|
}
|
|
1291
1596
|
return failure(
|
|
1292
1597
|
`Kling task ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -1657,8 +1962,8 @@ var OpenAIAdapter = class {
|
|
|
1657
1962
|
// src/adapters/qwen.adapter.ts
|
|
1658
1963
|
var DEFAULT_BASE_URL6 = "https://dashscope-intl.aliyuncs.com";
|
|
1659
1964
|
var TASKS_PATH2 = "/api/v1/tasks";
|
|
1660
|
-
var
|
|
1661
|
-
var
|
|
1965
|
+
var POLL_INTERVAL_MS5 = 5e3;
|
|
1966
|
+
var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
|
|
1662
1967
|
var QwenAdapter = class {
|
|
1663
1968
|
providerName = "qwen";
|
|
1664
1969
|
supportedTypes = ["image", "video"];
|
|
@@ -1865,7 +2170,7 @@ var QwenAdapter = class {
|
|
|
1865
2170
|
params,
|
|
1866
2171
|
submittedAt: new Date(startedAt).toISOString()
|
|
1867
2172
|
};
|
|
1868
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
2173
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
|
|
1869
2174
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1870
2175
|
const snapshot = await this.completeJob(job);
|
|
1871
2176
|
if (snapshot.status === "failed") {
|
|
@@ -1874,7 +2179,7 @@ var QwenAdapter = class {
|
|
|
1874
2179
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
1875
2180
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
1876
2181
|
}
|
|
1877
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
2182
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
|
|
1878
2183
|
}
|
|
1879
2184
|
return failure(
|
|
1880
2185
|
`DashScope task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -2256,6 +2561,45 @@ async function persistOutputs(storage, outputs) {
|
|
|
2256
2561
|
);
|
|
2257
2562
|
}
|
|
2258
2563
|
|
|
2564
|
+
// src/lib/webhook.ts
|
|
2565
|
+
function resolveWebhook(value) {
|
|
2566
|
+
if (!value) return void 0;
|
|
2567
|
+
if (typeof value === "string") {
|
|
2568
|
+
return isHttpUrl(value) ? { url: value } : void 0;
|
|
2569
|
+
}
|
|
2570
|
+
if (!isHttpUrl(value.url)) return void 0;
|
|
2571
|
+
return {
|
|
2572
|
+
url: value.url,
|
|
2573
|
+
secret: value.secret,
|
|
2574
|
+
headers: value.headers
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
function isHttpUrl(value) {
|
|
2578
|
+
try {
|
|
2579
|
+
const parsed = new URL(value);
|
|
2580
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
2581
|
+
} catch {
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
async function deliverWebhook(config, payload) {
|
|
2586
|
+
try {
|
|
2587
|
+
await fetch(config.url, {
|
|
2588
|
+
method: "POST",
|
|
2589
|
+
headers: {
|
|
2590
|
+
"content-type": "application/json",
|
|
2591
|
+
"user-agent": "@brotu/ai",
|
|
2592
|
+
"x-brotu-event": payload.event,
|
|
2593
|
+
...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
|
|
2594
|
+
...config.headers
|
|
2595
|
+
},
|
|
2596
|
+
body: JSON.stringify(payload),
|
|
2597
|
+
signal: AbortSignal.timeout(1e4)
|
|
2598
|
+
});
|
|
2599
|
+
} catch {
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2259
2603
|
// src/client.ts
|
|
2260
2604
|
var NATIVE_PROVIDERS = [
|
|
2261
2605
|
"byteplus",
|
|
@@ -2266,10 +2610,17 @@ var NATIVE_PROVIDERS = [
|
|
|
2266
2610
|
"qwen"
|
|
2267
2611
|
];
|
|
2268
2612
|
function brotuClient(options) {
|
|
2269
|
-
|
|
2270
|
-
|
|
2613
|
+
const apiKey = options.apiKey?.trim();
|
|
2614
|
+
if (!apiKey) {
|
|
2615
|
+
throw new Error(
|
|
2616
|
+
"Pass a Brotu API key (brotu_sk_\u2026). Get one at https://brotu.app."
|
|
2617
|
+
);
|
|
2271
2618
|
}
|
|
2619
|
+
const optionsWithKey = { ...options, apiKey };
|
|
2620
|
+
const vendors = optionsWithKey.providers ?? {};
|
|
2272
2621
|
registerModels(options.models);
|
|
2622
|
+
let registeredWebhook = resolveWebhook(options.webhook);
|
|
2623
|
+
const notifiedJobIds = /* @__PURE__ */ new Set();
|
|
2273
2624
|
const storage = options.storage ? createS3Storage(options.storage) : void 0;
|
|
2274
2625
|
const shouldPersist = storage && options.storage?.persistOutputs !== false;
|
|
2275
2626
|
const adapters = /* @__PURE__ */ new Map();
|
|
@@ -2290,7 +2641,7 @@ function brotuClient(options) {
|
|
|
2290
2641
|
}
|
|
2291
2642
|
let provider;
|
|
2292
2643
|
try {
|
|
2293
|
-
provider = resolveProvider(modelId,
|
|
2644
|
+
provider = resolveProvider(modelId, optionsWithKey);
|
|
2294
2645
|
} catch (error) {
|
|
2295
2646
|
return failFrom("missing_key", error, { model: modelId });
|
|
2296
2647
|
}
|
|
@@ -2311,6 +2662,13 @@ function brotuClient(options) {
|
|
|
2311
2662
|
return ok({ adapter, provider: provider.id, model: modelId });
|
|
2312
2663
|
}
|
|
2313
2664
|
function buildAdapter(provider) {
|
|
2665
|
+
if (provider.id === "brotu") {
|
|
2666
|
+
return new BrotuAdapter({
|
|
2667
|
+
apiKey,
|
|
2668
|
+
apiUrl: optionsWithKey.apiUrl,
|
|
2669
|
+
workspaceId: optionsWithKey.workspaceId
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2314
2672
|
if (provider.id === "kling") {
|
|
2315
2673
|
return new KlingAdapter({
|
|
2316
2674
|
apiKey: provider.apiKey,
|
|
@@ -2359,6 +2717,31 @@ function brotuClient(options) {
|
|
|
2359
2717
|
return adapter.generateAudio(params);
|
|
2360
2718
|
return adapter.generateText(params);
|
|
2361
2719
|
}
|
|
2720
|
+
function webhookFor(params) {
|
|
2721
|
+
return resolveWebhook(params?.webhook) ?? registeredWebhook;
|
|
2722
|
+
}
|
|
2723
|
+
async function notifySettled(input) {
|
|
2724
|
+
const config = webhookFor(input.params);
|
|
2725
|
+
if (!config) return;
|
|
2726
|
+
const jobId = input.job?.id;
|
|
2727
|
+
if (jobId) {
|
|
2728
|
+
if (notifiedJobIds.has(jobId)) return;
|
|
2729
|
+
notifiedJobIds.add(jobId);
|
|
2730
|
+
}
|
|
2731
|
+
const payload = {
|
|
2732
|
+
event: input.event,
|
|
2733
|
+
jobId,
|
|
2734
|
+
provider: input.provider ?? input.job?.provider,
|
|
2735
|
+
model: input.model ?? input.job?.model,
|
|
2736
|
+
kind: input.kind ?? input.job?.kind,
|
|
2737
|
+
outputs: input.outputs,
|
|
2738
|
+
error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
|
|
2739
|
+
metadata: input.metadata ?? input.job?.metadata,
|
|
2740
|
+
processingTimeMs: input.processingTimeMs,
|
|
2741
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2742
|
+
};
|
|
2743
|
+
await deliverWebhook(config, payload);
|
|
2744
|
+
}
|
|
2362
2745
|
async function toGeneration(raw, metadata) {
|
|
2363
2746
|
if (!raw.success) {
|
|
2364
2747
|
return fail({
|
|
@@ -2381,15 +2764,50 @@ function brotuClient(options) {
|
|
|
2381
2764
|
const routed = route(params.model);
|
|
2382
2765
|
if (routed.error) return fail(routed.error);
|
|
2383
2766
|
try {
|
|
2384
|
-
|
|
2767
|
+
const result = await toGeneration(
|
|
2385
2768
|
await generateWith(routed.data.adapter, kind, params),
|
|
2386
2769
|
params.metadata
|
|
2387
2770
|
);
|
|
2771
|
+
if (result.error) {
|
|
2772
|
+
await notifySettled({
|
|
2773
|
+
event: "generation.failed",
|
|
2774
|
+
params,
|
|
2775
|
+
kind,
|
|
2776
|
+
provider: routed.data.provider,
|
|
2777
|
+
model: routed.data.model,
|
|
2778
|
+
error: result.error,
|
|
2779
|
+
metadata: params.metadata
|
|
2780
|
+
});
|
|
2781
|
+
return result;
|
|
2782
|
+
}
|
|
2783
|
+
await notifySettled({
|
|
2784
|
+
event: "generation.succeeded",
|
|
2785
|
+
params,
|
|
2786
|
+
kind,
|
|
2787
|
+
provider: result.data.provider,
|
|
2788
|
+
model: result.data.model,
|
|
2789
|
+
outputs: result.data.outputs,
|
|
2790
|
+
metadata: result.data.metadata,
|
|
2791
|
+
processingTimeMs: result.data.processingTimeMs
|
|
2792
|
+
});
|
|
2793
|
+
return result;
|
|
2388
2794
|
} catch (error) {
|
|
2389
|
-
|
|
2795
|
+
const failed = failFrom("provider_error", error, {
|
|
2390
2796
|
provider: routed.data.provider,
|
|
2391
2797
|
model: routed.data.model
|
|
2392
2798
|
});
|
|
2799
|
+
if (failed.error) {
|
|
2800
|
+
await notifySettled({
|
|
2801
|
+
event: "generation.failed",
|
|
2802
|
+
params,
|
|
2803
|
+
kind,
|
|
2804
|
+
provider: routed.data.provider,
|
|
2805
|
+
model: routed.data.model,
|
|
2806
|
+
error: failed.error,
|
|
2807
|
+
metadata: params.metadata
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
return failed;
|
|
2393
2811
|
}
|
|
2394
2812
|
}
|
|
2395
2813
|
async function submit(kind, params) {
|
|
@@ -2408,18 +2826,40 @@ function brotuClient(options) {
|
|
|
2408
2826
|
() => generateWith(routed.data.adapter, kind, params)
|
|
2409
2827
|
);
|
|
2410
2828
|
if (!raw.success) {
|
|
2411
|
-
|
|
2829
|
+
const error = {
|
|
2412
2830
|
code: "provider_error",
|
|
2413
2831
|
message: raw.error ?? "The provider rejected the request.",
|
|
2414
2832
|
provider: raw.provider,
|
|
2415
2833
|
model: raw.model
|
|
2834
|
+
};
|
|
2835
|
+
await notifySettled({
|
|
2836
|
+
event: "generation.failed",
|
|
2837
|
+
params,
|
|
2838
|
+
kind,
|
|
2839
|
+
provider: raw.provider,
|
|
2840
|
+
model: raw.model,
|
|
2841
|
+
error,
|
|
2842
|
+
metadata: params.metadata
|
|
2416
2843
|
});
|
|
2844
|
+
return fail(error);
|
|
2417
2845
|
}
|
|
2418
|
-
|
|
2846
|
+
const job = {
|
|
2419
2847
|
...base,
|
|
2420
2848
|
id: `inline-${base.model}-${base.submittedAt}`,
|
|
2421
2849
|
result: raw
|
|
2850
|
+
};
|
|
2851
|
+
await notifySettled({
|
|
2852
|
+
event: "generation.succeeded",
|
|
2853
|
+
params,
|
|
2854
|
+
job,
|
|
2855
|
+
kind,
|
|
2856
|
+
provider: raw.provider,
|
|
2857
|
+
model: raw.model,
|
|
2858
|
+
outputs: raw.outputs,
|
|
2859
|
+
metadata: params.metadata,
|
|
2860
|
+
processingTimeMs: raw.processingTimeMs
|
|
2422
2861
|
});
|
|
2862
|
+
return ok(job);
|
|
2423
2863
|
} catch (error) {
|
|
2424
2864
|
if (!isPendingJob(error)) {
|
|
2425
2865
|
return failFrom("provider_error", error, {
|
|
@@ -2434,11 +2874,54 @@ function brotuClient(options) {
|
|
|
2434
2874
|
});
|
|
2435
2875
|
}
|
|
2436
2876
|
}
|
|
2877
|
+
async function notifyFromSnapshot(job, snapshot) {
|
|
2878
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2879
|
+
await notifySettled({
|
|
2880
|
+
event: "generation.succeeded",
|
|
2881
|
+
params: job.params,
|
|
2882
|
+
job,
|
|
2883
|
+
outputs: snapshot.result.outputs,
|
|
2884
|
+
provider: snapshot.result.provider,
|
|
2885
|
+
model: snapshot.result.model,
|
|
2886
|
+
processingTimeMs: snapshot.result.processingTimeMs,
|
|
2887
|
+
metadata: job.metadata
|
|
2888
|
+
});
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
if (snapshot.status === "failed") {
|
|
2892
|
+
await notifySettled({
|
|
2893
|
+
event: "generation.failed",
|
|
2894
|
+
params: job.params,
|
|
2895
|
+
job,
|
|
2896
|
+
error: {
|
|
2897
|
+
code: "provider_error",
|
|
2898
|
+
message: snapshot.error ?? "The job failed.",
|
|
2899
|
+
provider: job.provider,
|
|
2900
|
+
model: job.model
|
|
2901
|
+
},
|
|
2902
|
+
metadata: job.metadata
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
async function finalizeSnapshot(job, snapshot) {
|
|
2907
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2908
|
+
const persisted = await toGeneration(snapshot.result, job.metadata);
|
|
2909
|
+
if (!persisted.error) {
|
|
2910
|
+
snapshot = {
|
|
2911
|
+
status: "succeeded",
|
|
2912
|
+
result: {
|
|
2913
|
+
...snapshot.result,
|
|
2914
|
+
outputs: persisted.data.outputs
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
await notifyFromSnapshot(job, snapshot);
|
|
2920
|
+
return snapshot;
|
|
2921
|
+
}
|
|
2437
2922
|
async function pollJob(job) {
|
|
2438
2923
|
if (job.result) {
|
|
2439
|
-
return ok(
|
|
2440
|
-
job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }
|
|
2441
|
-
);
|
|
2924
|
+
return ok(await finalizeSnapshot(job, job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }));
|
|
2442
2925
|
}
|
|
2443
2926
|
const routed = route(job.model);
|
|
2444
2927
|
if (routed.error) return fail(routed.error);
|
|
@@ -2451,7 +2934,7 @@ function brotuClient(options) {
|
|
|
2451
2934
|
});
|
|
2452
2935
|
}
|
|
2453
2936
|
try {
|
|
2454
|
-
return ok(await adapter.completeJob(job));
|
|
2937
|
+
return ok(await finalizeSnapshot(job, await adapter.completeJob(job)));
|
|
2455
2938
|
} catch (error) {
|
|
2456
2939
|
return failFrom("provider_error", error, {
|
|
2457
2940
|
provider: job.provider,
|
|
@@ -2460,7 +2943,7 @@ function brotuClient(options) {
|
|
|
2460
2943
|
}
|
|
2461
2944
|
}
|
|
2462
2945
|
function klingNamespace() {
|
|
2463
|
-
const configured =
|
|
2946
|
+
const configured = vendors.kling;
|
|
2464
2947
|
if (!configured) return void 0;
|
|
2465
2948
|
const adapter = new KlingAdapter({
|
|
2466
2949
|
apiKey: configured.apiKey,
|
|
@@ -2485,7 +2968,7 @@ function brotuClient(options) {
|
|
|
2485
2968
|
};
|
|
2486
2969
|
}
|
|
2487
2970
|
function googleNamespace() {
|
|
2488
|
-
const configured =
|
|
2971
|
+
const configured = vendors.google;
|
|
2489
2972
|
if (!configured) return void 0;
|
|
2490
2973
|
const adapter = new GoogleAdapter({
|
|
2491
2974
|
apiKey: configured.apiKey,
|
|
@@ -2520,6 +3003,17 @@ function brotuClient(options) {
|
|
|
2520
3003
|
submit: (params) => submit("audio", params),
|
|
2521
3004
|
generate: (params) => generate("audio", params)
|
|
2522
3005
|
},
|
|
3006
|
+
webhook: {
|
|
3007
|
+
set(value) {
|
|
3008
|
+
registeredWebhook = resolveWebhook(value);
|
|
3009
|
+
},
|
|
3010
|
+
clear() {
|
|
3011
|
+
registeredWebhook = void 0;
|
|
3012
|
+
},
|
|
3013
|
+
get() {
|
|
3014
|
+
return registeredWebhook;
|
|
3015
|
+
}
|
|
3016
|
+
},
|
|
2523
3017
|
jobs: {
|
|
2524
3018
|
poll: pollJob,
|
|
2525
3019
|
wait: async (job, waitOptions) => {
|
|
@@ -2529,7 +3023,13 @@ function brotuClient(options) {
|
|
|
2529
3023
|
if (polled.error) return fail(polled.error);
|
|
2530
3024
|
const snapshot = polled.data;
|
|
2531
3025
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2532
|
-
return
|
|
3026
|
+
return ok({
|
|
3027
|
+
outputs: snapshot.result.outputs,
|
|
3028
|
+
provider: snapshot.result.provider,
|
|
3029
|
+
model: snapshot.result.model,
|
|
3030
|
+
processingTimeMs: snapshot.result.processingTimeMs,
|
|
3031
|
+
metadata: job.metadata
|
|
3032
|
+
});
|
|
2533
3033
|
}
|
|
2534
3034
|
if (snapshot.status === "failed") {
|
|
2535
3035
|
return fail({
|
|
@@ -2551,7 +3051,7 @@ function brotuClient(options) {
|
|
|
2551
3051
|
}
|
|
2552
3052
|
}
|
|
2553
3053
|
},
|
|
2554
|
-
models: () => getAvailableModels(
|
|
3054
|
+
models: () => getAvailableModels(optionsWithKey),
|
|
2555
3055
|
estimateCost: async (type, params) => {
|
|
2556
3056
|
const routed = route(params.model);
|
|
2557
3057
|
if (routed.error) return fail(routed.error);
|
|
@@ -2569,7 +3069,9 @@ function brotuClient(options) {
|
|
|
2569
3069
|
export {
|
|
2570
3070
|
BYTEPLUS_CATALOG,
|
|
2571
3071
|
BYTEPLUS_MODELS,
|
|
3072
|
+
BrotuAdapter,
|
|
2572
3073
|
BytePlusAdapter,
|
|
3074
|
+
DEFAULT_BROTU_API_URL,
|
|
2573
3075
|
ELEVENLABS_CATALOG,
|
|
2574
3076
|
ELEVENLABS_MODELS,
|
|
2575
3077
|
ELEVENLABS_OUTPUT_FORMATS,
|
|
@@ -2598,6 +3100,7 @@ export {
|
|
|
2598
3100
|
QwenAdapter,
|
|
2599
3101
|
brotuClient,
|
|
2600
3102
|
createS3Storage,
|
|
3103
|
+
deliverWebhook,
|
|
2601
3104
|
fail,
|
|
2602
3105
|
getAvailableModels,
|
|
2603
3106
|
getModel,
|
|
@@ -2609,5 +3112,6 @@ export {
|
|
|
2609
3112
|
persistOutputs,
|
|
2610
3113
|
registerModels,
|
|
2611
3114
|
resetCatalog,
|
|
2612
|
-
resolveProvider
|
|
3115
|
+
resolveProvider,
|
|
3116
|
+
resolveWebhook
|
|
2613
3117
|
};
|