@brotu/ai 0.3.0 → 0.5.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 +57 -12
- package/dist/adapters/brotu.adapter.d.ts +46 -0
- package/dist/adapters/brotu.adapter.d.ts.map +1 -0
- package/dist/catalog.cjs +72 -16
- package/dist/catalog.d.ts +23 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +7 -1
- package/dist/{chunk-PL534BFN.js → chunk-3MC3RDAJ.js} +69 -16
- package/dist/client.d.ts +14 -3
- 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 +504 -76
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +409 -32
- package/dist/lib/hooks.d.ts +32 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/types.d.ts +23 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BROTU_SUPPORTED_CATEGORIES,
|
|
2
3
|
BYTEPLUS_CATALOG,
|
|
3
4
|
BYTEPLUS_IMAGE_MODELS,
|
|
4
5
|
BYTEPLUS_MODELS,
|
|
6
|
+
DEFAULT_BROTU_API_URL,
|
|
5
7
|
ELEVENLABS_CATALOG,
|
|
6
8
|
ELEVENLABS_MODELS,
|
|
7
9
|
ELEVENLABS_OUTPUT_FORMATS,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
QWEN_IMAGE_PATHS,
|
|
26
28
|
QWEN_TEXT_MODELS,
|
|
27
29
|
QWEN_VIDEO_MODELS,
|
|
30
|
+
describeModels,
|
|
28
31
|
fieldsFor,
|
|
29
32
|
getAvailableModels,
|
|
30
33
|
getModel,
|
|
@@ -35,7 +38,7 @@ import {
|
|
|
35
38
|
resetCatalog,
|
|
36
39
|
resolveProvider,
|
|
37
40
|
videoPathFor
|
|
38
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-3MC3RDAJ.js";
|
|
39
42
|
|
|
40
43
|
// src/lib/jobs.ts
|
|
41
44
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -91,12 +94,316 @@ function estimateFor(provider, type, params, defaults) {
|
|
|
91
94
|
};
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
// src/adapters/brotu.adapter.ts
|
|
98
|
+
var DEFAULT_API_URL = "https://api.brotu.app";
|
|
99
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
100
|
+
var DEFAULT_MAX_POLL_ATTEMPTS = 400;
|
|
101
|
+
var PLATFORM_MODEL_IDS = {
|
|
102
|
+
"kling/v2-6": "kling-2.6",
|
|
103
|
+
"kling/v3": "kling-3.0/video",
|
|
104
|
+
"dreamina-seedance-2-5-260628": "bytedance/seedance-2-5",
|
|
105
|
+
"dreamina-seedance-2-0-260128": "bytedance/seedance-2",
|
|
106
|
+
"dreamina-seedance-2-0-fast-260128": "bytedance/seedance-2-fast",
|
|
107
|
+
"gpt-image-1.5": "gpt-image/1.5"
|
|
108
|
+
};
|
|
109
|
+
var UNSUPPORTED_VIA_CREDITS = "Speech and text generate on the vendor. Pass that provider's key.";
|
|
110
|
+
var BrotuAdapter = class {
|
|
111
|
+
constructor(opts) {
|
|
112
|
+
this.opts = opts;
|
|
113
|
+
}
|
|
114
|
+
opts;
|
|
115
|
+
providerName = "brotu";
|
|
116
|
+
supportedTypes = BROTU_SUPPORTED_CATEGORIES;
|
|
117
|
+
workspaceIdCache;
|
|
118
|
+
get origin() {
|
|
119
|
+
return (this.opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
120
|
+
}
|
|
121
|
+
headers() {
|
|
122
|
+
return {
|
|
123
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
124
|
+
"Content-Type": "application/json"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async workspaceId() {
|
|
128
|
+
if (this.opts.workspaceId) return this.opts.workspaceId;
|
|
129
|
+
if (this.workspaceIdCache) return this.workspaceIdCache;
|
|
130
|
+
const payload = await this.request("/api/v1/studio/default-workspace");
|
|
131
|
+
const id = payload.workspaceId?.trim();
|
|
132
|
+
if (!id) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"This Brotu account has no workspace. Open https://brotu.app and create one."
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
this.workspaceIdCache = id;
|
|
138
|
+
return id;
|
|
139
|
+
}
|
|
140
|
+
studioPath(path, workspaceId) {
|
|
141
|
+
const joiner = path.includes("?") ? "&" : "?";
|
|
142
|
+
return `${path}${joiner}workspaceId=${encodeURIComponent(workspaceId)}`;
|
|
143
|
+
}
|
|
144
|
+
async request(path, init) {
|
|
145
|
+
const response = await fetch(`${this.origin}${path}`, {
|
|
146
|
+
...init,
|
|
147
|
+
headers: { ...this.headers(), ...init?.headers }
|
|
148
|
+
});
|
|
149
|
+
const text = await response.text();
|
|
150
|
+
let body = void 0;
|
|
151
|
+
if (text) {
|
|
152
|
+
try {
|
|
153
|
+
body = JSON.parse(text);
|
|
154
|
+
} catch {
|
|
155
|
+
body = text;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
const parsed = body;
|
|
160
|
+
const message = parsed && typeof parsed === "object" ? parsed.error?.message || parsed.message || text : text;
|
|
161
|
+
throw new Error(message || `Brotu API ${response.status}`);
|
|
162
|
+
}
|
|
163
|
+
return body;
|
|
164
|
+
}
|
|
165
|
+
platformModel(modelId) {
|
|
166
|
+
return PLATFORM_MODEL_IDS[modelId] ?? modelId;
|
|
167
|
+
}
|
|
168
|
+
async startGeneration(kind, params) {
|
|
169
|
+
const modelId = params.model ?? "";
|
|
170
|
+
const workspaceId = await this.workspaceId();
|
|
171
|
+
const path = kind === "video" ? "/api/v1/studio/videos" : "/api/v1/studio/images";
|
|
172
|
+
const body = kind === "video" ? this.videoBody(params, modelId) : this.imageBody(params, modelId);
|
|
173
|
+
const result = await this.request(
|
|
174
|
+
this.studioPath(path, workspaceId),
|
|
175
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
176
|
+
);
|
|
177
|
+
const generationId = result.generationId?.trim();
|
|
178
|
+
if (!generationId) {
|
|
179
|
+
throw new Error("Brotu accepted the request but returned no generation id.");
|
|
180
|
+
}
|
|
181
|
+
return { generationId, workspaceId };
|
|
182
|
+
}
|
|
183
|
+
imageBody(params, modelId) {
|
|
184
|
+
return {
|
|
185
|
+
prompt: params.prompt,
|
|
186
|
+
model: this.platformModel(modelId),
|
|
187
|
+
aspectRatio: params.aspectRatio,
|
|
188
|
+
resolution: params.resolution,
|
|
189
|
+
negativePrompt: params.negativePrompt,
|
|
190
|
+
outputFormat: params.outputFormat,
|
|
191
|
+
seed: params.seed,
|
|
192
|
+
referenceImages: params.referenceImages
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
videoBody(params, modelId) {
|
|
196
|
+
return {
|
|
197
|
+
prompt: params.prompt,
|
|
198
|
+
model: this.platformModel(modelId),
|
|
199
|
+
duration: params.duration,
|
|
200
|
+
resolution: params.resolution,
|
|
201
|
+
aspectRatio: params.aspectRatio,
|
|
202
|
+
mode: params.mode,
|
|
203
|
+
withAudio: params.withAudio,
|
|
204
|
+
seed: params.seed,
|
|
205
|
+
imageUrl: params.imageUrl,
|
|
206
|
+
imageUrls: params.imageUrls,
|
|
207
|
+
videoUrl: params.videoUrl,
|
|
208
|
+
videoUrls: params.videoUrls,
|
|
209
|
+
referenceImages: params.referenceImages
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async run(kind, params) {
|
|
213
|
+
const startedAt = Date.now();
|
|
214
|
+
const modelId = params.model ?? "(none)";
|
|
215
|
+
const failure = (error) => ({
|
|
216
|
+
success: false,
|
|
217
|
+
outputs: [],
|
|
218
|
+
creditsUsed: 0,
|
|
219
|
+
provider: this.providerName,
|
|
220
|
+
model: modelId,
|
|
221
|
+
processingTimeMs: Date.now() - startedAt,
|
|
222
|
+
error
|
|
223
|
+
});
|
|
224
|
+
let submitted;
|
|
225
|
+
try {
|
|
226
|
+
submitted = await this.startGeneration(kind, params);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
229
|
+
}
|
|
230
|
+
const pollEndpoint = this.studioPath(
|
|
231
|
+
`/api/v1/studio/generations/${submitted.generationId}`,
|
|
232
|
+
submitted.workspaceId
|
|
233
|
+
);
|
|
234
|
+
if (isSubmitMode()) {
|
|
235
|
+
throw new PendingJob(submitted.generationId, pollEndpoint);
|
|
236
|
+
}
|
|
237
|
+
const job = {
|
|
238
|
+
id: submitted.generationId,
|
|
239
|
+
provider: this.providerName,
|
|
240
|
+
model: modelId,
|
|
241
|
+
kind,
|
|
242
|
+
pollEndpoint,
|
|
243
|
+
params,
|
|
244
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
245
|
+
};
|
|
246
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS;
|
|
247
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
248
|
+
const snapshot = await this.completeJob(job);
|
|
249
|
+
if (snapshot.status === "failed") {
|
|
250
|
+
return failure(snapshot.error ?? "Brotu generation failed.");
|
|
251
|
+
}
|
|
252
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
253
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
254
|
+
}
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
256
|
+
}
|
|
257
|
+
return failure(
|
|
258
|
+
`Brotu generation ${submitted.generationId} did not finish after ${maxAttempts} checks.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
async completeJob(job) {
|
|
262
|
+
const workspaceId = this.workspaceFromPoll(job.pollEndpoint) ?? await this.workspaceId();
|
|
263
|
+
const path = this.studioPath(
|
|
264
|
+
`/api/v1/studio/generations/${job.id}`,
|
|
265
|
+
workspaceId
|
|
266
|
+
);
|
|
267
|
+
const generation = await this.request(path);
|
|
268
|
+
const status = (generation.status ?? "").toLowerCase();
|
|
269
|
+
if (status === "failed") {
|
|
270
|
+
return {
|
|
271
|
+
status: "failed",
|
|
272
|
+
error: generation.errorMessage ?? "Brotu generation failed."
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (status !== "completed") {
|
|
276
|
+
return { status: "pending" };
|
|
277
|
+
}
|
|
278
|
+
const outputs = outputsFrom(generation, job.kind);
|
|
279
|
+
return {
|
|
280
|
+
status: "succeeded",
|
|
281
|
+
result: {
|
|
282
|
+
success: true,
|
|
283
|
+
outputs,
|
|
284
|
+
creditsUsed: generation.creditsUsed ?? 0,
|
|
285
|
+
provider: this.providerName,
|
|
286
|
+
model: job.model,
|
|
287
|
+
processingTimeMs: 0
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
workspaceFromPoll(pollEndpoint) {
|
|
292
|
+
if (!pollEndpoint) return void 0;
|
|
293
|
+
try {
|
|
294
|
+
const url = new URL(pollEndpoint, "https://brotu.invalid");
|
|
295
|
+
return url.searchParams.get("workspaceId") ?? void 0;
|
|
296
|
+
} catch {
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
generateImage(params) {
|
|
301
|
+
return this.run("image", params);
|
|
302
|
+
}
|
|
303
|
+
generateVideo(params) {
|
|
304
|
+
return this.run("video", params);
|
|
305
|
+
}
|
|
306
|
+
async generateText(params) {
|
|
307
|
+
return {
|
|
308
|
+
success: false,
|
|
309
|
+
outputs: [],
|
|
310
|
+
creditsUsed: 0,
|
|
311
|
+
provider: this.providerName,
|
|
312
|
+
model: params.model ?? "(none)",
|
|
313
|
+
processingTimeMs: 0,
|
|
314
|
+
error: UNSUPPORTED_VIA_CREDITS
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async generateAudio(params) {
|
|
318
|
+
return {
|
|
319
|
+
success: false,
|
|
320
|
+
outputs: [],
|
|
321
|
+
creditsUsed: 0,
|
|
322
|
+
provider: this.providerName,
|
|
323
|
+
model: params.model ?? "(none)",
|
|
324
|
+
processingTimeMs: 0,
|
|
325
|
+
error: UNSUPPORTED_VIA_CREDITS
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async estimateCost(type, params) {
|
|
329
|
+
if (type === "audio" || type === "text") {
|
|
330
|
+
const estimate = estimateFor(this.providerName, type, params);
|
|
331
|
+
return {
|
|
332
|
+
...estimate,
|
|
333
|
+
usd: null,
|
|
334
|
+
note: UNSUPPORTED_VIA_CREDITS
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const workspaceId = await this.workspaceId();
|
|
339
|
+
const video = params;
|
|
340
|
+
const image = params;
|
|
341
|
+
const payload = await this.request(
|
|
342
|
+
this.studioPath("/api/v1/studio/estimate", workspaceId),
|
|
343
|
+
{
|
|
344
|
+
method: "POST",
|
|
345
|
+
body: JSON.stringify({
|
|
346
|
+
model: this.platformModel(params.model ?? ""),
|
|
347
|
+
duration: video.duration,
|
|
348
|
+
resolution: video.resolution ?? image.resolution,
|
|
349
|
+
aspectRatio: video.aspectRatio ?? image.aspectRatio,
|
|
350
|
+
withAudio: video.withAudio,
|
|
351
|
+
hasReferenceImage: Boolean(
|
|
352
|
+
video.imageUrl || video.imageUrls?.length || video.referenceImages?.length || image.referenceImages?.length
|
|
353
|
+
),
|
|
354
|
+
hasReferenceVideo: Boolean(video.videoUrl || video.videoUrls?.length),
|
|
355
|
+
quality: image.quality
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
const credits = payload.estimatedCredits ?? 0;
|
|
360
|
+
return {
|
|
361
|
+
unit: type === "video" ? "second" : "image",
|
|
362
|
+
units: credits,
|
|
363
|
+
usd: null,
|
|
364
|
+
note: `${credits} Brotu credit${credits === 1 ? "" : "s"}. A vendor key generates on that provider.`,
|
|
365
|
+
provider: this.providerName,
|
|
366
|
+
model: params.model ?? ""
|
|
367
|
+
};
|
|
368
|
+
} catch (error) {
|
|
369
|
+
const estimate = estimateFor(this.providerName, type, params);
|
|
370
|
+
return {
|
|
371
|
+
...estimate,
|
|
372
|
+
usd: null,
|
|
373
|
+
note: error instanceof Error ? error.message : "Could not estimate Brotu credits."
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
supportsModel() {
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
getAvailableModels() {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
function outputsFrom(generation, kind) {
|
|
385
|
+
const bucket = generation.outputs;
|
|
386
|
+
const urls = kind === "video" ? bucket?.videos ?? bucket?.images ?? [] : bucket?.images ?? bucket?.videos ?? [];
|
|
387
|
+
const copies = bucket?.copies ?? [];
|
|
388
|
+
const mimeType = kind === "video" ? "video/mp4" : "image/png";
|
|
389
|
+
const fromUrls = urls.filter(Boolean).map((url) => ({
|
|
390
|
+
url,
|
|
391
|
+
mimeType
|
|
392
|
+
}));
|
|
393
|
+
if (fromUrls.length > 0) return fromUrls;
|
|
394
|
+
return copies.filter(Boolean).map((url) => ({
|
|
395
|
+
url,
|
|
396
|
+
mimeType: "text/plain",
|
|
397
|
+
raw: { text: url }
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
|
|
94
401
|
// src/adapters/byteplus.adapter.ts
|
|
95
402
|
var DEFAULT_BASE_URL = "https://ark.ap-southeast.bytepluses.com";
|
|
96
403
|
var TASKS_PATH = "/api/v3/contents/generations/tasks";
|
|
97
404
|
var IMAGES_PATH = "/api/v3/images/generations";
|
|
98
|
-
var
|
|
99
|
-
var
|
|
405
|
+
var POLL_INTERVAL_MS2 = 5e3;
|
|
406
|
+
var DEFAULT_MAX_POLL_ATTEMPTS2 = 240;
|
|
100
407
|
var BytePlusAdapter = class {
|
|
101
408
|
providerName = "byteplus";
|
|
102
409
|
supportedTypes = ["image", "video"];
|
|
@@ -296,7 +603,7 @@ var BytePlusAdapter = class {
|
|
|
296
603
|
params,
|
|
297
604
|
submittedAt: new Date(startedAt).toISOString()
|
|
298
605
|
};
|
|
299
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
606
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS2;
|
|
300
607
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
301
608
|
const snapshot = await this.completeJob(job);
|
|
302
609
|
if (snapshot.status === "failed") {
|
|
@@ -305,7 +612,7 @@ var BytePlusAdapter = class {
|
|
|
305
612
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
306
613
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
307
614
|
}
|
|
308
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
615
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS2));
|
|
309
616
|
}
|
|
310
617
|
return failure(
|
|
311
618
|
`Ark task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -582,8 +889,8 @@ var ElevenLabsAdapter = class {
|
|
|
582
889
|
// src/adapters/google.adapter.ts
|
|
583
890
|
var DEFAULT_BASE_URL3 = "https://generativelanguage.googleapis.com";
|
|
584
891
|
var INTERACTIONS_PATH = "/v1beta/interactions";
|
|
585
|
-
var
|
|
586
|
-
var
|
|
892
|
+
var POLL_INTERVAL_MS3 = 1e4;
|
|
893
|
+
var DEFAULT_MAX_POLL_ATTEMPTS3 = 120;
|
|
587
894
|
var GoogleAdapter = class {
|
|
588
895
|
providerName = "google";
|
|
589
896
|
supportedTypes = [
|
|
@@ -855,7 +1162,7 @@ var GoogleAdapter = class {
|
|
|
855
1162
|
params,
|
|
856
1163
|
submittedAt: new Date(startedAt).toISOString()
|
|
857
1164
|
};
|
|
858
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
1165
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS3;
|
|
859
1166
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
860
1167
|
const snapshot = await this.completeJob(job);
|
|
861
1168
|
if (snapshot.status === "failed") {
|
|
@@ -864,7 +1171,7 @@ var GoogleAdapter = class {
|
|
|
864
1171
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
865
1172
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
866
1173
|
}
|
|
867
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1174
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS3));
|
|
868
1175
|
}
|
|
869
1176
|
return failure(`Veo operation ${operation} did not finish in time.`);
|
|
870
1177
|
}
|
|
@@ -1079,8 +1386,8 @@ var GoogleAdapter = class {
|
|
|
1079
1386
|
|
|
1080
1387
|
// src/adapters/kling.adapter.ts
|
|
1081
1388
|
var DEFAULT_BASE_URL4 = "https://api-singapore.klingai.com";
|
|
1082
|
-
var
|
|
1083
|
-
var
|
|
1389
|
+
var POLL_INTERVAL_MS4 = 5e3;
|
|
1390
|
+
var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
|
|
1084
1391
|
function stateOf(task) {
|
|
1085
1392
|
const raw = (task.status ?? task.task_status ?? "").toLowerCase();
|
|
1086
1393
|
if (raw === "failed") return "failed";
|
|
@@ -1277,7 +1584,7 @@ var KlingAdapter = class {
|
|
|
1277
1584
|
params,
|
|
1278
1585
|
submittedAt: new Date(startedAt).toISOString()
|
|
1279
1586
|
};
|
|
1280
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
1587
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
|
|
1281
1588
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1282
1589
|
const snapshot = await this.completeJob(job);
|
|
1283
1590
|
if (snapshot.status === "failed") {
|
|
@@ -1286,7 +1593,7 @@ var KlingAdapter = class {
|
|
|
1286
1593
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
1287
1594
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
1288
1595
|
}
|
|
1289
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
1596
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
|
|
1290
1597
|
}
|
|
1291
1598
|
return failure(
|
|
1292
1599
|
`Kling task ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -1657,8 +1964,8 @@ var OpenAIAdapter = class {
|
|
|
1657
1964
|
// src/adapters/qwen.adapter.ts
|
|
1658
1965
|
var DEFAULT_BASE_URL6 = "https://dashscope-intl.aliyuncs.com";
|
|
1659
1966
|
var TASKS_PATH2 = "/api/v1/tasks";
|
|
1660
|
-
var
|
|
1661
|
-
var
|
|
1967
|
+
var POLL_INTERVAL_MS5 = 5e3;
|
|
1968
|
+
var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
|
|
1662
1969
|
var QwenAdapter = class {
|
|
1663
1970
|
providerName = "qwen";
|
|
1664
1971
|
supportedTypes = ["image", "video"];
|
|
@@ -1865,7 +2172,7 @@ var QwenAdapter = class {
|
|
|
1865
2172
|
params,
|
|
1866
2173
|
submittedAt: new Date(startedAt).toISOString()
|
|
1867
2174
|
};
|
|
1868
|
-
const maxAttempts = this.opts.maxPollAttempts ??
|
|
2175
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
|
|
1869
2176
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1870
2177
|
const snapshot = await this.completeJob(job);
|
|
1871
2178
|
if (snapshot.status === "failed") {
|
|
@@ -1874,7 +2181,7 @@ var QwenAdapter = class {
|
|
|
1874
2181
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
1875
2182
|
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
1876
2183
|
}
|
|
1877
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
2184
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
|
|
1878
2185
|
}
|
|
1879
2186
|
return failure(
|
|
1880
2187
|
`DashScope task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
@@ -2134,6 +2441,22 @@ function failFrom(code, cause, context) {
|
|
|
2134
2441
|
});
|
|
2135
2442
|
}
|
|
2136
2443
|
|
|
2444
|
+
// src/lib/hooks.ts
|
|
2445
|
+
function hookName(kind, stage) {
|
|
2446
|
+
return `on${kind[0].toUpperCase()}${kind.slice(1)}${stage}`;
|
|
2447
|
+
}
|
|
2448
|
+
function findHook(hooks, kind, stage) {
|
|
2449
|
+
if (!hooks || !kind) return void 0;
|
|
2450
|
+
return hooks[hookName(kind, stage)];
|
|
2451
|
+
}
|
|
2452
|
+
async function runHook(hook, event) {
|
|
2453
|
+
if (!hook) return;
|
|
2454
|
+
try {
|
|
2455
|
+
await hook(event);
|
|
2456
|
+
} catch {
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2137
2460
|
// src/lib/storage.ts
|
|
2138
2461
|
async function loadS3(config) {
|
|
2139
2462
|
let s3;
|
|
@@ -2304,10 +2627,15 @@ var NATIVE_PROVIDERS = [
|
|
|
2304
2627
|
"openai",
|
|
2305
2628
|
"qwen"
|
|
2306
2629
|
];
|
|
2307
|
-
function
|
|
2308
|
-
|
|
2309
|
-
|
|
2630
|
+
function brotu(options) {
|
|
2631
|
+
const apiKey = options.apiKey?.trim();
|
|
2632
|
+
if (!apiKey) {
|
|
2633
|
+
throw new Error(
|
|
2634
|
+
"Pass a Brotu API key (brotu_sk_\u2026). Get one at https://brotu.app."
|
|
2635
|
+
);
|
|
2310
2636
|
}
|
|
2637
|
+
const optionsWithKey = { ...options, apiKey };
|
|
2638
|
+
const vendors = optionsWithKey.providers ?? {};
|
|
2311
2639
|
registerModels(options.models);
|
|
2312
2640
|
let registeredWebhook = resolveWebhook(options.webhook);
|
|
2313
2641
|
const notifiedJobIds = /* @__PURE__ */ new Set();
|
|
@@ -2331,7 +2659,7 @@ function brotuClient(options) {
|
|
|
2331
2659
|
}
|
|
2332
2660
|
let provider;
|
|
2333
2661
|
try {
|
|
2334
|
-
provider = resolveProvider(modelId,
|
|
2662
|
+
provider = resolveProvider(modelId, optionsWithKey);
|
|
2335
2663
|
} catch (error) {
|
|
2336
2664
|
return failFrom("missing_key", error, { model: modelId });
|
|
2337
2665
|
}
|
|
@@ -2352,6 +2680,13 @@ function brotuClient(options) {
|
|
|
2352
2680
|
return ok({ adapter, provider: provider.id, model: modelId });
|
|
2353
2681
|
}
|
|
2354
2682
|
function buildAdapter(provider) {
|
|
2683
|
+
if (provider.id === "brotu") {
|
|
2684
|
+
return new BrotuAdapter({
|
|
2685
|
+
apiKey,
|
|
2686
|
+
apiUrl: optionsWithKey.apiUrl,
|
|
2687
|
+
workspaceId: optionsWithKey.workspaceId
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2355
2690
|
if (provider.id === "kling") {
|
|
2356
2691
|
return new KlingAdapter({
|
|
2357
2692
|
apiKey: provider.apiKey,
|
|
@@ -2405,7 +2740,10 @@ function brotuClient(options) {
|
|
|
2405
2740
|
}
|
|
2406
2741
|
async function notifySettled(input) {
|
|
2407
2742
|
const config = webhookFor(input.params);
|
|
2408
|
-
|
|
2743
|
+
const kind = input.kind ?? input.job?.kind;
|
|
2744
|
+
const stage = input.event === "generation.succeeded" ? "Success" : "Error";
|
|
2745
|
+
const hook = findHook(options.hooks, kind, stage);
|
|
2746
|
+
if (!config && !hook) return;
|
|
2409
2747
|
const jobId = input.job?.id;
|
|
2410
2748
|
if (jobId) {
|
|
2411
2749
|
if (notifiedJobIds.has(jobId)) return;
|
|
@@ -2416,14 +2754,38 @@ function brotuClient(options) {
|
|
|
2416
2754
|
jobId,
|
|
2417
2755
|
provider: input.provider ?? input.job?.provider,
|
|
2418
2756
|
model: input.model ?? input.job?.model,
|
|
2419
|
-
kind
|
|
2757
|
+
kind,
|
|
2420
2758
|
outputs: input.outputs,
|
|
2421
2759
|
error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
|
|
2422
2760
|
metadata: input.metadata ?? input.job?.metadata,
|
|
2423
2761
|
processingTimeMs: input.processingTimeMs,
|
|
2424
2762
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2425
2763
|
};
|
|
2426
|
-
|
|
2764
|
+
if (hook && kind) {
|
|
2765
|
+
await runHook(hook, {
|
|
2766
|
+
kind,
|
|
2767
|
+
stage,
|
|
2768
|
+
provider: payload.provider ?? "",
|
|
2769
|
+
model: payload.model ?? "",
|
|
2770
|
+
jobId: payload.jobId,
|
|
2771
|
+
outputs: payload.outputs,
|
|
2772
|
+
error: payload.error,
|
|
2773
|
+
metadata: payload.metadata,
|
|
2774
|
+
processingTimeMs: payload.processingTimeMs,
|
|
2775
|
+
at: payload.completedAt
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
if (config) await deliverWebhook(config, payload);
|
|
2779
|
+
}
|
|
2780
|
+
function notifyLoading(kind, routed, params) {
|
|
2781
|
+
return runHook(findHook(options.hooks, kind, "Loading"), {
|
|
2782
|
+
kind,
|
|
2783
|
+
stage: "Loading",
|
|
2784
|
+
provider: routed.provider,
|
|
2785
|
+
model: routed.model,
|
|
2786
|
+
metadata: params.metadata,
|
|
2787
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2788
|
+
});
|
|
2427
2789
|
}
|
|
2428
2790
|
async function toGeneration(raw, metadata) {
|
|
2429
2791
|
if (!raw.success) {
|
|
@@ -2446,6 +2808,7 @@ function brotuClient(options) {
|
|
|
2446
2808
|
async function generate(kind, params) {
|
|
2447
2809
|
const routed = route(params.model);
|
|
2448
2810
|
if (routed.error) return fail(routed.error);
|
|
2811
|
+
await notifyLoading(kind, routed.data, params);
|
|
2449
2812
|
try {
|
|
2450
2813
|
const result = await toGeneration(
|
|
2451
2814
|
await generateWith(routed.data.adapter, kind, params),
|
|
@@ -2496,6 +2859,7 @@ function brotuClient(options) {
|
|
|
2496
2859
|
async function submit(kind, params) {
|
|
2497
2860
|
const routed = route(params.model);
|
|
2498
2861
|
if (routed.error) return fail(routed.error);
|
|
2862
|
+
await notifyLoading(kind, routed.data, params);
|
|
2499
2863
|
const base = {
|
|
2500
2864
|
provider: routed.data.provider,
|
|
2501
2865
|
model: routed.data.model,
|
|
@@ -2626,7 +2990,7 @@ function brotuClient(options) {
|
|
|
2626
2990
|
}
|
|
2627
2991
|
}
|
|
2628
2992
|
function klingNamespace() {
|
|
2629
|
-
const configured =
|
|
2993
|
+
const configured = vendors.kling;
|
|
2630
2994
|
if (!configured) return void 0;
|
|
2631
2995
|
const adapter = new KlingAdapter({
|
|
2632
2996
|
apiKey: configured.apiKey,
|
|
@@ -2651,7 +3015,7 @@ function brotuClient(options) {
|
|
|
2651
3015
|
};
|
|
2652
3016
|
}
|
|
2653
3017
|
function googleNamespace() {
|
|
2654
|
-
const configured =
|
|
3018
|
+
const configured = vendors.google;
|
|
2655
3019
|
if (!configured) return void 0;
|
|
2656
3020
|
const adapter = new GoogleAdapter({
|
|
2657
3021
|
apiKey: configured.apiKey,
|
|
@@ -2667,24 +3031,33 @@ function brotuClient(options) {
|
|
|
2667
3031
|
}
|
|
2668
3032
|
};
|
|
2669
3033
|
}
|
|
3034
|
+
function listFor(category) {
|
|
3035
|
+
return describeModels(optionsWithKey).filter(
|
|
3036
|
+
(entry) => entry.model.category === category
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
2670
3039
|
return {
|
|
2671
3040
|
google: googleNamespace(),
|
|
2672
3041
|
kling: klingNamespace(),
|
|
2673
3042
|
image: {
|
|
2674
3043
|
submit: (params) => submit("image", params),
|
|
2675
|
-
generate: (params) => generate("image", params)
|
|
3044
|
+
generate: (params) => generate("image", params),
|
|
3045
|
+
list: () => listFor("image")
|
|
2676
3046
|
},
|
|
2677
3047
|
video: {
|
|
2678
3048
|
submit: (params) => submit("video", params),
|
|
2679
|
-
generate: (params) => generate("video", params)
|
|
3049
|
+
generate: (params) => generate("video", params),
|
|
3050
|
+
list: () => listFor("video")
|
|
2680
3051
|
},
|
|
2681
3052
|
text: {
|
|
2682
3053
|
submit: (params) => submit("text", params),
|
|
2683
|
-
generate: (params) => generate("text", params)
|
|
3054
|
+
generate: (params) => generate("text", params),
|
|
3055
|
+
list: () => listFor("text")
|
|
2684
3056
|
},
|
|
2685
3057
|
audio: {
|
|
2686
3058
|
submit: (params) => submit("audio", params),
|
|
2687
|
-
generate: (params) => generate("audio", params)
|
|
3059
|
+
generate: (params) => generate("audio", params),
|
|
3060
|
+
list: () => listFor("audio")
|
|
2688
3061
|
},
|
|
2689
3062
|
webhook: {
|
|
2690
3063
|
set(value) {
|
|
@@ -2734,7 +3107,7 @@ function brotuClient(options) {
|
|
|
2734
3107
|
}
|
|
2735
3108
|
}
|
|
2736
3109
|
},
|
|
2737
|
-
models: () => getAvailableModels(
|
|
3110
|
+
models: () => getAvailableModels(optionsWithKey),
|
|
2738
3111
|
estimateCost: async (type, params) => {
|
|
2739
3112
|
const routed = route(params.model);
|
|
2740
3113
|
if (routed.error) return fail(routed.error);
|
|
@@ -2750,9 +3123,12 @@ function brotuClient(options) {
|
|
|
2750
3123
|
};
|
|
2751
3124
|
}
|
|
2752
3125
|
export {
|
|
3126
|
+
BROTU_SUPPORTED_CATEGORIES,
|
|
2753
3127
|
BYTEPLUS_CATALOG,
|
|
2754
3128
|
BYTEPLUS_MODELS,
|
|
3129
|
+
BrotuAdapter,
|
|
2755
3130
|
BytePlusAdapter,
|
|
3131
|
+
DEFAULT_BROTU_API_URL,
|
|
2756
3132
|
ELEVENLABS_CATALOG,
|
|
2757
3133
|
ELEVENLABS_MODELS,
|
|
2758
3134
|
ELEVENLABS_OUTPUT_FORMATS,
|
|
@@ -2779,9 +3155,10 @@ export {
|
|
|
2779
3155
|
QWEN_TEXT_MODELS,
|
|
2780
3156
|
QWEN_VIDEO_MODELS,
|
|
2781
3157
|
QwenAdapter,
|
|
2782
|
-
|
|
3158
|
+
brotu,
|
|
2783
3159
|
createS3Storage,
|
|
2784
3160
|
deliverWebhook,
|
|
3161
|
+
describeModels,
|
|
2785
3162
|
fail,
|
|
2786
3163
|
getAvailableModels,
|
|
2787
3164
|
getModel,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AIError } from "../helpers/result";
|
|
2
|
+
import type { GenerationOutput, GenerationType } from "../ports/content-generator.port";
|
|
3
|
+
/** Where a generation is when the hook fires. */
|
|
4
|
+
export type HookStage = "Loading" | "Success" | "Error";
|
|
5
|
+
/** What every hook receives. Fields absent at that stage stay undefined. */
|
|
6
|
+
export interface HookEvent {
|
|
7
|
+
kind: GenerationType;
|
|
8
|
+
stage: HookStage;
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
jobId?: string;
|
|
12
|
+
outputs?: GenerationOutput[];
|
|
13
|
+
error?: Pick<AIError, "code" | "message">;
|
|
14
|
+
metadata?: Record<string, string>;
|
|
15
|
+
processingTimeMs?: number;
|
|
16
|
+
at: string;
|
|
17
|
+
}
|
|
18
|
+
export type HookFn = (event: HookEvent) => void | Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* One optional callback per kind and stage — `onVideoLoading`,
|
|
21
|
+
* `onImageSuccess`, `onAudioError`, and so on for every combination.
|
|
22
|
+
*/
|
|
23
|
+
export type Hooks = Partial<Record<`on${Capitalize<GenerationType>}${HookStage}`, HookFn>>;
|
|
24
|
+
/** The hook registered for this kind and stage, if any. */
|
|
25
|
+
export declare function findHook(hooks: Hooks | undefined, kind: GenerationType | undefined, stage: HookStage): HookFn | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Run a hook without letting it break the caller. Same contract as the
|
|
28
|
+
* webhook: your logging or your mailer going down must not fail a generation
|
|
29
|
+
* that already succeeded.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runHook(hook: HookFn | undefined, event: HookEvent): Promise<void>;
|
|
32
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/lib/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EACX,gBAAgB,EAChB,cAAc,EACd,MAAM,iCAAiC,CAAC;AAEzC,iDAAiD;AACjD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAExD,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEhE;;;GAGG;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,CAC1B,MAAM,CAAC,KAAK,UAAU,CAAC,cAAc,CAAC,GAAG,SAAS,EAAE,EAAE,MAAM,CAAC,CAC7D,CAAC;AAMF,2DAA2D;AAC3D,wBAAgB,QAAQ,CACvB,KAAK,EAAE,KAAK,GAAG,SAAS,EACxB,IAAI,EAAE,cAAc,GAAG,SAAS,EAChC,KAAK,EAAE,SAAS,GACd,MAAM,GAAG,SAAS,CAGpB;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAC5B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,KAAK,EAAE,SAAS,GACd,OAAO,CAAC,IAAI,CAAC,CAOf"}
|