@brotu/ai 0.5.0 → 0.7.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
@@ -62,6 +62,11 @@ __export(index_exports, {
62
62
  QWEN_TEXT_MODELS: () => QWEN_TEXT_MODELS,
63
63
  QWEN_VIDEO_MODELS: () => QWEN_VIDEO_MODELS,
64
64
  QwenAdapter: () => QwenAdapter,
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,
65
70
  brotu: () => brotu,
66
71
  createS3Storage: () => createS3Storage,
67
72
  deliverWebhook: () => deliverWebhook,
@@ -834,7 +839,7 @@ var imageOmni = {
834
839
  prompt: input.prompt
835
840
  };
836
841
  if (images.length > 0) {
837
- body.image_list = images.map((image) => ({ image }));
842
+ body.image_list = images.map((image2) => ({ image: image2 }));
838
843
  }
839
844
  if (elements.length > 0) {
840
845
  body.element_list = elements.map((id) => ({ element_id: id }));
@@ -1236,6 +1241,105 @@ var QWEN_CATALOG = [
1236
1241
  )
1237
1242
  ];
1238
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
+
1239
1343
  // src/catalog.ts
1240
1344
  var PROVIDER_BASE_URLS = {
1241
1345
  byteplus: "https://ark.ap-southeast.bytepluses.com",
@@ -1244,6 +1348,7 @@ var PROVIDER_BASE_URLS = {
1244
1348
  google: "https://generativelanguage.googleapis.com",
1245
1349
  openai: "https://api.openai.com",
1246
1350
  qwen: "https://dashscope-intl.aliyuncs.com",
1351
+ topaz: "https://api.topazlabs.com",
1247
1352
  brotu: "https://api.brotu.app"
1248
1353
  };
1249
1354
  var DEFAULT_BROTU_API_URL = PROVIDER_BASE_URLS.brotu;
@@ -1261,7 +1366,8 @@ var BUILT_IN = [
1261
1366
  ...QWEN_CATALOG,
1262
1367
  ...OPENAI_CATALOG,
1263
1368
  ...GOOGLE_CATALOG,
1264
- ...ELEVENLABS_CATALOG
1369
+ ...ELEVENLABS_CATALOG,
1370
+ ...TOPAZ_CATALOG
1265
1371
  ];
1266
1372
  var catalog = BUILT_IN;
1267
1373
  var catalogMap = byId(BUILT_IN);
@@ -1403,7 +1509,7 @@ function expiresInHours(hours) {
1403
1509
  function estimateFor(provider, type, params, defaults) {
1404
1510
  const modelId = params.model ?? "";
1405
1511
  const model = getModel(modelId);
1406
- const video = params;
1512
+ const video2 = params;
1407
1513
  const unit = model?.pricing?.unit ?? (type === "video" ? "second" : type === "audio" ? "character" : type === "text" ? "token" : "image");
1408
1514
  let units = 1;
1409
1515
  if (unit === "character") {
@@ -1411,13 +1517,18 @@ function estimateFor(provider, type, params, defaults) {
1411
1517
  } else if (unit === "token") {
1412
1518
  units = 0;
1413
1519
  } else if (unit === "second") {
1414
- units = video.duration ?? defaults?.durationSeconds ?? model?.minDuration ?? 5;
1520
+ units = video2.duration ?? defaults?.durationSeconds ?? model?.minDuration ?? 5;
1415
1521
  }
1416
1522
  const resolution = params.resolution;
1417
1523
  const rate = (resolution ? model?.pricing?.byResolution?.[resolution] : void 0) ?? model?.pricing?.usdPerUnit;
1524
+ const tier = model?.runtimePricingTiers?.find(
1525
+ (candidate) => (candidate.resolution === void 0 || candidate.resolution === resolution) && (candidate.durationSeconds === void 0 || candidate.durationSeconds === units)
1526
+ );
1527
+ const creditsPerUnit = tier?.creditsPerUnit ?? model?.creditsPerUnit;
1418
1528
  return {
1419
1529
  unit,
1420
1530
  units,
1531
+ credits: creditsPerUnit ? creditsPerUnit * units : null,
1421
1532
  usd: rate === void 0 || unit === "token" ? null : Number((rate * units).toFixed(4)),
1422
1533
  note: unit === "token" && rate !== void 0 ? `Billed per token at $${(rate * 1e6).toFixed(2)} per million output tokens. The total depends on how much the model writes, so it is only known after generating.` : rate === void 0 ? `No verified rate for "${modelId}". It bills to your own ${provider} account; ${units} ${unit}${units === 1 ? "" : "s"} will be charged.` : void 0,
1423
1534
  provider,
@@ -1667,23 +1778,23 @@ var BrotuAdapter = class {
1667
1778
  }
1668
1779
  try {
1669
1780
  const workspaceId = await this.workspaceId();
1670
- const video = params;
1671
- const image = params;
1781
+ const video2 = params;
1782
+ const image2 = params;
1672
1783
  const payload = await this.request(
1673
1784
  this.studioPath("/api/v1/studio/estimate", workspaceId),
1674
1785
  {
1675
1786
  method: "POST",
1676
1787
  body: JSON.stringify({
1677
1788
  model: this.platformModel(params.model ?? ""),
1678
- duration: video.duration,
1679
- resolution: video.resolution ?? image.resolution,
1680
- aspectRatio: video.aspectRatio ?? image.aspectRatio,
1681
- withAudio: video.withAudio,
1789
+ duration: video2.duration,
1790
+ resolution: video2.resolution ?? image2.resolution,
1791
+ aspectRatio: video2.aspectRatio ?? image2.aspectRatio,
1792
+ withAudio: video2.withAudio,
1682
1793
  hasReferenceImage: Boolean(
1683
- video.imageUrl || video.imageUrls?.length || video.referenceImages?.length || image.referenceImages?.length
1794
+ video2.imageUrl || video2.imageUrls?.length || video2.referenceImages?.length || image2.referenceImages?.length
1684
1795
  ),
1685
- hasReferenceVideo: Boolean(video.videoUrl || video.videoUrls?.length),
1686
- quality: image.quality
1796
+ hasReferenceVideo: Boolean(video2.videoUrl || video2.videoUrls?.length),
1797
+ quality: image2.quality
1687
1798
  })
1688
1799
  }
1689
1800
  );
@@ -1691,6 +1802,7 @@ var BrotuAdapter = class {
1691
1802
  return {
1692
1803
  unit: type === "video" ? "second" : "image",
1693
1804
  units: credits,
1805
+ credits,
1694
1806
  usd: null,
1695
1807
  note: `${credits} Brotu credit${credits === 1 ? "" : "s"}. A vendor key generates on that provider.`,
1696
1808
  provider: this.providerName,
@@ -2286,12 +2398,12 @@ var GoogleAdapter = class {
2286
2398
  const input = [
2287
2399
  { type: "text", text: params.prompt }
2288
2400
  ];
2289
- for (const image of params.referenceImages ?? []) {
2290
- const [meta, data] = image.split(",");
2401
+ for (const image2 of params.referenceImages ?? []) {
2402
+ const [meta, data] = image2.split(",");
2291
2403
  input.push({
2292
2404
  type: "image",
2293
2405
  mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
2294
- data: data ?? image
2406
+ data: data ?? image2
2295
2407
  });
2296
2408
  }
2297
2409
  const payload = await this.request(
@@ -2383,10 +2495,10 @@ var GoogleAdapter = class {
2383
2495
  };
2384
2496
  const first = params.imageUrl ?? params.referenceImages?.[0];
2385
2497
  if (first) attach("image", first);
2386
- for (const image of params.imageUrls ?? []) attach("image", image);
2387
- const video = params.videoUrl ?? params.videoUrls?.[0];
2388
- if (video) attach("video", video);
2389
- const task = video ? "edit" : params.imageUrls?.length ? "reference_to_video" : first ? "image_to_video" : "text_to_video";
2498
+ for (const image2 of params.imageUrls ?? []) attach("image", image2);
2499
+ const video2 = params.videoUrl ?? params.videoUrls?.[0];
2500
+ if (video2) attach("video", video2);
2501
+ const task = video2 ? "edit" : params.imageUrls?.length ? "reference_to_video" : first ? "image_to_video" : "text_to_video";
2390
2502
  return {
2391
2503
  model: modelId,
2392
2504
  input,
@@ -2555,12 +2667,12 @@ var GoogleAdapter = class {
2555
2667
  const input = [
2556
2668
  { type: "text", text: params.prompt }
2557
2669
  ];
2558
- for (const image of params.referenceImages ?? []) {
2559
- const [meta, data] = image.split(",");
2670
+ for (const image2 of params.referenceImages ?? []) {
2671
+ const [meta, data] = image2.split(",");
2560
2672
  input.push({
2561
2673
  type: "image",
2562
2674
  mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
2563
- data: data ?? image
2675
+ data: data ?? image2
2564
2676
  });
2565
2677
  }
2566
2678
  return {
@@ -3370,11 +3482,11 @@ var QwenAdapter = class {
3370
3482
  case "media": {
3371
3483
  const media = [];
3372
3484
  if (binding.mode === "video-edit") {
3373
- const video = params.videoUrl ?? params.videoUrls?.[0];
3374
- if (!video) {
3485
+ const video2 = params.videoUrl ?? params.videoUrls?.[0];
3486
+ if (!video2) {
3375
3487
  throw new Error(`Qwen "${modelId}" needs a video \u2014 pass videoUrl.`);
3376
3488
  }
3377
- media.push({ type: "video", url: video });
3489
+ media.push({ type: "video", url: video2 });
3378
3490
  } else if (binding.mode === "r2v") {
3379
3491
  for (const url of references) {
3380
3492
  media.push({ type: "reference_image", url });
@@ -3414,7 +3526,7 @@ var QwenAdapter = class {
3414
3526
  ...output.video_url ? [output.video_url] : [],
3415
3527
  ...(output.results ?? []).map((result) => result.url).filter((url) => Boolean(url)),
3416
3528
  ...(output.choices ?? []).flatMap(
3417
- (choice) => (choice.message?.content ?? []).map((part) => part.image).filter((image) => Boolean(image))
3529
+ (choice) => (choice.message?.content ?? []).map((part) => part.image).filter((image2) => Boolean(image2))
3418
3530
  )
3419
3531
  ];
3420
3532
  return urls.map((url) => ({
@@ -3562,8 +3674,8 @@ var QwenAdapter = class {
3562
3674
  }
3563
3675
  try {
3564
3676
  const content = [];
3565
- for (const image of params.referenceImages ?? []) {
3566
- content.push({ image });
3677
+ for (const image2 of params.referenceImages ?? []) {
3678
+ content.push({ image: image2 });
3567
3679
  }
3568
3680
  content.push({ text: params.prompt });
3569
3681
  const parameters = {
@@ -3756,6 +3868,475 @@ var QwenAdapter = class {
3756
3868
  }
3757
3869
  };
3758
3870
 
3871
+ // src/adapters/topaz.adapter.ts
3872
+ var DEFAULT_BASE_URL7 = "https://api.topazlabs.com";
3873
+ var POLL_INTERVAL_MS6 = 5e3;
3874
+ var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
3875
+ var DEFAULT_FPS = 30;
3876
+ var PIXELS2 = {
3877
+ "720p": { width: 1280, height: 720 },
3878
+ "720P": { width: 1280, height: 720 },
3879
+ "1080p": { width: 1920, height: 1080 },
3880
+ "1080P": { width: 1920, height: 1080 },
3881
+ "4k": { width: 3840, height: 2160 },
3882
+ "4K": { width: 3840, height: 2160 },
3883
+ "1K": { width: 1024, height: 1024 },
3884
+ "2K": { width: 2048, height: 2048 }
3885
+ };
3886
+ var TopazAdapter = class {
3887
+ providerName = "topaz";
3888
+ supportedTypes = ["video", "image"];
3889
+ opts;
3890
+ constructor(opts) {
3891
+ this.opts = opts;
3892
+ }
3893
+ get baseUrl() {
3894
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
3895
+ }
3896
+ get headers() {
3897
+ return {
3898
+ "X-API-Key": this.opts.apiKey,
3899
+ accept: "application/json",
3900
+ "Content-Type": "application/json"
3901
+ };
3902
+ }
3903
+ binding(modelId) {
3904
+ const id = modelId ?? "";
3905
+ const found = TOPAZ_MODELS[id];
3906
+ if (!found) {
3907
+ throw new Error(
3908
+ `"${id}" is not a Topaz model. Known: ${TOPAZ_CATALOG.map((model) => model.id).join(", ")}.`
3909
+ );
3910
+ }
3911
+ return [id, found];
3912
+ }
3913
+ /** 720p / 1080p / 4k, or an explicit "1920x1080" / "1920*1080" pair. */
3914
+ resolutionOf(resolution) {
3915
+ if (!resolution) return PIXELS2["1080p"];
3916
+ const named = PIXELS2[resolution];
3917
+ if (named) return named;
3918
+ const match = /^(\d{2,5})[x*](\d{2,5})$/i.exec(resolution.trim());
3919
+ if (match?.[1] && match[2]) {
3920
+ return { width: Number(match[1]), height: Number(match[2]) };
3921
+ }
3922
+ throw new Error(
3923
+ `Topaz wants 720p, 1080p, 4k or WxH, not "${resolution}".`
3924
+ );
3925
+ }
3926
+ containerOf(url) {
3927
+ const path = url.split("?")[0]?.toLowerCase() ?? "";
3928
+ if (path.endsWith(".mov")) return "mov";
3929
+ if (path.endsWith(".mkv")) return "mkv";
3930
+ return "mp4";
3931
+ }
3932
+ filterOf(binding, params) {
3933
+ const extras = params.providerOptions?.topaz ?? {};
3934
+ if (binding.kind === "upscale") {
3935
+ return { auto: "Auto", ...extras, model: binding.vendorModel };
3936
+ }
3937
+ const slowmo = numberish(extras.slowmo) ?? 1;
3938
+ const fps = numberish(extras.fps) ?? DEFAULT_FPS;
3939
+ if (slowmo < 1 || slowmo > 16) {
3940
+ throw new Error(`Topaz interpolation slowmo is 1\u201316, not ${slowmo}.`);
3941
+ }
3942
+ if (fps < 15 || fps > 240) {
3943
+ throw new Error(`Topaz interpolation fps is 15\u2013240, not ${fps}.`);
3944
+ }
3945
+ const filter = {
3946
+ model: binding.vendorModel,
3947
+ slowmo,
3948
+ fps
3949
+ };
3950
+ if (typeof extras.duplicate === "boolean") {
3951
+ filter.duplicate = extras.duplicate;
3952
+ }
3953
+ if (extras.duplicateThreshold !== void 0) {
3954
+ filter.duplicateThreshold = extras.duplicateThreshold;
3955
+ }
3956
+ return filter;
3957
+ }
3958
+ requestBody(binding, params, sourceUrl) {
3959
+ const extras = params.providerOptions?.topaz ?? {};
3960
+ const fps = numberish(extras.fps) ?? DEFAULT_FPS;
3961
+ return {
3962
+ source: { container: this.containerOf(sourceUrl) },
3963
+ filters: [this.filterOf(binding, params)],
3964
+ output: {
3965
+ resolution: this.resolutionOf(params.resolution),
3966
+ frameRate: fps,
3967
+ audioCodec: "AAC",
3968
+ audioTransfer: "Copy",
3969
+ dynamicCompressionLevel: "High",
3970
+ container: "mp4"
3971
+ }
3972
+ };
3973
+ }
3974
+ async request(path, init) {
3975
+ const response = await fetch(`${this.baseUrl}${path}`, {
3976
+ method: init?.method ?? "GET",
3977
+ headers: this.headers,
3978
+ body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
3979
+ });
3980
+ const payload = await response.json();
3981
+ if (!response.ok) {
3982
+ throw new Error(
3983
+ payload.message ?? `Topaz returned ${response.status} for ${path}.`
3984
+ );
3985
+ }
3986
+ return payload;
3987
+ }
3988
+ async downloadSource(url) {
3989
+ const response = await fetch(url);
3990
+ if (!response.ok) {
3991
+ throw new Error(`Could not download the source video (${response.status}).`);
3992
+ }
3993
+ return {
3994
+ bytes: await response.arrayBuffer(),
3995
+ contentType: response.headers.get("content-type") ?? "video/mp4"
3996
+ };
3997
+ }
3998
+ async submitTask(params) {
3999
+ const [, binding] = this.binding(params.model);
4000
+ if (binding.surface !== "video") {
4001
+ throw new Error(
4002
+ `Topaz "${params.model}" is an image model. Use ai.image.`
4003
+ );
4004
+ }
4005
+ const sourceUrl = params.videoUrl ?? params.videoUrls?.[0];
4006
+ if (!sourceUrl) {
4007
+ throw new Error(
4008
+ `Topaz "${params.model}" needs a source video \u2014 pass videoUrl.`
4009
+ );
4010
+ }
4011
+ const source = await this.downloadSource(sourceUrl);
4012
+ const created = await this.request("/video/express", {
4013
+ method: "POST",
4014
+ body: this.requestBody(binding, params, sourceUrl)
4015
+ });
4016
+ const taskId = created.requestId?.trim();
4017
+ const uploadUrl = created.uploadUrls?.[0];
4018
+ if (!taskId || !uploadUrl) {
4019
+ throw new Error(
4020
+ "Topaz accepted the request but returned no request id or upload URL."
4021
+ );
4022
+ }
4023
+ const uploaded = await fetch(uploadUrl, {
4024
+ method: "PUT",
4025
+ headers: { "Content-Type": source.contentType },
4026
+ body: source.bytes
4027
+ });
4028
+ if (!uploaded.ok) {
4029
+ throw new Error(`Topaz upload failed (${uploaded.status}).`);
4030
+ }
4031
+ return { taskId, pollEndpoint: `/video/${taskId}/status` };
4032
+ }
4033
+ outputsFrom(payload, job) {
4034
+ const url = payload.download?.url;
4035
+ if (!url) return [];
4036
+ const expiresAtMs = payload.download?.expiresAt;
4037
+ return [
4038
+ {
4039
+ url,
4040
+ mimeType: "video/mp4",
4041
+ taskId: job.id,
4042
+ expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : expiresInHours(24),
4043
+ raw: {
4044
+ status: payload.status,
4045
+ progress: payload.progress,
4046
+ estimates: payload.estimates,
4047
+ outputSize: payload.outputSize
4048
+ }
4049
+ }
4050
+ ];
4051
+ }
4052
+ snapshot(payload, job) {
4053
+ const raw = (payload.status ?? "").toLowerCase();
4054
+ if (raw === "failed" || raw === "canceled") {
4055
+ return {
4056
+ status: "failed",
4057
+ error: payload.message ?? `Topaz request ${raw}.`
4058
+ };
4059
+ }
4060
+ if (raw !== "complete") return { status: "pending" };
4061
+ const outputs = this.outputsFrom(payload, job);
4062
+ if (outputs.length === 0) return { status: "pending" };
4063
+ return {
4064
+ status: "succeeded",
4065
+ result: {
4066
+ success: true,
4067
+ outputs,
4068
+ creditsUsed: 0,
4069
+ provider: this.providerName,
4070
+ model: job.model,
4071
+ processingTimeMs: 0
4072
+ }
4073
+ };
4074
+ }
4075
+ async run(params) {
4076
+ const startedAt = Date.now();
4077
+ let modelId = params.model ?? "(none)";
4078
+ const failure = (error) => ({
4079
+ success: false,
4080
+ outputs: [],
4081
+ creditsUsed: 0,
4082
+ provider: this.providerName,
4083
+ model: modelId,
4084
+ processingTimeMs: Date.now() - startedAt,
4085
+ error
4086
+ });
4087
+ let submitted;
4088
+ try {
4089
+ modelId = this.binding(params.model)[0];
4090
+ submitted = await this.submitTask(params);
4091
+ } catch (error) {
4092
+ if (isPendingJob(error)) throw error;
4093
+ return failure(error instanceof Error ? error.message : String(error));
4094
+ }
4095
+ if (isSubmitMode()) {
4096
+ throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
4097
+ }
4098
+ const job = {
4099
+ id: submitted.taskId,
4100
+ provider: this.providerName,
4101
+ model: modelId,
4102
+ kind: "video",
4103
+ pollEndpoint: submitted.pollEndpoint,
4104
+ params,
4105
+ submittedAt: new Date(startedAt).toISOString()
4106
+ };
4107
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
4108
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4109
+ const snapshot = await this.completeJob(job);
4110
+ if (snapshot.status === "failed") {
4111
+ return failure(snapshot.error ?? "Topaz request failed.");
4112
+ }
4113
+ if (snapshot.status === "succeeded" && snapshot.result) {
4114
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
4115
+ }
4116
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
4117
+ }
4118
+ return failure(
4119
+ `Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
4120
+ );
4121
+ }
4122
+ async completeJob(job) {
4123
+ try {
4124
+ if (job.pollEndpoint?.startsWith("/image/")) {
4125
+ return this.imageSnapshot(job);
4126
+ }
4127
+ const payload = await this.request(
4128
+ job.pollEndpoint ?? `/video/${job.id}/status`
4129
+ );
4130
+ return this.snapshot(payload, job);
4131
+ } catch (error) {
4132
+ return {
4133
+ status: "failed",
4134
+ error: error instanceof Error ? error.message : String(error)
4135
+ };
4136
+ }
4137
+ }
4138
+ generateVideo(params) {
4139
+ return this.run(params);
4140
+ }
4141
+ imageBody(binding, params, sourceUrl) {
4142
+ const extras = params.providerOptions?.topaz ?? {};
4143
+ const pixels = params.resolution ? this.resolutionOf(params.resolution) : void 0;
4144
+ const fields = {
4145
+ source_url: sourceUrl,
4146
+ model: binding.vendorModel,
4147
+ output_format: params.outputFormat === "png" ? "png" : params.outputFormat === "jpeg" ? "jpeg" : "jpeg"
4148
+ };
4149
+ if (pixels) {
4150
+ fields.output_width = String(pixels.width);
4151
+ fields.output_height = String(pixels.height);
4152
+ }
4153
+ for (const [key, value] of Object.entries(extras)) {
4154
+ if (value === void 0) continue;
4155
+ fields[key] = typeof value === "string" ? value : String(value);
4156
+ }
4157
+ return fields;
4158
+ }
4159
+ async submitImage(params) {
4160
+ const [, binding] = this.binding(params.model);
4161
+ if (binding.surface !== "image" || !binding.imagePath) {
4162
+ throw new Error(
4163
+ `Topaz "${params.model}" is a video model. Use ai.video with videoUrl.`
4164
+ );
4165
+ }
4166
+ const sourceUrl = params.referenceImages?.[0];
4167
+ if (!sourceUrl) {
4168
+ throw new Error(
4169
+ `Topaz "${params.model}" needs a source image \u2014 pass referenceImages.`
4170
+ );
4171
+ }
4172
+ const form = new FormData();
4173
+ for (const [key, value] of Object.entries(
4174
+ this.imageBody(binding, params, sourceUrl)
4175
+ )) {
4176
+ form.set(key, value);
4177
+ }
4178
+ const response = await fetch(`${this.baseUrl}${binding.imagePath}`, {
4179
+ method: "POST",
4180
+ headers: {
4181
+ "X-API-Key": this.opts.apiKey,
4182
+ accept: "application/json"
4183
+ },
4184
+ body: form
4185
+ });
4186
+ const payload = await response.json();
4187
+ if (!response.ok) {
4188
+ throw new Error(
4189
+ payload.message ?? `Topaz returned ${response.status} enhancing the image.`
4190
+ );
4191
+ }
4192
+ const taskId = payload.process_id?.trim();
4193
+ if (!taskId) {
4194
+ throw new Error(
4195
+ "Topaz accepted the image but returned no process id."
4196
+ );
4197
+ }
4198
+ return { taskId, pollEndpoint: `/image/v1/status/${taskId}` };
4199
+ }
4200
+ async imageSnapshot(job) {
4201
+ const payload = await this.request(
4202
+ job.pollEndpoint ?? `/image/v1/status/${job.id}`
4203
+ );
4204
+ const raw = (payload.status ?? "").toLowerCase();
4205
+ if (raw === "failed" || raw === "cancelled") {
4206
+ return {
4207
+ status: "failed",
4208
+ error: `Topaz image ${raw}.`
4209
+ };
4210
+ }
4211
+ if (raw !== "completed") return { status: "pending" };
4212
+ let url = payload.download_url;
4213
+ let expiresAt = payload.eta ? new Date(payload.eta > 1e10 ? payload.eta : payload.eta * 1e3).toISOString() : void 0;
4214
+ if (!url) {
4215
+ const download = await this.request(
4216
+ `/image/v1/download/${job.id}`
4217
+ );
4218
+ url = download.download_url;
4219
+ if (download.expiry) {
4220
+ expiresAt = new Date(
4221
+ download.expiry > 1e10 ? download.expiry : download.expiry * 1e3
4222
+ ).toISOString();
4223
+ }
4224
+ }
4225
+ if (!url) return { status: "pending" };
4226
+ return {
4227
+ status: "succeeded",
4228
+ result: {
4229
+ success: true,
4230
+ outputs: [
4231
+ {
4232
+ url,
4233
+ mimeType: "image/jpeg",
4234
+ taskId: job.id,
4235
+ expiresAt: expiresAt ?? expiresInHours(1),
4236
+ raw: {
4237
+ status: payload.status,
4238
+ progress: payload.progress,
4239
+ model: payload.model
4240
+ }
4241
+ }
4242
+ ],
4243
+ creditsUsed: payload.credits ?? 0,
4244
+ provider: this.providerName,
4245
+ model: job.model,
4246
+ processingTimeMs: 0
4247
+ }
4248
+ };
4249
+ }
4250
+ async runImage(params) {
4251
+ const startedAt = Date.now();
4252
+ let modelId = params.model ?? "(none)";
4253
+ const failure = (error) => ({
4254
+ success: false,
4255
+ outputs: [],
4256
+ creditsUsed: 0,
4257
+ provider: this.providerName,
4258
+ model: modelId,
4259
+ processingTimeMs: Date.now() - startedAt,
4260
+ error
4261
+ });
4262
+ let submitted;
4263
+ try {
4264
+ modelId = this.binding(params.model)[0];
4265
+ submitted = await this.submitImage(params);
4266
+ } catch (error) {
4267
+ if (isPendingJob(error)) throw error;
4268
+ return failure(error instanceof Error ? error.message : String(error));
4269
+ }
4270
+ if (isSubmitMode()) {
4271
+ throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
4272
+ }
4273
+ const job = {
4274
+ id: submitted.taskId,
4275
+ provider: this.providerName,
4276
+ model: modelId,
4277
+ kind: "image",
4278
+ pollEndpoint: submitted.pollEndpoint,
4279
+ params,
4280
+ submittedAt: new Date(startedAt).toISOString()
4281
+ };
4282
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
4283
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4284
+ const snapshot = await this.completeJob(job);
4285
+ if (snapshot.status === "failed") {
4286
+ return failure(snapshot.error ?? "Topaz image failed.");
4287
+ }
4288
+ if (snapshot.status === "succeeded" && snapshot.result) {
4289
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
4290
+ }
4291
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
4292
+ }
4293
+ return failure(
4294
+ `Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
4295
+ );
4296
+ }
4297
+ generateImage(params) {
4298
+ return this.runImage(params);
4299
+ }
4300
+ generateText(params) {
4301
+ return Promise.resolve(unsupported(this.providerName, params.model, "text"));
4302
+ }
4303
+ generateAudio(params) {
4304
+ return Promise.resolve(unsupported(this.providerName, params.model, "audio"));
4305
+ }
4306
+ estimateCost(type, params) {
4307
+ return Promise.resolve(estimateFor(this.providerName, type, params));
4308
+ }
4309
+ supportsModel(model) {
4310
+ return model in TOPAZ_MODELS;
4311
+ }
4312
+ getAvailableModels() {
4313
+ return TOPAZ_CATALOG.map((model) => ({
4314
+ id: model.id,
4315
+ name: model.name,
4316
+ type: model.category
4317
+ }));
4318
+ }
4319
+ };
4320
+ function numberish(value) {
4321
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4322
+ if (typeof value === "string" && value.length > 0) {
4323
+ const parsed = Number(value);
4324
+ return Number.isFinite(parsed) ? parsed : void 0;
4325
+ }
4326
+ return void 0;
4327
+ }
4328
+ function unsupported(provider, model, kind) {
4329
+ return {
4330
+ success: false,
4331
+ outputs: [],
4332
+ creditsUsed: 0,
4333
+ provider,
4334
+ model: model ?? "",
4335
+ processingTimeMs: 0,
4336
+ error: `Topaz does not generate ${kind} on this adapter.`
4337
+ };
4338
+ }
4339
+
3759
4340
  // src/helpers/result.ts
3760
4341
  function ok(data) {
3761
4342
  return { data, error: null };
@@ -3956,7 +4537,8 @@ var NATIVE_PROVIDERS = [
3956
4537
  "google",
3957
4538
  "kling",
3958
4539
  "openai",
3959
- "qwen"
4540
+ "qwen",
4541
+ "topaz"
3960
4542
  ];
3961
4543
  function brotu(options) {
3962
4544
  const apiKey = options.apiKey?.trim();
@@ -4055,6 +4637,12 @@ function brotu(options) {
4055
4637
  baseUrl: provider.baseUrl
4056
4638
  });
4057
4639
  }
4640
+ if (provider.id === "topaz") {
4641
+ return new TopazAdapter({
4642
+ apiKey: provider.apiKey,
4643
+ baseUrl: provider.baseUrl
4644
+ });
4645
+ }
4058
4646
  return void 0;
4059
4647
  }
4060
4648
  function generateWith(adapter, kind, params) {
@@ -4089,6 +4677,7 @@ function brotu(options) {
4089
4677
  outputs: input.outputs,
4090
4678
  error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
4091
4679
  metadata: input.metadata ?? input.job?.metadata,
4680
+ creditsUsed: input.params?.credits ?? input.creditsUsed,
4092
4681
  processingTimeMs: input.processingTimeMs,
4093
4682
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
4094
4683
  };
@@ -4102,6 +4691,7 @@ function brotu(options) {
4102
4691
  outputs: payload.outputs,
4103
4692
  error: payload.error,
4104
4693
  metadata: payload.metadata,
4694
+ creditsUsed: payload.creditsUsed,
4105
4695
  processingTimeMs: payload.processingTimeMs,
4106
4696
  at: payload.completedAt
4107
4697
  });
@@ -4118,7 +4708,7 @@ function brotu(options) {
4118
4708
  at: (/* @__PURE__ */ new Date()).toISOString()
4119
4709
  });
4120
4710
  }
4121
- async function toGeneration(raw, metadata) {
4711
+ async function toGeneration(raw, metadata, credits) {
4122
4712
  if (!raw.success) {
4123
4713
  return fail({
4124
4714
  code: "provider_error",
@@ -4133,6 +4723,9 @@ function brotu(options) {
4133
4723
  provider: raw.provider,
4134
4724
  model: raw.model,
4135
4725
  processingTimeMs: raw.processingTimeMs,
4726
+ // What you said to charge wins over what the platform reported: the
4727
+ // caller's number is the one their ledger has to match.
4728
+ creditsUsed: credits ?? raw.creditsUsed,
4136
4729
  metadata
4137
4730
  });
4138
4731
  }
@@ -4143,7 +4736,8 @@ function brotu(options) {
4143
4736
  try {
4144
4737
  const result = await toGeneration(
4145
4738
  await generateWith(routed.data.adapter, kind, params),
4146
- params.metadata
4739
+ params.metadata,
4740
+ params.credits
4147
4741
  );
4148
4742
  if (result.error) {
4149
4743
  await notifySettled({
@@ -4165,6 +4759,7 @@ function brotu(options) {
4165
4759
  model: result.data.model,
4166
4760
  outputs: result.data.outputs,
4167
4761
  metadata: result.data.metadata,
4762
+ creditsUsed: result.data.creditsUsed,
4168
4763
  processingTimeMs: result.data.processingTimeMs
4169
4764
  });
4170
4765
  return result;
@@ -4261,6 +4856,7 @@ function brotu(options) {
4261
4856
  outputs: snapshot.result.outputs,
4262
4857
  provider: snapshot.result.provider,
4263
4858
  model: snapshot.result.model,
4859
+ creditsUsed: snapshot.result.creditsUsed,
4264
4860
  processingTimeMs: snapshot.result.processingTimeMs,
4265
4861
  metadata: job.metadata
4266
4862
  });
@@ -4283,7 +4879,11 @@ function brotu(options) {
4283
4879
  }
4284
4880
  async function finalizeSnapshot(job, snapshot) {
4285
4881
  if (snapshot.status === "succeeded" && snapshot.result) {
4286
- const persisted = await toGeneration(snapshot.result, job.metadata);
4882
+ const persisted = await toGeneration(
4883
+ snapshot.result,
4884
+ job.metadata,
4885
+ job.params.credits
4886
+ );
4287
4887
  if (!persisted.error) {
4288
4888
  snapshot = {
4289
4889
  status: "succeeded",
@@ -4373,12 +4973,67 @@ function brotu(options) {
4373
4973
  image: {
4374
4974
  submit: (params) => submit("image", params),
4375
4975
  generate: (params) => generate("image", params),
4376
- list: () => listFor("image")
4976
+ list: () => listFor("image"),
4977
+ upscale: (params) => {
4978
+ if (!params.imageUrl?.trim()) {
4979
+ return Promise.resolve(
4980
+ fail({
4981
+ code: "invalid_request",
4982
+ message: "image.upscale needs imageUrl.",
4983
+ model: params.model
4984
+ })
4985
+ );
4986
+ }
4987
+ const model = getModel(params.model);
4988
+ if (model && !model.nodeTypes.includes("image_upscale")) {
4989
+ return Promise.resolve(
4990
+ fail({
4991
+ code: "invalid_request",
4992
+ message: `"${params.model}" is not an image upscale model.`,
4993
+ model: params.model
4994
+ })
4995
+ );
4996
+ }
4997
+ return generate("image", {
4998
+ ...params,
4999
+ prompt: params.prompt ?? "",
5000
+ referenceImages: [
5001
+ params.imageUrl,
5002
+ ...params.referenceImages ?? []
5003
+ ]
5004
+ });
5005
+ }
4377
5006
  },
4378
5007
  video: {
4379
5008
  submit: (params) => submit("video", params),
4380
5009
  generate: (params) => generate("video", params),
4381
- list: () => listFor("video")
5010
+ list: () => listFor("video"),
5011
+ upscale: (params) => {
5012
+ if (!params.videoUrl?.trim()) {
5013
+ return Promise.resolve(
5014
+ fail({
5015
+ code: "invalid_request",
5016
+ message: "video.upscale needs videoUrl.",
5017
+ model: params.model
5018
+ })
5019
+ );
5020
+ }
5021
+ const model = getModel(params.model);
5022
+ if (model && !model.nodeTypes.includes("video_upscale")) {
5023
+ return Promise.resolve(
5024
+ fail({
5025
+ code: "invalid_request",
5026
+ message: `"${params.model}" is not a video upscale model. Interpolation models use ai.video.submit.`,
5027
+ model: params.model
5028
+ })
5029
+ );
5030
+ }
5031
+ return submit("video", {
5032
+ ...params,
5033
+ prompt: params.prompt ?? "",
5034
+ videoUrl: params.videoUrl
5035
+ });
5036
+ }
4382
5037
  },
4383
5038
  text: {
4384
5039
  submit: (params) => submit("text", params),
@@ -4415,6 +5070,7 @@ function brotu(options) {
4415
5070
  provider: snapshot.result.provider,
4416
5071
  model: snapshot.result.model,
4417
5072
  processingTimeMs: snapshot.result.processingTimeMs,
5073
+ creditsUsed: job.params.credits ?? snapshot.result.creditsUsed,
4418
5074
  metadata: job.metadata
4419
5075
  });
4420
5076
  }
@@ -4487,6 +5143,11 @@ function brotu(options) {
4487
5143
  QWEN_TEXT_MODELS,
4488
5144
  QWEN_VIDEO_MODELS,
4489
5145
  QwenAdapter,
5146
+ TOPAZ_CATALOG,
5147
+ TOPAZ_IMAGE_MODELS,
5148
+ TOPAZ_MODELS,
5149
+ TOPAZ_VIDEO_MODELS,
5150
+ TopazAdapter,
4490
5151
  brotu,
4491
5152
  createS3Storage,
4492
5153
  deliverWebhook,