@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/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-PL534BFN.js";
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 POLL_INTERVAL_MS = 5e3;
99
- var DEFAULT_MAX_POLL_ATTEMPTS = 240;
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 ?? DEFAULT_MAX_POLL_ATTEMPTS;
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, POLL_INTERVAL_MS));
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 POLL_INTERVAL_MS2 = 1e4;
586
- var DEFAULT_MAX_POLL_ATTEMPTS2 = 120;
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 ?? DEFAULT_MAX_POLL_ATTEMPTS2;
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, POLL_INTERVAL_MS2));
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 POLL_INTERVAL_MS3 = 5e3;
1083
- var DEFAULT_MAX_POLL_ATTEMPTS3 = 240;
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 ?? DEFAULT_MAX_POLL_ATTEMPTS3;
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, POLL_INTERVAL_MS3));
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 POLL_INTERVAL_MS4 = 5e3;
1661
- var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
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 ?? DEFAULT_MAX_POLL_ATTEMPTS4;
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, POLL_INTERVAL_MS4));
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
- if (Object.keys(options.providers).length === 0) {
2270
- throw new Error("brotuClient requires at least one provider API key.");
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, options);
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
- return await toGeneration(
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
- return failFrom("provider_error", error, {
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
- return fail({
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
- return ok({
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 = options.providers.kling;
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 = options.providers.google;
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 toGeneration(snapshot.result, job.metadata);
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(options),
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
  };