@brotu/ai 0.1.0 → 0.3.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.d.ts CHANGED
@@ -17,13 +17,14 @@ export type * from "./constants/model.types";
17
17
  export { type AIError, type AIErrorCode, fail, type Generation, ok, type Result, } from "./helpers/result";
18
18
  export { isPendingJob, type Job, type JobSnapshot, type JobStatus, PendingJob, } from "./lib/jobs";
19
19
  export { createS3Storage, persistOutputs, type S3StorageConfig, type Storage, } from "./lib/storage";
20
+ export { deliverWebhook, resolveWebhook, type WebhookConfig, type WebhookEvent, type WebhookEventName, } from "./lib/webhook";
20
21
  export type * from "./ports/content-generator.port";
21
22
  export { BYTEPLUS_CATALOG, BYTEPLUS_MODELS, type BytePlusModelBinding, } from "./providers/byteplus.models";
22
23
  export { ELEVENLABS_CATALOG, ELEVENLABS_MODELS, ELEVENLABS_OUTPUT_FORMATS, } from "./providers/elevenlabs.models";
23
- export { GOOGLE_CATALOG, GOOGLE_IMAGE_MODELS, GOOGLE_VIDEO_MODELS, } from "./providers/google.models";
24
+ export { GOOGLE_AUDIO_MODELS, GOOGLE_CATALOG, GOOGLE_IMAGE_MODELS, GOOGLE_TEXT_MODELS, GOOGLE_TTS_VOICES, GOOGLE_VIDEO_MODELS, } from "./providers/google.models";
24
25
  export type { KlingModelBinding } from "./providers/kling.models";
25
26
  export { type AvatarInput, type ImageOmniInput, KLING_CAPABILITIES, KLING_CATALOG, KLING_MODELS, type KlingCapability, type KlingRequestOptions, type KlingResultField, type MotionControlInput, type OmniVideoInput, type OutpaintingInput, } from "./providers/kling.models";
26
- export { OPENAI_CATALOG, OPENAI_IMAGE_MODELS, } from "./providers/openai.models";
27
+ export { OPENAI_CATALOG, OPENAI_IMAGE_MODELS, OPENAI_TEXT_MODELS, } from "./providers/openai.models";
27
28
  export { QWEN_AUDIO_MODELS, QWEN_CATALOG, QWEN_IMAGE_MODELS, QWEN_TEXT_MODELS, QWEN_VIDEO_MODELS, type QwenImageBinding, type QwenVideoBinding, } from "./providers/qwen.models";
28
29
  export type { BrotuAIOptions, ProviderConfig, ReferenceVideoInfo, ResolvedProvider, } from "./types";
29
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EACX,wBAAwB,EACxB,eAAe,GACf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACN,kBAAkB,EAClB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,eAAe,GACf,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,mBAAmB,yBAAyB,CAAC;AAC7C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,IAAI,EACJ,KAAK,UAAU,EACf,EAAE,EACF,KAAK,MAAM,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACN,YAAY,EACZ,KAAK,GAAG,EACR,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,UAAU,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,eAAe,EACf,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,OAAO,GACZ,MAAM,eAAe,CAAC;AACvB,mBAAmB,gCAAgC,CAAC;AACpD,OAAO,EACN,gBAAgB,EAChB,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACN,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,GACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACN,cAAc,EACd,mBAAmB,EACnB,mBAAmB,GACnB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,cAAc,EACd,mBAAmB,GACnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACrB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACX,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GAChB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EACX,wBAAwB,EACxB,eAAe,GACf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACN,kBAAkB,EAClB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,eAAe,GACf,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,mBAAmB,yBAAyB,CAAC;AAC7C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,IAAI,EACJ,KAAK,UAAU,EACf,EAAE,EACF,KAAK,MAAM,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACN,YAAY,EACZ,KAAK,GAAG,EACR,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,UAAU,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,eAAe,EACf,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,OAAO,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,cAAc,EACd,cAAc,EACd,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,gBAAgB,GACrB,MAAM,eAAe,CAAC;AACvB,mBAAmB,gCAAgC,CAAC;AACpD,OAAO,EACN,gBAAgB,EAChB,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACN,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,GACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACN,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,GACnB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,cAAc,EACd,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACrB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACX,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GAChB,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -5,8 +5,11 @@ import {
5
5
  ELEVENLABS_CATALOG,
6
6
  ELEVENLABS_MODELS,
7
7
  ELEVENLABS_OUTPUT_FORMATS,
8
+ GOOGLE_AUDIO_MODELS,
8
9
  GOOGLE_CATALOG,
9
10
  GOOGLE_IMAGE_MODELS,
11
+ GOOGLE_TEXT_MODELS,
12
+ GOOGLE_TTS_VOICES,
10
13
  GOOGLE_VIDEO_MODELS,
11
14
  KLING_AUDIO_MODEL,
12
15
  KLING_CAPABILITIES,
@@ -15,6 +18,7 @@ import {
15
18
  KLING_VOICES,
16
19
  OPENAI_CATALOG,
17
20
  OPENAI_IMAGE_MODELS,
21
+ OPENAI_TEXT_MODELS,
18
22
  QWEN_AUDIO_MODELS,
19
23
  QWEN_CATALOG,
20
24
  QWEN_IMAGE_MODELS,
@@ -31,7 +35,7 @@ import {
31
35
  resetCatalog,
32
36
  resolveProvider,
33
37
  videoPathFor
34
- } from "./chunk-GKLTQ55S.js";
38
+ } from "./chunk-PL534BFN.js";
35
39
 
36
40
  // src/lib/jobs.ts
37
41
  import { AsyncLocalStorage } from "async_hooks";
@@ -582,7 +586,12 @@ var POLL_INTERVAL_MS2 = 1e4;
582
586
  var DEFAULT_MAX_POLL_ATTEMPTS2 = 120;
583
587
  var GoogleAdapter = class {
584
588
  providerName = "google";
585
- supportedTypes = ["image", "video"];
589
+ supportedTypes = [
590
+ "image",
591
+ "video",
592
+ "text",
593
+ "audio"
594
+ ];
586
595
  opts;
587
596
  constructor(opts) {
588
597
  this.opts = opts;
@@ -904,17 +913,143 @@ var GoogleAdapter = class {
904
913
  generateVideo(params) {
905
914
  return this.runVideo(params);
906
915
  }
907
- generateAudio(_params) {
908
- throw new Error("Google has no speech synthesis wired up here.");
916
+ textBody(modelId, params) {
917
+ const input = [
918
+ { type: "text", text: params.prompt }
919
+ ];
920
+ for (const image of params.referenceImages ?? []) {
921
+ const [meta, data] = image.split(",");
922
+ input.push({
923
+ type: "image",
924
+ mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
925
+ data: data ?? image
926
+ });
927
+ }
928
+ return {
929
+ model: modelId,
930
+ input,
931
+ ...params.systemPrompt ? { instructions: params.systemPrompt } : {},
932
+ generation_config: {
933
+ ...params.maxTokens ? { max_output_tokens: params.maxTokens } : {},
934
+ ...params.temperature !== void 0 ? { temperature: params.temperature } : {},
935
+ ...params.topP !== void 0 ? { top_p: params.topP } : {}
936
+ },
937
+ ...params.providerOptions?.google ?? {}
938
+ };
909
939
  }
910
- generateText(_params) {
911
- throw new Error("The Google adapter here covers image and video only.");
940
+ speechBody(modelId, params, voice) {
941
+ return {
942
+ model: modelId,
943
+ input: params.prompt,
944
+ response_format: { type: "audio" },
945
+ generation_config: {
946
+ speech_config: [{ voice }]
947
+ },
948
+ ...params.providerOptions?.google ?? {}
949
+ };
950
+ }
951
+ async generateText(params) {
952
+ const startedAt = Date.now();
953
+ const modelId = params.model ?? "";
954
+ const failure = (error) => ({
955
+ success: false,
956
+ outputs: [],
957
+ creditsUsed: 0,
958
+ provider: this.providerName,
959
+ model: modelId,
960
+ processingTimeMs: Date.now() - startedAt,
961
+ error
962
+ });
963
+ if (!(modelId in GOOGLE_TEXT_MODELS)) {
964
+ return failure(
965
+ `"${modelId}" is not a Gemini text model. Known: ${Object.keys(GOOGLE_TEXT_MODELS).join(", ")}.`
966
+ );
967
+ }
968
+ try {
969
+ const payload = await this.request(
970
+ INTERACTIONS_PATH,
971
+ {
972
+ method: "POST",
973
+ body: this.textBody(modelId, params)
974
+ }
975
+ );
976
+ const text = payload.output_text ?? payload.steps?.flatMap((step) => step.content ?? []).find((part) => part.type === "text" || part.text)?.text;
977
+ if (!text) return failure("Google returned no text.");
978
+ return {
979
+ success: true,
980
+ outputs: [
981
+ {
982
+ url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
983
+ mimeType: "text/plain",
984
+ raw: { text }
985
+ }
986
+ ],
987
+ creditsUsed: 0,
988
+ provider: this.providerName,
989
+ model: modelId,
990
+ processingTimeMs: Date.now() - startedAt
991
+ };
992
+ } catch (error) {
993
+ return failure(error instanceof Error ? error.message : String(error));
994
+ }
995
+ }
996
+ async generateAudio(params) {
997
+ const startedAt = Date.now();
998
+ const modelId = params.model ?? "";
999
+ const failure = (error) => ({
1000
+ success: false,
1001
+ outputs: [],
1002
+ creditsUsed: 0,
1003
+ provider: this.providerName,
1004
+ model: modelId,
1005
+ processingTimeMs: Date.now() - startedAt,
1006
+ error
1007
+ });
1008
+ const binding = GOOGLE_AUDIO_MODELS[modelId];
1009
+ if (!binding) {
1010
+ return failure(
1011
+ `"${modelId}" is not a Gemini speech model. Known: ${Object.keys(GOOGLE_AUDIO_MODELS).join(", ")}.`
1012
+ );
1013
+ }
1014
+ const voice = params.voice ?? "Kore";
1015
+ if (!binding.voices.includes(voice)) {
1016
+ return failure(
1017
+ `Gemini TTS offers ${GOOGLE_TTS_VOICES.join(", ")}, not "${voice}".`
1018
+ );
1019
+ }
1020
+ try {
1021
+ const payload = await this.request(
1022
+ INTERACTIONS_PATH,
1023
+ {
1024
+ method: "POST",
1025
+ body: this.speechBody(modelId, params, voice)
1026
+ }
1027
+ );
1028
+ const data = payload.output_audio?.data;
1029
+ if (!data) return failure("Google returned no audio.");
1030
+ return {
1031
+ success: true,
1032
+ outputs: [
1033
+ {
1034
+ url: `data:audio/wav;base64,${data}`,
1035
+ mimeType: "audio/wav",
1036
+ raw: { inline: true, voice }
1037
+ }
1038
+ ],
1039
+ creditsUsed: 0,
1040
+ provider: this.providerName,
1041
+ model: modelId,
1042
+ processingTimeMs: Date.now() - startedAt
1043
+ };
1044
+ } catch (error) {
1045
+ return failure(error instanceof Error ? error.message : String(error));
1046
+ }
912
1047
  }
913
1048
  async estimateCost(type, params) {
914
1049
  return estimateFor(this.providerName, type, params);
915
1050
  }
916
1051
  supportsModel(model) {
917
- return model in GOOGLE_IMAGE_MODELS || model in GOOGLE_VIDEO_MODELS;
1052
+ return model in GOOGLE_IMAGE_MODELS || model in GOOGLE_VIDEO_MODELS || model in GOOGLE_TEXT_MODELS || model in GOOGLE_AUDIO_MODELS;
918
1053
  }
919
1054
  getAvailableModels() {
920
1055
  return [
@@ -927,6 +1062,16 @@ var GoogleAdapter = class {
927
1062
  id,
928
1063
  name: id,
929
1064
  type: "video"
1065
+ })),
1066
+ ...Object.keys(GOOGLE_TEXT_MODELS).map((id) => ({
1067
+ id,
1068
+ name: id,
1069
+ type: "text"
1070
+ })),
1071
+ ...Object.keys(GOOGLE_AUDIO_MODELS).map((id) => ({
1072
+ id,
1073
+ name: id,
1074
+ type: "audio"
930
1075
  }))
931
1076
  ];
932
1077
  }
@@ -1283,7 +1428,7 @@ var IMAGES_PATH2 = "/v1/images/generations";
1283
1428
  var OpenAIAdapter = class {
1284
1429
  providerName = "openai";
1285
1430
  // No video: the Videos API shuts down on 24 September 2026.
1286
- supportedTypes = ["image"];
1431
+ supportedTypes = ["image", "text"];
1287
1432
  opts;
1288
1433
  constructor(opts) {
1289
1434
  this.opts = opts;
@@ -1398,8 +1543,79 @@ var OpenAIAdapter = class {
1398
1543
  generateAudio(_params) {
1399
1544
  throw new Error("OpenAI has no speech synthesis wired up here.");
1400
1545
  }
1401
- generateText(_params) {
1402
- throw new Error("The OpenAI adapter here covers images only.");
1546
+ textBody(params) {
1547
+ const content = params.referenceImages?.length ? [
1548
+ { type: "input_text", text: params.prompt },
1549
+ ...params.referenceImages.map((url) => ({
1550
+ type: "input_image",
1551
+ image_url: url
1552
+ }))
1553
+ ] : params.prompt;
1554
+ const input = [];
1555
+ if (params.systemPrompt) {
1556
+ input.push({ role: "system", content: params.systemPrompt });
1557
+ }
1558
+ input.push({ role: "user", content });
1559
+ return {
1560
+ model: params.model,
1561
+ input,
1562
+ ...params.maxTokens ? { max_output_tokens: params.maxTokens } : {},
1563
+ ...params.temperature !== void 0 ? { temperature: params.temperature } : {},
1564
+ ...params.topP !== void 0 ? { top_p: params.topP } : {},
1565
+ ...params.providerOptions?.openai ?? {}
1566
+ };
1567
+ }
1568
+ async generateText(params) {
1569
+ const startedAt = Date.now();
1570
+ const modelId = params.model ?? "";
1571
+ const failure = (error) => ({
1572
+ success: false,
1573
+ outputs: [],
1574
+ creditsUsed: 0,
1575
+ provider: this.providerName,
1576
+ model: modelId,
1577
+ processingTimeMs: Date.now() - startedAt,
1578
+ error
1579
+ });
1580
+ if (!(modelId in OPENAI_TEXT_MODELS)) {
1581
+ return failure(
1582
+ `"${modelId}" is not an OpenAI text model. Known: ${Object.keys(OPENAI_TEXT_MODELS).join(", ")}.`
1583
+ );
1584
+ }
1585
+ try {
1586
+ const response = await fetch(`${this.baseUrl}/v1/responses`, {
1587
+ method: "POST",
1588
+ headers: {
1589
+ Authorization: `Bearer ${this.opts.apiKey}`,
1590
+ "Content-Type": "application/json",
1591
+ ...this.opts.organization ? { "OpenAI-Organization": this.opts.organization } : {}
1592
+ },
1593
+ body: JSON.stringify(this.textBody(params))
1594
+ });
1595
+ const payload = await response.json();
1596
+ if (payload.error) throw new Error(payload.error.message);
1597
+ if (!response.ok) {
1598
+ throw new Error(`OpenAI returned ${response.status}.`);
1599
+ }
1600
+ const text = payload.output_text ?? payload.output?.flatMap((item) => item.content ?? []).find((part) => part.type === "output_text" || part.text)?.text;
1601
+ if (!text) return failure("OpenAI returned no text.");
1602
+ return {
1603
+ success: true,
1604
+ outputs: [
1605
+ {
1606
+ url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
1607
+ mimeType: "text/plain",
1608
+ raw: { text, tokens: payload.usage?.output_tokens }
1609
+ }
1610
+ ],
1611
+ creditsUsed: 0,
1612
+ provider: this.providerName,
1613
+ model: modelId,
1614
+ processingTimeMs: Date.now() - startedAt
1615
+ };
1616
+ } catch (error) {
1617
+ return failure(error instanceof Error ? error.message : String(error));
1618
+ }
1403
1619
  }
1404
1620
  /** Synchronous provider: there is never a queued job to come back to. */
1405
1621
  async completeJob(job) {
@@ -1420,14 +1636,21 @@ var OpenAIAdapter = class {
1420
1636
  };
1421
1637
  }
1422
1638
  supportsModel(model) {
1423
- return model in OPENAI_IMAGE_MODELS;
1639
+ return model in OPENAI_IMAGE_MODELS || model in OPENAI_TEXT_MODELS;
1424
1640
  }
1425
1641
  getAvailableModels() {
1426
- return Object.keys(OPENAI_IMAGE_MODELS).map((id) => ({
1427
- id,
1428
- name: id,
1429
- type: "image"
1430
- }));
1642
+ return [
1643
+ ...Object.keys(OPENAI_IMAGE_MODELS).map((id) => ({
1644
+ id,
1645
+ name: id,
1646
+ type: "image"
1647
+ })),
1648
+ ...Object.keys(OPENAI_TEXT_MODELS).map((id) => ({
1649
+ id,
1650
+ name: id,
1651
+ type: "text"
1652
+ }))
1653
+ ];
1431
1654
  }
1432
1655
  };
1433
1656
 
@@ -2033,6 +2256,45 @@ async function persistOutputs(storage, outputs) {
2033
2256
  );
2034
2257
  }
2035
2258
 
2259
+ // src/lib/webhook.ts
2260
+ function resolveWebhook(value) {
2261
+ if (!value) return void 0;
2262
+ if (typeof value === "string") {
2263
+ return isHttpUrl(value) ? { url: value } : void 0;
2264
+ }
2265
+ if (!isHttpUrl(value.url)) return void 0;
2266
+ return {
2267
+ url: value.url,
2268
+ secret: value.secret,
2269
+ headers: value.headers
2270
+ };
2271
+ }
2272
+ function isHttpUrl(value) {
2273
+ try {
2274
+ const parsed = new URL(value);
2275
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
2276
+ } catch {
2277
+ return false;
2278
+ }
2279
+ }
2280
+ async function deliverWebhook(config, payload) {
2281
+ try {
2282
+ await fetch(config.url, {
2283
+ method: "POST",
2284
+ headers: {
2285
+ "content-type": "application/json",
2286
+ "user-agent": "@brotu/ai",
2287
+ "x-brotu-event": payload.event,
2288
+ ...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
2289
+ ...config.headers
2290
+ },
2291
+ body: JSON.stringify(payload),
2292
+ signal: AbortSignal.timeout(1e4)
2293
+ });
2294
+ } catch {
2295
+ }
2296
+ }
2297
+
2036
2298
  // src/client.ts
2037
2299
  var NATIVE_PROVIDERS = [
2038
2300
  "byteplus",
@@ -2047,6 +2309,8 @@ function brotuClient(options) {
2047
2309
  throw new Error("brotuClient requires at least one provider API key.");
2048
2310
  }
2049
2311
  registerModels(options.models);
2312
+ let registeredWebhook = resolveWebhook(options.webhook);
2313
+ const notifiedJobIds = /* @__PURE__ */ new Set();
2050
2314
  const storage = options.storage ? createS3Storage(options.storage) : void 0;
2051
2315
  const shouldPersist = storage && options.storage?.persistOutputs !== false;
2052
2316
  const adapters = /* @__PURE__ */ new Map();
@@ -2136,6 +2400,31 @@ function brotuClient(options) {
2136
2400
  return adapter.generateAudio(params);
2137
2401
  return adapter.generateText(params);
2138
2402
  }
2403
+ function webhookFor(params) {
2404
+ return resolveWebhook(params?.webhook) ?? registeredWebhook;
2405
+ }
2406
+ async function notifySettled(input) {
2407
+ const config = webhookFor(input.params);
2408
+ if (!config) return;
2409
+ const jobId = input.job?.id;
2410
+ if (jobId) {
2411
+ if (notifiedJobIds.has(jobId)) return;
2412
+ notifiedJobIds.add(jobId);
2413
+ }
2414
+ const payload = {
2415
+ event: input.event,
2416
+ jobId,
2417
+ provider: input.provider ?? input.job?.provider,
2418
+ model: input.model ?? input.job?.model,
2419
+ kind: input.kind ?? input.job?.kind,
2420
+ outputs: input.outputs,
2421
+ error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
2422
+ metadata: input.metadata ?? input.job?.metadata,
2423
+ processingTimeMs: input.processingTimeMs,
2424
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
2425
+ };
2426
+ await deliverWebhook(config, payload);
2427
+ }
2139
2428
  async function toGeneration(raw, metadata) {
2140
2429
  if (!raw.success) {
2141
2430
  return fail({
@@ -2158,15 +2447,50 @@ function brotuClient(options) {
2158
2447
  const routed = route(params.model);
2159
2448
  if (routed.error) return fail(routed.error);
2160
2449
  try {
2161
- return await toGeneration(
2450
+ const result = await toGeneration(
2162
2451
  await generateWith(routed.data.adapter, kind, params),
2163
2452
  params.metadata
2164
2453
  );
2454
+ if (result.error) {
2455
+ await notifySettled({
2456
+ event: "generation.failed",
2457
+ params,
2458
+ kind,
2459
+ provider: routed.data.provider,
2460
+ model: routed.data.model,
2461
+ error: result.error,
2462
+ metadata: params.metadata
2463
+ });
2464
+ return result;
2465
+ }
2466
+ await notifySettled({
2467
+ event: "generation.succeeded",
2468
+ params,
2469
+ kind,
2470
+ provider: result.data.provider,
2471
+ model: result.data.model,
2472
+ outputs: result.data.outputs,
2473
+ metadata: result.data.metadata,
2474
+ processingTimeMs: result.data.processingTimeMs
2475
+ });
2476
+ return result;
2165
2477
  } catch (error) {
2166
- return failFrom("provider_error", error, {
2478
+ const failed = failFrom("provider_error", error, {
2167
2479
  provider: routed.data.provider,
2168
2480
  model: routed.data.model
2169
2481
  });
2482
+ if (failed.error) {
2483
+ await notifySettled({
2484
+ event: "generation.failed",
2485
+ params,
2486
+ kind,
2487
+ provider: routed.data.provider,
2488
+ model: routed.data.model,
2489
+ error: failed.error,
2490
+ metadata: params.metadata
2491
+ });
2492
+ }
2493
+ return failed;
2170
2494
  }
2171
2495
  }
2172
2496
  async function submit(kind, params) {
@@ -2185,18 +2509,40 @@ function brotuClient(options) {
2185
2509
  () => generateWith(routed.data.adapter, kind, params)
2186
2510
  );
2187
2511
  if (!raw.success) {
2188
- return fail({
2512
+ const error = {
2189
2513
  code: "provider_error",
2190
2514
  message: raw.error ?? "The provider rejected the request.",
2191
2515
  provider: raw.provider,
2192
2516
  model: raw.model
2517
+ };
2518
+ await notifySettled({
2519
+ event: "generation.failed",
2520
+ params,
2521
+ kind,
2522
+ provider: raw.provider,
2523
+ model: raw.model,
2524
+ error,
2525
+ metadata: params.metadata
2193
2526
  });
2527
+ return fail(error);
2194
2528
  }
2195
- return ok({
2529
+ const job = {
2196
2530
  ...base,
2197
2531
  id: `inline-${base.model}-${base.submittedAt}`,
2198
2532
  result: raw
2533
+ };
2534
+ await notifySettled({
2535
+ event: "generation.succeeded",
2536
+ params,
2537
+ job,
2538
+ kind,
2539
+ provider: raw.provider,
2540
+ model: raw.model,
2541
+ outputs: raw.outputs,
2542
+ metadata: params.metadata,
2543
+ processingTimeMs: raw.processingTimeMs
2199
2544
  });
2545
+ return ok(job);
2200
2546
  } catch (error) {
2201
2547
  if (!isPendingJob(error)) {
2202
2548
  return failFrom("provider_error", error, {
@@ -2211,11 +2557,54 @@ function brotuClient(options) {
2211
2557
  });
2212
2558
  }
2213
2559
  }
2560
+ async function notifyFromSnapshot(job, snapshot) {
2561
+ if (snapshot.status === "succeeded" && snapshot.result) {
2562
+ await notifySettled({
2563
+ event: "generation.succeeded",
2564
+ params: job.params,
2565
+ job,
2566
+ outputs: snapshot.result.outputs,
2567
+ provider: snapshot.result.provider,
2568
+ model: snapshot.result.model,
2569
+ processingTimeMs: snapshot.result.processingTimeMs,
2570
+ metadata: job.metadata
2571
+ });
2572
+ return;
2573
+ }
2574
+ if (snapshot.status === "failed") {
2575
+ await notifySettled({
2576
+ event: "generation.failed",
2577
+ params: job.params,
2578
+ job,
2579
+ error: {
2580
+ code: "provider_error",
2581
+ message: snapshot.error ?? "The job failed.",
2582
+ provider: job.provider,
2583
+ model: job.model
2584
+ },
2585
+ metadata: job.metadata
2586
+ });
2587
+ }
2588
+ }
2589
+ async function finalizeSnapshot(job, snapshot) {
2590
+ if (snapshot.status === "succeeded" && snapshot.result) {
2591
+ const persisted = await toGeneration(snapshot.result, job.metadata);
2592
+ if (!persisted.error) {
2593
+ snapshot = {
2594
+ status: "succeeded",
2595
+ result: {
2596
+ ...snapshot.result,
2597
+ outputs: persisted.data.outputs
2598
+ }
2599
+ };
2600
+ }
2601
+ }
2602
+ await notifyFromSnapshot(job, snapshot);
2603
+ return snapshot;
2604
+ }
2214
2605
  async function pollJob(job) {
2215
2606
  if (job.result) {
2216
- return ok(
2217
- job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }
2218
- );
2607
+ return ok(await finalizeSnapshot(job, job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }));
2219
2608
  }
2220
2609
  const routed = route(job.model);
2221
2610
  if (routed.error) return fail(routed.error);
@@ -2228,7 +2617,7 @@ function brotuClient(options) {
2228
2617
  });
2229
2618
  }
2230
2619
  try {
2231
- return ok(await adapter.completeJob(job));
2620
+ return ok(await finalizeSnapshot(job, await adapter.completeJob(job)));
2232
2621
  } catch (error) {
2233
2622
  return failFrom("provider_error", error, {
2234
2623
  provider: job.provider,
@@ -2297,6 +2686,17 @@ function brotuClient(options) {
2297
2686
  submit: (params) => submit("audio", params),
2298
2687
  generate: (params) => generate("audio", params)
2299
2688
  },
2689
+ webhook: {
2690
+ set(value) {
2691
+ registeredWebhook = resolveWebhook(value);
2692
+ },
2693
+ clear() {
2694
+ registeredWebhook = void 0;
2695
+ },
2696
+ get() {
2697
+ return registeredWebhook;
2698
+ }
2699
+ },
2300
2700
  jobs: {
2301
2701
  poll: pollJob,
2302
2702
  wait: async (job, waitOptions) => {
@@ -2306,7 +2706,13 @@ function brotuClient(options) {
2306
2706
  if (polled.error) return fail(polled.error);
2307
2707
  const snapshot = polled.data;
2308
2708
  if (snapshot.status === "succeeded" && snapshot.result) {
2309
- return toGeneration(snapshot.result, job.metadata);
2709
+ return ok({
2710
+ outputs: snapshot.result.outputs,
2711
+ provider: snapshot.result.provider,
2712
+ model: snapshot.result.model,
2713
+ processingTimeMs: snapshot.result.processingTimeMs,
2714
+ metadata: job.metadata
2715
+ });
2310
2716
  }
2311
2717
  if (snapshot.status === "failed") {
2312
2718
  return fail({
@@ -2351,8 +2757,11 @@ export {
2351
2757
  ELEVENLABS_MODELS,
2352
2758
  ELEVENLABS_OUTPUT_FORMATS,
2353
2759
  ElevenLabsAdapter,
2760
+ GOOGLE_AUDIO_MODELS,
2354
2761
  GOOGLE_CATALOG,
2355
2762
  GOOGLE_IMAGE_MODELS,
2763
+ GOOGLE_TEXT_MODELS,
2764
+ GOOGLE_TTS_VOICES,
2356
2765
  GOOGLE_VIDEO_MODELS,
2357
2766
  GoogleAdapter,
2358
2767
  KLING_CAPABILITIES,
@@ -2361,6 +2770,7 @@ export {
2361
2770
  KlingAdapter,
2362
2771
  OPENAI_CATALOG,
2363
2772
  OPENAI_IMAGE_MODELS,
2773
+ OPENAI_TEXT_MODELS,
2364
2774
  OpenAIAdapter,
2365
2775
  PendingJob,
2366
2776
  QWEN_AUDIO_MODELS,
@@ -2371,6 +2781,7 @@ export {
2371
2781
  QwenAdapter,
2372
2782
  brotuClient,
2373
2783
  createS3Storage,
2784
+ deliverWebhook,
2374
2785
  fail,
2375
2786
  getAvailableModels,
2376
2787
  getModel,
@@ -2382,5 +2793,6 @@ export {
2382
2793
  persistOutputs,
2383
2794
  registerModels,
2384
2795
  resetCatalog,
2385
- resolveProvider
2796
+ resolveProvider,
2797
+ resolveWebhook
2386
2798
  };
@@ -0,0 +1,28 @@
1
+ import type { AIError } from "../helpers/result";
2
+ import type { GenerationOutput, GenerationType } from "../ports/content-generator.port";
3
+ export type WebhookEventName = "generation.succeeded" | "generation.failed";
4
+ export interface WebhookConfig {
5
+ url: string;
6
+ /** Sent as `x-brotu-webhook-secret` so your endpoint can reject strangers. */
7
+ secret?: string;
8
+ headers?: Record<string, string>;
9
+ }
10
+ export interface WebhookEvent {
11
+ event: WebhookEventName;
12
+ jobId?: string;
13
+ provider?: string;
14
+ model?: string;
15
+ kind?: GenerationType;
16
+ outputs?: GenerationOutput[];
17
+ error?: Pick<AIError, "code" | "message">;
18
+ metadata?: Record<string, string>;
19
+ processingTimeMs?: number;
20
+ completedAt: string;
21
+ }
22
+ export declare function resolveWebhook(value: string | WebhookConfig | undefined): WebhookConfig | undefined;
23
+ /**
24
+ * POST the settled generation at the registered URL. Failures are swallowed:
25
+ * a down hook must not fail the call the user is waiting on.
26
+ */
27
+ export declare function deliverWebhook(config: WebhookConfig, payload: WebhookEvent): Promise<void>;
28
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/lib/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EACX,gBAAgB,EAChB,cAAc,EACd,MAAM,iCAAiC,CAAC;AAEzC,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,GAAG,mBAAmB,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,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,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAC7B,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,GACvC,aAAa,GAAG,SAAS,CAW3B;AAWD;;;GAGG;AACH,wBAAsB,cAAc,CACnC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAmBf"}