@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/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-PL534BFN.js";
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 POLL_INTERVAL_MS = 5e3;
99
- var DEFAULT_MAX_POLL_ATTEMPTS = 240;
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 ?? DEFAULT_MAX_POLL_ATTEMPTS;
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, POLL_INTERVAL_MS));
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 POLL_INTERVAL_MS2 = 1e4;
586
- var DEFAULT_MAX_POLL_ATTEMPTS2 = 120;
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 ?? DEFAULT_MAX_POLL_ATTEMPTS2;
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, POLL_INTERVAL_MS2));
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 POLL_INTERVAL_MS3 = 5e3;
1083
- var DEFAULT_MAX_POLL_ATTEMPTS3 = 240;
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 ?? DEFAULT_MAX_POLL_ATTEMPTS3;
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, POLL_INTERVAL_MS3));
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 POLL_INTERVAL_MS4 = 5e3;
1661
- var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
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 ?? DEFAULT_MAX_POLL_ATTEMPTS4;
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, POLL_INTERVAL_MS4));
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 brotuClient(options) {
2308
- if (Object.keys(options.providers).length === 0) {
2309
- throw new Error("brotuClient requires at least one provider API key.");
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, options);
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
- if (!config) return;
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: input.kind ?? input.job?.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
- await deliverWebhook(config, payload);
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 = options.providers.kling;
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 = options.providers.google;
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(options),
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
- brotuClient,
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"}