@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/CATALOG.md +298 -4
- package/README.md +23 -6
- package/dist/adapters/brotu.adapter.d.ts.map +1 -1
- package/dist/adapters/estimate.d.ts.map +1 -1
- package/dist/adapters/topaz.adapter.d.ts +71 -0
- package/dist/adapters/topaz.adapter.d.ts.map +1 -0
- package/dist/catalog.cjs +102 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +1 -1
- package/dist/{chunk-3MC3RDAJ.js → chunk-QQIHMDJC.js} +107 -2
- package/dist/client.d.ts +12 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/helpers/result.d.ts +6 -0
- package/dist/helpers/result.d.ts.map +1 -1
- package/dist/index.cjs +696 -35
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +566 -7
- package/dist/lib/hooks.d.ts +2 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/webhook.d.ts +2 -0
- package/dist/lib/webhook.d.ts.map +1 -1
- package/dist/ports/content-generator.port.d.ts +26 -0
- package/dist/ports/content-generator.port.d.ts.map +1 -1
- package/dist/providers/topaz.models.d.ts +24 -0
- package/dist/providers/topaz.models.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
QWEN_IMAGE_PATHS,
|
|
28
28
|
QWEN_TEXT_MODELS,
|
|
29
29
|
QWEN_VIDEO_MODELS,
|
|
30
|
+
TOPAZ_CATALOG,
|
|
31
|
+
TOPAZ_IMAGE_MODELS,
|
|
32
|
+
TOPAZ_MODELS,
|
|
33
|
+
TOPAZ_VIDEO_MODELS,
|
|
30
34
|
describeModels,
|
|
31
35
|
fieldsFor,
|
|
32
36
|
getAvailableModels,
|
|
@@ -38,7 +42,7 @@ import {
|
|
|
38
42
|
resetCatalog,
|
|
39
43
|
resolveProvider,
|
|
40
44
|
videoPathFor
|
|
41
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-QQIHMDJC.js";
|
|
42
46
|
|
|
43
47
|
// src/lib/jobs.ts
|
|
44
48
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -84,9 +88,14 @@ function estimateFor(provider, type, params, defaults) {
|
|
|
84
88
|
}
|
|
85
89
|
const resolution = params.resolution;
|
|
86
90
|
const rate = (resolution ? model?.pricing?.byResolution?.[resolution] : void 0) ?? model?.pricing?.usdPerUnit;
|
|
91
|
+
const tier = model?.runtimePricingTiers?.find(
|
|
92
|
+
(candidate) => (candidate.resolution === void 0 || candidate.resolution === resolution) && (candidate.durationSeconds === void 0 || candidate.durationSeconds === units)
|
|
93
|
+
);
|
|
94
|
+
const creditsPerUnit = tier?.creditsPerUnit ?? model?.creditsPerUnit;
|
|
87
95
|
return {
|
|
88
96
|
unit,
|
|
89
97
|
units,
|
|
98
|
+
credits: creditsPerUnit ? creditsPerUnit * units : null,
|
|
90
99
|
usd: rate === void 0 || unit === "token" ? null : Number((rate * units).toFixed(4)),
|
|
91
100
|
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,
|
|
92
101
|
provider,
|
|
@@ -360,6 +369,7 @@ var BrotuAdapter = class {
|
|
|
360
369
|
return {
|
|
361
370
|
unit: type === "video" ? "second" : "image",
|
|
362
371
|
units: credits,
|
|
372
|
+
credits,
|
|
363
373
|
usd: null,
|
|
364
374
|
note: `${credits} Brotu credit${credits === 1 ? "" : "s"}. A vendor key generates on that provider.`,
|
|
365
375
|
provider: this.providerName,
|
|
@@ -2425,6 +2435,475 @@ var QwenAdapter = class {
|
|
|
2425
2435
|
}
|
|
2426
2436
|
};
|
|
2427
2437
|
|
|
2438
|
+
// src/adapters/topaz.adapter.ts
|
|
2439
|
+
var DEFAULT_BASE_URL7 = "https://api.topazlabs.com";
|
|
2440
|
+
var POLL_INTERVAL_MS6 = 5e3;
|
|
2441
|
+
var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
|
|
2442
|
+
var DEFAULT_FPS = 30;
|
|
2443
|
+
var PIXELS = {
|
|
2444
|
+
"720p": { width: 1280, height: 720 },
|
|
2445
|
+
"720P": { width: 1280, height: 720 },
|
|
2446
|
+
"1080p": { width: 1920, height: 1080 },
|
|
2447
|
+
"1080P": { width: 1920, height: 1080 },
|
|
2448
|
+
"4k": { width: 3840, height: 2160 },
|
|
2449
|
+
"4K": { width: 3840, height: 2160 },
|
|
2450
|
+
"1K": { width: 1024, height: 1024 },
|
|
2451
|
+
"2K": { width: 2048, height: 2048 }
|
|
2452
|
+
};
|
|
2453
|
+
var TopazAdapter = class {
|
|
2454
|
+
providerName = "topaz";
|
|
2455
|
+
supportedTypes = ["video", "image"];
|
|
2456
|
+
opts;
|
|
2457
|
+
constructor(opts) {
|
|
2458
|
+
this.opts = opts;
|
|
2459
|
+
}
|
|
2460
|
+
get baseUrl() {
|
|
2461
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
|
|
2462
|
+
}
|
|
2463
|
+
get headers() {
|
|
2464
|
+
return {
|
|
2465
|
+
"X-API-Key": this.opts.apiKey,
|
|
2466
|
+
accept: "application/json",
|
|
2467
|
+
"Content-Type": "application/json"
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
binding(modelId) {
|
|
2471
|
+
const id = modelId ?? "";
|
|
2472
|
+
const found = TOPAZ_MODELS[id];
|
|
2473
|
+
if (!found) {
|
|
2474
|
+
throw new Error(
|
|
2475
|
+
`"${id}" is not a Topaz model. Known: ${TOPAZ_CATALOG.map((model) => model.id).join(", ")}.`
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
return [id, found];
|
|
2479
|
+
}
|
|
2480
|
+
/** 720p / 1080p / 4k, or an explicit "1920x1080" / "1920*1080" pair. */
|
|
2481
|
+
resolutionOf(resolution) {
|
|
2482
|
+
if (!resolution) return PIXELS["1080p"];
|
|
2483
|
+
const named = PIXELS[resolution];
|
|
2484
|
+
if (named) return named;
|
|
2485
|
+
const match = /^(\d{2,5})[x*](\d{2,5})$/i.exec(resolution.trim());
|
|
2486
|
+
if (match?.[1] && match[2]) {
|
|
2487
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
2488
|
+
}
|
|
2489
|
+
throw new Error(
|
|
2490
|
+
`Topaz wants 720p, 1080p, 4k or WxH, not "${resolution}".`
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
containerOf(url) {
|
|
2494
|
+
const path = url.split("?")[0]?.toLowerCase() ?? "";
|
|
2495
|
+
if (path.endsWith(".mov")) return "mov";
|
|
2496
|
+
if (path.endsWith(".mkv")) return "mkv";
|
|
2497
|
+
return "mp4";
|
|
2498
|
+
}
|
|
2499
|
+
filterOf(binding, params) {
|
|
2500
|
+
const extras = params.providerOptions?.topaz ?? {};
|
|
2501
|
+
if (binding.kind === "upscale") {
|
|
2502
|
+
return { auto: "Auto", ...extras, model: binding.vendorModel };
|
|
2503
|
+
}
|
|
2504
|
+
const slowmo = numberish(extras.slowmo) ?? 1;
|
|
2505
|
+
const fps = numberish(extras.fps) ?? DEFAULT_FPS;
|
|
2506
|
+
if (slowmo < 1 || slowmo > 16) {
|
|
2507
|
+
throw new Error(`Topaz interpolation slowmo is 1\u201316, not ${slowmo}.`);
|
|
2508
|
+
}
|
|
2509
|
+
if (fps < 15 || fps > 240) {
|
|
2510
|
+
throw new Error(`Topaz interpolation fps is 15\u2013240, not ${fps}.`);
|
|
2511
|
+
}
|
|
2512
|
+
const filter = {
|
|
2513
|
+
model: binding.vendorModel,
|
|
2514
|
+
slowmo,
|
|
2515
|
+
fps
|
|
2516
|
+
};
|
|
2517
|
+
if (typeof extras.duplicate === "boolean") {
|
|
2518
|
+
filter.duplicate = extras.duplicate;
|
|
2519
|
+
}
|
|
2520
|
+
if (extras.duplicateThreshold !== void 0) {
|
|
2521
|
+
filter.duplicateThreshold = extras.duplicateThreshold;
|
|
2522
|
+
}
|
|
2523
|
+
return filter;
|
|
2524
|
+
}
|
|
2525
|
+
requestBody(binding, params, sourceUrl) {
|
|
2526
|
+
const extras = params.providerOptions?.topaz ?? {};
|
|
2527
|
+
const fps = numberish(extras.fps) ?? DEFAULT_FPS;
|
|
2528
|
+
return {
|
|
2529
|
+
source: { container: this.containerOf(sourceUrl) },
|
|
2530
|
+
filters: [this.filterOf(binding, params)],
|
|
2531
|
+
output: {
|
|
2532
|
+
resolution: this.resolutionOf(params.resolution),
|
|
2533
|
+
frameRate: fps,
|
|
2534
|
+
audioCodec: "AAC",
|
|
2535
|
+
audioTransfer: "Copy",
|
|
2536
|
+
dynamicCompressionLevel: "High",
|
|
2537
|
+
container: "mp4"
|
|
2538
|
+
}
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
async request(path, init) {
|
|
2542
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
2543
|
+
method: init?.method ?? "GET",
|
|
2544
|
+
headers: this.headers,
|
|
2545
|
+
body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
2546
|
+
});
|
|
2547
|
+
const payload = await response.json();
|
|
2548
|
+
if (!response.ok) {
|
|
2549
|
+
throw new Error(
|
|
2550
|
+
payload.message ?? `Topaz returned ${response.status} for ${path}.`
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
return payload;
|
|
2554
|
+
}
|
|
2555
|
+
async downloadSource(url) {
|
|
2556
|
+
const response = await fetch(url);
|
|
2557
|
+
if (!response.ok) {
|
|
2558
|
+
throw new Error(`Could not download the source video (${response.status}).`);
|
|
2559
|
+
}
|
|
2560
|
+
return {
|
|
2561
|
+
bytes: await response.arrayBuffer(),
|
|
2562
|
+
contentType: response.headers.get("content-type") ?? "video/mp4"
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
async submitTask(params) {
|
|
2566
|
+
const [, binding] = this.binding(params.model);
|
|
2567
|
+
if (binding.surface !== "video") {
|
|
2568
|
+
throw new Error(
|
|
2569
|
+
`Topaz "${params.model}" is an image model. Use ai.image.`
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
const sourceUrl = params.videoUrl ?? params.videoUrls?.[0];
|
|
2573
|
+
if (!sourceUrl) {
|
|
2574
|
+
throw new Error(
|
|
2575
|
+
`Topaz "${params.model}" needs a source video \u2014 pass videoUrl.`
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
const source = await this.downloadSource(sourceUrl);
|
|
2579
|
+
const created = await this.request("/video/express", {
|
|
2580
|
+
method: "POST",
|
|
2581
|
+
body: this.requestBody(binding, params, sourceUrl)
|
|
2582
|
+
});
|
|
2583
|
+
const taskId = created.requestId?.trim();
|
|
2584
|
+
const uploadUrl = created.uploadUrls?.[0];
|
|
2585
|
+
if (!taskId || !uploadUrl) {
|
|
2586
|
+
throw new Error(
|
|
2587
|
+
"Topaz accepted the request but returned no request id or upload URL."
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
const uploaded = await fetch(uploadUrl, {
|
|
2591
|
+
method: "PUT",
|
|
2592
|
+
headers: { "Content-Type": source.contentType },
|
|
2593
|
+
body: source.bytes
|
|
2594
|
+
});
|
|
2595
|
+
if (!uploaded.ok) {
|
|
2596
|
+
throw new Error(`Topaz upload failed (${uploaded.status}).`);
|
|
2597
|
+
}
|
|
2598
|
+
return { taskId, pollEndpoint: `/video/${taskId}/status` };
|
|
2599
|
+
}
|
|
2600
|
+
outputsFrom(payload, job) {
|
|
2601
|
+
const url = payload.download?.url;
|
|
2602
|
+
if (!url) return [];
|
|
2603
|
+
const expiresAtMs = payload.download?.expiresAt;
|
|
2604
|
+
return [
|
|
2605
|
+
{
|
|
2606
|
+
url,
|
|
2607
|
+
mimeType: "video/mp4",
|
|
2608
|
+
taskId: job.id,
|
|
2609
|
+
expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : expiresInHours(24),
|
|
2610
|
+
raw: {
|
|
2611
|
+
status: payload.status,
|
|
2612
|
+
progress: payload.progress,
|
|
2613
|
+
estimates: payload.estimates,
|
|
2614
|
+
outputSize: payload.outputSize
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
];
|
|
2618
|
+
}
|
|
2619
|
+
snapshot(payload, job) {
|
|
2620
|
+
const raw = (payload.status ?? "").toLowerCase();
|
|
2621
|
+
if (raw === "failed" || raw === "canceled") {
|
|
2622
|
+
return {
|
|
2623
|
+
status: "failed",
|
|
2624
|
+
error: payload.message ?? `Topaz request ${raw}.`
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
if (raw !== "complete") return { status: "pending" };
|
|
2628
|
+
const outputs = this.outputsFrom(payload, job);
|
|
2629
|
+
if (outputs.length === 0) return { status: "pending" };
|
|
2630
|
+
return {
|
|
2631
|
+
status: "succeeded",
|
|
2632
|
+
result: {
|
|
2633
|
+
success: true,
|
|
2634
|
+
outputs,
|
|
2635
|
+
creditsUsed: 0,
|
|
2636
|
+
provider: this.providerName,
|
|
2637
|
+
model: job.model,
|
|
2638
|
+
processingTimeMs: 0
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
async run(params) {
|
|
2643
|
+
const startedAt = Date.now();
|
|
2644
|
+
let modelId = params.model ?? "(none)";
|
|
2645
|
+
const failure = (error) => ({
|
|
2646
|
+
success: false,
|
|
2647
|
+
outputs: [],
|
|
2648
|
+
creditsUsed: 0,
|
|
2649
|
+
provider: this.providerName,
|
|
2650
|
+
model: modelId,
|
|
2651
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2652
|
+
error
|
|
2653
|
+
});
|
|
2654
|
+
let submitted;
|
|
2655
|
+
try {
|
|
2656
|
+
modelId = this.binding(params.model)[0];
|
|
2657
|
+
submitted = await this.submitTask(params);
|
|
2658
|
+
} catch (error) {
|
|
2659
|
+
if (isPendingJob(error)) throw error;
|
|
2660
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2661
|
+
}
|
|
2662
|
+
if (isSubmitMode()) {
|
|
2663
|
+
throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
|
|
2664
|
+
}
|
|
2665
|
+
const job = {
|
|
2666
|
+
id: submitted.taskId,
|
|
2667
|
+
provider: this.providerName,
|
|
2668
|
+
model: modelId,
|
|
2669
|
+
kind: "video",
|
|
2670
|
+
pollEndpoint: submitted.pollEndpoint,
|
|
2671
|
+
params,
|
|
2672
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
2673
|
+
};
|
|
2674
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
|
|
2675
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2676
|
+
const snapshot = await this.completeJob(job);
|
|
2677
|
+
if (snapshot.status === "failed") {
|
|
2678
|
+
return failure(snapshot.error ?? "Topaz request failed.");
|
|
2679
|
+
}
|
|
2680
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2681
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2682
|
+
}
|
|
2683
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
|
|
2684
|
+
}
|
|
2685
|
+
return failure(
|
|
2686
|
+
`Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
async completeJob(job) {
|
|
2690
|
+
try {
|
|
2691
|
+
if (job.pollEndpoint?.startsWith("/image/")) {
|
|
2692
|
+
return this.imageSnapshot(job);
|
|
2693
|
+
}
|
|
2694
|
+
const payload = await this.request(
|
|
2695
|
+
job.pollEndpoint ?? `/video/${job.id}/status`
|
|
2696
|
+
);
|
|
2697
|
+
return this.snapshot(payload, job);
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
return {
|
|
2700
|
+
status: "failed",
|
|
2701
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
generateVideo(params) {
|
|
2706
|
+
return this.run(params);
|
|
2707
|
+
}
|
|
2708
|
+
imageBody(binding, params, sourceUrl) {
|
|
2709
|
+
const extras = params.providerOptions?.topaz ?? {};
|
|
2710
|
+
const pixels = params.resolution ? this.resolutionOf(params.resolution) : void 0;
|
|
2711
|
+
const fields = {
|
|
2712
|
+
source_url: sourceUrl,
|
|
2713
|
+
model: binding.vendorModel,
|
|
2714
|
+
output_format: params.outputFormat === "png" ? "png" : params.outputFormat === "jpeg" ? "jpeg" : "jpeg"
|
|
2715
|
+
};
|
|
2716
|
+
if (pixels) {
|
|
2717
|
+
fields.output_width = String(pixels.width);
|
|
2718
|
+
fields.output_height = String(pixels.height);
|
|
2719
|
+
}
|
|
2720
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
2721
|
+
if (value === void 0) continue;
|
|
2722
|
+
fields[key] = typeof value === "string" ? value : String(value);
|
|
2723
|
+
}
|
|
2724
|
+
return fields;
|
|
2725
|
+
}
|
|
2726
|
+
async submitImage(params) {
|
|
2727
|
+
const [, binding] = this.binding(params.model);
|
|
2728
|
+
if (binding.surface !== "image" || !binding.imagePath) {
|
|
2729
|
+
throw new Error(
|
|
2730
|
+
`Topaz "${params.model}" is a video model. Use ai.video with videoUrl.`
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
const sourceUrl = params.referenceImages?.[0];
|
|
2734
|
+
if (!sourceUrl) {
|
|
2735
|
+
throw new Error(
|
|
2736
|
+
`Topaz "${params.model}" needs a source image \u2014 pass referenceImages.`
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
const form = new FormData();
|
|
2740
|
+
for (const [key, value] of Object.entries(
|
|
2741
|
+
this.imageBody(binding, params, sourceUrl)
|
|
2742
|
+
)) {
|
|
2743
|
+
form.set(key, value);
|
|
2744
|
+
}
|
|
2745
|
+
const response = await fetch(`${this.baseUrl}${binding.imagePath}`, {
|
|
2746
|
+
method: "POST",
|
|
2747
|
+
headers: {
|
|
2748
|
+
"X-API-Key": this.opts.apiKey,
|
|
2749
|
+
accept: "application/json"
|
|
2750
|
+
},
|
|
2751
|
+
body: form
|
|
2752
|
+
});
|
|
2753
|
+
const payload = await response.json();
|
|
2754
|
+
if (!response.ok) {
|
|
2755
|
+
throw new Error(
|
|
2756
|
+
payload.message ?? `Topaz returned ${response.status} enhancing the image.`
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
const taskId = payload.process_id?.trim();
|
|
2760
|
+
if (!taskId) {
|
|
2761
|
+
throw new Error(
|
|
2762
|
+
"Topaz accepted the image but returned no process id."
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
return { taskId, pollEndpoint: `/image/v1/status/${taskId}` };
|
|
2766
|
+
}
|
|
2767
|
+
async imageSnapshot(job) {
|
|
2768
|
+
const payload = await this.request(
|
|
2769
|
+
job.pollEndpoint ?? `/image/v1/status/${job.id}`
|
|
2770
|
+
);
|
|
2771
|
+
const raw = (payload.status ?? "").toLowerCase();
|
|
2772
|
+
if (raw === "failed" || raw === "cancelled") {
|
|
2773
|
+
return {
|
|
2774
|
+
status: "failed",
|
|
2775
|
+
error: `Topaz image ${raw}.`
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
if (raw !== "completed") return { status: "pending" };
|
|
2779
|
+
let url = payload.download_url;
|
|
2780
|
+
let expiresAt = payload.eta ? new Date(payload.eta > 1e10 ? payload.eta : payload.eta * 1e3).toISOString() : void 0;
|
|
2781
|
+
if (!url) {
|
|
2782
|
+
const download = await this.request(
|
|
2783
|
+
`/image/v1/download/${job.id}`
|
|
2784
|
+
);
|
|
2785
|
+
url = download.download_url;
|
|
2786
|
+
if (download.expiry) {
|
|
2787
|
+
expiresAt = new Date(
|
|
2788
|
+
download.expiry > 1e10 ? download.expiry : download.expiry * 1e3
|
|
2789
|
+
).toISOString();
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
if (!url) return { status: "pending" };
|
|
2793
|
+
return {
|
|
2794
|
+
status: "succeeded",
|
|
2795
|
+
result: {
|
|
2796
|
+
success: true,
|
|
2797
|
+
outputs: [
|
|
2798
|
+
{
|
|
2799
|
+
url,
|
|
2800
|
+
mimeType: "image/jpeg",
|
|
2801
|
+
taskId: job.id,
|
|
2802
|
+
expiresAt: expiresAt ?? expiresInHours(1),
|
|
2803
|
+
raw: {
|
|
2804
|
+
status: payload.status,
|
|
2805
|
+
progress: payload.progress,
|
|
2806
|
+
model: payload.model
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
],
|
|
2810
|
+
creditsUsed: payload.credits ?? 0,
|
|
2811
|
+
provider: this.providerName,
|
|
2812
|
+
model: job.model,
|
|
2813
|
+
processingTimeMs: 0
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
async runImage(params) {
|
|
2818
|
+
const startedAt = Date.now();
|
|
2819
|
+
let modelId = params.model ?? "(none)";
|
|
2820
|
+
const failure = (error) => ({
|
|
2821
|
+
success: false,
|
|
2822
|
+
outputs: [],
|
|
2823
|
+
creditsUsed: 0,
|
|
2824
|
+
provider: this.providerName,
|
|
2825
|
+
model: modelId,
|
|
2826
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2827
|
+
error
|
|
2828
|
+
});
|
|
2829
|
+
let submitted;
|
|
2830
|
+
try {
|
|
2831
|
+
modelId = this.binding(params.model)[0];
|
|
2832
|
+
submitted = await this.submitImage(params);
|
|
2833
|
+
} catch (error) {
|
|
2834
|
+
if (isPendingJob(error)) throw error;
|
|
2835
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2836
|
+
}
|
|
2837
|
+
if (isSubmitMode()) {
|
|
2838
|
+
throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
|
|
2839
|
+
}
|
|
2840
|
+
const job = {
|
|
2841
|
+
id: submitted.taskId,
|
|
2842
|
+
provider: this.providerName,
|
|
2843
|
+
model: modelId,
|
|
2844
|
+
kind: "image",
|
|
2845
|
+
pollEndpoint: submitted.pollEndpoint,
|
|
2846
|
+
params,
|
|
2847
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
2848
|
+
};
|
|
2849
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
|
|
2850
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2851
|
+
const snapshot = await this.completeJob(job);
|
|
2852
|
+
if (snapshot.status === "failed") {
|
|
2853
|
+
return failure(snapshot.error ?? "Topaz image failed.");
|
|
2854
|
+
}
|
|
2855
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2856
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2857
|
+
}
|
|
2858
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
|
|
2859
|
+
}
|
|
2860
|
+
return failure(
|
|
2861
|
+
`Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
generateImage(params) {
|
|
2865
|
+
return this.runImage(params);
|
|
2866
|
+
}
|
|
2867
|
+
generateText(params) {
|
|
2868
|
+
return Promise.resolve(unsupported(this.providerName, params.model, "text"));
|
|
2869
|
+
}
|
|
2870
|
+
generateAudio(params) {
|
|
2871
|
+
return Promise.resolve(unsupported(this.providerName, params.model, "audio"));
|
|
2872
|
+
}
|
|
2873
|
+
estimateCost(type, params) {
|
|
2874
|
+
return Promise.resolve(estimateFor(this.providerName, type, params));
|
|
2875
|
+
}
|
|
2876
|
+
supportsModel(model) {
|
|
2877
|
+
return model in TOPAZ_MODELS;
|
|
2878
|
+
}
|
|
2879
|
+
getAvailableModels() {
|
|
2880
|
+
return TOPAZ_CATALOG.map((model) => ({
|
|
2881
|
+
id: model.id,
|
|
2882
|
+
name: model.name,
|
|
2883
|
+
type: model.category
|
|
2884
|
+
}));
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
function numberish(value) {
|
|
2888
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2889
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2890
|
+
const parsed = Number(value);
|
|
2891
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2892
|
+
}
|
|
2893
|
+
return void 0;
|
|
2894
|
+
}
|
|
2895
|
+
function unsupported(provider, model, kind) {
|
|
2896
|
+
return {
|
|
2897
|
+
success: false,
|
|
2898
|
+
outputs: [],
|
|
2899
|
+
creditsUsed: 0,
|
|
2900
|
+
provider,
|
|
2901
|
+
model: model ?? "",
|
|
2902
|
+
processingTimeMs: 0,
|
|
2903
|
+
error: `Topaz does not generate ${kind} on this adapter.`
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2428
2907
|
// src/helpers/result.ts
|
|
2429
2908
|
function ok(data) {
|
|
2430
2909
|
return { data, error: null };
|
|
@@ -2625,7 +3104,8 @@ var NATIVE_PROVIDERS = [
|
|
|
2625
3104
|
"google",
|
|
2626
3105
|
"kling",
|
|
2627
3106
|
"openai",
|
|
2628
|
-
"qwen"
|
|
3107
|
+
"qwen",
|
|
3108
|
+
"topaz"
|
|
2629
3109
|
];
|
|
2630
3110
|
function brotu(options) {
|
|
2631
3111
|
const apiKey = options.apiKey?.trim();
|
|
@@ -2724,6 +3204,12 @@ function brotu(options) {
|
|
|
2724
3204
|
baseUrl: provider.baseUrl
|
|
2725
3205
|
});
|
|
2726
3206
|
}
|
|
3207
|
+
if (provider.id === "topaz") {
|
|
3208
|
+
return new TopazAdapter({
|
|
3209
|
+
apiKey: provider.apiKey,
|
|
3210
|
+
baseUrl: provider.baseUrl
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
2727
3213
|
return void 0;
|
|
2728
3214
|
}
|
|
2729
3215
|
function generateWith(adapter, kind, params) {
|
|
@@ -2758,6 +3244,7 @@ function brotu(options) {
|
|
|
2758
3244
|
outputs: input.outputs,
|
|
2759
3245
|
error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
|
|
2760
3246
|
metadata: input.metadata ?? input.job?.metadata,
|
|
3247
|
+
creditsUsed: input.params?.credits ?? input.creditsUsed,
|
|
2761
3248
|
processingTimeMs: input.processingTimeMs,
|
|
2762
3249
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2763
3250
|
};
|
|
@@ -2771,6 +3258,7 @@ function brotu(options) {
|
|
|
2771
3258
|
outputs: payload.outputs,
|
|
2772
3259
|
error: payload.error,
|
|
2773
3260
|
metadata: payload.metadata,
|
|
3261
|
+
creditsUsed: payload.creditsUsed,
|
|
2774
3262
|
processingTimeMs: payload.processingTimeMs,
|
|
2775
3263
|
at: payload.completedAt
|
|
2776
3264
|
});
|
|
@@ -2787,7 +3275,7 @@ function brotu(options) {
|
|
|
2787
3275
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2788
3276
|
});
|
|
2789
3277
|
}
|
|
2790
|
-
async function toGeneration(raw, metadata) {
|
|
3278
|
+
async function toGeneration(raw, metadata, credits) {
|
|
2791
3279
|
if (!raw.success) {
|
|
2792
3280
|
return fail({
|
|
2793
3281
|
code: "provider_error",
|
|
@@ -2802,6 +3290,9 @@ function brotu(options) {
|
|
|
2802
3290
|
provider: raw.provider,
|
|
2803
3291
|
model: raw.model,
|
|
2804
3292
|
processingTimeMs: raw.processingTimeMs,
|
|
3293
|
+
// What you said to charge wins over what the platform reported: the
|
|
3294
|
+
// caller's number is the one their ledger has to match.
|
|
3295
|
+
creditsUsed: credits ?? raw.creditsUsed,
|
|
2805
3296
|
metadata
|
|
2806
3297
|
});
|
|
2807
3298
|
}
|
|
@@ -2812,7 +3303,8 @@ function brotu(options) {
|
|
|
2812
3303
|
try {
|
|
2813
3304
|
const result = await toGeneration(
|
|
2814
3305
|
await generateWith(routed.data.adapter, kind, params),
|
|
2815
|
-
params.metadata
|
|
3306
|
+
params.metadata,
|
|
3307
|
+
params.credits
|
|
2816
3308
|
);
|
|
2817
3309
|
if (result.error) {
|
|
2818
3310
|
await notifySettled({
|
|
@@ -2834,6 +3326,7 @@ function brotu(options) {
|
|
|
2834
3326
|
model: result.data.model,
|
|
2835
3327
|
outputs: result.data.outputs,
|
|
2836
3328
|
metadata: result.data.metadata,
|
|
3329
|
+
creditsUsed: result.data.creditsUsed,
|
|
2837
3330
|
processingTimeMs: result.data.processingTimeMs
|
|
2838
3331
|
});
|
|
2839
3332
|
return result;
|
|
@@ -2930,6 +3423,7 @@ function brotu(options) {
|
|
|
2930
3423
|
outputs: snapshot.result.outputs,
|
|
2931
3424
|
provider: snapshot.result.provider,
|
|
2932
3425
|
model: snapshot.result.model,
|
|
3426
|
+
creditsUsed: snapshot.result.creditsUsed,
|
|
2933
3427
|
processingTimeMs: snapshot.result.processingTimeMs,
|
|
2934
3428
|
metadata: job.metadata
|
|
2935
3429
|
});
|
|
@@ -2952,7 +3446,11 @@ function brotu(options) {
|
|
|
2952
3446
|
}
|
|
2953
3447
|
async function finalizeSnapshot(job, snapshot) {
|
|
2954
3448
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2955
|
-
const persisted = await toGeneration(
|
|
3449
|
+
const persisted = await toGeneration(
|
|
3450
|
+
snapshot.result,
|
|
3451
|
+
job.metadata,
|
|
3452
|
+
job.params.credits
|
|
3453
|
+
);
|
|
2956
3454
|
if (!persisted.error) {
|
|
2957
3455
|
snapshot = {
|
|
2958
3456
|
status: "succeeded",
|
|
@@ -3042,12 +3540,67 @@ function brotu(options) {
|
|
|
3042
3540
|
image: {
|
|
3043
3541
|
submit: (params) => submit("image", params),
|
|
3044
3542
|
generate: (params) => generate("image", params),
|
|
3045
|
-
list: () => listFor("image")
|
|
3543
|
+
list: () => listFor("image"),
|
|
3544
|
+
upscale: (params) => {
|
|
3545
|
+
if (!params.imageUrl?.trim()) {
|
|
3546
|
+
return Promise.resolve(
|
|
3547
|
+
fail({
|
|
3548
|
+
code: "invalid_request",
|
|
3549
|
+
message: "image.upscale needs imageUrl.",
|
|
3550
|
+
model: params.model
|
|
3551
|
+
})
|
|
3552
|
+
);
|
|
3553
|
+
}
|
|
3554
|
+
const model = getModel(params.model);
|
|
3555
|
+
if (model && !model.nodeTypes.includes("image_upscale")) {
|
|
3556
|
+
return Promise.resolve(
|
|
3557
|
+
fail({
|
|
3558
|
+
code: "invalid_request",
|
|
3559
|
+
message: `"${params.model}" is not an image upscale model.`,
|
|
3560
|
+
model: params.model
|
|
3561
|
+
})
|
|
3562
|
+
);
|
|
3563
|
+
}
|
|
3564
|
+
return generate("image", {
|
|
3565
|
+
...params,
|
|
3566
|
+
prompt: params.prompt ?? "",
|
|
3567
|
+
referenceImages: [
|
|
3568
|
+
params.imageUrl,
|
|
3569
|
+
...params.referenceImages ?? []
|
|
3570
|
+
]
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3046
3573
|
},
|
|
3047
3574
|
video: {
|
|
3048
3575
|
submit: (params) => submit("video", params),
|
|
3049
3576
|
generate: (params) => generate("video", params),
|
|
3050
|
-
list: () => listFor("video")
|
|
3577
|
+
list: () => listFor("video"),
|
|
3578
|
+
upscale: (params) => {
|
|
3579
|
+
if (!params.videoUrl?.trim()) {
|
|
3580
|
+
return Promise.resolve(
|
|
3581
|
+
fail({
|
|
3582
|
+
code: "invalid_request",
|
|
3583
|
+
message: "video.upscale needs videoUrl.",
|
|
3584
|
+
model: params.model
|
|
3585
|
+
})
|
|
3586
|
+
);
|
|
3587
|
+
}
|
|
3588
|
+
const model = getModel(params.model);
|
|
3589
|
+
if (model && !model.nodeTypes.includes("video_upscale")) {
|
|
3590
|
+
return Promise.resolve(
|
|
3591
|
+
fail({
|
|
3592
|
+
code: "invalid_request",
|
|
3593
|
+
message: `"${params.model}" is not a video upscale model. Interpolation models use ai.video.submit.`,
|
|
3594
|
+
model: params.model
|
|
3595
|
+
})
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
return submit("video", {
|
|
3599
|
+
...params,
|
|
3600
|
+
prompt: params.prompt ?? "",
|
|
3601
|
+
videoUrl: params.videoUrl
|
|
3602
|
+
});
|
|
3603
|
+
}
|
|
3051
3604
|
},
|
|
3052
3605
|
text: {
|
|
3053
3606
|
submit: (params) => submit("text", params),
|
|
@@ -3084,6 +3637,7 @@ function brotu(options) {
|
|
|
3084
3637
|
provider: snapshot.result.provider,
|
|
3085
3638
|
model: snapshot.result.model,
|
|
3086
3639
|
processingTimeMs: snapshot.result.processingTimeMs,
|
|
3640
|
+
creditsUsed: job.params.credits ?? snapshot.result.creditsUsed,
|
|
3087
3641
|
metadata: job.metadata
|
|
3088
3642
|
});
|
|
3089
3643
|
}
|
|
@@ -3155,6 +3709,11 @@ export {
|
|
|
3155
3709
|
QWEN_TEXT_MODELS,
|
|
3156
3710
|
QWEN_VIDEO_MODELS,
|
|
3157
3711
|
QwenAdapter,
|
|
3712
|
+
TOPAZ_CATALOG,
|
|
3713
|
+
TOPAZ_IMAGE_MODELS,
|
|
3714
|
+
TOPAZ_MODELS,
|
|
3715
|
+
TOPAZ_VIDEO_MODELS,
|
|
3716
|
+
TopazAdapter,
|
|
3158
3717
|
brotu,
|
|
3159
3718
|
createS3Storage,
|
|
3160
3719
|
deliverWebhook,
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface HookEvent {
|
|
|
12
12
|
outputs?: GenerationOutput[];
|
|
13
13
|
error?: Pick<AIError, "code" | "message">;
|
|
14
14
|
metadata?: Record<string, string>;
|
|
15
|
+
/** Credits to charge for this generation, as `Generation.creditsUsed`. */
|
|
16
|
+
creditsUsed?: number;
|
|
15
17
|
processingTimeMs?: number;
|
|
16
18
|
at: string;
|
|
17
19
|
}
|