@brotu/ai 0.6.0 → 0.8.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
@@ -50,6 +50,7 @@ __export(index_exports, {
50
50
  KLING_CAPABILITIES: () => KLING_CAPABILITIES,
51
51
  KLING_CATALOG: () => KLING_CATALOG,
52
52
  KLING_MODELS: () => KLING_MODELS,
53
+ KieAdapter: () => KieAdapter,
53
54
  KlingAdapter: () => KlingAdapter,
54
55
  OPENAI_CATALOG: () => OPENAI_CATALOG,
55
56
  OPENAI_IMAGE_MODELS: () => OPENAI_IMAGE_MODELS,
@@ -78,7 +79,9 @@ __export(index_exports, {
78
79
  getProviders: () => getProviders,
79
80
  hasModel: () => hasModel,
80
81
  isPendingJob: () => isPendingJob,
82
+ kieSnapshot: () => kieSnapshot,
81
83
  ok: () => ok,
84
+ parseKieCallback: () => parseKieCallback,
82
85
  persistOutputs: () => persistOutputs,
83
86
  registerModels: () => registerModels,
84
87
  resetCatalog: () => resetCatalog,
@@ -1349,6 +1352,7 @@ var PROVIDER_BASE_URLS = {
1349
1352
  openai: "https://api.openai.com",
1350
1353
  qwen: "https://dashscope-intl.aliyuncs.com",
1351
1354
  topaz: "https://api.topazlabs.com",
1355
+ kie: "https://api.kie.ai/api/v1",
1352
1356
  brotu: "https://api.brotu.app"
1353
1357
  };
1354
1358
  var DEFAULT_BROTU_API_URL = PROVIDER_BASE_URLS.brotu;
@@ -1424,6 +1428,14 @@ function resolveProvider(modelId, options) {
1424
1428
  }
1425
1429
  return { id, apiKey: configured.apiKey, baseUrl: baseUrl.replace(/\/$/, "") };
1426
1430
  }
1431
+ const kie = vendorProviders(options).kie;
1432
+ if (kie && id !== "kie") {
1433
+ return {
1434
+ id: "kie",
1435
+ apiKey: kie.apiKey,
1436
+ baseUrl: (kie.baseUrl ?? PROVIDER_BASE_URLS.kie).replace(/\/$/, "")
1437
+ };
1438
+ }
1427
1439
  const platformKey = brotuApiKey(options);
1428
1440
  if (platformKey) {
1429
1441
  return {
@@ -1521,9 +1533,14 @@ function estimateFor(provider, type, params, defaults) {
1521
1533
  }
1522
1534
  const resolution = params.resolution;
1523
1535
  const rate = (resolution ? model?.pricing?.byResolution?.[resolution] : void 0) ?? model?.pricing?.usdPerUnit;
1536
+ const tier = model?.runtimePricingTiers?.find(
1537
+ (candidate) => (candidate.resolution === void 0 || candidate.resolution === resolution) && (candidate.durationSeconds === void 0 || candidate.durationSeconds === units)
1538
+ );
1539
+ const creditsPerUnit = tier?.creditsPerUnit ?? model?.creditsPerUnit;
1524
1540
  return {
1525
1541
  unit,
1526
1542
  units,
1543
+ credits: creditsPerUnit ? creditsPerUnit * units : null,
1527
1544
  usd: rate === void 0 || unit === "token" ? null : Number((rate * units).toFixed(4)),
1528
1545
  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,
1529
1546
  provider,
@@ -1797,6 +1814,7 @@ var BrotuAdapter = class {
1797
1814
  return {
1798
1815
  unit: type === "video" ? "second" : "image",
1799
1816
  units: credits,
1817
+ credits,
1800
1818
  usd: null,
1801
1819
  note: `${credits} Brotu credit${credits === 1 ? "" : "s"}. A vendor key generates on that provider.`,
1802
1820
  provider: this.providerName,
@@ -2821,10 +2839,383 @@ var GoogleAdapter = class {
2821
2839
  }
2822
2840
  };
2823
2841
 
2842
+ // src/lib/webhook.ts
2843
+ function resolveWebhook(value) {
2844
+ if (!value) return void 0;
2845
+ if (typeof value === "string") {
2846
+ return isHttpUrl(value) ? { url: value } : void 0;
2847
+ }
2848
+ if (!isHttpUrl(value.url)) return void 0;
2849
+ return {
2850
+ url: value.url,
2851
+ secret: value.secret,
2852
+ headers: value.headers
2853
+ };
2854
+ }
2855
+ function isHttpUrl(value) {
2856
+ try {
2857
+ const parsed = new URL(value);
2858
+ return parsed.protocol === "https:" || parsed.protocol === "http:";
2859
+ } catch {
2860
+ return false;
2861
+ }
2862
+ }
2863
+ async function deliverWebhook(config, payload) {
2864
+ try {
2865
+ await fetch(config.url, {
2866
+ method: "POST",
2867
+ headers: {
2868
+ "content-type": "application/json",
2869
+ "user-agent": "@brotu/ai",
2870
+ "x-brotu-event": payload.event,
2871
+ ...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
2872
+ ...config.headers
2873
+ },
2874
+ body: JSON.stringify(payload),
2875
+ signal: AbortSignal.timeout(1e4)
2876
+ });
2877
+ } catch {
2878
+ }
2879
+ }
2880
+
2881
+ // src/adapters/kie.adapter.ts
2882
+ var DEFAULT_BASE_URL4 = "https://api.kie.ai/api/v1";
2883
+ var POLL_INTERVAL_MS4 = 3e3;
2884
+ var DEFAULT_MAX_POLL_ATTEMPTS4 = 400;
2885
+ var KIE_MODEL_IDS = {
2886
+ "kling/v2-6": "kling-2.6/{mode}-to-video",
2887
+ "kling/v3": "kling-3.0/video",
2888
+ "dreamina-seedance-2-5-260628": "bytedance/seedance-2-5",
2889
+ "dreamina-seedance-2-0-260128": "bytedance/seedance-2",
2890
+ "dreamina-seedance-2-0-fast-260128": "bytedance/seedance-2-fast",
2891
+ "dreamina-seedance-2-0-mini-260615": "bytedance/seedance-2-mini",
2892
+ "wan2.6-t2v": "wan/2-6-text-to-video",
2893
+ "wan2.6-i2v": "wan/2-6-image-to-video",
2894
+ "wan2.7-t2v": "wan/2-7-text-to-video",
2895
+ "wan2.7-i2v": "wan/2-7-image-to-video",
2896
+ "gpt-image-1.5": "gpt-image/1.5-{mode}-to-image",
2897
+ "gpt-image-2": "gpt-image-2-{mode}-to-image",
2898
+ "topaz/video-upscale": "topaz/video-upscale"
2899
+ };
2900
+ var KieAdapter = class {
2901
+ constructor(opts) {
2902
+ this.opts = opts;
2903
+ }
2904
+ opts;
2905
+ providerName = "kie";
2906
+ supportedTypes = [
2907
+ "image",
2908
+ "video",
2909
+ "text",
2910
+ "audio"
2911
+ ];
2912
+ get origin() {
2913
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL4).replace(/\/$/, "");
2914
+ }
2915
+ async request(path, init) {
2916
+ const response = await fetch(`${this.origin}${path}`, {
2917
+ ...init,
2918
+ headers: {
2919
+ Authorization: `Bearer ${this.opts.apiKey}`,
2920
+ "Content-Type": "application/json",
2921
+ ...init?.headers
2922
+ }
2923
+ });
2924
+ const text = await response.text();
2925
+ let body;
2926
+ try {
2927
+ body = text ? JSON.parse(text) : void 0;
2928
+ } catch {
2929
+ body = void 0;
2930
+ }
2931
+ const code = body?.code;
2932
+ if (!response.ok || typeof code === "number" && code !== 200) {
2933
+ const message = body?.msg || body?.message || text;
2934
+ throw new Error(message || `kie API ${response.status}`);
2935
+ }
2936
+ if (body?.data === void 0) {
2937
+ throw new Error("kie answered without a payload.");
2938
+ }
2939
+ return body.data;
2940
+ }
2941
+ /** Which market model serves this request. */
2942
+ marketModel(modelId, params) {
2943
+ const fromCatalog = getModel(modelId)?.endpoint?.replace(
2944
+ /^\/market\//,
2945
+ ""
2946
+ );
2947
+ const mapped = fromCatalog || KIE_MODEL_IDS[modelId] || modelId;
2948
+ if (!mapped.includes("{mode}")) return mapped;
2949
+ return mapped.replace("{mode}", hasInputImage(params) ? "image" : "text");
2950
+ }
2951
+ callbackFor(params) {
2952
+ const perRequest = resolveWebhook(params.webhook);
2953
+ return perRequest?.url ?? this.opts.callbackUrl;
2954
+ }
2955
+ async createTask(kind, params) {
2956
+ const modelId = params.model ?? "";
2957
+ const body = {
2958
+ model: this.marketModel(modelId, params),
2959
+ input: buildInput(kind, params, modelId)
2960
+ };
2961
+ const callBackUrl = this.callbackFor(params);
2962
+ if (callBackUrl) body.callBackUrl = callBackUrl;
2963
+ const data = await this.request("/jobs/createTask", {
2964
+ method: "POST",
2965
+ body: JSON.stringify(body)
2966
+ });
2967
+ const taskId = data.taskId?.trim();
2968
+ if (!taskId) {
2969
+ throw new Error("kie accepted the task but returned no taskId.");
2970
+ }
2971
+ return taskId;
2972
+ }
2973
+ /** Ask kie about one task. The same shape a callback carries. */
2974
+ async task(taskId) {
2975
+ const record = await this.request(
2976
+ `/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`
2977
+ );
2978
+ return readTaskRecord(record, taskId);
2979
+ }
2980
+ async run(kind, params) {
2981
+ const startedAt = Date.now();
2982
+ const modelId = params.model ?? "(none)";
2983
+ const failure = (error) => ({
2984
+ success: false,
2985
+ outputs: [],
2986
+ creditsUsed: 0,
2987
+ provider: this.providerName,
2988
+ model: modelId,
2989
+ processingTimeMs: Date.now() - startedAt,
2990
+ error
2991
+ });
2992
+ let taskId;
2993
+ try {
2994
+ taskId = await this.createTask(kind, params);
2995
+ } catch (error) {
2996
+ return failure(error instanceof Error ? error.message : String(error));
2997
+ }
2998
+ const pollEndpoint = `/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`;
2999
+ if (isSubmitMode()) {
3000
+ throw new PendingJob(taskId, pollEndpoint);
3001
+ }
3002
+ const job = {
3003
+ id: taskId,
3004
+ provider: this.providerName,
3005
+ model: modelId,
3006
+ kind,
3007
+ pollEndpoint,
3008
+ params,
3009
+ submittedAt: new Date(startedAt).toISOString()
3010
+ };
3011
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
3012
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
3013
+ const snapshot = await this.completeJob(job);
3014
+ if (snapshot.status === "failed") {
3015
+ return failure(snapshot.error ?? "kie task failed.");
3016
+ }
3017
+ if (snapshot.status === "succeeded" && snapshot.result) {
3018
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
3019
+ }
3020
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
3021
+ }
3022
+ return failure(
3023
+ `kie task ${taskId} did not finish after ${maxAttempts} checks.`
3024
+ );
3025
+ }
3026
+ async completeJob(job) {
3027
+ const settled = await this.task(job.id);
3028
+ return snapshotFrom(settled, job.model);
3029
+ }
3030
+ generateImage(params) {
3031
+ return this.run("image", params);
3032
+ }
3033
+ generateVideo(params) {
3034
+ return this.run("video", params);
3035
+ }
3036
+ generateText(params) {
3037
+ return this.run("text", params);
3038
+ }
3039
+ generateAudio(params) {
3040
+ return this.run("audio", params);
3041
+ }
3042
+ async estimateCost(type, params) {
3043
+ return estimateFor(this.providerName, type, params);
3044
+ }
3045
+ supportsModel() {
3046
+ return true;
3047
+ }
3048
+ getAvailableModels() {
3049
+ return [];
3050
+ }
3051
+ };
3052
+ function parseKieCallback(body) {
3053
+ let parsed = body;
3054
+ if (typeof body === "string") {
3055
+ try {
3056
+ parsed = JSON.parse(body);
3057
+ } catch {
3058
+ throw new Error("kie callback body is not JSON.");
3059
+ }
3060
+ }
3061
+ const root = asRecord(parsed);
3062
+ const record = asRecord(root?.data) ?? root ?? {};
3063
+ const taskId = record.taskId?.trim();
3064
+ if (!taskId) {
3065
+ throw new Error("kie callback carries no taskId.");
3066
+ }
3067
+ return readTaskRecord(record, taskId);
3068
+ }
3069
+ function kieSnapshot(settled, model) {
3070
+ return snapshotFrom(settled, model ?? settled.model ?? "");
3071
+ }
3072
+ function snapshotFrom(settled, model) {
3073
+ if (settled.status === "failed") {
3074
+ return { status: "failed", error: settled.error ?? "kie task failed." };
3075
+ }
3076
+ if (settled.status === "pending") return { status: "pending" };
3077
+ return {
3078
+ status: "succeeded",
3079
+ result: {
3080
+ success: true,
3081
+ outputs: settled.outputs,
3082
+ creditsUsed: settled.creditsUsed,
3083
+ provider: "kie",
3084
+ model,
3085
+ processingTimeMs: 0
3086
+ }
3087
+ };
3088
+ }
3089
+ function readTaskRecord(record, taskId) {
3090
+ const state = (record.state ?? "").toLowerCase();
3091
+ const creditsUsed = record.creditsConsumed ?? 0;
3092
+ if (state === "fail") {
3093
+ return {
3094
+ taskId,
3095
+ model: record.model,
3096
+ status: "failed",
3097
+ outputs: [],
3098
+ creditsUsed,
3099
+ error: record.failMsg || (record.failCode ? `kie failCode ${record.failCode}` : void 0) || "kie task failed."
3100
+ };
3101
+ }
3102
+ if (state !== "success") {
3103
+ return {
3104
+ taskId,
3105
+ model: record.model,
3106
+ status: "pending",
3107
+ outputs: [],
3108
+ creditsUsed
3109
+ };
3110
+ }
3111
+ return {
3112
+ taskId,
3113
+ model: record.model,
3114
+ status: "succeeded",
3115
+ outputs: outputsFrom2(record.resultJson),
3116
+ creditsUsed
3117
+ };
3118
+ }
3119
+ function outputsFrom2(resultJson) {
3120
+ if (!resultJson) return [];
3121
+ let parsed;
3122
+ try {
3123
+ parsed = JSON.parse(resultJson);
3124
+ } catch {
3125
+ return [];
3126
+ }
3127
+ const record = asRecord(parsed);
3128
+ const urls = Array.isArray(record?.resultUrls) ? record.resultUrls.filter(
3129
+ (url) => typeof url === "string" && url.length > 0
3130
+ ) : [];
3131
+ if (urls.length > 0) return urls.map((url) => ({ url, mimeType: mimeFor(url) }));
3132
+ const object = record?.resultObject;
3133
+ if (object !== void 0) {
3134
+ const text = typeof object === "string" ? object : JSON.stringify(object);
3135
+ return [
3136
+ {
3137
+ url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
3138
+ mimeType: "text/plain",
3139
+ raw: { text }
3140
+ }
3141
+ ];
3142
+ }
3143
+ return [];
3144
+ }
3145
+ function mimeFor(url) {
3146
+ const path = url.split("?")[0]?.toLowerCase() ?? "";
3147
+ if (/\.(mp4|mov|webm|m4v)$/.test(path)) return "video/mp4";
3148
+ if (/\.(mp3|wav|m4a|aac)$/.test(path)) return "audio/mpeg";
3149
+ if (/\.jpe?g$/.test(path)) return "image/jpeg";
3150
+ if (/\.webp$/.test(path)) return "image/webp";
3151
+ return "image/png";
3152
+ }
3153
+ function asRecord(value) {
3154
+ return value && typeof value === "object" ? value : void 0;
3155
+ }
3156
+ function hasInputImage(params) {
3157
+ const p = params;
3158
+ return Boolean(
3159
+ p.imageUrl || p.imageUrls?.length || p.referenceImages?.length
3160
+ );
3161
+ }
3162
+ function buildInput(kind, params, modelId) {
3163
+ const raw = params.providerOptions?.kie ?? {};
3164
+ const base = modelId.startsWith("kling/") ? klingInput(params) : modelId.includes("seedance") ? seedanceInput(params) : commonInput(kind, params);
3165
+ return prune({ ...base, ...raw });
3166
+ }
3167
+ function klingInput(params) {
3168
+ const p = params;
3169
+ return {
3170
+ prompt: p.prompt,
3171
+ negative_prompt: p.negativePrompt,
3172
+ aspect_ratio: p.aspectRatio,
3173
+ // kie's kling enum is a string, unlike every other family.
3174
+ duration: p.duration === void 0 ? void 0 : String(p.duration),
3175
+ sound: p.withAudio,
3176
+ image_url: p.imageUrl ?? p.referenceImages?.[0]
3177
+ };
3178
+ }
3179
+ function seedanceInput(params) {
3180
+ const p = params;
3181
+ return {
3182
+ prompt: p.prompt,
3183
+ duration: p.duration,
3184
+ resolution: p.resolution,
3185
+ aspect_ratio: p.aspectRatio,
3186
+ first_frame_url: p.imageUrl ?? p.referenceImages?.[0],
3187
+ last_frame_url: p.referenceImages?.[1],
3188
+ reference_image_urls: p.imageUrls,
3189
+ reference_video_urls: p.videoUrls ?? (p.videoUrl ? [p.videoUrl] : void 0),
3190
+ generate_audio: p.withAudio
3191
+ };
3192
+ }
3193
+ function commonInput(kind, params) {
3194
+ const p = params;
3195
+ const images = p.imageUrls ?? p.referenceImages;
3196
+ return {
3197
+ prompt: p.prompt,
3198
+ negative_prompt: p.negativePrompt,
3199
+ aspect_ratio: p.aspectRatio,
3200
+ resolution: p.resolution,
3201
+ duration: kind === "video" ? p.duration : void 0,
3202
+ seed: p.seed,
3203
+ output_format: p.outputFormat,
3204
+ image_url: p.imageUrl ?? images?.[0],
3205
+ image_urls: images && images.length > 1 ? images : void 0,
3206
+ video_url: p.videoUrl
3207
+ };
3208
+ }
3209
+ function prune(input) {
3210
+ return Object.fromEntries(
3211
+ Object.entries(input).filter(([, value]) => value !== void 0)
3212
+ );
3213
+ }
3214
+
2824
3215
  // src/adapters/kling.adapter.ts
2825
- var DEFAULT_BASE_URL4 = "https://api-singapore.klingai.com";
2826
- var POLL_INTERVAL_MS4 = 5e3;
2827
- var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
3216
+ var DEFAULT_BASE_URL5 = "https://api-singapore.klingai.com";
3217
+ var POLL_INTERVAL_MS5 = 5e3;
3218
+ var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
2828
3219
  function stateOf(task) {
2829
3220
  const raw = (task.status ?? task.task_status ?? "").toLowerCase();
2830
3221
  if (raw === "failed") return "failed";
@@ -2839,7 +3230,7 @@ var KlingAdapter = class {
2839
3230
  this.opts = opts;
2840
3231
  }
2841
3232
  get baseUrl() {
2842
- return (this.opts.baseUrl ?? DEFAULT_BASE_URL4).replace(/\/$/, "");
3233
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL5).replace(/\/$/, "");
2843
3234
  }
2844
3235
  async request(path, init) {
2845
3236
  const response = await fetch(`${this.baseUrl}${path}`, {
@@ -3021,7 +3412,7 @@ var KlingAdapter = class {
3021
3412
  params,
3022
3413
  submittedAt: new Date(startedAt).toISOString()
3023
3414
  };
3024
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
3415
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
3025
3416
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
3026
3417
  const snapshot = await this.completeJob(job);
3027
3418
  if (snapshot.status === "failed") {
@@ -3030,7 +3421,7 @@ var KlingAdapter = class {
3030
3421
  if (snapshot.status === "succeeded" && snapshot.result) {
3031
3422
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
3032
3423
  }
3033
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
3424
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
3034
3425
  }
3035
3426
  return failure(
3036
3427
  `Kling task ${submitted.taskId} did not finish after ${maxAttempts} checks.`
@@ -3167,7 +3558,7 @@ var KlingAdapter = class {
3167
3558
  };
3168
3559
 
3169
3560
  // src/adapters/openai.adapter.ts
3170
- var DEFAULT_BASE_URL5 = "https://api.openai.com";
3561
+ var DEFAULT_BASE_URL6 = "https://api.openai.com";
3171
3562
  var IMAGES_PATH2 = "/v1/images/generations";
3172
3563
  var OpenAIAdapter = class {
3173
3564
  providerName = "openai";
@@ -3178,7 +3569,7 @@ var OpenAIAdapter = class {
3178
3569
  this.opts = opts;
3179
3570
  }
3180
3571
  get baseUrl() {
3181
- return (this.opts.baseUrl ?? DEFAULT_BASE_URL5).replace(/\/$/, "");
3572
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL6).replace(/\/$/, "");
3182
3573
  }
3183
3574
  binding(modelId) {
3184
3575
  const id = modelId ?? "";
@@ -3399,10 +3790,10 @@ var OpenAIAdapter = class {
3399
3790
  };
3400
3791
 
3401
3792
  // src/adapters/qwen.adapter.ts
3402
- var DEFAULT_BASE_URL6 = "https://dashscope-intl.aliyuncs.com";
3793
+ var DEFAULT_BASE_URL7 = "https://dashscope-intl.aliyuncs.com";
3403
3794
  var TASKS_PATH2 = "/api/v1/tasks";
3404
- var POLL_INTERVAL_MS5 = 5e3;
3405
- var DEFAULT_MAX_POLL_ATTEMPTS5 = 240;
3795
+ var POLL_INTERVAL_MS6 = 5e3;
3796
+ var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
3406
3797
  var QwenAdapter = class {
3407
3798
  providerName = "qwen";
3408
3799
  supportedTypes = ["image", "video"];
@@ -3411,7 +3802,7 @@ var QwenAdapter = class {
3411
3802
  this.opts = opts;
3412
3803
  }
3413
3804
  get baseUrl() {
3414
- return (this.opts.baseUrl ?? DEFAULT_BASE_URL6).replace(/\/$/, "");
3805
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
3415
3806
  }
3416
3807
  async request(path, init) {
3417
3808
  const headers = {
@@ -3609,7 +4000,7 @@ var QwenAdapter = class {
3609
4000
  params,
3610
4001
  submittedAt: new Date(startedAt).toISOString()
3611
4002
  };
3612
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS5;
4003
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
3613
4004
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
3614
4005
  const snapshot = await this.completeJob(job);
3615
4006
  if (snapshot.status === "failed") {
@@ -3618,7 +4009,7 @@ var QwenAdapter = class {
3618
4009
  if (snapshot.status === "succeeded" && snapshot.result) {
3619
4010
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
3620
4011
  }
3621
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS5));
4012
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
3622
4013
  }
3623
4014
  return failure(
3624
4015
  `DashScope task ${taskId} did not finish after ${maxAttempts} checks.`
@@ -3863,9 +4254,9 @@ var QwenAdapter = class {
3863
4254
  };
3864
4255
 
3865
4256
  // 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;
4257
+ var DEFAULT_BASE_URL8 = "https://api.topazlabs.com";
4258
+ var POLL_INTERVAL_MS7 = 5e3;
4259
+ var DEFAULT_MAX_POLL_ATTEMPTS7 = 240;
3869
4260
  var DEFAULT_FPS = 30;
3870
4261
  var PIXELS2 = {
3871
4262
  "720p": { width: 1280, height: 720 },
@@ -3885,7 +4276,7 @@ var TopazAdapter = class {
3885
4276
  this.opts = opts;
3886
4277
  }
3887
4278
  get baseUrl() {
3888
- return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
4279
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL8).replace(/\/$/, "");
3889
4280
  }
3890
4281
  get headers() {
3891
4282
  return {
@@ -4098,7 +4489,7 @@ var TopazAdapter = class {
4098
4489
  params,
4099
4490
  submittedAt: new Date(startedAt).toISOString()
4100
4491
  };
4101
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
4492
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS7;
4102
4493
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
4103
4494
  const snapshot = await this.completeJob(job);
4104
4495
  if (snapshot.status === "failed") {
@@ -4107,7 +4498,7 @@ var TopazAdapter = class {
4107
4498
  if (snapshot.status === "succeeded" && snapshot.result) {
4108
4499
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
4109
4500
  }
4110
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
4501
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS7));
4111
4502
  }
4112
4503
  return failure(
4113
4504
  `Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
@@ -4273,7 +4664,7 @@ var TopazAdapter = class {
4273
4664
  params,
4274
4665
  submittedAt: new Date(startedAt).toISOString()
4275
4666
  };
4276
- const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
4667
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS7;
4277
4668
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
4278
4669
  const snapshot = await this.completeJob(job);
4279
4670
  if (snapshot.status === "failed") {
@@ -4282,7 +4673,7 @@ var TopazAdapter = class {
4282
4673
  if (snapshot.status === "succeeded" && snapshot.result) {
4283
4674
  return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
4284
4675
  }
4285
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
4676
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS7));
4286
4677
  }
4287
4678
  return failure(
4288
4679
  `Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
@@ -4485,50 +4876,12 @@ async function persistOutputs(storage, outputs) {
4485
4876
  );
4486
4877
  }
4487
4878
 
4488
- // src/lib/webhook.ts
4489
- function resolveWebhook(value) {
4490
- if (!value) return void 0;
4491
- if (typeof value === "string") {
4492
- return isHttpUrl(value) ? { url: value } : void 0;
4493
- }
4494
- if (!isHttpUrl(value.url)) return void 0;
4495
- return {
4496
- url: value.url,
4497
- secret: value.secret,
4498
- headers: value.headers
4499
- };
4500
- }
4501
- function isHttpUrl(value) {
4502
- try {
4503
- const parsed = new URL(value);
4504
- return parsed.protocol === "https:" || parsed.protocol === "http:";
4505
- } catch {
4506
- return false;
4507
- }
4508
- }
4509
- async function deliverWebhook(config, payload) {
4510
- try {
4511
- await fetch(config.url, {
4512
- method: "POST",
4513
- headers: {
4514
- "content-type": "application/json",
4515
- "user-agent": "@brotu/ai",
4516
- "x-brotu-event": payload.event,
4517
- ...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
4518
- ...config.headers
4519
- },
4520
- body: JSON.stringify(payload),
4521
- signal: AbortSignal.timeout(1e4)
4522
- });
4523
- } catch {
4524
- }
4525
- }
4526
-
4527
4879
  // src/client.ts
4528
4880
  var NATIVE_PROVIDERS = [
4529
4881
  "byteplus",
4530
4882
  "elevenlabs",
4531
4883
  "google",
4884
+ "kie",
4532
4885
  "kling",
4533
4886
  "openai",
4534
4887
  "qwen",
@@ -4594,6 +4947,16 @@ function brotu(options) {
4594
4947
  workspaceId: optionsWithKey.workspaceId
4595
4948
  });
4596
4949
  }
4950
+ if (provider.id === "kie") {
4951
+ return new KieAdapter({
4952
+ apiKey: provider.apiKey,
4953
+ baseUrl: provider.baseUrl,
4954
+ // The client-level webhook doubles as kie's callback, so a submit
4955
+ // never has to be polled. kie posts its own payload there — read it
4956
+ // with `parseKieCallback`.
4957
+ callbackUrl: registeredWebhook?.url
4958
+ });
4959
+ }
4597
4960
  if (provider.id === "kling") {
4598
4961
  return new KlingAdapter({
4599
4962
  apiKey: provider.apiKey,
@@ -4671,6 +5034,7 @@ function brotu(options) {
4671
5034
  outputs: input.outputs,
4672
5035
  error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
4673
5036
  metadata: input.metadata ?? input.job?.metadata,
5037
+ creditsUsed: input.params?.credits ?? input.creditsUsed,
4674
5038
  processingTimeMs: input.processingTimeMs,
4675
5039
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
4676
5040
  };
@@ -4684,6 +5048,7 @@ function brotu(options) {
4684
5048
  outputs: payload.outputs,
4685
5049
  error: payload.error,
4686
5050
  metadata: payload.metadata,
5051
+ creditsUsed: payload.creditsUsed,
4687
5052
  processingTimeMs: payload.processingTimeMs,
4688
5053
  at: payload.completedAt
4689
5054
  });
@@ -4700,7 +5065,7 @@ function brotu(options) {
4700
5065
  at: (/* @__PURE__ */ new Date()).toISOString()
4701
5066
  });
4702
5067
  }
4703
- async function toGeneration(raw, metadata) {
5068
+ async function toGeneration(raw, metadata, credits) {
4704
5069
  if (!raw.success) {
4705
5070
  return fail({
4706
5071
  code: "provider_error",
@@ -4715,6 +5080,9 @@ function brotu(options) {
4715
5080
  provider: raw.provider,
4716
5081
  model: raw.model,
4717
5082
  processingTimeMs: raw.processingTimeMs,
5083
+ // What you said to charge wins over what the platform reported: the
5084
+ // caller's number is the one their ledger has to match.
5085
+ creditsUsed: credits ?? raw.creditsUsed,
4718
5086
  metadata
4719
5087
  });
4720
5088
  }
@@ -4725,7 +5093,8 @@ function brotu(options) {
4725
5093
  try {
4726
5094
  const result = await toGeneration(
4727
5095
  await generateWith(routed.data.adapter, kind, params),
4728
- params.metadata
5096
+ params.metadata,
5097
+ params.credits
4729
5098
  );
4730
5099
  if (result.error) {
4731
5100
  await notifySettled({
@@ -4747,6 +5116,7 @@ function brotu(options) {
4747
5116
  model: result.data.model,
4748
5117
  outputs: result.data.outputs,
4749
5118
  metadata: result.data.metadata,
5119
+ creditsUsed: result.data.creditsUsed,
4750
5120
  processingTimeMs: result.data.processingTimeMs
4751
5121
  });
4752
5122
  return result;
@@ -4843,6 +5213,7 @@ function brotu(options) {
4843
5213
  outputs: snapshot.result.outputs,
4844
5214
  provider: snapshot.result.provider,
4845
5215
  model: snapshot.result.model,
5216
+ creditsUsed: snapshot.result.creditsUsed,
4846
5217
  processingTimeMs: snapshot.result.processingTimeMs,
4847
5218
  metadata: job.metadata
4848
5219
  });
@@ -4865,7 +5236,11 @@ function brotu(options) {
4865
5236
  }
4866
5237
  async function finalizeSnapshot(job, snapshot) {
4867
5238
  if (snapshot.status === "succeeded" && snapshot.result) {
4868
- const persisted = await toGeneration(snapshot.result, job.metadata);
5239
+ const persisted = await toGeneration(
5240
+ snapshot.result,
5241
+ job.metadata,
5242
+ job.params.credits
5243
+ );
4869
5244
  if (!persisted.error) {
4870
5245
  snapshot = {
4871
5246
  status: "succeeded",
@@ -5052,6 +5427,7 @@ function brotu(options) {
5052
5427
  provider: snapshot.result.provider,
5053
5428
  model: snapshot.result.model,
5054
5429
  processingTimeMs: snapshot.result.processingTimeMs,
5430
+ creditsUsed: job.params.credits ?? snapshot.result.creditsUsed,
5055
5431
  metadata: job.metadata
5056
5432
  });
5057
5433
  }
@@ -5112,6 +5488,7 @@ function brotu(options) {
5112
5488
  KLING_CAPABILITIES,
5113
5489
  KLING_CATALOG,
5114
5490
  KLING_MODELS,
5491
+ KieAdapter,
5115
5492
  KlingAdapter,
5116
5493
  OPENAI_CATALOG,
5117
5494
  OPENAI_IMAGE_MODELS,
@@ -5140,7 +5517,9 @@ function brotu(options) {
5140
5517
  getProviders,
5141
5518
  hasModel,
5142
5519
  isPendingJob,
5520
+ kieSnapshot,
5143
5521
  ok,
5522
+ parseKieCallback,
5144
5523
  persistOutputs,
5145
5524
  registerModels,
5146
5525
  resetCatalog,