@brotu/ai 0.4.0 → 0.6.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.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BROTU_SUPPORTED_CATEGORIES: () => BROTU_SUPPORTED_CATEGORIES,
33
34
  BYTEPLUS_CATALOG: () => BYTEPLUS_CATALOG,
34
35
  BYTEPLUS_MODELS: () => BYTEPLUS_MODELS,
35
36
  BrotuAdapter: () => BrotuAdapter,
@@ -61,9 +62,15 @@ __export(index_exports, {
61
62
  QWEN_TEXT_MODELS: () => QWEN_TEXT_MODELS,
62
63
  QWEN_VIDEO_MODELS: () => QWEN_VIDEO_MODELS,
63
64
  QwenAdapter: () => QwenAdapter,
64
- brotuClient: () => brotuClient,
65
+ TOPAZ_CATALOG: () => TOPAZ_CATALOG,
66
+ TOPAZ_IMAGE_MODELS: () => TOPAZ_IMAGE_MODELS,
67
+ TOPAZ_MODELS: () => TOPAZ_MODELS,
68
+ TOPAZ_VIDEO_MODELS: () => TOPAZ_VIDEO_MODELS,
69
+ TopazAdapter: () => TopazAdapter,
70
+ brotu: () => brotu,
65
71
  createS3Storage: () => createS3Storage,
66
72
  deliverWebhook: () => deliverWebhook,
73
+ describeModels: () => describeModels,
67
74
  fail: () => fail,
68
75
  getAvailableModels: () => getAvailableModels,
69
76
  getModel: () => getModel,
@@ -80,34 +87,6 @@ __export(index_exports, {
80
87
  });
81
88
  module.exports = __toCommonJS(index_exports);
82
89
 
83
- // src/lib/jobs.ts
84
- var import_node_async_hooks = require("async_hooks");
85
- var PendingJob = class extends Error {
86
- taskId;
87
- pollEndpoint;
88
- constructor(taskId, pollEndpoint) {
89
- super(`Job ${taskId} was submitted and is still running.`);
90
- this.name = "PendingJob";
91
- this.taskId = taskId;
92
- this.pollEndpoint = pollEndpoint;
93
- }
94
- };
95
- function isPendingJob(error) {
96
- return error instanceof PendingJob;
97
- }
98
- var submitMode = new import_node_async_hooks.AsyncLocalStorage();
99
- function runInSubmitMode(fn) {
100
- return submitMode.run(true, fn);
101
- }
102
- function isSubmitMode() {
103
- return submitMode.getStore() === true;
104
- }
105
-
106
- // src/ports/content-generator.port.ts
107
- function expiresInHours(hours) {
108
- return new Date(Date.now() + hours * 36e5).toISOString();
109
- }
110
-
111
90
  // src/providers/byteplus.models.ts
112
91
  var FPS = 24;
113
92
  var PIXELS = {
@@ -860,7 +839,7 @@ var imageOmni = {
860
839
  prompt: input.prompt
861
840
  };
862
841
  if (images.length > 0) {
863
- body.image_list = images.map((image) => ({ image }));
842
+ body.image_list = images.map((image2) => ({ image: image2 }));
864
843
  }
865
844
  if (elements.length > 0) {
866
845
  body.element_list = elements.map((id) => ({ element_id: id }));
@@ -1262,6 +1241,105 @@ var QWEN_CATALOG = [
1262
1241
  )
1263
1242
  ];
1264
1243
 
1244
+ // src/providers/topaz.models.ts
1245
+ var VIDEO_RES = ["720p", "1080p", "4k"];
1246
+ function video(id, name, kind, description) {
1247
+ return [id, { name, vendorModel: id, surface: "video", kind, description }];
1248
+ }
1249
+ function image(id, name, vendorModel, imagePath, description) {
1250
+ return [
1251
+ id,
1252
+ { name, vendorModel, surface: "image", imagePath, description }
1253
+ ];
1254
+ }
1255
+ var ENTRIES = [
1256
+ // Precision video upscale (Proteus family)
1257
+ video("prob-4", "Proteus", "upscale", "Default video upscale. Preserves the source."),
1258
+ video("pnat-1", "Proteus Natural", "upscale", "Softer Proteus. 2\xD7 only."),
1259
+ video("rhea-1", "Rhea", "upscale", "High-fidelity 4\xD7 upscale for fine detail."),
1260
+ video("thd-3", "Theia Fine Tune Detail", "upscale", "More aggressive detail."),
1261
+ video("thf-4", "Theia Fine Tune Fidelity", "upscale", "Lighter touch, closer to the source."),
1262
+ video("thm-2", "Themis 2", "upscale", "Motion deblur."),
1263
+ video("aaa-9", "Artemis Aliasing / Moire", "upscale", "Aliasing and moire cleanup."),
1264
+ video("ahq-12", "Artemis High Quality", "upscale", "Artemis for clean footage."),
1265
+ video("amq-13", "Artemis Medium Quality", "upscale", "Artemis for typical compression."),
1266
+ video("amqs-2", "Artemis Medium Halo", "upscale", "Medium halo reduction."),
1267
+ video("alq-13", "Artemis Low Quality", "upscale", "Artemis for degraded sources."),
1268
+ video("alqs-2", "Artemis Strong Halo", "upscale", "Strong halo reduction."),
1269
+ video("iris-3", "Iris Medium Quality", "upscale", "Face recovery, medium quality."),
1270
+ video("iris-2", "Iris Low Quality", "upscale", "Face recovery, low quality."),
1271
+ video("dtv-4", "Dione TV", "upscale", "Deinterlace broadcast footage."),
1272
+ video("ddv-3", "Dione DV", "upscale", "Deinterlace camcorder footage."),
1273
+ video("dtvs-2", "Dione Robust", "upscale", "Deinterlace degraded interlaced sources."),
1274
+ video("dtd-4", "Dione Dehalo", "upscale", "Deinterlace with halo cleanup."),
1275
+ video("dtds-2", "Dione Robust Dehalo", "upscale", "Deinterlace, degraded, with halo cleanup."),
1276
+ video("ghq-5", "Gaia High Quality", "upscale", "CGI and animation, high quality."),
1277
+ video("gcg-5", "Gaia CG", "upscale", "General CGI and renders."),
1278
+ video("nyx-3", "Nyx", "upscale", "Default video denoise."),
1279
+ video("nxl-1", "Nyx XL", "upscale", "Heavy video denoise."),
1280
+ video("nxhf-1", "Nyx High Fidelity", "upscale", "Light denoise, keep texture."),
1281
+ video("nxf-1", "Nyx Fast", "upscale", "Fast video denoise."),
1282
+ video("color-1", "Video Colorization", "upscale", "Black-and-white to color."),
1283
+ // Generative / creative video
1284
+ video("slp-2.6", "Starlight Precise 2.6", "upscale", "Generative upscale to 4K. Faces, texture, text."),
1285
+ video("slhq-1", "Starlight HQ", "upscale", "Maximum quality generative enhance."),
1286
+ video("slf-2", "Starlight Fast 2", "upscale", "Faster generative upscale."),
1287
+ video("slm-1", "Starlight Mini", "upscale", "Archival and interlaced restoration."),
1288
+ video("wonder-1", "Starlight Sharp", "upscale", "Local-first sharpening restore."),
1289
+ video("ast-2", "Astra 2", "upscale", "Creative GenAI video upscale."),
1290
+ // Frame interpolation — not upscalers
1291
+ video("aion-1", "Aion", "interpolation", "Extreme slow motion on complex, high-res motion."),
1292
+ video("apo-8", "Apollo", "interpolation", "Default slow-mo and frame-rate conversion."),
1293
+ video("apf-2", "Apollo Fast", "interpolation", "Faster Apollo."),
1294
+ video("chr-2", "Chronos", "interpolation", "Interpolation for harder motion."),
1295
+ video("chf-3", "Chronos Fast", "interpolation", "Faster Chronos."),
1296
+ // Image — Gigapixel / enhance
1297
+ image("standard-v2", "Standard 2", "Standard V2", "/image/v1/enhance/async", "Default image upscale."),
1298
+ image("high-fidelity-v2", "High Fidelity 2", "High Fidelity V2", "/image/v1/enhance/async", "Maximum source preservation."),
1299
+ image("low-resolution-v2", "Low Resolution 2", "Low Resolution V2", "/image/v1/enhance/async", "Tiny or heavily compressed sources."),
1300
+ image("cgi", "Art & CGI", "CGI", "/image/v1/enhance/async", "Illustrations, renders, game art."),
1301
+ image("text-refine", "Text & Shapes", "Text Refine", "/image/v1/enhance/async", "Documents, screenshots, type."),
1302
+ // Image — generative enhance
1303
+ image("standard-max", "Standard Max", "Standard MAX", "/image/v1/enhance-gen/async", "Default generative image upscale."),
1304
+ image("wonder", "Wonder", "Wonder", "/image/v1/enhance-gen/async", "One-click generative restore."),
1305
+ image("wonder-3.5", "Wonder 3.5", "Wonder 3.5", "/image/v1/enhance-gen/async", "Highest-quality generative image upscale."),
1306
+ image("redefine", "Redefine", "Redefine", "/image/v1/enhance-gen/async", "Prompt-guided generative enhance."),
1307
+ image("recovery-v2", "Recover 3", "Recovery V2", "/image/v1/enhance-gen/async", "Heavily damaged sources."),
1308
+ image("bloom-2", "Bloom 2", "Bloom 2", "/image/v1/enhance-gen/async", "Creative upscale for GenAI images."),
1309
+ image("denoise-max", "Denoise Max", "Denoise Max", "/image/v1/denoise-gen/async", "Highest-quality image denoise.")
1310
+ ];
1311
+ var TOPAZ_MODELS = Object.fromEntries(
1312
+ ENTRIES.flatMap(([id, binding]) => {
1313
+ const rows = [[id, binding]];
1314
+ if (binding.surface === "image" && binding.vendorModel !== id) {
1315
+ rows.push([binding.vendorModel, binding]);
1316
+ }
1317
+ return rows;
1318
+ })
1319
+ );
1320
+ function nodeTypes(binding) {
1321
+ if (binding.surface === "image") return ["image_upscale"];
1322
+ return binding.kind === "interpolation" ? ["video_gen"] : ["video_upscale"];
1323
+ }
1324
+ var TOPAZ_CATALOG = ENTRIES.map(([id, binding]) => ({
1325
+ id,
1326
+ name: binding.name,
1327
+ category: binding.surface,
1328
+ inputType: binding.surface === "video" ? "video_required" : "image_required",
1329
+ nodeTypes: nodeTypes(binding),
1330
+ creditsPerUnit: 0,
1331
+ creditUnit: binding.surface === "video" ? "second" : "image",
1332
+ supportedResolutions: binding.surface === "video" ? [...VIDEO_RES] : void 0,
1333
+ provider: "topaz",
1334
+ description: binding.description
1335
+ }));
1336
+ var TOPAZ_VIDEO_MODELS = Object.fromEntries(
1337
+ ENTRIES.filter(([, binding]) => binding.surface === "video")
1338
+ );
1339
+ var TOPAZ_IMAGE_MODELS = Object.fromEntries(
1340
+ ENTRIES.filter(([, binding]) => binding.surface === "image")
1341
+ );
1342
+
1265
1343
  // src/catalog.ts
1266
1344
  var PROVIDER_BASE_URLS = {
1267
1345
  byteplus: "https://ark.ap-southeast.bytepluses.com",
@@ -1270,6 +1348,7 @@ var PROVIDER_BASE_URLS = {
1270
1348
  google: "https://generativelanguage.googleapis.com",
1271
1349
  openai: "https://api.openai.com",
1272
1350
  qwen: "https://dashscope-intl.aliyuncs.com",
1351
+ topaz: "https://api.topazlabs.com",
1273
1352
  brotu: "https://api.brotu.app"
1274
1353
  };
1275
1354
  var DEFAULT_BROTU_API_URL = PROVIDER_BASE_URLS.brotu;
@@ -1287,7 +1366,8 @@ var BUILT_IN = [
1287
1366
  ...QWEN_CATALOG,
1288
1367
  ...OPENAI_CATALOG,
1289
1368
  ...GOOGLE_CATALOG,
1290
- ...ELEVENLABS_CATALOG
1369
+ ...ELEVENLABS_CATALOG,
1370
+ ...TOPAZ_CATALOG
1291
1371
  ];
1292
1372
  var catalog = BUILT_IN;
1293
1373
  var catalogMap = byId(BUILT_IN);
@@ -1357,21 +1437,79 @@ function resolveProvider(modelId, options) {
1357
1437
  `Model "${modelId}" runs on "${id}", but no API key was given for it (configured: ${owned}). Pass a Brotu key to generate on the platform.`
1358
1438
  );
1359
1439
  }
1440
+ var BROTU_SUPPORTED_CATEGORIES = ["image", "video"];
1441
+ function describeModels(options) {
1442
+ const vendors = vendorProviders(options);
1443
+ const platformKey = brotuApiKey(options);
1444
+ return catalog.map((model) => {
1445
+ const provider = model.provider;
1446
+ if (!provider) {
1447
+ return {
1448
+ model,
1449
+ runsOn: "(none)",
1450
+ runnable: false,
1451
+ reason: "The model declares no provider."
1452
+ };
1453
+ }
1454
+ if (provider in vendors) {
1455
+ return { model, runsOn: provider, runnable: true };
1456
+ }
1457
+ if (platformKey) {
1458
+ if (BROTU_SUPPORTED_CATEGORIES.includes(model.category)) {
1459
+ return { model, runsOn: "brotu", runnable: true };
1460
+ }
1461
+ return {
1462
+ model,
1463
+ runsOn: provider,
1464
+ runnable: false,
1465
+ reason: `Brotu generates ${BROTU_SUPPORTED_CATEGORIES.join(" and ")} only. Pass providers.${provider} to run this one.`
1466
+ };
1467
+ }
1468
+ return {
1469
+ model,
1470
+ runsOn: provider,
1471
+ runnable: false,
1472
+ reason: `No key for "${provider}". Pass a Brotu key or providers.${provider}.`
1473
+ };
1474
+ });
1475
+ }
1360
1476
  function getAvailableModels(options) {
1361
- if (brotuApiKey(options)) {
1362
- return catalog.filter((model) => Boolean(model.provider));
1477
+ return describeModels(options).filter((entry) => entry.runnable).map((entry) => entry.model);
1478
+ }
1479
+
1480
+ // src/lib/jobs.ts
1481
+ var import_node_async_hooks = require("async_hooks");
1482
+ var PendingJob = class extends Error {
1483
+ taskId;
1484
+ pollEndpoint;
1485
+ constructor(taskId, pollEndpoint) {
1486
+ super(`Job ${taskId} was submitted and is still running.`);
1487
+ this.name = "PendingJob";
1488
+ this.taskId = taskId;
1489
+ this.pollEndpoint = pollEndpoint;
1363
1490
  }
1364
- const vendors = vendorProviders(options);
1365
- return catalog.filter(
1366
- (model) => model.provider && model.provider in vendors
1367
- );
1491
+ };
1492
+ function isPendingJob(error) {
1493
+ return error instanceof PendingJob;
1494
+ }
1495
+ var submitMode = new import_node_async_hooks.AsyncLocalStorage();
1496
+ function runInSubmitMode(fn) {
1497
+ return submitMode.run(true, fn);
1498
+ }
1499
+ function isSubmitMode() {
1500
+ return submitMode.getStore() === true;
1501
+ }
1502
+
1503
+ // src/ports/content-generator.port.ts
1504
+ function expiresInHours(hours) {
1505
+ return new Date(Date.now() + hours * 36e5).toISOString();
1368
1506
  }
1369
1507
 
1370
1508
  // src/adapters/estimate.ts
1371
1509
  function estimateFor(provider, type, params, defaults) {
1372
1510
  const modelId = params.model ?? "";
1373
1511
  const model = getModel(modelId);
1374
- const video = params;
1512
+ const video2 = params;
1375
1513
  const unit = model?.pricing?.unit ?? (type === "video" ? "second" : type === "audio" ? "character" : type === "text" ? "token" : "image");
1376
1514
  let units = 1;
1377
1515
  if (unit === "character") {
@@ -1379,7 +1517,7 @@ function estimateFor(provider, type, params, defaults) {
1379
1517
  } else if (unit === "token") {
1380
1518
  units = 0;
1381
1519
  } else if (unit === "second") {
1382
- units = video.duration ?? defaults?.durationSeconds ?? model?.minDuration ?? 5;
1520
+ units = video2.duration ?? defaults?.durationSeconds ?? model?.minDuration ?? 5;
1383
1521
  }
1384
1522
  const resolution = params.resolution;
1385
1523
  const rate = (resolution ? model?.pricing?.byResolution?.[resolution] : void 0) ?? model?.pricing?.usdPerUnit;
@@ -1412,7 +1550,7 @@ var BrotuAdapter = class {
1412
1550
  }
1413
1551
  opts;
1414
1552
  providerName = "brotu";
1415
- supportedTypes = ["image", "video"];
1553
+ supportedTypes = BROTU_SUPPORTED_CATEGORIES;
1416
1554
  workspaceIdCache;
1417
1555
  get origin() {
1418
1556
  return (this.opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
@@ -1635,23 +1773,23 @@ var BrotuAdapter = class {
1635
1773
  }
1636
1774
  try {
1637
1775
  const workspaceId = await this.workspaceId();
1638
- const video = params;
1639
- const image = params;
1776
+ const video2 = params;
1777
+ const image2 = params;
1640
1778
  const payload = await this.request(
1641
1779
  this.studioPath("/api/v1/studio/estimate", workspaceId),
1642
1780
  {
1643
1781
  method: "POST",
1644
1782
  body: JSON.stringify({
1645
1783
  model: this.platformModel(params.model ?? ""),
1646
- duration: video.duration,
1647
- resolution: video.resolution ?? image.resolution,
1648
- aspectRatio: video.aspectRatio ?? image.aspectRatio,
1649
- withAudio: video.withAudio,
1784
+ duration: video2.duration,
1785
+ resolution: video2.resolution ?? image2.resolution,
1786
+ aspectRatio: video2.aspectRatio ?? image2.aspectRatio,
1787
+ withAudio: video2.withAudio,
1650
1788
  hasReferenceImage: Boolean(
1651
- video.imageUrl || video.imageUrls?.length || video.referenceImages?.length || image.referenceImages?.length
1789
+ video2.imageUrl || video2.imageUrls?.length || video2.referenceImages?.length || image2.referenceImages?.length
1652
1790
  ),
1653
- hasReferenceVideo: Boolean(video.videoUrl || video.videoUrls?.length),
1654
- quality: image.quality
1791
+ hasReferenceVideo: Boolean(video2.videoUrl || video2.videoUrls?.length),
1792
+ quality: image2.quality
1655
1793
  })
1656
1794
  }
1657
1795
  );
@@ -2254,12 +2392,12 @@ var GoogleAdapter = class {
2254
2392
  const input = [
2255
2393
  { type: "text", text: params.prompt }
2256
2394
  ];
2257
- for (const image of params.referenceImages ?? []) {
2258
- const [meta, data] = image.split(",");
2395
+ for (const image2 of params.referenceImages ?? []) {
2396
+ const [meta, data] = image2.split(",");
2259
2397
  input.push({
2260
2398
  type: "image",
2261
2399
  mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
2262
- data: data ?? image
2400
+ data: data ?? image2
2263
2401
  });
2264
2402
  }
2265
2403
  const payload = await this.request(
@@ -2351,10 +2489,10 @@ var GoogleAdapter = class {
2351
2489
  };
2352
2490
  const first = params.imageUrl ?? params.referenceImages?.[0];
2353
2491
  if (first) attach("image", first);
2354
- for (const image of params.imageUrls ?? []) attach("image", image);
2355
- const video = params.videoUrl ?? params.videoUrls?.[0];
2356
- if (video) attach("video", video);
2357
- const task = video ? "edit" : params.imageUrls?.length ? "reference_to_video" : first ? "image_to_video" : "text_to_video";
2492
+ for (const image2 of params.imageUrls ?? []) attach("image", image2);
2493
+ const video2 = params.videoUrl ?? params.videoUrls?.[0];
2494
+ if (video2) attach("video", video2);
2495
+ const task = video2 ? "edit" : params.imageUrls?.length ? "reference_to_video" : first ? "image_to_video" : "text_to_video";
2358
2496
  return {
2359
2497
  model: modelId,
2360
2498
  input,
@@ -2523,12 +2661,12 @@ var GoogleAdapter = class {
2523
2661
  const input = [
2524
2662
  { type: "text", text: params.prompt }
2525
2663
  ];
2526
- for (const image of params.referenceImages ?? []) {
2527
- const [meta, data] = image.split(",");
2664
+ for (const image2 of params.referenceImages ?? []) {
2665
+ const [meta, data] = image2.split(",");
2528
2666
  input.push({
2529
2667
  type: "image",
2530
2668
  mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
2531
- data: data ?? image
2669
+ data: data ?? image2
2532
2670
  });
2533
2671
  }
2534
2672
  return {
@@ -3338,11 +3476,11 @@ var QwenAdapter = class {
3338
3476
  case "media": {
3339
3477
  const media = [];
3340
3478
  if (binding.mode === "video-edit") {
3341
- const video = params.videoUrl ?? params.videoUrls?.[0];
3342
- if (!video) {
3479
+ const video2 = params.videoUrl ?? params.videoUrls?.[0];
3480
+ if (!video2) {
3343
3481
  throw new Error(`Qwen "${modelId}" needs a video \u2014 pass videoUrl.`);
3344
3482
  }
3345
- media.push({ type: "video", url: video });
3483
+ media.push({ type: "video", url: video2 });
3346
3484
  } else if (binding.mode === "r2v") {
3347
3485
  for (const url of references) {
3348
3486
  media.push({ type: "reference_image", url });
@@ -3382,7 +3520,7 @@ var QwenAdapter = class {
3382
3520
  ...output.video_url ? [output.video_url] : [],
3383
3521
  ...(output.results ?? []).map((result) => result.url).filter((url) => Boolean(url)),
3384
3522
  ...(output.choices ?? []).flatMap(
3385
- (choice) => (choice.message?.content ?? []).map((part) => part.image).filter((image) => Boolean(image))
3523
+ (choice) => (choice.message?.content ?? []).map((part) => part.image).filter((image2) => Boolean(image2))
3386
3524
  )
3387
3525
  ];
3388
3526
  return urls.map((url) => ({
@@ -3530,8 +3668,8 @@ var QwenAdapter = class {
3530
3668
  }
3531
3669
  try {
3532
3670
  const content = [];
3533
- for (const image of params.referenceImages ?? []) {
3534
- content.push({ image });
3671
+ for (const image2 of params.referenceImages ?? []) {
3672
+ content.push({ image: image2 });
3535
3673
  }
3536
3674
  content.push({ text: params.prompt });
3537
3675
  const parameters = {
@@ -3724,6 +3862,475 @@ var QwenAdapter = class {
3724
3862
  }
3725
3863
  };
3726
3864
 
3865
+ // src/adapters/topaz.adapter.ts
3866
+ var DEFAULT_BASE_URL7 = "https://api.topazlabs.com";
3867
+ var POLL_INTERVAL_MS6 = 5e3;
3868
+ var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
3869
+ var DEFAULT_FPS = 30;
3870
+ var PIXELS2 = {
3871
+ "720p": { width: 1280, height: 720 },
3872
+ "720P": { width: 1280, height: 720 },
3873
+ "1080p": { width: 1920, height: 1080 },
3874
+ "1080P": { width: 1920, height: 1080 },
3875
+ "4k": { width: 3840, height: 2160 },
3876
+ "4K": { width: 3840, height: 2160 },
3877
+ "1K": { width: 1024, height: 1024 },
3878
+ "2K": { width: 2048, height: 2048 }
3879
+ };
3880
+ var TopazAdapter = class {
3881
+ providerName = "topaz";
3882
+ supportedTypes = ["video", "image"];
3883
+ opts;
3884
+ constructor(opts) {
3885
+ this.opts = opts;
3886
+ }
3887
+ get baseUrl() {
3888
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
3889
+ }
3890
+ get headers() {
3891
+ return {
3892
+ "X-API-Key": this.opts.apiKey,
3893
+ accept: "application/json",
3894
+ "Content-Type": "application/json"
3895
+ };
3896
+ }
3897
+ binding(modelId) {
3898
+ const id = modelId ?? "";
3899
+ const found = TOPAZ_MODELS[id];
3900
+ if (!found) {
3901
+ throw new Error(
3902
+ `"${id}" is not a Topaz model. Known: ${TOPAZ_CATALOG.map((model) => model.id).join(", ")}.`
3903
+ );
3904
+ }
3905
+ return [id, found];
3906
+ }
3907
+ /** 720p / 1080p / 4k, or an explicit "1920x1080" / "1920*1080" pair. */
3908
+ resolutionOf(resolution) {
3909
+ if (!resolution) return PIXELS2["1080p"];
3910
+ const named = PIXELS2[resolution];
3911
+ if (named) return named;
3912
+ const match = /^(\d{2,5})[x*](\d{2,5})$/i.exec(resolution.trim());
3913
+ if (match?.[1] && match[2]) {
3914
+ return { width: Number(match[1]), height: Number(match[2]) };
3915
+ }
3916
+ throw new Error(
3917
+ `Topaz wants 720p, 1080p, 4k or WxH, not "${resolution}".`
3918
+ );
3919
+ }
3920
+ containerOf(url) {
3921
+ const path = url.split("?")[0]?.toLowerCase() ?? "";
3922
+ if (path.endsWith(".mov")) return "mov";
3923
+ if (path.endsWith(".mkv")) return "mkv";
3924
+ return "mp4";
3925
+ }
3926
+ filterOf(binding, params) {
3927
+ const extras = params.providerOptions?.topaz ?? {};
3928
+ if (binding.kind === "upscale") {
3929
+ return { auto: "Auto", ...extras, model: binding.vendorModel };
3930
+ }
3931
+ const slowmo = numberish(extras.slowmo) ?? 1;
3932
+ const fps = numberish(extras.fps) ?? DEFAULT_FPS;
3933
+ if (slowmo < 1 || slowmo > 16) {
3934
+ throw new Error(`Topaz interpolation slowmo is 1\u201316, not ${slowmo}.`);
3935
+ }
3936
+ if (fps < 15 || fps > 240) {
3937
+ throw new Error(`Topaz interpolation fps is 15\u2013240, not ${fps}.`);
3938
+ }
3939
+ const filter = {
3940
+ model: binding.vendorModel,
3941
+ slowmo,
3942
+ fps
3943
+ };
3944
+ if (typeof extras.duplicate === "boolean") {
3945
+ filter.duplicate = extras.duplicate;
3946
+ }
3947
+ if (extras.duplicateThreshold !== void 0) {
3948
+ filter.duplicateThreshold = extras.duplicateThreshold;
3949
+ }
3950
+ return filter;
3951
+ }
3952
+ requestBody(binding, params, sourceUrl) {
3953
+ const extras = params.providerOptions?.topaz ?? {};
3954
+ const fps = numberish(extras.fps) ?? DEFAULT_FPS;
3955
+ return {
3956
+ source: { container: this.containerOf(sourceUrl) },
3957
+ filters: [this.filterOf(binding, params)],
3958
+ output: {
3959
+ resolution: this.resolutionOf(params.resolution),
3960
+ frameRate: fps,
3961
+ audioCodec: "AAC",
3962
+ audioTransfer: "Copy",
3963
+ dynamicCompressionLevel: "High",
3964
+ container: "mp4"
3965
+ }
3966
+ };
3967
+ }
3968
+ async request(path, init) {
3969
+ const response = await fetch(`${this.baseUrl}${path}`, {
3970
+ method: init?.method ?? "GET",
3971
+ headers: this.headers,
3972
+ body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
3973
+ });
3974
+ const payload = await response.json();
3975
+ if (!response.ok) {
3976
+ throw new Error(
3977
+ payload.message ?? `Topaz returned ${response.status} for ${path}.`
3978
+ );
3979
+ }
3980
+ return payload;
3981
+ }
3982
+ async downloadSource(url) {
3983
+ const response = await fetch(url);
3984
+ if (!response.ok) {
3985
+ throw new Error(`Could not download the source video (${response.status}).`);
3986
+ }
3987
+ return {
3988
+ bytes: await response.arrayBuffer(),
3989
+ contentType: response.headers.get("content-type") ?? "video/mp4"
3990
+ };
3991
+ }
3992
+ async submitTask(params) {
3993
+ const [, binding] = this.binding(params.model);
3994
+ if (binding.surface !== "video") {
3995
+ throw new Error(
3996
+ `Topaz "${params.model}" is an image model. Use ai.image.`
3997
+ );
3998
+ }
3999
+ const sourceUrl = params.videoUrl ?? params.videoUrls?.[0];
4000
+ if (!sourceUrl) {
4001
+ throw new Error(
4002
+ `Topaz "${params.model}" needs a source video \u2014 pass videoUrl.`
4003
+ );
4004
+ }
4005
+ const source = await this.downloadSource(sourceUrl);
4006
+ const created = await this.request("/video/express", {
4007
+ method: "POST",
4008
+ body: this.requestBody(binding, params, sourceUrl)
4009
+ });
4010
+ const taskId = created.requestId?.trim();
4011
+ const uploadUrl = created.uploadUrls?.[0];
4012
+ if (!taskId || !uploadUrl) {
4013
+ throw new Error(
4014
+ "Topaz accepted the request but returned no request id or upload URL."
4015
+ );
4016
+ }
4017
+ const uploaded = await fetch(uploadUrl, {
4018
+ method: "PUT",
4019
+ headers: { "Content-Type": source.contentType },
4020
+ body: source.bytes
4021
+ });
4022
+ if (!uploaded.ok) {
4023
+ throw new Error(`Topaz upload failed (${uploaded.status}).`);
4024
+ }
4025
+ return { taskId, pollEndpoint: `/video/${taskId}/status` };
4026
+ }
4027
+ outputsFrom(payload, job) {
4028
+ const url = payload.download?.url;
4029
+ if (!url) return [];
4030
+ const expiresAtMs = payload.download?.expiresAt;
4031
+ return [
4032
+ {
4033
+ url,
4034
+ mimeType: "video/mp4",
4035
+ taskId: job.id,
4036
+ expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : expiresInHours(24),
4037
+ raw: {
4038
+ status: payload.status,
4039
+ progress: payload.progress,
4040
+ estimates: payload.estimates,
4041
+ outputSize: payload.outputSize
4042
+ }
4043
+ }
4044
+ ];
4045
+ }
4046
+ snapshot(payload, job) {
4047
+ const raw = (payload.status ?? "").toLowerCase();
4048
+ if (raw === "failed" || raw === "canceled") {
4049
+ return {
4050
+ status: "failed",
4051
+ error: payload.message ?? `Topaz request ${raw}.`
4052
+ };
4053
+ }
4054
+ if (raw !== "complete") return { status: "pending" };
4055
+ const outputs = this.outputsFrom(payload, job);
4056
+ if (outputs.length === 0) return { status: "pending" };
4057
+ return {
4058
+ status: "succeeded",
4059
+ result: {
4060
+ success: true,
4061
+ outputs,
4062
+ creditsUsed: 0,
4063
+ provider: this.providerName,
4064
+ model: job.model,
4065
+ processingTimeMs: 0
4066
+ }
4067
+ };
4068
+ }
4069
+ async run(params) {
4070
+ const startedAt = Date.now();
4071
+ let modelId = params.model ?? "(none)";
4072
+ const failure = (error) => ({
4073
+ success: false,
4074
+ outputs: [],
4075
+ creditsUsed: 0,
4076
+ provider: this.providerName,
4077
+ model: modelId,
4078
+ processingTimeMs: Date.now() - startedAt,
4079
+ error
4080
+ });
4081
+ let submitted;
4082
+ try {
4083
+ modelId = this.binding(params.model)[0];
4084
+ submitted = await this.submitTask(params);
4085
+ } catch (error) {
4086
+ if (isPendingJob(error)) throw error;
4087
+ return failure(error instanceof Error ? error.message : String(error));
4088
+ }
4089
+ if (isSubmitMode()) {
4090
+ throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
4091
+ }
4092
+ const job = {
4093
+ id: submitted.taskId,
4094
+ provider: this.providerName,
4095
+ model: modelId,
4096
+ kind: "video",
4097
+ pollEndpoint: submitted.pollEndpoint,
4098
+ params,
4099
+ submittedAt: new Date(startedAt).toISOString()
4100
+ };
4101
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
4102
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4103
+ const snapshot = await this.completeJob(job);
4104
+ if (snapshot.status === "failed") {
4105
+ return failure(snapshot.error ?? "Topaz request failed.");
4106
+ }
4107
+ if (snapshot.status === "succeeded" && snapshot.result) {
4108
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
4109
+ }
4110
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
4111
+ }
4112
+ return failure(
4113
+ `Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
4114
+ );
4115
+ }
4116
+ async completeJob(job) {
4117
+ try {
4118
+ if (job.pollEndpoint?.startsWith("/image/")) {
4119
+ return this.imageSnapshot(job);
4120
+ }
4121
+ const payload = await this.request(
4122
+ job.pollEndpoint ?? `/video/${job.id}/status`
4123
+ );
4124
+ return this.snapshot(payload, job);
4125
+ } catch (error) {
4126
+ return {
4127
+ status: "failed",
4128
+ error: error instanceof Error ? error.message : String(error)
4129
+ };
4130
+ }
4131
+ }
4132
+ generateVideo(params) {
4133
+ return this.run(params);
4134
+ }
4135
+ imageBody(binding, params, sourceUrl) {
4136
+ const extras = params.providerOptions?.topaz ?? {};
4137
+ const pixels = params.resolution ? this.resolutionOf(params.resolution) : void 0;
4138
+ const fields = {
4139
+ source_url: sourceUrl,
4140
+ model: binding.vendorModel,
4141
+ output_format: params.outputFormat === "png" ? "png" : params.outputFormat === "jpeg" ? "jpeg" : "jpeg"
4142
+ };
4143
+ if (pixels) {
4144
+ fields.output_width = String(pixels.width);
4145
+ fields.output_height = String(pixels.height);
4146
+ }
4147
+ for (const [key, value] of Object.entries(extras)) {
4148
+ if (value === void 0) continue;
4149
+ fields[key] = typeof value === "string" ? value : String(value);
4150
+ }
4151
+ return fields;
4152
+ }
4153
+ async submitImage(params) {
4154
+ const [, binding] = this.binding(params.model);
4155
+ if (binding.surface !== "image" || !binding.imagePath) {
4156
+ throw new Error(
4157
+ `Topaz "${params.model}" is a video model. Use ai.video with videoUrl.`
4158
+ );
4159
+ }
4160
+ const sourceUrl = params.referenceImages?.[0];
4161
+ if (!sourceUrl) {
4162
+ throw new Error(
4163
+ `Topaz "${params.model}" needs a source image \u2014 pass referenceImages.`
4164
+ );
4165
+ }
4166
+ const form = new FormData();
4167
+ for (const [key, value] of Object.entries(
4168
+ this.imageBody(binding, params, sourceUrl)
4169
+ )) {
4170
+ form.set(key, value);
4171
+ }
4172
+ const response = await fetch(`${this.baseUrl}${binding.imagePath}`, {
4173
+ method: "POST",
4174
+ headers: {
4175
+ "X-API-Key": this.opts.apiKey,
4176
+ accept: "application/json"
4177
+ },
4178
+ body: form
4179
+ });
4180
+ const payload = await response.json();
4181
+ if (!response.ok) {
4182
+ throw new Error(
4183
+ payload.message ?? `Topaz returned ${response.status} enhancing the image.`
4184
+ );
4185
+ }
4186
+ const taskId = payload.process_id?.trim();
4187
+ if (!taskId) {
4188
+ throw new Error(
4189
+ "Topaz accepted the image but returned no process id."
4190
+ );
4191
+ }
4192
+ return { taskId, pollEndpoint: `/image/v1/status/${taskId}` };
4193
+ }
4194
+ async imageSnapshot(job) {
4195
+ const payload = await this.request(
4196
+ job.pollEndpoint ?? `/image/v1/status/${job.id}`
4197
+ );
4198
+ const raw = (payload.status ?? "").toLowerCase();
4199
+ if (raw === "failed" || raw === "cancelled") {
4200
+ return {
4201
+ status: "failed",
4202
+ error: `Topaz image ${raw}.`
4203
+ };
4204
+ }
4205
+ if (raw !== "completed") return { status: "pending" };
4206
+ let url = payload.download_url;
4207
+ let expiresAt = payload.eta ? new Date(payload.eta > 1e10 ? payload.eta : payload.eta * 1e3).toISOString() : void 0;
4208
+ if (!url) {
4209
+ const download = await this.request(
4210
+ `/image/v1/download/${job.id}`
4211
+ );
4212
+ url = download.download_url;
4213
+ if (download.expiry) {
4214
+ expiresAt = new Date(
4215
+ download.expiry > 1e10 ? download.expiry : download.expiry * 1e3
4216
+ ).toISOString();
4217
+ }
4218
+ }
4219
+ if (!url) return { status: "pending" };
4220
+ return {
4221
+ status: "succeeded",
4222
+ result: {
4223
+ success: true,
4224
+ outputs: [
4225
+ {
4226
+ url,
4227
+ mimeType: "image/jpeg",
4228
+ taskId: job.id,
4229
+ expiresAt: expiresAt ?? expiresInHours(1),
4230
+ raw: {
4231
+ status: payload.status,
4232
+ progress: payload.progress,
4233
+ model: payload.model
4234
+ }
4235
+ }
4236
+ ],
4237
+ creditsUsed: payload.credits ?? 0,
4238
+ provider: this.providerName,
4239
+ model: job.model,
4240
+ processingTimeMs: 0
4241
+ }
4242
+ };
4243
+ }
4244
+ async runImage(params) {
4245
+ const startedAt = Date.now();
4246
+ let modelId = params.model ?? "(none)";
4247
+ const failure = (error) => ({
4248
+ success: false,
4249
+ outputs: [],
4250
+ creditsUsed: 0,
4251
+ provider: this.providerName,
4252
+ model: modelId,
4253
+ processingTimeMs: Date.now() - startedAt,
4254
+ error
4255
+ });
4256
+ let submitted;
4257
+ try {
4258
+ modelId = this.binding(params.model)[0];
4259
+ submitted = await this.submitImage(params);
4260
+ } catch (error) {
4261
+ if (isPendingJob(error)) throw error;
4262
+ return failure(error instanceof Error ? error.message : String(error));
4263
+ }
4264
+ if (isSubmitMode()) {
4265
+ throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
4266
+ }
4267
+ const job = {
4268
+ id: submitted.taskId,
4269
+ provider: this.providerName,
4270
+ model: modelId,
4271
+ kind: "image",
4272
+ pollEndpoint: submitted.pollEndpoint,
4273
+ params,
4274
+ submittedAt: new Date(startedAt).toISOString()
4275
+ };
4276
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
4277
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4278
+ const snapshot = await this.completeJob(job);
4279
+ if (snapshot.status === "failed") {
4280
+ return failure(snapshot.error ?? "Topaz image failed.");
4281
+ }
4282
+ if (snapshot.status === "succeeded" && snapshot.result) {
4283
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
4284
+ }
4285
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
4286
+ }
4287
+ return failure(
4288
+ `Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
4289
+ );
4290
+ }
4291
+ generateImage(params) {
4292
+ return this.runImage(params);
4293
+ }
4294
+ generateText(params) {
4295
+ return Promise.resolve(unsupported(this.providerName, params.model, "text"));
4296
+ }
4297
+ generateAudio(params) {
4298
+ return Promise.resolve(unsupported(this.providerName, params.model, "audio"));
4299
+ }
4300
+ estimateCost(type, params) {
4301
+ return Promise.resolve(estimateFor(this.providerName, type, params));
4302
+ }
4303
+ supportsModel(model) {
4304
+ return model in TOPAZ_MODELS;
4305
+ }
4306
+ getAvailableModels() {
4307
+ return TOPAZ_CATALOG.map((model) => ({
4308
+ id: model.id,
4309
+ name: model.name,
4310
+ type: model.category
4311
+ }));
4312
+ }
4313
+ };
4314
+ function numberish(value) {
4315
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4316
+ if (typeof value === "string" && value.length > 0) {
4317
+ const parsed = Number(value);
4318
+ return Number.isFinite(parsed) ? parsed : void 0;
4319
+ }
4320
+ return void 0;
4321
+ }
4322
+ function unsupported(provider, model, kind) {
4323
+ return {
4324
+ success: false,
4325
+ outputs: [],
4326
+ creditsUsed: 0,
4327
+ provider,
4328
+ model: model ?? "",
4329
+ processingTimeMs: 0,
4330
+ error: `Topaz does not generate ${kind} on this adapter.`
4331
+ };
4332
+ }
4333
+
3727
4334
  // src/helpers/result.ts
3728
4335
  function ok(data) {
3729
4336
  return { data, error: null };
@@ -3740,6 +4347,22 @@ function failFrom(code, cause, context) {
3740
4347
  });
3741
4348
  }
3742
4349
 
4350
+ // src/lib/hooks.ts
4351
+ function hookName(kind, stage) {
4352
+ return `on${kind[0].toUpperCase()}${kind.slice(1)}${stage}`;
4353
+ }
4354
+ function findHook(hooks, kind, stage) {
4355
+ if (!hooks || !kind) return void 0;
4356
+ return hooks[hookName(kind, stage)];
4357
+ }
4358
+ async function runHook(hook, event) {
4359
+ if (!hook) return;
4360
+ try {
4361
+ await hook(event);
4362
+ } catch {
4363
+ }
4364
+ }
4365
+
3743
4366
  // src/lib/storage.ts
3744
4367
  async function loadS3(config) {
3745
4368
  let s3;
@@ -3908,9 +4531,10 @@ var NATIVE_PROVIDERS = [
3908
4531
  "google",
3909
4532
  "kling",
3910
4533
  "openai",
3911
- "qwen"
4534
+ "qwen",
4535
+ "topaz"
3912
4536
  ];
3913
- function brotuClient(options) {
4537
+ function brotu(options) {
3914
4538
  const apiKey = options.apiKey?.trim();
3915
4539
  if (!apiKey) {
3916
4540
  throw new Error(
@@ -4007,6 +4631,12 @@ function brotuClient(options) {
4007
4631
  baseUrl: provider.baseUrl
4008
4632
  });
4009
4633
  }
4634
+ if (provider.id === "topaz") {
4635
+ return new TopazAdapter({
4636
+ apiKey: provider.apiKey,
4637
+ baseUrl: provider.baseUrl
4638
+ });
4639
+ }
4010
4640
  return void 0;
4011
4641
  }
4012
4642
  function generateWith(adapter, kind, params) {
@@ -4023,7 +4653,10 @@ function brotuClient(options) {
4023
4653
  }
4024
4654
  async function notifySettled(input) {
4025
4655
  const config = webhookFor(input.params);
4026
- if (!config) return;
4656
+ const kind = input.kind ?? input.job?.kind;
4657
+ const stage = input.event === "generation.succeeded" ? "Success" : "Error";
4658
+ const hook = findHook(options.hooks, kind, stage);
4659
+ if (!config && !hook) return;
4027
4660
  const jobId = input.job?.id;
4028
4661
  if (jobId) {
4029
4662
  if (notifiedJobIds.has(jobId)) return;
@@ -4034,14 +4667,38 @@ function brotuClient(options) {
4034
4667
  jobId,
4035
4668
  provider: input.provider ?? input.job?.provider,
4036
4669
  model: input.model ?? input.job?.model,
4037
- kind: input.kind ?? input.job?.kind,
4670
+ kind,
4038
4671
  outputs: input.outputs,
4039
4672
  error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
4040
4673
  metadata: input.metadata ?? input.job?.metadata,
4041
4674
  processingTimeMs: input.processingTimeMs,
4042
4675
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
4043
4676
  };
4044
- await deliverWebhook(config, payload);
4677
+ if (hook && kind) {
4678
+ await runHook(hook, {
4679
+ kind,
4680
+ stage,
4681
+ provider: payload.provider ?? "",
4682
+ model: payload.model ?? "",
4683
+ jobId: payload.jobId,
4684
+ outputs: payload.outputs,
4685
+ error: payload.error,
4686
+ metadata: payload.metadata,
4687
+ processingTimeMs: payload.processingTimeMs,
4688
+ at: payload.completedAt
4689
+ });
4690
+ }
4691
+ if (config) await deliverWebhook(config, payload);
4692
+ }
4693
+ function notifyLoading(kind, routed, params) {
4694
+ return runHook(findHook(options.hooks, kind, "Loading"), {
4695
+ kind,
4696
+ stage: "Loading",
4697
+ provider: routed.provider,
4698
+ model: routed.model,
4699
+ metadata: params.metadata,
4700
+ at: (/* @__PURE__ */ new Date()).toISOString()
4701
+ });
4045
4702
  }
4046
4703
  async function toGeneration(raw, metadata) {
4047
4704
  if (!raw.success) {
@@ -4064,6 +4721,7 @@ function brotuClient(options) {
4064
4721
  async function generate(kind, params) {
4065
4722
  const routed = route(params.model);
4066
4723
  if (routed.error) return fail(routed.error);
4724
+ await notifyLoading(kind, routed.data, params);
4067
4725
  try {
4068
4726
  const result = await toGeneration(
4069
4727
  await generateWith(routed.data.adapter, kind, params),
@@ -4114,6 +4772,7 @@ function brotuClient(options) {
4114
4772
  async function submit(kind, params) {
4115
4773
  const routed = route(params.model);
4116
4774
  if (routed.error) return fail(routed.error);
4775
+ await notifyLoading(kind, routed.data, params);
4117
4776
  const base = {
4118
4777
  provider: routed.data.provider,
4119
4778
  model: routed.data.model,
@@ -4285,24 +4944,88 @@ function brotuClient(options) {
4285
4944
  }
4286
4945
  };
4287
4946
  }
4947
+ function listFor(category) {
4948
+ return describeModels(optionsWithKey).filter(
4949
+ (entry) => entry.model.category === category
4950
+ );
4951
+ }
4288
4952
  return {
4289
4953
  google: googleNamespace(),
4290
4954
  kling: klingNamespace(),
4291
4955
  image: {
4292
4956
  submit: (params) => submit("image", params),
4293
- generate: (params) => generate("image", params)
4957
+ generate: (params) => generate("image", params),
4958
+ list: () => listFor("image"),
4959
+ upscale: (params) => {
4960
+ if (!params.imageUrl?.trim()) {
4961
+ return Promise.resolve(
4962
+ fail({
4963
+ code: "invalid_request",
4964
+ message: "image.upscale needs imageUrl.",
4965
+ model: params.model
4966
+ })
4967
+ );
4968
+ }
4969
+ const model = getModel(params.model);
4970
+ if (model && !model.nodeTypes.includes("image_upscale")) {
4971
+ return Promise.resolve(
4972
+ fail({
4973
+ code: "invalid_request",
4974
+ message: `"${params.model}" is not an image upscale model.`,
4975
+ model: params.model
4976
+ })
4977
+ );
4978
+ }
4979
+ return generate("image", {
4980
+ ...params,
4981
+ prompt: params.prompt ?? "",
4982
+ referenceImages: [
4983
+ params.imageUrl,
4984
+ ...params.referenceImages ?? []
4985
+ ]
4986
+ });
4987
+ }
4294
4988
  },
4295
4989
  video: {
4296
4990
  submit: (params) => submit("video", params),
4297
- generate: (params) => generate("video", params)
4991
+ generate: (params) => generate("video", params),
4992
+ list: () => listFor("video"),
4993
+ upscale: (params) => {
4994
+ if (!params.videoUrl?.trim()) {
4995
+ return Promise.resolve(
4996
+ fail({
4997
+ code: "invalid_request",
4998
+ message: "video.upscale needs videoUrl.",
4999
+ model: params.model
5000
+ })
5001
+ );
5002
+ }
5003
+ const model = getModel(params.model);
5004
+ if (model && !model.nodeTypes.includes("video_upscale")) {
5005
+ return Promise.resolve(
5006
+ fail({
5007
+ code: "invalid_request",
5008
+ message: `"${params.model}" is not a video upscale model. Interpolation models use ai.video.submit.`,
5009
+ model: params.model
5010
+ })
5011
+ );
5012
+ }
5013
+ return submit("video", {
5014
+ ...params,
5015
+ prompt: params.prompt ?? "",
5016
+ videoUrl: params.videoUrl
5017
+ });
5018
+ }
4298
5019
  },
4299
5020
  text: {
4300
5021
  submit: (params) => submit("text", params),
4301
- generate: (params) => generate("text", params)
5022
+ generate: (params) => generate("text", params),
5023
+ list: () => listFor("text")
4302
5024
  },
4303
5025
  audio: {
4304
5026
  submit: (params) => submit("audio", params),
4305
- generate: (params) => generate("audio", params)
5027
+ generate: (params) => generate("audio", params),
5028
+ list: () => listFor("audio")
4306
5029
  },
4307
5030
  webhook: {
4308
5031
  set(value) {
@@ -4369,6 +5092,7 @@ function brotuClient(options) {
4369
5092
  }
4370
5093
  // Annotate the CommonJS export names for ESM import in node:
4371
5094
  0 && (module.exports = {
5095
+ BROTU_SUPPORTED_CATEGORIES,
4372
5096
  BYTEPLUS_CATALOG,
4373
5097
  BYTEPLUS_MODELS,
4374
5098
  BrotuAdapter,
@@ -4400,9 +5124,15 @@ function brotuClient(options) {
4400
5124
  QWEN_TEXT_MODELS,
4401
5125
  QWEN_VIDEO_MODELS,
4402
5126
  QwenAdapter,
4403
- brotuClient,
5127
+ TOPAZ_CATALOG,
5128
+ TOPAZ_IMAGE_MODELS,
5129
+ TOPAZ_MODELS,
5130
+ TOPAZ_VIDEO_MODELS,
5131
+ TopazAdapter,
5132
+ brotu,
4404
5133
  createS3Storage,
4405
5134
  deliverWebhook,
5135
+ describeModels,
4406
5136
  fail,
4407
5137
  getAvailableModels,
4408
5138
  getModel,