@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/CATALOG.md +154 -2
- package/LICENSE +1 -1
- package/README.md +242 -58
- package/dist/adapters/google.adapter.d.ts +4 -2
- package/dist/adapters/google.adapter.d.ts.map +1 -1
- package/dist/adapters/openai.adapter.d.ts +2 -1
- package/dist/adapters/openai.adapter.d.ts.map +1 -1
- package/dist/catalog.cjs +114 -17
- package/dist/catalog.js +1 -1
- package/dist/{chunk-GKLTQ55S.js → chunk-PL534BFN.js} +118 -17
- package/dist/client.d.ts +10 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +554 -43
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +438 -26
- package/dist/lib/webhook.d.ts +28 -0
- package/dist/lib/webhook.d.ts.map +1 -0
- package/dist/ports/content-generator.port.d.ts +6 -0
- package/dist/ports/content-generator.port.d.ts.map +1 -1
- package/dist/providers/google.models.d.ts +23 -0
- package/dist/providers/google.models.d.ts.map +1 -1
- package/dist/providers/openai.models.d.ts +7 -0
- package/dist/providers/openai.models.d.ts.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
package/dist/index.cjs
CHANGED
|
@@ -37,8 +37,11 @@ __export(index_exports, {
|
|
|
37
37
|
ELEVENLABS_MODELS: () => ELEVENLABS_MODELS,
|
|
38
38
|
ELEVENLABS_OUTPUT_FORMATS: () => ELEVENLABS_OUTPUT_FORMATS,
|
|
39
39
|
ElevenLabsAdapter: () => ElevenLabsAdapter,
|
|
40
|
+
GOOGLE_AUDIO_MODELS: () => GOOGLE_AUDIO_MODELS,
|
|
40
41
|
GOOGLE_CATALOG: () => GOOGLE_CATALOG,
|
|
41
42
|
GOOGLE_IMAGE_MODELS: () => GOOGLE_IMAGE_MODELS,
|
|
43
|
+
GOOGLE_TEXT_MODELS: () => GOOGLE_TEXT_MODELS,
|
|
44
|
+
GOOGLE_TTS_VOICES: () => GOOGLE_TTS_VOICES,
|
|
42
45
|
GOOGLE_VIDEO_MODELS: () => GOOGLE_VIDEO_MODELS,
|
|
43
46
|
GoogleAdapter: () => GoogleAdapter,
|
|
44
47
|
KLING_CAPABILITIES: () => KLING_CAPABILITIES,
|
|
@@ -47,6 +50,7 @@ __export(index_exports, {
|
|
|
47
50
|
KlingAdapter: () => KlingAdapter,
|
|
48
51
|
OPENAI_CATALOG: () => OPENAI_CATALOG,
|
|
49
52
|
OPENAI_IMAGE_MODELS: () => OPENAI_IMAGE_MODELS,
|
|
53
|
+
OPENAI_TEXT_MODELS: () => OPENAI_TEXT_MODELS,
|
|
50
54
|
OpenAIAdapter: () => OpenAIAdapter,
|
|
51
55
|
PendingJob: () => PendingJob,
|
|
52
56
|
QWEN_AUDIO_MODELS: () => QWEN_AUDIO_MODELS,
|
|
@@ -57,6 +61,7 @@ __export(index_exports, {
|
|
|
57
61
|
QwenAdapter: () => QwenAdapter,
|
|
58
62
|
brotuClient: () => brotuClient,
|
|
59
63
|
createS3Storage: () => createS3Storage,
|
|
64
|
+
deliverWebhook: () => deliverWebhook,
|
|
60
65
|
fail: () => fail,
|
|
61
66
|
getAvailableModels: () => getAvailableModels,
|
|
62
67
|
getModel: () => getModel,
|
|
@@ -68,7 +73,8 @@ __export(index_exports, {
|
|
|
68
73
|
persistOutputs: () => persistOutputs,
|
|
69
74
|
registerModels: () => registerModels,
|
|
70
75
|
resetCatalog: () => resetCatalog,
|
|
71
|
-
resolveProvider: () => resolveProvider
|
|
76
|
+
resolveProvider: () => resolveProvider,
|
|
77
|
+
resolveWebhook: () => resolveWebhook
|
|
72
78
|
});
|
|
73
79
|
module.exports = __toCommonJS(index_exports);
|
|
74
80
|
|
|
@@ -398,6 +404,55 @@ var GOOGLE_VIDEO_MODELS = {
|
|
|
398
404
|
conversationalEditing: true
|
|
399
405
|
}
|
|
400
406
|
};
|
|
407
|
+
var GOOGLE_TEXT_MODELS = {
|
|
408
|
+
"gemini-3.7-flash": { usdPerMillionOutput: 3.75 },
|
|
409
|
+
"gemini-3.6-flash": { usdPerMillionOutput: 3.75 },
|
|
410
|
+
"gemini-3.5-flash": { usdPerMillionOutput: 9 },
|
|
411
|
+
"gemini-3.5-flash-lite": {},
|
|
412
|
+
"gemini-3.1-flash-lite": {},
|
|
413
|
+
"gemini-3.1-pro-preview": { usdPerMillionOutput: 12 },
|
|
414
|
+
"gemini-3-flash-preview": {},
|
|
415
|
+
"gemini-2.5-pro": {},
|
|
416
|
+
"gemini-2.5-flash": {},
|
|
417
|
+
"gemini-2.5-flash-lite": {}
|
|
418
|
+
};
|
|
419
|
+
var GOOGLE_TTS_VOICES = [
|
|
420
|
+
"Zephyr",
|
|
421
|
+
"Puck",
|
|
422
|
+
"Charon",
|
|
423
|
+
"Kore",
|
|
424
|
+
"Fenrir",
|
|
425
|
+
"Leda",
|
|
426
|
+
"Orus",
|
|
427
|
+
"Aoede",
|
|
428
|
+
"Callirrhoe",
|
|
429
|
+
"Autonoe",
|
|
430
|
+
"Enceladus",
|
|
431
|
+
"Iapetus",
|
|
432
|
+
"Umbriel",
|
|
433
|
+
"Algieba",
|
|
434
|
+
"Despina",
|
|
435
|
+
"Erinome",
|
|
436
|
+
"Algenib",
|
|
437
|
+
"Rasalgethi",
|
|
438
|
+
"Laomedeia",
|
|
439
|
+
"Achernar",
|
|
440
|
+
"Alnilam",
|
|
441
|
+
"Schedar",
|
|
442
|
+
"Gacrux",
|
|
443
|
+
"Pulcherrima",
|
|
444
|
+
"Achird",
|
|
445
|
+
"Zubenelgenubi",
|
|
446
|
+
"Vindemiatrix",
|
|
447
|
+
"Sadachbia",
|
|
448
|
+
"Sadaltager",
|
|
449
|
+
"Sulafat"
|
|
450
|
+
];
|
|
451
|
+
var GOOGLE_AUDIO_MODELS = {
|
|
452
|
+
"gemini-3.1-flash-tts-preview": { voices: GOOGLE_TTS_VOICES },
|
|
453
|
+
"gemini-2.5-flash-preview-tts": { voices: GOOGLE_TTS_VOICES },
|
|
454
|
+
"gemini-2.5-pro-preview-tts": { voices: GOOGLE_TTS_VOICES }
|
|
455
|
+
};
|
|
401
456
|
var GOOGLE_CATALOG = [
|
|
402
457
|
...Object.entries(GOOGLE_IMAGE_MODELS).map(
|
|
403
458
|
([id, binding]) => ({
|
|
@@ -434,6 +489,35 @@ var GOOGLE_CATALOG = [
|
|
|
434
489
|
byResolution: binding.usdByResolution
|
|
435
490
|
}
|
|
436
491
|
})
|
|
492
|
+
),
|
|
493
|
+
...Object.entries(GOOGLE_TEXT_MODELS).map(
|
|
494
|
+
([id, binding]) => ({
|
|
495
|
+
id,
|
|
496
|
+
name: id,
|
|
497
|
+
category: "text",
|
|
498
|
+
inputType: "image_optional",
|
|
499
|
+
nodeTypes: ["text"],
|
|
500
|
+
creditsPerUnit: 0,
|
|
501
|
+
creditUnit: "token",
|
|
502
|
+
provider: "google",
|
|
503
|
+
pricing: binding.usdPerMillionOutput === void 0 ? void 0 : {
|
|
504
|
+
unit: "token",
|
|
505
|
+
usdPerUnit: binding.usdPerMillionOutput / 1e6
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
),
|
|
509
|
+
...Object.entries(GOOGLE_AUDIO_MODELS).map(
|
|
510
|
+
([id, binding]) => ({
|
|
511
|
+
id,
|
|
512
|
+
name: id,
|
|
513
|
+
category: "audio",
|
|
514
|
+
inputType: "text_only",
|
|
515
|
+
nodeTypes: ["text"],
|
|
516
|
+
creditsPerUnit: 0,
|
|
517
|
+
creditUnit: "character",
|
|
518
|
+
provider: "google",
|
|
519
|
+
description: `Voices: ${binding.voices.join(", ")}`
|
|
520
|
+
})
|
|
437
521
|
)
|
|
438
522
|
];
|
|
439
523
|
|
|
@@ -824,23 +908,42 @@ var OPENAI_IMAGE_MODELS = {
|
|
|
824
908
|
usd: { low: 0.011, medium: 0.042, high: 0.167 }
|
|
825
909
|
}
|
|
826
910
|
};
|
|
827
|
-
var
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
911
|
+
var OPENAI_TEXT_MODELS = {
|
|
912
|
+
"gpt-5.6-sol": { usdPerMillionOutput: 30 },
|
|
913
|
+
"gpt-5.6-terra": { usdPerMillionOutput: 12 },
|
|
914
|
+
"gpt-5.6-luna": { usdPerMillionOutput: 1.2 }
|
|
915
|
+
};
|
|
916
|
+
var OPENAI_CATALOG = [
|
|
917
|
+
...Object.entries(OPENAI_IMAGE_MODELS).map(([id, binding]) => ({
|
|
918
|
+
id,
|
|
919
|
+
name: id,
|
|
920
|
+
category: "image",
|
|
921
|
+
inputType: "image_optional",
|
|
922
|
+
nodeTypes: ["image_gen"],
|
|
923
|
+
creditsPerUnit: 0,
|
|
924
|
+
creditUnit: "image",
|
|
925
|
+
supportedResolutions: binding.sizes === "any" ? void 0 : binding.sizes,
|
|
926
|
+
provider: "openai",
|
|
927
|
+
// Medium is the sane middle; `quality: auto` can silently pick high, which
|
|
928
|
+
// is 35x the low tier.
|
|
929
|
+
pricing: { usdPerUnit: binding.usd.medium, unit: "image" },
|
|
930
|
+
description: binding.shutdownOn ? `Deprecated \u2014 OpenAI shuts this down on ${binding.shutdownOn}.` : void 0
|
|
931
|
+
})),
|
|
932
|
+
...Object.entries(OPENAI_TEXT_MODELS).map(([id, binding]) => ({
|
|
933
|
+
id,
|
|
934
|
+
name: id,
|
|
935
|
+
category: "text",
|
|
936
|
+
inputType: "image_optional",
|
|
937
|
+
nodeTypes: ["text"],
|
|
938
|
+
creditsPerUnit: 0,
|
|
939
|
+
creditUnit: "token",
|
|
940
|
+
provider: "openai",
|
|
941
|
+
pricing: {
|
|
942
|
+
unit: "token",
|
|
943
|
+
usdPerUnit: binding.usdPerMillionOutput / 1e6
|
|
944
|
+
}
|
|
945
|
+
}))
|
|
946
|
+
];
|
|
844
947
|
|
|
845
948
|
// src/providers/qwen.models.ts
|
|
846
949
|
var VIDEO_PATH = "/api/v1/services/aigc/video-generation/video-synthesis";
|
|
@@ -1763,7 +1866,12 @@ var POLL_INTERVAL_MS2 = 1e4;
|
|
|
1763
1866
|
var DEFAULT_MAX_POLL_ATTEMPTS2 = 120;
|
|
1764
1867
|
var GoogleAdapter = class {
|
|
1765
1868
|
providerName = "google";
|
|
1766
|
-
supportedTypes = [
|
|
1869
|
+
supportedTypes = [
|
|
1870
|
+
"image",
|
|
1871
|
+
"video",
|
|
1872
|
+
"text",
|
|
1873
|
+
"audio"
|
|
1874
|
+
];
|
|
1767
1875
|
opts;
|
|
1768
1876
|
constructor(opts) {
|
|
1769
1877
|
this.opts = opts;
|
|
@@ -2085,17 +2193,143 @@ var GoogleAdapter = class {
|
|
|
2085
2193
|
generateVideo(params) {
|
|
2086
2194
|
return this.runVideo(params);
|
|
2087
2195
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2196
|
+
textBody(modelId, params) {
|
|
2197
|
+
const input = [
|
|
2198
|
+
{ type: "text", text: params.prompt }
|
|
2199
|
+
];
|
|
2200
|
+
for (const image of params.referenceImages ?? []) {
|
|
2201
|
+
const [meta, data] = image.split(",");
|
|
2202
|
+
input.push({
|
|
2203
|
+
type: "image",
|
|
2204
|
+
mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
|
|
2205
|
+
data: data ?? image
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
return {
|
|
2209
|
+
model: modelId,
|
|
2210
|
+
input,
|
|
2211
|
+
...params.systemPrompt ? { instructions: params.systemPrompt } : {},
|
|
2212
|
+
generation_config: {
|
|
2213
|
+
...params.maxTokens ? { max_output_tokens: params.maxTokens } : {},
|
|
2214
|
+
...params.temperature !== void 0 ? { temperature: params.temperature } : {},
|
|
2215
|
+
...params.topP !== void 0 ? { top_p: params.topP } : {}
|
|
2216
|
+
},
|
|
2217
|
+
...params.providerOptions?.google ?? {}
|
|
2218
|
+
};
|
|
2090
2219
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2220
|
+
speechBody(modelId, params, voice) {
|
|
2221
|
+
return {
|
|
2222
|
+
model: modelId,
|
|
2223
|
+
input: params.prompt,
|
|
2224
|
+
response_format: { type: "audio" },
|
|
2225
|
+
generation_config: {
|
|
2226
|
+
speech_config: [{ voice }]
|
|
2227
|
+
},
|
|
2228
|
+
...params.providerOptions?.google ?? {}
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
async generateText(params) {
|
|
2232
|
+
const startedAt = Date.now();
|
|
2233
|
+
const modelId = params.model ?? "";
|
|
2234
|
+
const failure = (error) => ({
|
|
2235
|
+
success: false,
|
|
2236
|
+
outputs: [],
|
|
2237
|
+
creditsUsed: 0,
|
|
2238
|
+
provider: this.providerName,
|
|
2239
|
+
model: modelId,
|
|
2240
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2241
|
+
error
|
|
2242
|
+
});
|
|
2243
|
+
if (!(modelId in GOOGLE_TEXT_MODELS)) {
|
|
2244
|
+
return failure(
|
|
2245
|
+
`"${modelId}" is not a Gemini text model. Known: ${Object.keys(GOOGLE_TEXT_MODELS).join(", ")}.`
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
try {
|
|
2249
|
+
const payload = await this.request(
|
|
2250
|
+
INTERACTIONS_PATH,
|
|
2251
|
+
{
|
|
2252
|
+
method: "POST",
|
|
2253
|
+
body: this.textBody(modelId, params)
|
|
2254
|
+
}
|
|
2255
|
+
);
|
|
2256
|
+
const text = payload.output_text ?? payload.steps?.flatMap((step) => step.content ?? []).find((part) => part.type === "text" || part.text)?.text;
|
|
2257
|
+
if (!text) return failure("Google returned no text.");
|
|
2258
|
+
return {
|
|
2259
|
+
success: true,
|
|
2260
|
+
outputs: [
|
|
2261
|
+
{
|
|
2262
|
+
url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
|
|
2263
|
+
mimeType: "text/plain",
|
|
2264
|
+
raw: { text }
|
|
2265
|
+
}
|
|
2266
|
+
],
|
|
2267
|
+
creditsUsed: 0,
|
|
2268
|
+
provider: this.providerName,
|
|
2269
|
+
model: modelId,
|
|
2270
|
+
processingTimeMs: Date.now() - startedAt
|
|
2271
|
+
};
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async generateAudio(params) {
|
|
2277
|
+
const startedAt = Date.now();
|
|
2278
|
+
const modelId = params.model ?? "";
|
|
2279
|
+
const failure = (error) => ({
|
|
2280
|
+
success: false,
|
|
2281
|
+
outputs: [],
|
|
2282
|
+
creditsUsed: 0,
|
|
2283
|
+
provider: this.providerName,
|
|
2284
|
+
model: modelId,
|
|
2285
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2286
|
+
error
|
|
2287
|
+
});
|
|
2288
|
+
const binding = GOOGLE_AUDIO_MODELS[modelId];
|
|
2289
|
+
if (!binding) {
|
|
2290
|
+
return failure(
|
|
2291
|
+
`"${modelId}" is not a Gemini speech model. Known: ${Object.keys(GOOGLE_AUDIO_MODELS).join(", ")}.`
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
const voice = params.voice ?? "Kore";
|
|
2295
|
+
if (!binding.voices.includes(voice)) {
|
|
2296
|
+
return failure(
|
|
2297
|
+
`Gemini TTS offers ${GOOGLE_TTS_VOICES.join(", ")}, not "${voice}".`
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
try {
|
|
2301
|
+
const payload = await this.request(
|
|
2302
|
+
INTERACTIONS_PATH,
|
|
2303
|
+
{
|
|
2304
|
+
method: "POST",
|
|
2305
|
+
body: this.speechBody(modelId, params, voice)
|
|
2306
|
+
}
|
|
2307
|
+
);
|
|
2308
|
+
const data = payload.output_audio?.data;
|
|
2309
|
+
if (!data) return failure("Google returned no audio.");
|
|
2310
|
+
return {
|
|
2311
|
+
success: true,
|
|
2312
|
+
outputs: [
|
|
2313
|
+
{
|
|
2314
|
+
url: `data:audio/wav;base64,${data}`,
|
|
2315
|
+
mimeType: "audio/wav",
|
|
2316
|
+
raw: { inline: true, voice }
|
|
2317
|
+
}
|
|
2318
|
+
],
|
|
2319
|
+
creditsUsed: 0,
|
|
2320
|
+
provider: this.providerName,
|
|
2321
|
+
model: modelId,
|
|
2322
|
+
processingTimeMs: Date.now() - startedAt
|
|
2323
|
+
};
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2326
|
+
}
|
|
2093
2327
|
}
|
|
2094
2328
|
async estimateCost(type, params) {
|
|
2095
2329
|
return estimateFor(this.providerName, type, params);
|
|
2096
2330
|
}
|
|
2097
2331
|
supportsModel(model) {
|
|
2098
|
-
return model in GOOGLE_IMAGE_MODELS || model in GOOGLE_VIDEO_MODELS;
|
|
2332
|
+
return model in GOOGLE_IMAGE_MODELS || model in GOOGLE_VIDEO_MODELS || model in GOOGLE_TEXT_MODELS || model in GOOGLE_AUDIO_MODELS;
|
|
2099
2333
|
}
|
|
2100
2334
|
getAvailableModels() {
|
|
2101
2335
|
return [
|
|
@@ -2108,6 +2342,16 @@ var GoogleAdapter = class {
|
|
|
2108
2342
|
id,
|
|
2109
2343
|
name: id,
|
|
2110
2344
|
type: "video"
|
|
2345
|
+
})),
|
|
2346
|
+
...Object.keys(GOOGLE_TEXT_MODELS).map((id) => ({
|
|
2347
|
+
id,
|
|
2348
|
+
name: id,
|
|
2349
|
+
type: "text"
|
|
2350
|
+
})),
|
|
2351
|
+
...Object.keys(GOOGLE_AUDIO_MODELS).map((id) => ({
|
|
2352
|
+
id,
|
|
2353
|
+
name: id,
|
|
2354
|
+
type: "audio"
|
|
2111
2355
|
}))
|
|
2112
2356
|
];
|
|
2113
2357
|
}
|
|
@@ -2464,7 +2708,7 @@ var IMAGES_PATH2 = "/v1/images/generations";
|
|
|
2464
2708
|
var OpenAIAdapter = class {
|
|
2465
2709
|
providerName = "openai";
|
|
2466
2710
|
// No video: the Videos API shuts down on 24 September 2026.
|
|
2467
|
-
supportedTypes = ["image"];
|
|
2711
|
+
supportedTypes = ["image", "text"];
|
|
2468
2712
|
opts;
|
|
2469
2713
|
constructor(opts) {
|
|
2470
2714
|
this.opts = opts;
|
|
@@ -2579,8 +2823,79 @@ var OpenAIAdapter = class {
|
|
|
2579
2823
|
generateAudio(_params) {
|
|
2580
2824
|
throw new Error("OpenAI has no speech synthesis wired up here.");
|
|
2581
2825
|
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2826
|
+
textBody(params) {
|
|
2827
|
+
const content = params.referenceImages?.length ? [
|
|
2828
|
+
{ type: "input_text", text: params.prompt },
|
|
2829
|
+
...params.referenceImages.map((url) => ({
|
|
2830
|
+
type: "input_image",
|
|
2831
|
+
image_url: url
|
|
2832
|
+
}))
|
|
2833
|
+
] : params.prompt;
|
|
2834
|
+
const input = [];
|
|
2835
|
+
if (params.systemPrompt) {
|
|
2836
|
+
input.push({ role: "system", content: params.systemPrompt });
|
|
2837
|
+
}
|
|
2838
|
+
input.push({ role: "user", content });
|
|
2839
|
+
return {
|
|
2840
|
+
model: params.model,
|
|
2841
|
+
input,
|
|
2842
|
+
...params.maxTokens ? { max_output_tokens: params.maxTokens } : {},
|
|
2843
|
+
...params.temperature !== void 0 ? { temperature: params.temperature } : {},
|
|
2844
|
+
...params.topP !== void 0 ? { top_p: params.topP } : {},
|
|
2845
|
+
...params.providerOptions?.openai ?? {}
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
async generateText(params) {
|
|
2849
|
+
const startedAt = Date.now();
|
|
2850
|
+
const modelId = params.model ?? "";
|
|
2851
|
+
const failure = (error) => ({
|
|
2852
|
+
success: false,
|
|
2853
|
+
outputs: [],
|
|
2854
|
+
creditsUsed: 0,
|
|
2855
|
+
provider: this.providerName,
|
|
2856
|
+
model: modelId,
|
|
2857
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2858
|
+
error
|
|
2859
|
+
});
|
|
2860
|
+
if (!(modelId in OPENAI_TEXT_MODELS)) {
|
|
2861
|
+
return failure(
|
|
2862
|
+
`"${modelId}" is not an OpenAI text model. Known: ${Object.keys(OPENAI_TEXT_MODELS).join(", ")}.`
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
try {
|
|
2866
|
+
const response = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
2867
|
+
method: "POST",
|
|
2868
|
+
headers: {
|
|
2869
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
2870
|
+
"Content-Type": "application/json",
|
|
2871
|
+
...this.opts.organization ? { "OpenAI-Organization": this.opts.organization } : {}
|
|
2872
|
+
},
|
|
2873
|
+
body: JSON.stringify(this.textBody(params))
|
|
2874
|
+
});
|
|
2875
|
+
const payload = await response.json();
|
|
2876
|
+
if (payload.error) throw new Error(payload.error.message);
|
|
2877
|
+
if (!response.ok) {
|
|
2878
|
+
throw new Error(`OpenAI returned ${response.status}.`);
|
|
2879
|
+
}
|
|
2880
|
+
const text = payload.output_text ?? payload.output?.flatMap((item) => item.content ?? []).find((part) => part.type === "output_text" || part.text)?.text;
|
|
2881
|
+
if (!text) return failure("OpenAI returned no text.");
|
|
2882
|
+
return {
|
|
2883
|
+
success: true,
|
|
2884
|
+
outputs: [
|
|
2885
|
+
{
|
|
2886
|
+
url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
|
|
2887
|
+
mimeType: "text/plain",
|
|
2888
|
+
raw: { text, tokens: payload.usage?.output_tokens }
|
|
2889
|
+
}
|
|
2890
|
+
],
|
|
2891
|
+
creditsUsed: 0,
|
|
2892
|
+
provider: this.providerName,
|
|
2893
|
+
model: modelId,
|
|
2894
|
+
processingTimeMs: Date.now() - startedAt
|
|
2895
|
+
};
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2898
|
+
}
|
|
2584
2899
|
}
|
|
2585
2900
|
/** Synchronous provider: there is never a queued job to come back to. */
|
|
2586
2901
|
async completeJob(job) {
|
|
@@ -2601,14 +2916,21 @@ var OpenAIAdapter = class {
|
|
|
2601
2916
|
};
|
|
2602
2917
|
}
|
|
2603
2918
|
supportsModel(model) {
|
|
2604
|
-
return model in OPENAI_IMAGE_MODELS;
|
|
2919
|
+
return model in OPENAI_IMAGE_MODELS || model in OPENAI_TEXT_MODELS;
|
|
2605
2920
|
}
|
|
2606
2921
|
getAvailableModels() {
|
|
2607
|
-
return
|
|
2608
|
-
id
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2922
|
+
return [
|
|
2923
|
+
...Object.keys(OPENAI_IMAGE_MODELS).map((id) => ({
|
|
2924
|
+
id,
|
|
2925
|
+
name: id,
|
|
2926
|
+
type: "image"
|
|
2927
|
+
})),
|
|
2928
|
+
...Object.keys(OPENAI_TEXT_MODELS).map((id) => ({
|
|
2929
|
+
id,
|
|
2930
|
+
name: id,
|
|
2931
|
+
type: "text"
|
|
2932
|
+
}))
|
|
2933
|
+
];
|
|
2612
2934
|
}
|
|
2613
2935
|
};
|
|
2614
2936
|
|
|
@@ -3214,6 +3536,45 @@ async function persistOutputs(storage, outputs) {
|
|
|
3214
3536
|
);
|
|
3215
3537
|
}
|
|
3216
3538
|
|
|
3539
|
+
// src/lib/webhook.ts
|
|
3540
|
+
function resolveWebhook(value) {
|
|
3541
|
+
if (!value) return void 0;
|
|
3542
|
+
if (typeof value === "string") {
|
|
3543
|
+
return isHttpUrl(value) ? { url: value } : void 0;
|
|
3544
|
+
}
|
|
3545
|
+
if (!isHttpUrl(value.url)) return void 0;
|
|
3546
|
+
return {
|
|
3547
|
+
url: value.url,
|
|
3548
|
+
secret: value.secret,
|
|
3549
|
+
headers: value.headers
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
function isHttpUrl(value) {
|
|
3553
|
+
try {
|
|
3554
|
+
const parsed = new URL(value);
|
|
3555
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
3556
|
+
} catch {
|
|
3557
|
+
return false;
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
async function deliverWebhook(config, payload) {
|
|
3561
|
+
try {
|
|
3562
|
+
await fetch(config.url, {
|
|
3563
|
+
method: "POST",
|
|
3564
|
+
headers: {
|
|
3565
|
+
"content-type": "application/json",
|
|
3566
|
+
"user-agent": "@brotu/ai",
|
|
3567
|
+
"x-brotu-event": payload.event,
|
|
3568
|
+
...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
|
|
3569
|
+
...config.headers
|
|
3570
|
+
},
|
|
3571
|
+
body: JSON.stringify(payload),
|
|
3572
|
+
signal: AbortSignal.timeout(1e4)
|
|
3573
|
+
});
|
|
3574
|
+
} catch {
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3217
3578
|
// src/client.ts
|
|
3218
3579
|
var NATIVE_PROVIDERS = [
|
|
3219
3580
|
"byteplus",
|
|
@@ -3228,6 +3589,8 @@ function brotuClient(options) {
|
|
|
3228
3589
|
throw new Error("brotuClient requires at least one provider API key.");
|
|
3229
3590
|
}
|
|
3230
3591
|
registerModels(options.models);
|
|
3592
|
+
let registeredWebhook = resolveWebhook(options.webhook);
|
|
3593
|
+
const notifiedJobIds = /* @__PURE__ */ new Set();
|
|
3231
3594
|
const storage = options.storage ? createS3Storage(options.storage) : void 0;
|
|
3232
3595
|
const shouldPersist = storage && options.storage?.persistOutputs !== false;
|
|
3233
3596
|
const adapters = /* @__PURE__ */ new Map();
|
|
@@ -3317,6 +3680,31 @@ function brotuClient(options) {
|
|
|
3317
3680
|
return adapter.generateAudio(params);
|
|
3318
3681
|
return adapter.generateText(params);
|
|
3319
3682
|
}
|
|
3683
|
+
function webhookFor(params) {
|
|
3684
|
+
return resolveWebhook(params?.webhook) ?? registeredWebhook;
|
|
3685
|
+
}
|
|
3686
|
+
async function notifySettled(input) {
|
|
3687
|
+
const config = webhookFor(input.params);
|
|
3688
|
+
if (!config) return;
|
|
3689
|
+
const jobId = input.job?.id;
|
|
3690
|
+
if (jobId) {
|
|
3691
|
+
if (notifiedJobIds.has(jobId)) return;
|
|
3692
|
+
notifiedJobIds.add(jobId);
|
|
3693
|
+
}
|
|
3694
|
+
const payload = {
|
|
3695
|
+
event: input.event,
|
|
3696
|
+
jobId,
|
|
3697
|
+
provider: input.provider ?? input.job?.provider,
|
|
3698
|
+
model: input.model ?? input.job?.model,
|
|
3699
|
+
kind: input.kind ?? input.job?.kind,
|
|
3700
|
+
outputs: input.outputs,
|
|
3701
|
+
error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
|
|
3702
|
+
metadata: input.metadata ?? input.job?.metadata,
|
|
3703
|
+
processingTimeMs: input.processingTimeMs,
|
|
3704
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3705
|
+
};
|
|
3706
|
+
await deliverWebhook(config, payload);
|
|
3707
|
+
}
|
|
3320
3708
|
async function toGeneration(raw, metadata) {
|
|
3321
3709
|
if (!raw.success) {
|
|
3322
3710
|
return fail({
|
|
@@ -3339,15 +3727,50 @@ function brotuClient(options) {
|
|
|
3339
3727
|
const routed = route(params.model);
|
|
3340
3728
|
if (routed.error) return fail(routed.error);
|
|
3341
3729
|
try {
|
|
3342
|
-
|
|
3730
|
+
const result = await toGeneration(
|
|
3343
3731
|
await generateWith(routed.data.adapter, kind, params),
|
|
3344
3732
|
params.metadata
|
|
3345
3733
|
);
|
|
3734
|
+
if (result.error) {
|
|
3735
|
+
await notifySettled({
|
|
3736
|
+
event: "generation.failed",
|
|
3737
|
+
params,
|
|
3738
|
+
kind,
|
|
3739
|
+
provider: routed.data.provider,
|
|
3740
|
+
model: routed.data.model,
|
|
3741
|
+
error: result.error,
|
|
3742
|
+
metadata: params.metadata
|
|
3743
|
+
});
|
|
3744
|
+
return result;
|
|
3745
|
+
}
|
|
3746
|
+
await notifySettled({
|
|
3747
|
+
event: "generation.succeeded",
|
|
3748
|
+
params,
|
|
3749
|
+
kind,
|
|
3750
|
+
provider: result.data.provider,
|
|
3751
|
+
model: result.data.model,
|
|
3752
|
+
outputs: result.data.outputs,
|
|
3753
|
+
metadata: result.data.metadata,
|
|
3754
|
+
processingTimeMs: result.data.processingTimeMs
|
|
3755
|
+
});
|
|
3756
|
+
return result;
|
|
3346
3757
|
} catch (error) {
|
|
3347
|
-
|
|
3758
|
+
const failed = failFrom("provider_error", error, {
|
|
3348
3759
|
provider: routed.data.provider,
|
|
3349
3760
|
model: routed.data.model
|
|
3350
3761
|
});
|
|
3762
|
+
if (failed.error) {
|
|
3763
|
+
await notifySettled({
|
|
3764
|
+
event: "generation.failed",
|
|
3765
|
+
params,
|
|
3766
|
+
kind,
|
|
3767
|
+
provider: routed.data.provider,
|
|
3768
|
+
model: routed.data.model,
|
|
3769
|
+
error: failed.error,
|
|
3770
|
+
metadata: params.metadata
|
|
3771
|
+
});
|
|
3772
|
+
}
|
|
3773
|
+
return failed;
|
|
3351
3774
|
}
|
|
3352
3775
|
}
|
|
3353
3776
|
async function submit(kind, params) {
|
|
@@ -3366,18 +3789,40 @@ function brotuClient(options) {
|
|
|
3366
3789
|
() => generateWith(routed.data.adapter, kind, params)
|
|
3367
3790
|
);
|
|
3368
3791
|
if (!raw.success) {
|
|
3369
|
-
|
|
3792
|
+
const error = {
|
|
3370
3793
|
code: "provider_error",
|
|
3371
3794
|
message: raw.error ?? "The provider rejected the request.",
|
|
3372
3795
|
provider: raw.provider,
|
|
3373
3796
|
model: raw.model
|
|
3797
|
+
};
|
|
3798
|
+
await notifySettled({
|
|
3799
|
+
event: "generation.failed",
|
|
3800
|
+
params,
|
|
3801
|
+
kind,
|
|
3802
|
+
provider: raw.provider,
|
|
3803
|
+
model: raw.model,
|
|
3804
|
+
error,
|
|
3805
|
+
metadata: params.metadata
|
|
3374
3806
|
});
|
|
3807
|
+
return fail(error);
|
|
3375
3808
|
}
|
|
3376
|
-
|
|
3809
|
+
const job = {
|
|
3377
3810
|
...base,
|
|
3378
3811
|
id: `inline-${base.model}-${base.submittedAt}`,
|
|
3379
3812
|
result: raw
|
|
3813
|
+
};
|
|
3814
|
+
await notifySettled({
|
|
3815
|
+
event: "generation.succeeded",
|
|
3816
|
+
params,
|
|
3817
|
+
job,
|
|
3818
|
+
kind,
|
|
3819
|
+
provider: raw.provider,
|
|
3820
|
+
model: raw.model,
|
|
3821
|
+
outputs: raw.outputs,
|
|
3822
|
+
metadata: params.metadata,
|
|
3823
|
+
processingTimeMs: raw.processingTimeMs
|
|
3380
3824
|
});
|
|
3825
|
+
return ok(job);
|
|
3381
3826
|
} catch (error) {
|
|
3382
3827
|
if (!isPendingJob(error)) {
|
|
3383
3828
|
return failFrom("provider_error", error, {
|
|
@@ -3392,11 +3837,54 @@ function brotuClient(options) {
|
|
|
3392
3837
|
});
|
|
3393
3838
|
}
|
|
3394
3839
|
}
|
|
3840
|
+
async function notifyFromSnapshot(job, snapshot) {
|
|
3841
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
3842
|
+
await notifySettled({
|
|
3843
|
+
event: "generation.succeeded",
|
|
3844
|
+
params: job.params,
|
|
3845
|
+
job,
|
|
3846
|
+
outputs: snapshot.result.outputs,
|
|
3847
|
+
provider: snapshot.result.provider,
|
|
3848
|
+
model: snapshot.result.model,
|
|
3849
|
+
processingTimeMs: snapshot.result.processingTimeMs,
|
|
3850
|
+
metadata: job.metadata
|
|
3851
|
+
});
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
if (snapshot.status === "failed") {
|
|
3855
|
+
await notifySettled({
|
|
3856
|
+
event: "generation.failed",
|
|
3857
|
+
params: job.params,
|
|
3858
|
+
job,
|
|
3859
|
+
error: {
|
|
3860
|
+
code: "provider_error",
|
|
3861
|
+
message: snapshot.error ?? "The job failed.",
|
|
3862
|
+
provider: job.provider,
|
|
3863
|
+
model: job.model
|
|
3864
|
+
},
|
|
3865
|
+
metadata: job.metadata
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
async function finalizeSnapshot(job, snapshot) {
|
|
3870
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
3871
|
+
const persisted = await toGeneration(snapshot.result, job.metadata);
|
|
3872
|
+
if (!persisted.error) {
|
|
3873
|
+
snapshot = {
|
|
3874
|
+
status: "succeeded",
|
|
3875
|
+
result: {
|
|
3876
|
+
...snapshot.result,
|
|
3877
|
+
outputs: persisted.data.outputs
|
|
3878
|
+
}
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
await notifyFromSnapshot(job, snapshot);
|
|
3883
|
+
return snapshot;
|
|
3884
|
+
}
|
|
3395
3885
|
async function pollJob(job) {
|
|
3396
3886
|
if (job.result) {
|
|
3397
|
-
return ok(
|
|
3398
|
-
job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }
|
|
3399
|
-
);
|
|
3887
|
+
return ok(await finalizeSnapshot(job, job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }));
|
|
3400
3888
|
}
|
|
3401
3889
|
const routed = route(job.model);
|
|
3402
3890
|
if (routed.error) return fail(routed.error);
|
|
@@ -3409,7 +3897,7 @@ function brotuClient(options) {
|
|
|
3409
3897
|
});
|
|
3410
3898
|
}
|
|
3411
3899
|
try {
|
|
3412
|
-
return ok(await adapter.completeJob(job));
|
|
3900
|
+
return ok(await finalizeSnapshot(job, await adapter.completeJob(job)));
|
|
3413
3901
|
} catch (error) {
|
|
3414
3902
|
return failFrom("provider_error", error, {
|
|
3415
3903
|
provider: job.provider,
|
|
@@ -3478,6 +3966,17 @@ function brotuClient(options) {
|
|
|
3478
3966
|
submit: (params) => submit("audio", params),
|
|
3479
3967
|
generate: (params) => generate("audio", params)
|
|
3480
3968
|
},
|
|
3969
|
+
webhook: {
|
|
3970
|
+
set(value) {
|
|
3971
|
+
registeredWebhook = resolveWebhook(value);
|
|
3972
|
+
},
|
|
3973
|
+
clear() {
|
|
3974
|
+
registeredWebhook = void 0;
|
|
3975
|
+
},
|
|
3976
|
+
get() {
|
|
3977
|
+
return registeredWebhook;
|
|
3978
|
+
}
|
|
3979
|
+
},
|
|
3481
3980
|
jobs: {
|
|
3482
3981
|
poll: pollJob,
|
|
3483
3982
|
wait: async (job, waitOptions) => {
|
|
@@ -3487,7 +3986,13 @@ function brotuClient(options) {
|
|
|
3487
3986
|
if (polled.error) return fail(polled.error);
|
|
3488
3987
|
const snapshot = polled.data;
|
|
3489
3988
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
3490
|
-
return
|
|
3989
|
+
return ok({
|
|
3990
|
+
outputs: snapshot.result.outputs,
|
|
3991
|
+
provider: snapshot.result.provider,
|
|
3992
|
+
model: snapshot.result.model,
|
|
3993
|
+
processingTimeMs: snapshot.result.processingTimeMs,
|
|
3994
|
+
metadata: job.metadata
|
|
3995
|
+
});
|
|
3491
3996
|
}
|
|
3492
3997
|
if (snapshot.status === "failed") {
|
|
3493
3998
|
return fail({
|
|
@@ -3533,8 +4038,11 @@ function brotuClient(options) {
|
|
|
3533
4038
|
ELEVENLABS_MODELS,
|
|
3534
4039
|
ELEVENLABS_OUTPUT_FORMATS,
|
|
3535
4040
|
ElevenLabsAdapter,
|
|
4041
|
+
GOOGLE_AUDIO_MODELS,
|
|
3536
4042
|
GOOGLE_CATALOG,
|
|
3537
4043
|
GOOGLE_IMAGE_MODELS,
|
|
4044
|
+
GOOGLE_TEXT_MODELS,
|
|
4045
|
+
GOOGLE_TTS_VOICES,
|
|
3538
4046
|
GOOGLE_VIDEO_MODELS,
|
|
3539
4047
|
GoogleAdapter,
|
|
3540
4048
|
KLING_CAPABILITIES,
|
|
@@ -3543,6 +4051,7 @@ function brotuClient(options) {
|
|
|
3543
4051
|
KlingAdapter,
|
|
3544
4052
|
OPENAI_CATALOG,
|
|
3545
4053
|
OPENAI_IMAGE_MODELS,
|
|
4054
|
+
OPENAI_TEXT_MODELS,
|
|
3546
4055
|
OpenAIAdapter,
|
|
3547
4056
|
PendingJob,
|
|
3548
4057
|
QWEN_AUDIO_MODELS,
|
|
@@ -3553,6 +4062,7 @@ function brotuClient(options) {
|
|
|
3553
4062
|
QwenAdapter,
|
|
3554
4063
|
brotuClient,
|
|
3555
4064
|
createS3Storage,
|
|
4065
|
+
deliverWebhook,
|
|
3556
4066
|
fail,
|
|
3557
4067
|
getAvailableModels,
|
|
3558
4068
|
getModel,
|
|
@@ -3564,5 +4074,6 @@ function brotuClient(options) {
|
|
|
3564
4074
|
persistOutputs,
|
|
3565
4075
|
registerModels,
|
|
3566
4076
|
resetCatalog,
|
|
3567
|
-
resolveProvider
|
|
4077
|
+
resolveProvider,
|
|
4078
|
+
resolveWebhook
|
|
3568
4079
|
});
|