@brotu/ai 0.1.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 +387 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/adapters/byteplus.adapter.d.ts +51 -0
- package/dist/adapters/byteplus.adapter.d.ts.map +1 -0
- package/dist/adapters/elevenlabs.adapter.d.ts +48 -0
- package/dist/adapters/elevenlabs.adapter.d.ts.map +1 -0
- package/dist/adapters/estimate.d.ts +10 -0
- package/dist/adapters/estimate.d.ts.map +1 -0
- package/dist/adapters/google.adapter.d.ts +52 -0
- package/dist/adapters/google.adapter.d.ts.map +1 -0
- package/dist/adapters/kling.adapter.d.ts +57 -0
- package/dist/adapters/kling.adapter.d.ts.map +1 -0
- package/dist/adapters/openai.adapter.d.ts +40 -0
- package/dist/adapters/openai.adapter.d.ts.map +1 -0
- package/dist/adapters/qwen.adapter.d.ts +53 -0
- package/dist/adapters/qwen.adapter.d.ts.map +1 -0
- package/dist/catalog.cjs +988 -0
- package/dist/catalog.d.ts +27 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +20 -0
- package/dist/chunk-GKLTQ55S.js +1176 -0
- package/dist/client.d.ts +86 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/constants/model.types.d.ts +118 -0
- package/dist/constants/model.types.d.ts.map +1 -0
- package/dist/helpers/result.d.ts +54 -0
- package/dist/helpers/result.d.ts.map +1 -0
- package/dist/index.cjs +3568 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2386 -0
- package/dist/lib/jobs.d.ts +46 -0
- package/dist/lib/jobs.d.ts.map +1 -0
- package/dist/lib/storage.d.ts +33 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/ports/content-generator.port.d.ts +170 -0
- package/dist/ports/content-generator.port.d.ts.map +1 -0
- package/dist/providers/byteplus.models.d.ts +51 -0
- package/dist/providers/byteplus.models.d.ts.map +1 -0
- package/dist/providers/elevenlabs.models.d.ts +27 -0
- package/dist/providers/elevenlabs.models.d.ts.map +1 -0
- package/dist/providers/google.models.d.ts +42 -0
- package/dist/providers/google.models.d.ts.map +1 -0
- package/dist/providers/kling.models.d.ts +159 -0
- package/dist/providers/kling.models.d.ts.map +1 -0
- package/dist/providers/openai.models.d.ts +27 -0
- package/dist/providers/openai.models.d.ts.map +1 -0
- package/dist/providers/qwen.models.d.ts +88 -0
- package/dist/providers/qwen.models.d.ts.map +1 -0
- package/dist/types.d.ts +55 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +72 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3568 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BYTEPLUS_CATALOG: () => BYTEPLUS_CATALOG,
|
|
34
|
+
BYTEPLUS_MODELS: () => BYTEPLUS_MODELS,
|
|
35
|
+
BytePlusAdapter: () => BytePlusAdapter,
|
|
36
|
+
ELEVENLABS_CATALOG: () => ELEVENLABS_CATALOG,
|
|
37
|
+
ELEVENLABS_MODELS: () => ELEVENLABS_MODELS,
|
|
38
|
+
ELEVENLABS_OUTPUT_FORMATS: () => ELEVENLABS_OUTPUT_FORMATS,
|
|
39
|
+
ElevenLabsAdapter: () => ElevenLabsAdapter,
|
|
40
|
+
GOOGLE_CATALOG: () => GOOGLE_CATALOG,
|
|
41
|
+
GOOGLE_IMAGE_MODELS: () => GOOGLE_IMAGE_MODELS,
|
|
42
|
+
GOOGLE_VIDEO_MODELS: () => GOOGLE_VIDEO_MODELS,
|
|
43
|
+
GoogleAdapter: () => GoogleAdapter,
|
|
44
|
+
KLING_CAPABILITIES: () => KLING_CAPABILITIES,
|
|
45
|
+
KLING_CATALOG: () => KLING_CATALOG,
|
|
46
|
+
KLING_MODELS: () => KLING_MODELS,
|
|
47
|
+
KlingAdapter: () => KlingAdapter,
|
|
48
|
+
OPENAI_CATALOG: () => OPENAI_CATALOG,
|
|
49
|
+
OPENAI_IMAGE_MODELS: () => OPENAI_IMAGE_MODELS,
|
|
50
|
+
OpenAIAdapter: () => OpenAIAdapter,
|
|
51
|
+
PendingJob: () => PendingJob,
|
|
52
|
+
QWEN_AUDIO_MODELS: () => QWEN_AUDIO_MODELS,
|
|
53
|
+
QWEN_CATALOG: () => QWEN_CATALOG,
|
|
54
|
+
QWEN_IMAGE_MODELS: () => QWEN_IMAGE_MODELS,
|
|
55
|
+
QWEN_TEXT_MODELS: () => QWEN_TEXT_MODELS,
|
|
56
|
+
QWEN_VIDEO_MODELS: () => QWEN_VIDEO_MODELS,
|
|
57
|
+
QwenAdapter: () => QwenAdapter,
|
|
58
|
+
brotuClient: () => brotuClient,
|
|
59
|
+
createS3Storage: () => createS3Storage,
|
|
60
|
+
fail: () => fail,
|
|
61
|
+
getAvailableModels: () => getAvailableModels,
|
|
62
|
+
getModel: () => getModel,
|
|
63
|
+
getModels: () => getModels,
|
|
64
|
+
getProviders: () => getProviders,
|
|
65
|
+
hasModel: () => hasModel,
|
|
66
|
+
isPendingJob: () => isPendingJob,
|
|
67
|
+
ok: () => ok,
|
|
68
|
+
persistOutputs: () => persistOutputs,
|
|
69
|
+
registerModels: () => registerModels,
|
|
70
|
+
resetCatalog: () => resetCatalog,
|
|
71
|
+
resolveProvider: () => resolveProvider
|
|
72
|
+
});
|
|
73
|
+
module.exports = __toCommonJS(index_exports);
|
|
74
|
+
|
|
75
|
+
// src/lib/jobs.ts
|
|
76
|
+
var import_node_async_hooks = require("async_hooks");
|
|
77
|
+
var PendingJob = class extends Error {
|
|
78
|
+
taskId;
|
|
79
|
+
pollEndpoint;
|
|
80
|
+
constructor(taskId, pollEndpoint) {
|
|
81
|
+
super(`Job ${taskId} was submitted and is still running.`);
|
|
82
|
+
this.name = "PendingJob";
|
|
83
|
+
this.taskId = taskId;
|
|
84
|
+
this.pollEndpoint = pollEndpoint;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function isPendingJob(error) {
|
|
88
|
+
return error instanceof PendingJob;
|
|
89
|
+
}
|
|
90
|
+
var submitMode = new import_node_async_hooks.AsyncLocalStorage();
|
|
91
|
+
function runInSubmitMode(fn) {
|
|
92
|
+
return submitMode.run(true, fn);
|
|
93
|
+
}
|
|
94
|
+
function isSubmitMode() {
|
|
95
|
+
return submitMode.getStore() === true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/ports/content-generator.port.ts
|
|
99
|
+
function expiresInHours(hours) {
|
|
100
|
+
return new Date(Date.now() + hours * 36e5).toISOString();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/providers/byteplus.models.ts
|
|
104
|
+
var FPS = 24;
|
|
105
|
+
var PIXELS = {
|
|
106
|
+
"480p": [854, 480],
|
|
107
|
+
"720p": [1280, 720],
|
|
108
|
+
"1080p": [1920, 1080],
|
|
109
|
+
"4k": [3840, 2160]
|
|
110
|
+
};
|
|
111
|
+
function usdPerSecond(usdPerMillionTokens) {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const [resolution, rate] of Object.entries(usdPerMillionTokens)) {
|
|
114
|
+
const [w, h] = PIXELS[resolution] ?? [0, 0];
|
|
115
|
+
const tokensPerSecond = w * h * FPS / 1024;
|
|
116
|
+
out[resolution] = Number((tokensPerSecond * rate / 1e6).toFixed(4));
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
var TOKEN_RATES = {
|
|
121
|
+
"dreamina-seedance-2-5-260628": { "480p": 10.7, "720p": 10.7, "1080p": 11.7 },
|
|
122
|
+
"dreamina-seedance-2-0-260128": {
|
|
123
|
+
"480p": 7,
|
|
124
|
+
"720p": 7,
|
|
125
|
+
"1080p": 7.7,
|
|
126
|
+
"4k": 4
|
|
127
|
+
},
|
|
128
|
+
"dreamina-seedance-2-0-fast-260128": { "480p": 5.6, "720p": 5.6 },
|
|
129
|
+
"dreamina-seedance-2-0-mini-260615": { "480p": 3.5, "720p": 3.5 }
|
|
130
|
+
};
|
|
131
|
+
var IMAGE_USD = {
|
|
132
|
+
"dola-seedream-5-0-pro-260628": 0.045,
|
|
133
|
+
"seedream-5-0-260128": 0.035,
|
|
134
|
+
"seedream-4-5-251128": 0.04,
|
|
135
|
+
"seedream-4-0-250828": 0.03,
|
|
136
|
+
"seedream-4-0-20260415": 0.03,
|
|
137
|
+
"seededit-3-0-i2i-250628": 0.03
|
|
138
|
+
};
|
|
139
|
+
var RATIOS = ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"];
|
|
140
|
+
var BYTEPLUS_MODELS = {
|
|
141
|
+
"seedance-1-0-lite-t2v-250428": {
|
|
142
|
+
family: "1.0",
|
|
143
|
+
textToVideo: true,
|
|
144
|
+
imageToVideo: false,
|
|
145
|
+
lastFrame: false,
|
|
146
|
+
referenceImages: 0,
|
|
147
|
+
referenceMedia: false,
|
|
148
|
+
durations: { min: 2, max: 12 },
|
|
149
|
+
resolutions: ["480p", "720p", "1080p"],
|
|
150
|
+
defaultResolution: "720p"
|
|
151
|
+
},
|
|
152
|
+
"seedance-1-0-lite-i2v-250428": {
|
|
153
|
+
family: "1.0",
|
|
154
|
+
textToVideo: false,
|
|
155
|
+
imageToVideo: true,
|
|
156
|
+
lastFrame: true,
|
|
157
|
+
referenceImages: 4,
|
|
158
|
+
referenceMedia: false,
|
|
159
|
+
durations: { min: 2, max: 12 },
|
|
160
|
+
resolutions: ["480p", "720p", "1080p"],
|
|
161
|
+
defaultResolution: "720p"
|
|
162
|
+
},
|
|
163
|
+
"seedance-1-0-pro-250528": {
|
|
164
|
+
family: "1.0",
|
|
165
|
+
textToVideo: true,
|
|
166
|
+
imageToVideo: true,
|
|
167
|
+
lastFrame: true,
|
|
168
|
+
referenceImages: 0,
|
|
169
|
+
referenceMedia: false,
|
|
170
|
+
durations: { min: 2, max: 12 },
|
|
171
|
+
resolutions: ["480p", "720p", "1080p"],
|
|
172
|
+
defaultResolution: "1080p"
|
|
173
|
+
},
|
|
174
|
+
// Verified in the docs: this one alone drops last-frame support.
|
|
175
|
+
"seedance-1-0-pro-fast-251015": {
|
|
176
|
+
family: "1.0",
|
|
177
|
+
textToVideo: true,
|
|
178
|
+
imageToVideo: true,
|
|
179
|
+
lastFrame: false,
|
|
180
|
+
referenceImages: 0,
|
|
181
|
+
referenceMedia: false,
|
|
182
|
+
durations: { min: 2, max: 12 },
|
|
183
|
+
resolutions: ["480p", "720p", "1080p"],
|
|
184
|
+
defaultResolution: "1080p"
|
|
185
|
+
},
|
|
186
|
+
"seedance-1-5-pro-251215": {
|
|
187
|
+
family: "1.5",
|
|
188
|
+
textToVideo: true,
|
|
189
|
+
imageToVideo: true,
|
|
190
|
+
lastFrame: true,
|
|
191
|
+
referenceImages: 0,
|
|
192
|
+
referenceMedia: false,
|
|
193
|
+
durations: { min: 4, max: 12 },
|
|
194
|
+
resolutions: ["480p", "720p", "1080p"],
|
|
195
|
+
defaultResolution: "720p"
|
|
196
|
+
},
|
|
197
|
+
"dreamina-seedance-2-0-260128": {
|
|
198
|
+
family: "2.x",
|
|
199
|
+
textToVideo: true,
|
|
200
|
+
imageToVideo: true,
|
|
201
|
+
lastFrame: true,
|
|
202
|
+
referenceImages: 9,
|
|
203
|
+
referenceMedia: true,
|
|
204
|
+
durations: { min: 4, max: 15 },
|
|
205
|
+
// The only Seedance model with a 4k tier. It is 10-bit HEVC, so a browser
|
|
206
|
+
// may refuse to play it inline.
|
|
207
|
+
resolutions: ["480p", "720p", "1080p", "4k"],
|
|
208
|
+
defaultResolution: "720p"
|
|
209
|
+
},
|
|
210
|
+
"dreamina-seedance-2-0-fast-260128": {
|
|
211
|
+
family: "2.x",
|
|
212
|
+
textToVideo: true,
|
|
213
|
+
imageToVideo: true,
|
|
214
|
+
lastFrame: true,
|
|
215
|
+
referenceImages: 9,
|
|
216
|
+
referenceMedia: true,
|
|
217
|
+
durations: { min: 4, max: 15 },
|
|
218
|
+
resolutions: ["480p", "720p"],
|
|
219
|
+
defaultResolution: "720p"
|
|
220
|
+
},
|
|
221
|
+
"dreamina-seedance-2-0-mini-260615": {
|
|
222
|
+
family: "2.x",
|
|
223
|
+
textToVideo: true,
|
|
224
|
+
imageToVideo: true,
|
|
225
|
+
lastFrame: true,
|
|
226
|
+
referenceImages: 9,
|
|
227
|
+
referenceMedia: true,
|
|
228
|
+
durations: { min: 4, max: 15 },
|
|
229
|
+
resolutions: ["480p", "720p"],
|
|
230
|
+
defaultResolution: "720p"
|
|
231
|
+
},
|
|
232
|
+
"dreamina-seedance-2-5-260628": {
|
|
233
|
+
family: "2.x",
|
|
234
|
+
textToVideo: true,
|
|
235
|
+
imageToVideo: true,
|
|
236
|
+
lastFrame: true,
|
|
237
|
+
referenceImages: 30,
|
|
238
|
+
referenceMedia: true,
|
|
239
|
+
durations: { min: 4, max: 30 },
|
|
240
|
+
resolutions: ["480p", "720p"],
|
|
241
|
+
defaultResolution: "720p"
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function fieldsFor(family) {
|
|
245
|
+
return {
|
|
246
|
+
seed: family !== "2.x",
|
|
247
|
+
cameraFixed: family !== "2.x",
|
|
248
|
+
generateAudio: family !== "1.0",
|
|
249
|
+
priority: family === "2.x"
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
var BYTEPLUS_IMAGE_MODELS = {
|
|
253
|
+
"seedream-3-0-t2i-250415": { sizes: ["1K", "2K"] },
|
|
254
|
+
"seededit-3-0-i2i-250628": { sizes: ["1K", "2K"] },
|
|
255
|
+
"seedream-4-0-250828": { sizes: ["1K", "2K", "4K"] },
|
|
256
|
+
"seedream-4-0-20260415": { sizes: ["1K", "2K", "4K"] },
|
|
257
|
+
"seedream-4-5-251128": { sizes: ["1K", "2K", "4K"] },
|
|
258
|
+
"seedream-5-0-260128": { sizes: ["1K", "2K", "4K"] },
|
|
259
|
+
"dola-seedream-5-0-pro-260628": { sizes: ["1K", "2K", "4K"] }
|
|
260
|
+
};
|
|
261
|
+
var BYTEPLUS_IMAGE_CATALOG = Object.entries(
|
|
262
|
+
BYTEPLUS_IMAGE_MODELS
|
|
263
|
+
).map(([id, spec]) => ({
|
|
264
|
+
id,
|
|
265
|
+
name: id.replace(/-\d{6,}$/, "").replace(/-/g, " "),
|
|
266
|
+
category: "image",
|
|
267
|
+
inputType: id.includes("i2i") ? "image_required" : "image_optional",
|
|
268
|
+
nodeTypes: ["image_gen"],
|
|
269
|
+
creditsPerUnit: 0,
|
|
270
|
+
creditUnit: "image",
|
|
271
|
+
supportedResolutions: spec.sizes,
|
|
272
|
+
supportedAspectRatios: RATIOS,
|
|
273
|
+
provider: "byteplus",
|
|
274
|
+
pricing: IMAGE_USD[id] ? { unit: "image", usdPerUnit: IMAGE_USD[id] } : void 0
|
|
275
|
+
}));
|
|
276
|
+
var BYTEPLUS_CATALOG = Object.entries(
|
|
277
|
+
BYTEPLUS_MODELS
|
|
278
|
+
).map(([id, binding]) => ({
|
|
279
|
+
id,
|
|
280
|
+
name: id.replace(/-\d{6,}$/, "").replace(/^dreamina-/, "").replace(/-/g, " "),
|
|
281
|
+
category: "video",
|
|
282
|
+
inputType: binding.textToVideo ? binding.imageToVideo ? "image_optional" : "text_only" : "image_required",
|
|
283
|
+
nodeTypes: ["video_gen"],
|
|
284
|
+
// BytePlus bills your own Ark account per output token.
|
|
285
|
+
creditsPerUnit: 0,
|
|
286
|
+
creditUnit: "second",
|
|
287
|
+
minDuration: binding.durations.min,
|
|
288
|
+
maxDuration: binding.durations.max,
|
|
289
|
+
supportedResolutions: binding.resolutions,
|
|
290
|
+
supportedAspectRatios: RATIOS,
|
|
291
|
+
provider: "byteplus",
|
|
292
|
+
pricing: TOKEN_RATES[id] ? {
|
|
293
|
+
unit: "second",
|
|
294
|
+
byResolution: usdPerSecond(TOKEN_RATES[id]),
|
|
295
|
+
usdPerUnit: usdPerSecond(TOKEN_RATES[id])[binding.defaultResolution] ?? 0
|
|
296
|
+
} : void 0
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
// src/providers/elevenlabs.models.ts
|
|
300
|
+
var ELEVENLABS_MODELS = {
|
|
301
|
+
eleven_v3: {
|
|
302
|
+
maxCharacters: 5e3,
|
|
303
|
+
languages: 70,
|
|
304
|
+
note: "Most expressive; widest language coverage."
|
|
305
|
+
},
|
|
306
|
+
eleven_multilingual_v2: {
|
|
307
|
+
maxCharacters: 1e4,
|
|
308
|
+
languages: 29,
|
|
309
|
+
note: "Lifelike and consistent across languages. The API default."
|
|
310
|
+
},
|
|
311
|
+
eleven_flash_v2_5: {
|
|
312
|
+
maxCharacters: 4e4,
|
|
313
|
+
languages: 32,
|
|
314
|
+
note: "~75ms latency, for real-time and agent use."
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var ELEVENLABS_OUTPUT_FORMATS = [
|
|
318
|
+
"mp3_22050_32",
|
|
319
|
+
"mp3_44100_32",
|
|
320
|
+
"mp3_44100_64",
|
|
321
|
+
"mp3_44100_96",
|
|
322
|
+
"mp3_44100_128",
|
|
323
|
+
"mp3_44100_192",
|
|
324
|
+
"pcm_16000",
|
|
325
|
+
"pcm_22050",
|
|
326
|
+
"pcm_24000",
|
|
327
|
+
"pcm_44100",
|
|
328
|
+
"ulaw_8000"
|
|
329
|
+
];
|
|
330
|
+
var ELEVENLABS_CATALOG = Object.entries(
|
|
331
|
+
ELEVENLABS_MODELS
|
|
332
|
+
).map(([id, binding]) => ({
|
|
333
|
+
id,
|
|
334
|
+
name: id,
|
|
335
|
+
category: "audio",
|
|
336
|
+
inputType: "text_only",
|
|
337
|
+
nodeTypes: ["text"],
|
|
338
|
+
creditsPerUnit: 0,
|
|
339
|
+
creditUnit: "character",
|
|
340
|
+
provider: "elevenlabs",
|
|
341
|
+
// Billed in credits whose dollar value depends on your plan, so no verified
|
|
342
|
+
// per-character rate goes in the catalog.
|
|
343
|
+
description: `${binding.note} Up to ${binding.maxCharacters.toLocaleString()} characters, ${binding.languages} languages.`
|
|
344
|
+
}));
|
|
345
|
+
|
|
346
|
+
// src/providers/google.models.ts
|
|
347
|
+
var GOOGLE_IMAGE_MODELS = {
|
|
348
|
+
// Google's recommended default.
|
|
349
|
+
"gemini-3.1-flash-image": { sizes: ["1K", "2K", "4K"], usdAt1K: 0.067 },
|
|
350
|
+
"gemini-3.1-flash-lite-image": {
|
|
351
|
+
// The only model that takes 512px, and note the lowercase "px" against
|
|
352
|
+
// the uppercase "K" of every other value.
|
|
353
|
+
sizes: ["512px", "1K", "2K"],
|
|
354
|
+
usdAt1K: 0.0336
|
|
355
|
+
},
|
|
356
|
+
"gemini-3-pro-image": { sizes: ["1K", "2K", "4K"], usdAt1K: 0.134 },
|
|
357
|
+
"gemini-2.5-flash-image": { sizes: ["1K", "2K"], usdAt1K: 0.039 }
|
|
358
|
+
};
|
|
359
|
+
var GOOGLE_VIDEO_MODELS = {
|
|
360
|
+
// Every current Veo id carries the -preview suffix; the bare name 404s.
|
|
361
|
+
"veo-3.1-generate-preview": {
|
|
362
|
+
api: "predict",
|
|
363
|
+
usdByResolution: { "720p": 0.4, "1080p": 0.4, "4k": 0.6 },
|
|
364
|
+
resolutions: ["720p", "1080p", "4k"],
|
|
365
|
+
durations: ["4", "6", "8"],
|
|
366
|
+
referenceImages: true,
|
|
367
|
+
usdPerSecond: 0.4
|
|
368
|
+
},
|
|
369
|
+
"veo-3.1-fast-generate-preview": {
|
|
370
|
+
api: "predict",
|
|
371
|
+
usdByResolution: { "720p": 0.1, "1080p": 0.12, "4k": 0.3 },
|
|
372
|
+
resolutions: ["720p", "1080p", "4k"],
|
|
373
|
+
durations: ["4", "6", "8"],
|
|
374
|
+
referenceImages: true,
|
|
375
|
+
usdPerSecond: 0.1
|
|
376
|
+
},
|
|
377
|
+
"veo-3.1-lite-generate-preview": {
|
|
378
|
+
api: "predict",
|
|
379
|
+
usdByResolution: { "720p": 0.05, "1080p": 0.08 },
|
|
380
|
+
resolutions: ["720p", "1080p"],
|
|
381
|
+
durations: ["4", "6", "8"],
|
|
382
|
+
referenceImages: false,
|
|
383
|
+
usdPerSecond: 0.05
|
|
384
|
+
},
|
|
385
|
+
/**
|
|
386
|
+
* Omni: text, image, audio and video in, video out, and results you refine by
|
|
387
|
+
* talking to them. It sits on the Interactions API rather than Veo's predict
|
|
388
|
+
* endpoint, which is why the binding carries `api`.
|
|
389
|
+
*
|
|
390
|
+
* No published rate — the pricing page lists Veo but not this, so the catalog
|
|
391
|
+
* leaves it unset rather than guessing.
|
|
392
|
+
*/
|
|
393
|
+
"gemini-omni-flash-preview": {
|
|
394
|
+
api: "interactions",
|
|
395
|
+
resolutions: ["720p"],
|
|
396
|
+
durations: ["10"],
|
|
397
|
+
referenceImages: true,
|
|
398
|
+
conversationalEditing: true
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
var GOOGLE_CATALOG = [
|
|
402
|
+
...Object.entries(GOOGLE_IMAGE_MODELS).map(
|
|
403
|
+
([id, binding]) => ({
|
|
404
|
+
id,
|
|
405
|
+
name: id,
|
|
406
|
+
category: "image",
|
|
407
|
+
inputType: "image_optional",
|
|
408
|
+
nodeTypes: ["image_gen"],
|
|
409
|
+
creditsPerUnit: 0,
|
|
410
|
+
creditUnit: "image",
|
|
411
|
+
supportedResolutions: binding.sizes,
|
|
412
|
+
provider: "google",
|
|
413
|
+
pricing: { usdPerUnit: binding.usdAt1K, unit: "image" }
|
|
414
|
+
})
|
|
415
|
+
),
|
|
416
|
+
...Object.entries(GOOGLE_VIDEO_MODELS).map(
|
|
417
|
+
([id, binding]) => ({
|
|
418
|
+
id,
|
|
419
|
+
name: id,
|
|
420
|
+
category: "video",
|
|
421
|
+
inputType: "image_optional",
|
|
422
|
+
nodeTypes: ["video_gen"],
|
|
423
|
+
creditsPerUnit: 0,
|
|
424
|
+
creditUnit: "second",
|
|
425
|
+
durationOptions: binding.durations.map(Number),
|
|
426
|
+
minDuration: Number(binding.durations[0]),
|
|
427
|
+
maxDuration: Number(binding.durations[binding.durations.length - 1]),
|
|
428
|
+
supportedResolutions: binding.resolutions,
|
|
429
|
+
supportedAspectRatios: ["16:9", "9:16"],
|
|
430
|
+
provider: "google",
|
|
431
|
+
pricing: binding.usdPerSecond === void 0 ? void 0 : {
|
|
432
|
+
unit: "second",
|
|
433
|
+
usdPerUnit: binding.usdPerSecond,
|
|
434
|
+
byResolution: binding.usdByResolution
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
)
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
// src/providers/kling.models.ts
|
|
441
|
+
var V1_DURATIONS = [5, 10];
|
|
442
|
+
var NEXT_DURATIONS = [3, 4, 5, 6, 7, 8, 10, 12, 15];
|
|
443
|
+
var HD = ["720p", "1080p"];
|
|
444
|
+
var UHD = ["720p", "1080p", "4k"];
|
|
445
|
+
var USD_PER_SECOND = {
|
|
446
|
+
"kling/v1-5": { "720p": 0.056, "1080p": 0.098 },
|
|
447
|
+
"kling/v1-6": { "720p": 0.056, "1080p": 0.098 },
|
|
448
|
+
"kling/v2-master": { "1080p": 0.28 },
|
|
449
|
+
"kling/v2-1": { "720p": 0.056, "1080p": 0.098 },
|
|
450
|
+
"kling/v2-1-master": { "1080p": 0.28 },
|
|
451
|
+
"kling/v2-5-turbo": { "720p": 0.042, "1080p": 0.07 },
|
|
452
|
+
"kling/v2-6": { "720p": 0.042, "1080p": 0.07 },
|
|
453
|
+
"kling/v3": { "720p": 0.084, "1080p": 0.112, "4k": 0.42 },
|
|
454
|
+
"kling/v3-turbo": { "720p": 0.112, "1080p": 0.14 }
|
|
455
|
+
};
|
|
456
|
+
var ASPECT_RATIOS = ["16:9", "9:16", "1:1"];
|
|
457
|
+
var KLING_MODELS = {
|
|
458
|
+
// ---- legacy API ----
|
|
459
|
+
"kling/v1": {
|
|
460
|
+
modelName: "kling-v1",
|
|
461
|
+
api: "v1",
|
|
462
|
+
kind: "video",
|
|
463
|
+
textToVideo: true,
|
|
464
|
+
imageToVideo: true,
|
|
465
|
+
durations: V1_DURATIONS
|
|
466
|
+
},
|
|
467
|
+
"kling/v1-5": {
|
|
468
|
+
modelName: "kling-v1-5",
|
|
469
|
+
api: "v1",
|
|
470
|
+
kind: "video",
|
|
471
|
+
textToVideo: true,
|
|
472
|
+
imageToVideo: true,
|
|
473
|
+
durations: V1_DURATIONS,
|
|
474
|
+
modes: ["pro"]
|
|
475
|
+
},
|
|
476
|
+
"kling/v1-6": {
|
|
477
|
+
modelName: "kling-v1-6",
|
|
478
|
+
api: "v1",
|
|
479
|
+
kind: "video",
|
|
480
|
+
textToVideo: true,
|
|
481
|
+
imageToVideo: true,
|
|
482
|
+
durations: V1_DURATIONS
|
|
483
|
+
},
|
|
484
|
+
"kling/v2-master": {
|
|
485
|
+
modelName: "kling-v2-master",
|
|
486
|
+
api: "v1",
|
|
487
|
+
kind: "video",
|
|
488
|
+
textToVideo: true,
|
|
489
|
+
imageToVideo: true,
|
|
490
|
+
durations: V1_DURATIONS
|
|
491
|
+
},
|
|
492
|
+
// Verified: rejected on text2video, accepted on image2video.
|
|
493
|
+
"kling/v2-1": {
|
|
494
|
+
modelName: "kling-v2-1",
|
|
495
|
+
api: "v1",
|
|
496
|
+
kind: "video",
|
|
497
|
+
imageToVideo: true,
|
|
498
|
+
durations: V1_DURATIONS
|
|
499
|
+
},
|
|
500
|
+
"kling/v2-1-master": {
|
|
501
|
+
modelName: "kling-v2-1-master",
|
|
502
|
+
api: "v1",
|
|
503
|
+
kind: "video",
|
|
504
|
+
textToVideo: true,
|
|
505
|
+
imageToVideo: true,
|
|
506
|
+
durations: V1_DURATIONS
|
|
507
|
+
},
|
|
508
|
+
// ---- path-versioned API ----
|
|
509
|
+
"kling/v2-5-turbo": {
|
|
510
|
+
modelName: "kling-2.5-turbo",
|
|
511
|
+
api: "next",
|
|
512
|
+
kind: "video",
|
|
513
|
+
textToVideo: true,
|
|
514
|
+
imageToVideo: true,
|
|
515
|
+
durations: V1_DURATIONS,
|
|
516
|
+
resolutions: HD
|
|
517
|
+
},
|
|
518
|
+
"kling/v2-6": {
|
|
519
|
+
modelName: "kling-2.6",
|
|
520
|
+
api: "next",
|
|
521
|
+
kind: "video",
|
|
522
|
+
textToVideo: true,
|
|
523
|
+
imageToVideo: true,
|
|
524
|
+
durations: V1_DURATIONS,
|
|
525
|
+
resolutions: HD
|
|
526
|
+
},
|
|
527
|
+
"kling/v3": {
|
|
528
|
+
modelName: "kling-3.0",
|
|
529
|
+
api: "next",
|
|
530
|
+
kind: "video",
|
|
531
|
+
textToVideo: true,
|
|
532
|
+
imageToVideo: true,
|
|
533
|
+
durations: NEXT_DURATIONS,
|
|
534
|
+
resolutions: UHD
|
|
535
|
+
},
|
|
536
|
+
"kling/v3-turbo": {
|
|
537
|
+
modelName: "kling-3.0-turbo",
|
|
538
|
+
api: "next",
|
|
539
|
+
kind: "video",
|
|
540
|
+
textToVideo: true,
|
|
541
|
+
imageToVideo: true,
|
|
542
|
+
durations: NEXT_DURATIONS,
|
|
543
|
+
// 3.0-turbo caps at 1080p, unlike 3.0.
|
|
544
|
+
resolutions: HD,
|
|
545
|
+
lastFrame: false
|
|
546
|
+
},
|
|
547
|
+
// ---- images, legacy API ----
|
|
548
|
+
"kling/image-v1": { modelName: "kling-v1", api: "v1", kind: "image" },
|
|
549
|
+
"kling/image-v1-5": { modelName: "kling-v1-5", api: "v1", kind: "image" },
|
|
550
|
+
"kling/image-v2": { modelName: "kling-v2", api: "v1", kind: "image" },
|
|
551
|
+
"kling/image-v2-new": { modelName: "kling-v2-new", api: "v1", kind: "image" },
|
|
552
|
+
"kling/image-v2-1": { modelName: "kling-v2-1", api: "v1", kind: "image" }
|
|
553
|
+
};
|
|
554
|
+
var KLING_VOICES = [
|
|
555
|
+
"genshin_vindi2",
|
|
556
|
+
"zhinen_xuesheng",
|
|
557
|
+
"ai_shatang",
|
|
558
|
+
"genshin_klee2",
|
|
559
|
+
"genshin_kirara",
|
|
560
|
+
"ai_kaiya",
|
|
561
|
+
"chat1_female_new-3",
|
|
562
|
+
"cartoon-boy-07",
|
|
563
|
+
"cartoon-girl-01",
|
|
564
|
+
"uk_boy1",
|
|
565
|
+
"uk_man2",
|
|
566
|
+
"PeppaPig_platform",
|
|
567
|
+
"ai_huangzhong_712",
|
|
568
|
+
"ai_huangyaoshi_712",
|
|
569
|
+
"ai_laoguowang_712",
|
|
570
|
+
"chengshu_jiejie",
|
|
571
|
+
"you_pingjing",
|
|
572
|
+
"calm_story1",
|
|
573
|
+
"laopopo_speech02",
|
|
574
|
+
"heainainai_speech02"
|
|
575
|
+
];
|
|
576
|
+
var KLING_AUDIO_MODEL = "kling/tts";
|
|
577
|
+
var KLING_AUDIO_CATALOG = [
|
|
578
|
+
{
|
|
579
|
+
id: KLING_AUDIO_MODEL,
|
|
580
|
+
name: "Kling TTS",
|
|
581
|
+
category: "audio",
|
|
582
|
+
inputType: "text_only",
|
|
583
|
+
nodeTypes: ["text"],
|
|
584
|
+
creditsPerUnit: 0,
|
|
585
|
+
creditUnit: "character",
|
|
586
|
+
provider: "kling",
|
|
587
|
+
description: `Preset voices: ${KLING_VOICES.join(", ")}. Cloned voices from your account work too.`
|
|
588
|
+
}
|
|
589
|
+
];
|
|
590
|
+
function label(id) {
|
|
591
|
+
const name = KLING_MODELS[id]?.modelName ?? id;
|
|
592
|
+
return `Kling ${name.replace(/^kling-v?/, "")}`;
|
|
593
|
+
}
|
|
594
|
+
var KLING_CATALOG = Object.entries(KLING_MODELS).map(
|
|
595
|
+
([id, binding]) => {
|
|
596
|
+
if (binding.kind === "image") {
|
|
597
|
+
return {
|
|
598
|
+
id,
|
|
599
|
+
name: `${label(id)} (image)`,
|
|
600
|
+
category: "image",
|
|
601
|
+
inputType: "text_only",
|
|
602
|
+
nodeTypes: ["image_gen"],
|
|
603
|
+
// Kling bills its own account, so there is nothing for us to price.
|
|
604
|
+
creditsPerUnit: 0,
|
|
605
|
+
creditUnit: "image",
|
|
606
|
+
supportedAspectRatios: ASPECT_RATIOS,
|
|
607
|
+
provider: "kling"
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const durations = binding.durations ?? V1_DURATIONS;
|
|
611
|
+
return {
|
|
612
|
+
id,
|
|
613
|
+
name: label(id),
|
|
614
|
+
category: "video",
|
|
615
|
+
inputType: binding.textToVideo ? binding.imageToVideo ? "image_optional" : "text_only" : "image_required",
|
|
616
|
+
nodeTypes: ["video_gen"],
|
|
617
|
+
creditsPerUnit: 0,
|
|
618
|
+
creditUnit: "second",
|
|
619
|
+
minDuration: durations[0],
|
|
620
|
+
maxDuration: durations[durations.length - 1],
|
|
621
|
+
durationOptions: durations,
|
|
622
|
+
supportedResolutions: binding.resolutions,
|
|
623
|
+
supportedAspectRatios: ASPECT_RATIOS,
|
|
624
|
+
provider: "kling",
|
|
625
|
+
pricing: USD_PER_SECOND[id] ? {
|
|
626
|
+
unit: "second",
|
|
627
|
+
byResolution: USD_PER_SECOND[id],
|
|
628
|
+
usdPerUnit: USD_PER_SECOND[id]["720p"] ?? USD_PER_SECOND[id]["1080p"] ?? 0
|
|
629
|
+
} : void 0
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
);
|
|
633
|
+
function optionsBlock(options) {
|
|
634
|
+
if (!options) return void 0;
|
|
635
|
+
const block = {};
|
|
636
|
+
if (options.callbackUrl) block.callback_url = options.callbackUrl;
|
|
637
|
+
if (options.externalTaskId) block.external_task_id = options.externalTaskId;
|
|
638
|
+
if (options.watermark !== void 0) {
|
|
639
|
+
block.watermark_info = { enabled: options.watermark };
|
|
640
|
+
}
|
|
641
|
+
return Object.keys(block).length > 0 ? block : void 0;
|
|
642
|
+
}
|
|
643
|
+
var motionControl = {
|
|
644
|
+
api: "next",
|
|
645
|
+
kind: "video",
|
|
646
|
+
results: "outputs",
|
|
647
|
+
path: (input) => `/motion-control/${input.model ?? "kling-2.6"}`,
|
|
648
|
+
body: (input) => {
|
|
649
|
+
const contents = [];
|
|
650
|
+
if (input.prompt) contents.push({ type: "prompt", text: input.prompt });
|
|
651
|
+
contents.push({ type: "image", url: input.imageUrl });
|
|
652
|
+
contents.push({ type: "video", url: input.videoUrl });
|
|
653
|
+
const settings = {
|
|
654
|
+
character_orientation: input.characterOrientation
|
|
655
|
+
};
|
|
656
|
+
if (input.resolution) settings.resolution = input.resolution;
|
|
657
|
+
if (input.audio) settings.audio = input.audio;
|
|
658
|
+
const options = optionsBlock(input.options);
|
|
659
|
+
return options ? { contents, settings, options } : { contents, settings };
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
var omniVideo = {
|
|
663
|
+
api: "next",
|
|
664
|
+
kind: "video",
|
|
665
|
+
results: "outputs",
|
|
666
|
+
path: (input) => `/omni-video/${input.model ?? "kling-3.0-omni"}`,
|
|
667
|
+
body: (input) => {
|
|
668
|
+
const contents = [
|
|
669
|
+
{ type: "prompt", text: input.prompt }
|
|
670
|
+
];
|
|
671
|
+
for (const reference of input.references ?? []) {
|
|
672
|
+
contents.push({
|
|
673
|
+
type: reference.type,
|
|
674
|
+
url: reference.url,
|
|
675
|
+
...reference.id ? { id: reference.id } : {}
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
for (const element of input.elements ?? []) {
|
|
679
|
+
contents.push({
|
|
680
|
+
type: "element",
|
|
681
|
+
element_id: element.elementId,
|
|
682
|
+
id: element.id
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const settings = {};
|
|
686
|
+
if (input.resolution) settings.resolution = input.resolution;
|
|
687
|
+
if (input.duration) settings.duration = input.duration;
|
|
688
|
+
if (input.audio) settings.audio = input.audio;
|
|
689
|
+
if (input.aspectRatio) settings.aspect_ratio = input.aspectRatio;
|
|
690
|
+
if (input.multiShot !== void 0) settings.multi_shot = input.multiShot;
|
|
691
|
+
const options = optionsBlock(input.options);
|
|
692
|
+
return {
|
|
693
|
+
contents,
|
|
694
|
+
...Object.keys(settings).length > 0 ? { settings } : {},
|
|
695
|
+
...options ? { options } : {}
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
var avatar = {
|
|
700
|
+
api: "v1",
|
|
701
|
+
kind: "video",
|
|
702
|
+
results: "videos",
|
|
703
|
+
path: () => "/v1/videos/avatar/image2video",
|
|
704
|
+
body: (input) => {
|
|
705
|
+
if (Boolean(input.audioId) === Boolean(input.soundFileUrl)) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
"Avatar needs exactly one of audioId or soundFileUrl, not both and not neither."
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
const body = { image: input.imageUrl };
|
|
711
|
+
if (input.audioId) body.audio_id = input.audioId;
|
|
712
|
+
if (input.soundFileUrl) body.sound_file = input.soundFileUrl;
|
|
713
|
+
if (input.prompt) body.prompt = input.prompt;
|
|
714
|
+
if (input.mode) body.mode = input.mode;
|
|
715
|
+
if (input.options?.callbackUrl)
|
|
716
|
+
body.callback_url = input.options.callbackUrl;
|
|
717
|
+
if (input.options?.externalTaskId) {
|
|
718
|
+
body.external_task_id = input.options.externalTaskId;
|
|
719
|
+
}
|
|
720
|
+
if (input.options?.watermark !== void 0) {
|
|
721
|
+
body.watermark_info = { enabled: input.options.watermark };
|
|
722
|
+
}
|
|
723
|
+
return body;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
var outpainting = {
|
|
727
|
+
api: "v1",
|
|
728
|
+
kind: "image",
|
|
729
|
+
results: "images",
|
|
730
|
+
path: () => "/v1/images/editing/expand",
|
|
731
|
+
body: (input) => {
|
|
732
|
+
for (const [name, value] of [
|
|
733
|
+
["up", input.up],
|
|
734
|
+
["down", input.down],
|
|
735
|
+
["left", input.left],
|
|
736
|
+
["right", input.right]
|
|
737
|
+
]) {
|
|
738
|
+
if (value < 0 || value > 2) {
|
|
739
|
+
throw new Error(
|
|
740
|
+
`Outpainting ${name} must be between 0 and 2, got ${value}.`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const body = {
|
|
745
|
+
image: input.imageUrl,
|
|
746
|
+
up_expansion_ratio: input.up,
|
|
747
|
+
down_expansion_ratio: input.down,
|
|
748
|
+
left_expansion_ratio: input.left,
|
|
749
|
+
right_expansion_ratio: input.right
|
|
750
|
+
};
|
|
751
|
+
if (input.prompt) body.prompt = input.prompt;
|
|
752
|
+
if (input.n) body.n = input.n;
|
|
753
|
+
if (input.options?.watermark !== void 0) {
|
|
754
|
+
body.watermark_info = { enabled: input.options.watermark };
|
|
755
|
+
}
|
|
756
|
+
return body;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
var imageOmni = {
|
|
760
|
+
api: "v1",
|
|
761
|
+
kind: "image",
|
|
762
|
+
results: "images",
|
|
763
|
+
path: () => "/v1/images/omni-image",
|
|
764
|
+
body: (input) => {
|
|
765
|
+
const images = input.imageUrls ?? [];
|
|
766
|
+
const elements = input.elementIds ?? [];
|
|
767
|
+
if (images.length + elements.length > 10) {
|
|
768
|
+
throw new Error(
|
|
769
|
+
`Image-omni takes at most 10 references in total, got ${images.length + elements.length}.`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
const body = {
|
|
773
|
+
model_name: input.model ?? "kling-image-o1",
|
|
774
|
+
prompt: input.prompt
|
|
775
|
+
};
|
|
776
|
+
if (images.length > 0) {
|
|
777
|
+
body.image_list = images.map((image) => ({ image }));
|
|
778
|
+
}
|
|
779
|
+
if (elements.length > 0) {
|
|
780
|
+
body.element_list = elements.map((id) => ({ element_id: id }));
|
|
781
|
+
}
|
|
782
|
+
if (input.resolution) body.resolution = input.resolution;
|
|
783
|
+
if (input.resultType) body.result_type = input.resultType;
|
|
784
|
+
if (input.seriesAmount) body.series_amount = input.seriesAmount;
|
|
785
|
+
if (input.n) body.n = input.n;
|
|
786
|
+
if (input.aspectRatio) body.aspect_ratio = input.aspectRatio;
|
|
787
|
+
if (input.options?.watermark !== void 0) {
|
|
788
|
+
body.watermark_info = { enabled: input.options.watermark };
|
|
789
|
+
}
|
|
790
|
+
return body;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
var KLING_CAPABILITIES = {
|
|
794
|
+
motionControl,
|
|
795
|
+
omniVideo,
|
|
796
|
+
avatar,
|
|
797
|
+
outpainting,
|
|
798
|
+
imageOmni
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
// src/providers/openai.models.ts
|
|
802
|
+
var CLASSIC_SIZES = ["1024x1024", "1024x1536", "1536x1024", "auto"];
|
|
803
|
+
var OPENAI_IMAGE_MODELS = {
|
|
804
|
+
"gpt-image-2": {
|
|
805
|
+
// Arbitrary WxH: max edge 3840, both edges divisible by 16, ratio up to 3:1.
|
|
806
|
+
sizes: "any",
|
|
807
|
+
qualities: ["low", "medium", "high"],
|
|
808
|
+
// A real capability regression against the older model.
|
|
809
|
+
transparentBackground: false,
|
|
810
|
+
usd: { low: 6e-3, medium: 0.053, high: 0.211 }
|
|
811
|
+
},
|
|
812
|
+
"gpt-image-1.5": {
|
|
813
|
+
sizes: CLASSIC_SIZES,
|
|
814
|
+
qualities: ["low", "medium", "high"],
|
|
815
|
+
transparentBackground: true,
|
|
816
|
+
shutdownOn: "2026-12-01",
|
|
817
|
+
usd: { low: 9e-3, medium: 0.04, high: 0.2 }
|
|
818
|
+
},
|
|
819
|
+
"gpt-image-1": {
|
|
820
|
+
sizes: CLASSIC_SIZES,
|
|
821
|
+
qualities: ["low", "medium", "high"],
|
|
822
|
+
// The only one left that can do transparency.
|
|
823
|
+
transparentBackground: true,
|
|
824
|
+
usd: { low: 0.011, medium: 0.042, high: 0.167 }
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
var OPENAI_CATALOG = Object.entries(
|
|
828
|
+
OPENAI_IMAGE_MODELS
|
|
829
|
+
).map(([id, binding]) => ({
|
|
830
|
+
id,
|
|
831
|
+
name: id,
|
|
832
|
+
category: "image",
|
|
833
|
+
inputType: "image_optional",
|
|
834
|
+
nodeTypes: ["image_gen"],
|
|
835
|
+
creditsPerUnit: 0,
|
|
836
|
+
creditUnit: "image",
|
|
837
|
+
supportedResolutions: binding.sizes === "any" ? void 0 : binding.sizes,
|
|
838
|
+
provider: "openai",
|
|
839
|
+
// Medium is the sane middle; `quality: auto` can silently pick high, which
|
|
840
|
+
// is 35x the low tier.
|
|
841
|
+
pricing: { usdPerUnit: binding.usd.medium, unit: "image" },
|
|
842
|
+
description: binding.shutdownOn ? `Deprecated \u2014 OpenAI shuts this down on ${binding.shutdownOn}.` : void 0
|
|
843
|
+
}));
|
|
844
|
+
|
|
845
|
+
// src/providers/qwen.models.ts
|
|
846
|
+
var VIDEO_PATH = "/api/v1/services/aigc/video-generation/video-synthesis";
|
|
847
|
+
var KF2V_PATH = "/api/v1/services/aigc/image2video/video-synthesis";
|
|
848
|
+
var HD2 = ["480P", "720P", "1080P"];
|
|
849
|
+
var QWEN_VIDEO_MODELS = {
|
|
850
|
+
// --- wan 2.7: media objects, resolution + ratio ---
|
|
851
|
+
"wan2.7-t2v": {
|
|
852
|
+
mode: "t2v",
|
|
853
|
+
shape: "media",
|
|
854
|
+
sizing: "resolution",
|
|
855
|
+
durations: { min: 2, max: 15 },
|
|
856
|
+
resolutions: ["720P", "1080P"]
|
|
857
|
+
},
|
|
858
|
+
"wan2.7-i2v": {
|
|
859
|
+
mode: "i2v",
|
|
860
|
+
shape: "media",
|
|
861
|
+
sizing: "resolution",
|
|
862
|
+
durations: { min: 2, max: 15 },
|
|
863
|
+
resolutions: ["720P", "1080P"]
|
|
864
|
+
},
|
|
865
|
+
// --- wan 2.6: img_url string, size ---
|
|
866
|
+
"wan2.6-t2v": {
|
|
867
|
+
mode: "t2v",
|
|
868
|
+
shape: "media",
|
|
869
|
+
sizing: "size",
|
|
870
|
+
durations: { min: 2, max: 15 },
|
|
871
|
+
watermarkOnByDefault: true
|
|
872
|
+
},
|
|
873
|
+
"wan2.6-i2v": {
|
|
874
|
+
mode: "i2v",
|
|
875
|
+
shape: "img_url",
|
|
876
|
+
sizing: "resolution",
|
|
877
|
+
durations: { min: 2, max: 15 },
|
|
878
|
+
resolutions: ["720P", "1080P"]
|
|
879
|
+
},
|
|
880
|
+
"wan2.6-i2v-flash": {
|
|
881
|
+
mode: "i2v",
|
|
882
|
+
shape: "img_url",
|
|
883
|
+
sizing: "resolution",
|
|
884
|
+
durations: { min: 2, max: 15 },
|
|
885
|
+
resolutions: ["720P", "1080P"]
|
|
886
|
+
},
|
|
887
|
+
"wan2.6-r2v": {
|
|
888
|
+
mode: "r2v",
|
|
889
|
+
shape: "reference_urls",
|
|
890
|
+
sizing: "size",
|
|
891
|
+
durations: { min: 2, max: 10 }
|
|
892
|
+
},
|
|
893
|
+
// --- keyframe: different path, fixed duration, its own fields ---
|
|
894
|
+
"wan2.2-kf2v-flash": {
|
|
895
|
+
mode: "kf2v",
|
|
896
|
+
shape: "kf2v",
|
|
897
|
+
sizing: "resolution",
|
|
898
|
+
path: KF2V_PATH,
|
|
899
|
+
durations: [5],
|
|
900
|
+
resolutions: HD2
|
|
901
|
+
},
|
|
902
|
+
// --- happyhorse: media objects, minimum duration 3, watermark on ---
|
|
903
|
+
"happyhorse-1.1-t2v": {
|
|
904
|
+
mode: "t2v",
|
|
905
|
+
shape: "media",
|
|
906
|
+
sizing: "resolution",
|
|
907
|
+
durations: { min: 3, max: 15 },
|
|
908
|
+
resolutions: HD2,
|
|
909
|
+
watermarkOnByDefault: true
|
|
910
|
+
},
|
|
911
|
+
"happyhorse-1.1-i2v": {
|
|
912
|
+
mode: "i2v",
|
|
913
|
+
shape: "media",
|
|
914
|
+
sizing: "resolution",
|
|
915
|
+
durations: { min: 3, max: 15 },
|
|
916
|
+
resolutions: HD2,
|
|
917
|
+
watermarkOnByDefault: true
|
|
918
|
+
},
|
|
919
|
+
"happyhorse-1.1-r2v": {
|
|
920
|
+
mode: "r2v",
|
|
921
|
+
shape: "media",
|
|
922
|
+
sizing: "resolution",
|
|
923
|
+
durations: { min: 3, max: 15 },
|
|
924
|
+
resolutions: HD2,
|
|
925
|
+
watermarkOnByDefault: true
|
|
926
|
+
},
|
|
927
|
+
"happyhorse-1.0-t2v": {
|
|
928
|
+
mode: "t2v",
|
|
929
|
+
shape: "media",
|
|
930
|
+
sizing: "resolution",
|
|
931
|
+
durations: { min: 3, max: 15 },
|
|
932
|
+
resolutions: HD2,
|
|
933
|
+
watermarkOnByDefault: true
|
|
934
|
+
},
|
|
935
|
+
"happyhorse-1.0-i2v": {
|
|
936
|
+
mode: "i2v",
|
|
937
|
+
shape: "media",
|
|
938
|
+
sizing: "resolution",
|
|
939
|
+
durations: { min: 3, max: 15 },
|
|
940
|
+
resolutions: HD2,
|
|
941
|
+
watermarkOnByDefault: true
|
|
942
|
+
},
|
|
943
|
+
"happyhorse-1.0-r2v": {
|
|
944
|
+
mode: "r2v",
|
|
945
|
+
shape: "media",
|
|
946
|
+
sizing: "resolution",
|
|
947
|
+
durations: { min: 3, max: 15 },
|
|
948
|
+
resolutions: HD2,
|
|
949
|
+
watermarkOnByDefault: true
|
|
950
|
+
},
|
|
951
|
+
"happyhorse-1.0-video-edit": {
|
|
952
|
+
mode: "video-edit",
|
|
953
|
+
shape: "media",
|
|
954
|
+
sizing: "resolution",
|
|
955
|
+
resolutions: ["720P", "1080P"],
|
|
956
|
+
watermarkOnByDefault: true
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
function videoPathFor(binding) {
|
|
960
|
+
return binding.path ?? VIDEO_PATH;
|
|
961
|
+
}
|
|
962
|
+
var QWEN_IMAGE_PATHS = {
|
|
963
|
+
multimodal: "/api/v1/services/aigc/multimodal-generation/generation",
|
|
964
|
+
"image-generation": "/api/v1/services/aigc/image-generation/generation",
|
|
965
|
+
text2image: "/api/v1/services/aigc/text2image/image-synthesis"
|
|
966
|
+
};
|
|
967
|
+
var QWEN_IMAGE_MODELS = {
|
|
968
|
+
// Sync and async both available; async uses the newer image-generation path.
|
|
969
|
+
"qwen-image-3.0-pro": {
|
|
970
|
+
sync: true,
|
|
971
|
+
path: "multimodal",
|
|
972
|
+
body: "messages",
|
|
973
|
+
maxN: 6
|
|
974
|
+
},
|
|
975
|
+
"qwen-image-3.0": {
|
|
976
|
+
sync: true,
|
|
977
|
+
path: "multimodal",
|
|
978
|
+
body: "messages",
|
|
979
|
+
maxN: 6
|
|
980
|
+
},
|
|
981
|
+
// Sync only — the async header would 429 these.
|
|
982
|
+
"qwen-image-2.0-pro": {
|
|
983
|
+
sync: true,
|
|
984
|
+
path: "multimodal",
|
|
985
|
+
body: "messages",
|
|
986
|
+
maxN: 6
|
|
987
|
+
},
|
|
988
|
+
"qwen-image-2.0": {
|
|
989
|
+
sync: true,
|
|
990
|
+
path: "multimodal",
|
|
991
|
+
body: "messages",
|
|
992
|
+
maxN: 6
|
|
993
|
+
},
|
|
994
|
+
"qwen-image-max": {
|
|
995
|
+
sync: true,
|
|
996
|
+
path: "multimodal",
|
|
997
|
+
body: "messages",
|
|
998
|
+
maxN: 1
|
|
999
|
+
},
|
|
1000
|
+
"z-image-turbo": {
|
|
1001
|
+
sync: true,
|
|
1002
|
+
path: "multimodal",
|
|
1003
|
+
body: "messages",
|
|
1004
|
+
maxN: 1
|
|
1005
|
+
},
|
|
1006
|
+
"wan2.7-image-pro": {
|
|
1007
|
+
sync: true,
|
|
1008
|
+
path: "multimodal",
|
|
1009
|
+
body: "messages",
|
|
1010
|
+
maxN: 4,
|
|
1011
|
+
resolutions: ["1K", "2K", "4K"]
|
|
1012
|
+
},
|
|
1013
|
+
"wan2.7-image": {
|
|
1014
|
+
sync: true,
|
|
1015
|
+
path: "multimodal",
|
|
1016
|
+
body: "messages",
|
|
1017
|
+
maxN: 4,
|
|
1018
|
+
resolutions: ["1K", "2K"]
|
|
1019
|
+
},
|
|
1020
|
+
"wan2.6-image": {
|
|
1021
|
+
sync: true,
|
|
1022
|
+
path: "multimodal",
|
|
1023
|
+
body: "messages",
|
|
1024
|
+
maxN: 4
|
|
1025
|
+
},
|
|
1026
|
+
"wan2.6-t2i": { sync: true, path: "multimodal", body: "messages", maxN: 4 }
|
|
1027
|
+
};
|
|
1028
|
+
var VIDEO_USD_PER_SECOND = {
|
|
1029
|
+
"happyhorse-1.1-t2v": { "480P": 0.07, "720P": 0.14, "1080P": 0.18 },
|
|
1030
|
+
"happyhorse-1.1-i2v": { "480P": 0.07, "720P": 0.14, "1080P": 0.18 },
|
|
1031
|
+
"happyhorse-1.1-r2v": { "480P": 0.07, "720P": 0.14, "1080P": 0.18 },
|
|
1032
|
+
"happyhorse-1.0-video-edit": { "720P": 0.14, "1080P": 0.24 }
|
|
1033
|
+
};
|
|
1034
|
+
var IMAGE_USD2 = {
|
|
1035
|
+
"qwen-image-3.0-pro": 0.04,
|
|
1036
|
+
"qwen-image-3.0": 0.03,
|
|
1037
|
+
"qwen-image-2.0-pro": 0.075,
|
|
1038
|
+
"qwen-image-2.0": 0.035,
|
|
1039
|
+
"wan2.7-image-pro": 0.075
|
|
1040
|
+
};
|
|
1041
|
+
var QWEN_VOICES = [
|
|
1042
|
+
"Cherry",
|
|
1043
|
+
"Ethan",
|
|
1044
|
+
"Jennifer",
|
|
1045
|
+
"Ryan",
|
|
1046
|
+
"Dylan",
|
|
1047
|
+
"Sunny",
|
|
1048
|
+
"Nofish"
|
|
1049
|
+
];
|
|
1050
|
+
var QWEN_AUDIO_MODELS = {
|
|
1051
|
+
"qwen3-tts-flash": { voices: QWEN_VOICES, usdPerCharacter: 15e-6 },
|
|
1052
|
+
"qwen3-tts-instruct-flash": {
|
|
1053
|
+
voices: QWEN_VOICES,
|
|
1054
|
+
usdPerCharacter: 15e-6
|
|
1055
|
+
},
|
|
1056
|
+
"qwen-audio-3.0-tts-flash": {
|
|
1057
|
+
voices: QWEN_VOICES,
|
|
1058
|
+
usdPerCharacter: 15e-6
|
|
1059
|
+
},
|
|
1060
|
+
"qwen-audio-3.0-tts-plus": { voices: QWEN_VOICES, usdPerCharacter: 3e-5 },
|
|
1061
|
+
"cosyvoice-v3-flash": { voices: QWEN_VOICES, usdPerCharacter: 15e-6 }
|
|
1062
|
+
};
|
|
1063
|
+
var QWEN_TEXT_MODELS = {
|
|
1064
|
+
"qwen3.8-max": { usdPerMillionOutput: 6 },
|
|
1065
|
+
"qwen3.7-plus": { usdPerMillionOutput: 1.6 },
|
|
1066
|
+
"qwen3.7-flash": { usdPerMillionOutput: 0.13 },
|
|
1067
|
+
"qwen-max": { usdPerMillionOutput: 7.5 },
|
|
1068
|
+
"qwen-plus": { usdPerMillionOutput: 3 },
|
|
1069
|
+
"qwen-turbo": { usdPerMillionOutput: 0.4 }
|
|
1070
|
+
};
|
|
1071
|
+
function inputTypeFor(mode) {
|
|
1072
|
+
if (mode === "t2v") return "text_only";
|
|
1073
|
+
if (mode === "video-edit") return "video_required";
|
|
1074
|
+
return "image_required";
|
|
1075
|
+
}
|
|
1076
|
+
function durationsOf(binding) {
|
|
1077
|
+
if (Array.isArray(binding.durations)) {
|
|
1078
|
+
return {
|
|
1079
|
+
options: binding.durations,
|
|
1080
|
+
min: binding.durations[0],
|
|
1081
|
+
max: binding.durations[binding.durations.length - 1]
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
options: void 0,
|
|
1086
|
+
min: binding.durations?.min,
|
|
1087
|
+
max: binding.durations?.max
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
var QWEN_CATALOG = [
|
|
1091
|
+
...Object.entries(QWEN_AUDIO_MODELS).map(
|
|
1092
|
+
([id, binding]) => ({
|
|
1093
|
+
id,
|
|
1094
|
+
name: id,
|
|
1095
|
+
category: "audio",
|
|
1096
|
+
inputType: "text_only",
|
|
1097
|
+
nodeTypes: ["text"],
|
|
1098
|
+
creditsPerUnit: 0,
|
|
1099
|
+
creditUnit: "character",
|
|
1100
|
+
provider: "qwen",
|
|
1101
|
+
pricing: { unit: "character", usdPerUnit: binding.usdPerCharacter },
|
|
1102
|
+
description: `Voices: ${binding.voices.join(", ")}`
|
|
1103
|
+
})
|
|
1104
|
+
),
|
|
1105
|
+
...Object.entries(QWEN_TEXT_MODELS).map(
|
|
1106
|
+
([id, binding]) => ({
|
|
1107
|
+
id,
|
|
1108
|
+
name: id,
|
|
1109
|
+
category: "text",
|
|
1110
|
+
inputType: "text_only",
|
|
1111
|
+
nodeTypes: ["text"],
|
|
1112
|
+
creditsPerUnit: 0,
|
|
1113
|
+
creditUnit: "token",
|
|
1114
|
+
provider: "qwen",
|
|
1115
|
+
pricing: {
|
|
1116
|
+
unit: "token",
|
|
1117
|
+
usdPerUnit: binding.usdPerMillionOutput / 1e6
|
|
1118
|
+
}
|
|
1119
|
+
})
|
|
1120
|
+
),
|
|
1121
|
+
...Object.entries(QWEN_VIDEO_MODELS).map(([id, binding]) => {
|
|
1122
|
+
const duration = durationsOf(binding);
|
|
1123
|
+
return {
|
|
1124
|
+
id,
|
|
1125
|
+
name: id,
|
|
1126
|
+
category: "video",
|
|
1127
|
+
inputType: inputTypeFor(binding.mode),
|
|
1128
|
+
nodeTypes: ["video_gen"],
|
|
1129
|
+
// Qwen bills your own DashScope account.
|
|
1130
|
+
creditsPerUnit: 0,
|
|
1131
|
+
creditUnit: "second",
|
|
1132
|
+
durationOptions: duration.options,
|
|
1133
|
+
minDuration: duration.min,
|
|
1134
|
+
maxDuration: duration.max,
|
|
1135
|
+
supportedResolutions: binding.resolutions,
|
|
1136
|
+
provider: "qwen",
|
|
1137
|
+
pricing: VIDEO_USD_PER_SECOND[id] ? {
|
|
1138
|
+
unit: "second",
|
|
1139
|
+
byResolution: VIDEO_USD_PER_SECOND[id],
|
|
1140
|
+
usdPerUnit: VIDEO_USD_PER_SECOND[id]["720P"] ?? 0
|
|
1141
|
+
} : void 0
|
|
1142
|
+
};
|
|
1143
|
+
}),
|
|
1144
|
+
...Object.entries(QWEN_IMAGE_MODELS).map(
|
|
1145
|
+
([id, binding]) => ({
|
|
1146
|
+
id,
|
|
1147
|
+
name: id,
|
|
1148
|
+
category: "image",
|
|
1149
|
+
inputType: "image_optional",
|
|
1150
|
+
nodeTypes: ["image_gen"],
|
|
1151
|
+
creditsPerUnit: 0,
|
|
1152
|
+
creditUnit: "image",
|
|
1153
|
+
supportedResolutions: binding.resolutions,
|
|
1154
|
+
provider: "qwen",
|
|
1155
|
+
pricing: IMAGE_USD2[id] ? { unit: "image", usdPerUnit: IMAGE_USD2[id] } : void 0
|
|
1156
|
+
})
|
|
1157
|
+
)
|
|
1158
|
+
];
|
|
1159
|
+
|
|
1160
|
+
// src/catalog.ts
|
|
1161
|
+
var PROVIDER_BASE_URLS = {
|
|
1162
|
+
byteplus: "https://ark.ap-southeast.bytepluses.com",
|
|
1163
|
+
kling: "https://api-singapore.klingai.com",
|
|
1164
|
+
elevenlabs: "https://api.elevenlabs.io",
|
|
1165
|
+
google: "https://generativelanguage.googleapis.com",
|
|
1166
|
+
openai: "https://api.openai.com",
|
|
1167
|
+
qwen: "https://dashscope-intl.aliyuncs.com"
|
|
1168
|
+
};
|
|
1169
|
+
var BUILT_IN = [
|
|
1170
|
+
...KLING_CATALOG,
|
|
1171
|
+
...KLING_AUDIO_CATALOG,
|
|
1172
|
+
...BYTEPLUS_CATALOG,
|
|
1173
|
+
...BYTEPLUS_IMAGE_CATALOG,
|
|
1174
|
+
...QWEN_CATALOG,
|
|
1175
|
+
...OPENAI_CATALOG,
|
|
1176
|
+
...GOOGLE_CATALOG,
|
|
1177
|
+
...ELEVENLABS_CATALOG
|
|
1178
|
+
];
|
|
1179
|
+
var catalog = BUILT_IN;
|
|
1180
|
+
var catalogMap = byId(BUILT_IN);
|
|
1181
|
+
function byId(models) {
|
|
1182
|
+
return Object.fromEntries(models.map((model) => [model.id, model]));
|
|
1183
|
+
}
|
|
1184
|
+
function registerModels(models = []) {
|
|
1185
|
+
if (models.length === 0) return;
|
|
1186
|
+
const merged = { ...catalogMap };
|
|
1187
|
+
for (const model of models) {
|
|
1188
|
+
merged[model.id] = { ...merged[model.id], ...model };
|
|
1189
|
+
}
|
|
1190
|
+
catalogMap = merged;
|
|
1191
|
+
catalog = Object.values(merged);
|
|
1192
|
+
}
|
|
1193
|
+
function resetCatalog() {
|
|
1194
|
+
catalog = BUILT_IN;
|
|
1195
|
+
catalogMap = byId(BUILT_IN);
|
|
1196
|
+
}
|
|
1197
|
+
function getModel(modelId) {
|
|
1198
|
+
return catalogMap[modelId];
|
|
1199
|
+
}
|
|
1200
|
+
function getModels() {
|
|
1201
|
+
return catalog;
|
|
1202
|
+
}
|
|
1203
|
+
function hasModel(modelId) {
|
|
1204
|
+
return modelId in catalogMap;
|
|
1205
|
+
}
|
|
1206
|
+
function getProviders() {
|
|
1207
|
+
return [
|
|
1208
|
+
...new Set(
|
|
1209
|
+
catalog.map((model) => model.provider).filter((provider) => Boolean(provider))
|
|
1210
|
+
)
|
|
1211
|
+
].sort();
|
|
1212
|
+
}
|
|
1213
|
+
function resolveProvider(modelId, options) {
|
|
1214
|
+
const model = getModel(modelId);
|
|
1215
|
+
if (!model) {
|
|
1216
|
+
throw new Error(`Unknown model "${modelId}".`);
|
|
1217
|
+
}
|
|
1218
|
+
const id = model.provider;
|
|
1219
|
+
if (!id) {
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
`Model "${modelId}" declares no provider, so nothing knows how to run it.`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
const configured = options.providers[id];
|
|
1225
|
+
if (!configured) {
|
|
1226
|
+
const owned = Object.keys(options.providers).join(", ") || "none";
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
`Model "${modelId}" runs on "${id}", but no API key was given for it (configured: ${owned}).`
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
const baseUrl = configured.baseUrl ?? PROVIDER_BASE_URLS[id];
|
|
1232
|
+
if (!baseUrl) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`Provider "${id}" has no known base URL \u2014 pass providers.${id}.baseUrl.`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return { id, apiKey: configured.apiKey, baseUrl: baseUrl.replace(/\/$/, "") };
|
|
1238
|
+
}
|
|
1239
|
+
function getAvailableModels(options) {
|
|
1240
|
+
return catalog.filter(
|
|
1241
|
+
(model) => model.provider && model.provider in options.providers
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/adapters/estimate.ts
|
|
1246
|
+
function estimateFor(provider, type, params, defaults) {
|
|
1247
|
+
const modelId = params.model ?? "";
|
|
1248
|
+
const model = getModel(modelId);
|
|
1249
|
+
const video = params;
|
|
1250
|
+
const unit = model?.pricing?.unit ?? (type === "video" ? "second" : type === "audio" ? "character" : type === "text" ? "token" : "image");
|
|
1251
|
+
let units = 1;
|
|
1252
|
+
if (unit === "character") {
|
|
1253
|
+
units = params.prompt?.length ?? 0;
|
|
1254
|
+
} else if (unit === "token") {
|
|
1255
|
+
units = 0;
|
|
1256
|
+
} else if (unit === "second") {
|
|
1257
|
+
units = video.duration ?? defaults?.durationSeconds ?? model?.minDuration ?? 5;
|
|
1258
|
+
}
|
|
1259
|
+
const resolution = params.resolution;
|
|
1260
|
+
const rate = (resolution ? model?.pricing?.byResolution?.[resolution] : void 0) ?? model?.pricing?.usdPerUnit;
|
|
1261
|
+
return {
|
|
1262
|
+
unit,
|
|
1263
|
+
units,
|
|
1264
|
+
usd: rate === void 0 || unit === "token" ? null : Number((rate * units).toFixed(4)),
|
|
1265
|
+
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,
|
|
1266
|
+
provider,
|
|
1267
|
+
model: modelId
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/adapters/byteplus.adapter.ts
|
|
1272
|
+
var DEFAULT_BASE_URL = "https://ark.ap-southeast.bytepluses.com";
|
|
1273
|
+
var TASKS_PATH = "/api/v3/contents/generations/tasks";
|
|
1274
|
+
var IMAGES_PATH = "/api/v3/images/generations";
|
|
1275
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
1276
|
+
var DEFAULT_MAX_POLL_ATTEMPTS = 240;
|
|
1277
|
+
var BytePlusAdapter = class {
|
|
1278
|
+
providerName = "byteplus";
|
|
1279
|
+
supportedTypes = ["image", "video"];
|
|
1280
|
+
opts;
|
|
1281
|
+
constructor(opts) {
|
|
1282
|
+
this.opts = opts;
|
|
1283
|
+
}
|
|
1284
|
+
get baseUrl() {
|
|
1285
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
1286
|
+
}
|
|
1287
|
+
async request(path, init) {
|
|
1288
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1289
|
+
method: init?.method ?? "GET",
|
|
1290
|
+
headers: {
|
|
1291
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
1292
|
+
"Content-Type": "application/json"
|
|
1293
|
+
},
|
|
1294
|
+
body: init ? JSON.stringify(init.body) : void 0
|
|
1295
|
+
});
|
|
1296
|
+
const payload = await response.json();
|
|
1297
|
+
if (payload.error) {
|
|
1298
|
+
throw new Error(`${payload.error.code}: ${payload.error.message}`);
|
|
1299
|
+
}
|
|
1300
|
+
if (!response.ok) {
|
|
1301
|
+
throw new Error(`Ark returned ${response.status} for ${path}.`);
|
|
1302
|
+
}
|
|
1303
|
+
return payload;
|
|
1304
|
+
}
|
|
1305
|
+
binding(modelId) {
|
|
1306
|
+
const id = modelId ?? "";
|
|
1307
|
+
const found = BYTEPLUS_MODELS[id];
|
|
1308
|
+
if (!found) {
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
`"${id}" is not a BytePlus model. Known: ${Object.keys(BYTEPLUS_MODELS).join(", ")}.`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
return [id, found];
|
|
1314
|
+
}
|
|
1315
|
+
buildBody(modelId, binding, params) {
|
|
1316
|
+
const content = [{ type: "text", text: params.prompt }];
|
|
1317
|
+
const firstFrame = params.imageUrl ?? params.referenceImages?.[0];
|
|
1318
|
+
const references = params.imageUrls ?? [];
|
|
1319
|
+
if (references.length > 0) {
|
|
1320
|
+
if (binding.referenceImages === 0) {
|
|
1321
|
+
throw new Error(
|
|
1322
|
+
`Seedance "${modelId}" takes no reference images \u2014 use imageUrl for a first frame.`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
for (const url of references.slice(0, binding.referenceImages)) {
|
|
1326
|
+
content.push({
|
|
1327
|
+
type: "image_url",
|
|
1328
|
+
image_url: { url },
|
|
1329
|
+
role: "reference_image"
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
} else if (firstFrame) {
|
|
1333
|
+
content.push({
|
|
1334
|
+
type: "image_url",
|
|
1335
|
+
image_url: { url: firstFrame },
|
|
1336
|
+
role: "first_frame"
|
|
1337
|
+
});
|
|
1338
|
+
const lastFrame = params.referenceImages?.[1];
|
|
1339
|
+
if (lastFrame) {
|
|
1340
|
+
if (!binding.lastFrame) {
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
`Seedance "${modelId}" does not accept a last frame.`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
content.push({
|
|
1346
|
+
type: "image_url",
|
|
1347
|
+
image_url: { url: lastFrame },
|
|
1348
|
+
role: "last_frame"
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
const allowed = fieldsFor(binding.family);
|
|
1353
|
+
const body = { model: modelId, content };
|
|
1354
|
+
body.resolution = params.resolution ?? binding.defaultResolution;
|
|
1355
|
+
if (params.aspectRatio && params.aspectRatio !== "auto") {
|
|
1356
|
+
body.ratio = params.aspectRatio;
|
|
1357
|
+
}
|
|
1358
|
+
if (params.duration !== void 0) body.duration = params.duration;
|
|
1359
|
+
if (allowed.seed && params.seed !== void 0) body.seed = params.seed;
|
|
1360
|
+
if (allowed.generateAudio && params.withAudio !== void 0) {
|
|
1361
|
+
body.generate_audio = params.withAudio;
|
|
1362
|
+
}
|
|
1363
|
+
return { ...body, ...params.providerOptions?.byteplus ?? {} };
|
|
1364
|
+
}
|
|
1365
|
+
validate(modelId, binding, kind, params) {
|
|
1366
|
+
if (kind !== "video") {
|
|
1367
|
+
throw new Error("The BytePlus adapter only generates video.");
|
|
1368
|
+
}
|
|
1369
|
+
const hasImage = Boolean(params.imageUrl ?? params.imageUrls?.length);
|
|
1370
|
+
if (!hasImage && !binding.textToVideo) {
|
|
1371
|
+
throw new Error(
|
|
1372
|
+
`Seedance "${modelId}" only runs image-to-video \u2014 pass imageUrl.`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (hasImage && !binding.imageToVideo) {
|
|
1376
|
+
throw new Error(`Seedance "${modelId}" only runs text-to-video.`);
|
|
1377
|
+
}
|
|
1378
|
+
if (params.duration !== void 0) {
|
|
1379
|
+
const { min, max } = binding.durations;
|
|
1380
|
+
const openEnded = params.duration === -1 && binding.family !== "1.0";
|
|
1381
|
+
if (!openEnded && (params.duration < min || params.duration > max)) {
|
|
1382
|
+
throw new Error(
|
|
1383
|
+
`Seedance "${modelId}" accepts durations from ${min} to ${max}s, not ${params.duration}s.`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (params.resolution && !binding.resolutions.includes(params.resolution)) {
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
`Seedance "${modelId}" supports ${binding.resolutions.join(", ")}, not ${params.resolution}.`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
outputsFrom(task) {
|
|
1394
|
+
const url = task.content?.video_url;
|
|
1395
|
+
if (!url) return [];
|
|
1396
|
+
return [
|
|
1397
|
+
{
|
|
1398
|
+
url,
|
|
1399
|
+
mimeType: "video/mp4",
|
|
1400
|
+
taskId: task.id,
|
|
1401
|
+
// Ark states 24 hours for a result URL.
|
|
1402
|
+
expiresAt: expiresInHours(24),
|
|
1403
|
+
// Ark bills output tokens only; input is always 0 for video.
|
|
1404
|
+
raw: { completionTokens: task.usage?.completion_tokens }
|
|
1405
|
+
}
|
|
1406
|
+
];
|
|
1407
|
+
}
|
|
1408
|
+
snapshot(task, job) {
|
|
1409
|
+
if (task.status === "failed" || task.status === "cancelled") {
|
|
1410
|
+
return {
|
|
1411
|
+
status: "failed",
|
|
1412
|
+
error: task.error?.message ?? `Ark reported the task as ${task.status}.`
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
if (task.status === "expired") {
|
|
1416
|
+
return {
|
|
1417
|
+
status: "failed",
|
|
1418
|
+
error: `Ark expired task ${task.id} before it ran.`
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
if (task.status !== "succeeded") return { status: "pending" };
|
|
1422
|
+
const outputs = this.outputsFrom(task);
|
|
1423
|
+
if (outputs.length === 0) return { status: "pending" };
|
|
1424
|
+
return {
|
|
1425
|
+
status: "succeeded",
|
|
1426
|
+
result: {
|
|
1427
|
+
success: true,
|
|
1428
|
+
outputs,
|
|
1429
|
+
creditsUsed: 0,
|
|
1430
|
+
provider: this.providerName,
|
|
1431
|
+
model: job.model,
|
|
1432
|
+
processingTimeMs: 0
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
async run(kind, params) {
|
|
1437
|
+
const startedAt = Date.now();
|
|
1438
|
+
let modelId = params.model ?? "(none)";
|
|
1439
|
+
const failure = (error) => ({
|
|
1440
|
+
success: false,
|
|
1441
|
+
outputs: [],
|
|
1442
|
+
creditsUsed: 0,
|
|
1443
|
+
provider: this.providerName,
|
|
1444
|
+
model: modelId,
|
|
1445
|
+
processingTimeMs: Date.now() - startedAt,
|
|
1446
|
+
error
|
|
1447
|
+
});
|
|
1448
|
+
let taskId;
|
|
1449
|
+
try {
|
|
1450
|
+
const [id, binding] = this.binding(params.model);
|
|
1451
|
+
modelId = id;
|
|
1452
|
+
this.validate(id, binding, kind, params);
|
|
1453
|
+
const task = await this.request(TASKS_PATH, {
|
|
1454
|
+
method: "POST",
|
|
1455
|
+
body: this.buildBody(id, binding, params)
|
|
1456
|
+
});
|
|
1457
|
+
if (!task.id) {
|
|
1458
|
+
throw new Error("Ark accepted the request but returned no task id.");
|
|
1459
|
+
}
|
|
1460
|
+
taskId = task.id;
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1463
|
+
}
|
|
1464
|
+
if (isSubmitMode()) {
|
|
1465
|
+
throw new PendingJob(taskId, TASKS_PATH);
|
|
1466
|
+
}
|
|
1467
|
+
const job = {
|
|
1468
|
+
id: taskId,
|
|
1469
|
+
provider: this.providerName,
|
|
1470
|
+
model: modelId,
|
|
1471
|
+
kind,
|
|
1472
|
+
pollEndpoint: TASKS_PATH,
|
|
1473
|
+
params,
|
|
1474
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
1475
|
+
};
|
|
1476
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS;
|
|
1477
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1478
|
+
const snapshot = await this.completeJob(job);
|
|
1479
|
+
if (snapshot.status === "failed") {
|
|
1480
|
+
return failure(snapshot.error ?? "Ark task failed.");
|
|
1481
|
+
}
|
|
1482
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
1483
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
1484
|
+
}
|
|
1485
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
1486
|
+
}
|
|
1487
|
+
return failure(
|
|
1488
|
+
`Ark task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
/** Resume a job submitted earlier, possibly by another process. */
|
|
1492
|
+
async completeJob(job) {
|
|
1493
|
+
try {
|
|
1494
|
+
const task = await this.request(
|
|
1495
|
+
`${job.pollEndpoint ?? TASKS_PATH}/${encodeURIComponent(job.id)}`
|
|
1496
|
+
);
|
|
1497
|
+
return this.snapshot(task, job);
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
return {
|
|
1500
|
+
status: "failed",
|
|
1501
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
generateVideo(params) {
|
|
1506
|
+
return this.run("video", params);
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Seedream. Unlike video, `/images/generations` answers synchronously — the
|
|
1510
|
+
* image is in the response and there is no task to poll, so `submit()` returns
|
|
1511
|
+
* an already-settled job for these models.
|
|
1512
|
+
*/
|
|
1513
|
+
async generateImage(params) {
|
|
1514
|
+
const startedAt = Date.now();
|
|
1515
|
+
const modelId = params.model ?? "";
|
|
1516
|
+
const failure = (error) => ({
|
|
1517
|
+
success: false,
|
|
1518
|
+
outputs: [],
|
|
1519
|
+
creditsUsed: 0,
|
|
1520
|
+
provider: this.providerName,
|
|
1521
|
+
model: modelId,
|
|
1522
|
+
processingTimeMs: Date.now() - startedAt,
|
|
1523
|
+
error
|
|
1524
|
+
});
|
|
1525
|
+
const spec = BYTEPLUS_IMAGE_MODELS[modelId];
|
|
1526
|
+
if (!spec) {
|
|
1527
|
+
return failure(
|
|
1528
|
+
`"${modelId}" is not a BytePlus image model. Known: ${Object.keys(BYTEPLUS_IMAGE_MODELS).join(", ")}.`
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
const size = params.resolution ?? "2K";
|
|
1532
|
+
if (!spec.sizes.includes(size)) {
|
|
1533
|
+
return failure(
|
|
1534
|
+
`Seedream "${modelId}" supports ${spec.sizes.join(", ")}, not ${size}.`
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
const body = {
|
|
1538
|
+
model: modelId,
|
|
1539
|
+
prompt: params.prompt,
|
|
1540
|
+
size,
|
|
1541
|
+
response_format: "url",
|
|
1542
|
+
// Ark defaults this to true. An SDK that stamps your paid output without
|
|
1543
|
+
// being asked is a surprise, so the default is inverted here.
|
|
1544
|
+
watermark: false
|
|
1545
|
+
};
|
|
1546
|
+
if (params.outputFormat) body.output_format = params.outputFormat;
|
|
1547
|
+
if (params.seed !== void 0) body.seed = params.seed;
|
|
1548
|
+
const reference = params.referenceImages?.[0];
|
|
1549
|
+
if (reference) body.image = reference;
|
|
1550
|
+
try {
|
|
1551
|
+
const response = await this.request(IMAGES_PATH, {
|
|
1552
|
+
method: "POST",
|
|
1553
|
+
body
|
|
1554
|
+
});
|
|
1555
|
+
const outputs = (response.data ?? []).filter((item) => Boolean(item.url)).map((item) => ({
|
|
1556
|
+
url: item.url,
|
|
1557
|
+
mimeType: params.outputFormat === "png" ? "image/png" : "image/jpeg",
|
|
1558
|
+
// The link dies in a day; configure storage to keep it.
|
|
1559
|
+
expiresAt: expiresInHours(24)
|
|
1560
|
+
}));
|
|
1561
|
+
if (outputs.length === 0) {
|
|
1562
|
+
return failure("Ark returned no image URL.");
|
|
1563
|
+
}
|
|
1564
|
+
return {
|
|
1565
|
+
success: true,
|
|
1566
|
+
outputs,
|
|
1567
|
+
creditsUsed: 0,
|
|
1568
|
+
provider: this.providerName,
|
|
1569
|
+
model: modelId,
|
|
1570
|
+
processingTimeMs: Date.now() - startedAt
|
|
1571
|
+
};
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/** Cancel a queued task, or delete the record of a finished one. */
|
|
1577
|
+
async cancelJob(job) {
|
|
1578
|
+
await fetch(`${this.baseUrl}${TASKS_PATH}/${encodeURIComponent(job.id)}`, {
|
|
1579
|
+
method: "DELETE",
|
|
1580
|
+
headers: { Authorization: `Bearer ${this.opts.apiKey}` }
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
generateAudio(_params) {
|
|
1584
|
+
throw new Error("BytePlus has no speech synthesis wired up here.");
|
|
1585
|
+
}
|
|
1586
|
+
generateText(_params) {
|
|
1587
|
+
throw new Error("The BytePlus adapter does not generate text.");
|
|
1588
|
+
}
|
|
1589
|
+
async estimateCost(type, params) {
|
|
1590
|
+
return estimateFor(this.providerName, type, params);
|
|
1591
|
+
}
|
|
1592
|
+
supportsModel(model) {
|
|
1593
|
+
return model in BYTEPLUS_MODELS || model in BYTEPLUS_IMAGE_MODELS;
|
|
1594
|
+
}
|
|
1595
|
+
getAvailableModels() {
|
|
1596
|
+
return [
|
|
1597
|
+
...Object.keys(BYTEPLUS_MODELS).map((id) => ({
|
|
1598
|
+
id,
|
|
1599
|
+
name: id,
|
|
1600
|
+
type: "video"
|
|
1601
|
+
})),
|
|
1602
|
+
...Object.keys(BYTEPLUS_IMAGE_MODELS).map((id) => ({
|
|
1603
|
+
id,
|
|
1604
|
+
name: id,
|
|
1605
|
+
type: "image"
|
|
1606
|
+
}))
|
|
1607
|
+
];
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
// src/adapters/elevenlabs.adapter.ts
|
|
1612
|
+
var DEFAULT_BASE_URL2 = "https://api.elevenlabs.io";
|
|
1613
|
+
var ElevenLabsAdapter = class {
|
|
1614
|
+
providerName = "elevenlabs";
|
|
1615
|
+
supportedTypes = ["audio"];
|
|
1616
|
+
opts;
|
|
1617
|
+
constructor(opts) {
|
|
1618
|
+
this.opts = opts;
|
|
1619
|
+
}
|
|
1620
|
+
get baseUrl() {
|
|
1621
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/$/, "");
|
|
1622
|
+
}
|
|
1623
|
+
get headers() {
|
|
1624
|
+
return { "xi-api-key": this.opts.apiKey };
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* The voices on your account. Voice ids are opaque and account-specific, so
|
|
1628
|
+
* there is no list the catalog could ship.
|
|
1629
|
+
*/
|
|
1630
|
+
async listVoices() {
|
|
1631
|
+
const response = await fetch(`${this.baseUrl}/v1/voices/search`, {
|
|
1632
|
+
headers: this.headers
|
|
1633
|
+
});
|
|
1634
|
+
if (!response.ok) {
|
|
1635
|
+
throw new Error(`ElevenLabs returned ${response.status} listing voices.`);
|
|
1636
|
+
}
|
|
1637
|
+
const payload = await response.json();
|
|
1638
|
+
return (payload.voices ?? []).filter((voice) => voice.voice_id).map((voice) => ({
|
|
1639
|
+
voiceId: voice.voice_id,
|
|
1640
|
+
name: voice.name ?? "",
|
|
1641
|
+
labels: voice.labels
|
|
1642
|
+
}));
|
|
1643
|
+
}
|
|
1644
|
+
async generateAudio(params) {
|
|
1645
|
+
const startedAt = Date.now();
|
|
1646
|
+
const modelId = params.model ?? "eleven_multilingual_v2";
|
|
1647
|
+
const failure = (error) => ({
|
|
1648
|
+
success: false,
|
|
1649
|
+
outputs: [],
|
|
1650
|
+
creditsUsed: 0,
|
|
1651
|
+
provider: this.providerName,
|
|
1652
|
+
model: modelId,
|
|
1653
|
+
processingTimeMs: Date.now() - startedAt,
|
|
1654
|
+
error
|
|
1655
|
+
});
|
|
1656
|
+
const binding = ELEVENLABS_MODELS[modelId];
|
|
1657
|
+
if (!binding) {
|
|
1658
|
+
return failure(
|
|
1659
|
+
`"${modelId}" is not an ElevenLabs model. Known: ${Object.keys(ELEVENLABS_MODELS).join(", ")}.`
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
const voiceId = params.voice ?? this.opts.defaultVoiceId;
|
|
1663
|
+
if (!voiceId) {
|
|
1664
|
+
return failure(
|
|
1665
|
+
"ElevenLabs needs a voice id. Pass `voice`, set `defaultVoiceId` on the provider, or call listVoices() to find one."
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
if (params.prompt.length > binding.maxCharacters) {
|
|
1669
|
+
return failure(
|
|
1670
|
+
`ElevenLabs "${modelId}" takes up to ${binding.maxCharacters} characters; got ${params.prompt.length}.`
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
const outputFormat = params.outputFormat ? (
|
|
1674
|
+
// The shared param is a codec; theirs is codec_rate_bitrate.
|
|
1675
|
+
`${params.outputFormat}_44100_128`
|
|
1676
|
+
) : "mp3_44100_128";
|
|
1677
|
+
if (!ELEVENLABS_OUTPUT_FORMATS.includes(outputFormat)) {
|
|
1678
|
+
return failure(
|
|
1679
|
+
`ElevenLabs has no output format "${outputFormat}". Pass one through providerOptions.elevenlabs.output_format.`
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
try {
|
|
1683
|
+
const response = await fetch(
|
|
1684
|
+
`${this.baseUrl}/v1/text-to-speech/${encodeURIComponent(voiceId)}`,
|
|
1685
|
+
{
|
|
1686
|
+
method: "POST",
|
|
1687
|
+
headers: { ...this.headers, "Content-Type": "application/json" },
|
|
1688
|
+
body: JSON.stringify({
|
|
1689
|
+
text: params.prompt,
|
|
1690
|
+
model_id: modelId,
|
|
1691
|
+
output_format: outputFormat,
|
|
1692
|
+
...params.language ? { language_code: params.language } : {},
|
|
1693
|
+
...params.speed !== void 0 ? { voice_settings: { speed: params.speed } } : {},
|
|
1694
|
+
...params.providerOptions?.elevenlabs ?? {}
|
|
1695
|
+
})
|
|
1696
|
+
}
|
|
1697
|
+
);
|
|
1698
|
+
if (!response.ok) {
|
|
1699
|
+
const detail = await response.text();
|
|
1700
|
+
throw new Error(`ElevenLabs returned ${response.status}: ${detail}`);
|
|
1701
|
+
}
|
|
1702
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
1703
|
+
const mimeType = outputFormat.startsWith("mp3") ? "audio/mpeg" : "audio/wav";
|
|
1704
|
+
return {
|
|
1705
|
+
success: true,
|
|
1706
|
+
outputs: [
|
|
1707
|
+
{
|
|
1708
|
+
url: `data:${mimeType};base64,${bytes.toString("base64")}`,
|
|
1709
|
+
mimeType,
|
|
1710
|
+
sizeBytes: bytes.byteLength,
|
|
1711
|
+
// Inline bytes: nothing hosted, so nothing expires.
|
|
1712
|
+
raw: { inline: true, voiceId, characters: params.prompt.length }
|
|
1713
|
+
}
|
|
1714
|
+
],
|
|
1715
|
+
creditsUsed: 0,
|
|
1716
|
+
provider: this.providerName,
|
|
1717
|
+
model: modelId,
|
|
1718
|
+
processingTimeMs: Date.now() - startedAt
|
|
1719
|
+
};
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
generateImage(_params) {
|
|
1725
|
+
throw new Error("ElevenLabs generates speech, not images.");
|
|
1726
|
+
}
|
|
1727
|
+
generateVideo(_params) {
|
|
1728
|
+
throw new Error("ElevenLabs generates speech, not video.");
|
|
1729
|
+
}
|
|
1730
|
+
generateText(_params) {
|
|
1731
|
+
throw new Error("ElevenLabs generates speech, not text.");
|
|
1732
|
+
}
|
|
1733
|
+
/** Synchronous provider: there is never a queued job to come back to. */
|
|
1734
|
+
async completeJob(job) {
|
|
1735
|
+
return {
|
|
1736
|
+
status: "failed",
|
|
1737
|
+
error: `ElevenLabs answers inline, so job ${job.id} has nothing to resume.`
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
async estimateCost(type, params) {
|
|
1741
|
+
const base = estimateFor(this.providerName, type, params);
|
|
1742
|
+
return {
|
|
1743
|
+
...base,
|
|
1744
|
+
note: "ElevenLabs bills credits whose dollar value depends on your plan, so no rate is on file. The character count is exact."
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
supportsModel(model) {
|
|
1748
|
+
return model in ELEVENLABS_MODELS;
|
|
1749
|
+
}
|
|
1750
|
+
getAvailableModels() {
|
|
1751
|
+
return Object.keys(ELEVENLABS_MODELS).map((id) => ({
|
|
1752
|
+
id,
|
|
1753
|
+
name: id,
|
|
1754
|
+
type: "audio"
|
|
1755
|
+
}));
|
|
1756
|
+
}
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// src/adapters/google.adapter.ts
|
|
1760
|
+
var DEFAULT_BASE_URL3 = "https://generativelanguage.googleapis.com";
|
|
1761
|
+
var INTERACTIONS_PATH = "/v1beta/interactions";
|
|
1762
|
+
var POLL_INTERVAL_MS2 = 1e4;
|
|
1763
|
+
var DEFAULT_MAX_POLL_ATTEMPTS2 = 120;
|
|
1764
|
+
var GoogleAdapter = class {
|
|
1765
|
+
providerName = "google";
|
|
1766
|
+
supportedTypes = ["image", "video"];
|
|
1767
|
+
opts;
|
|
1768
|
+
constructor(opts) {
|
|
1769
|
+
this.opts = opts;
|
|
1770
|
+
}
|
|
1771
|
+
get baseUrl() {
|
|
1772
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL3).replace(/\/$/, "");
|
|
1773
|
+
}
|
|
1774
|
+
get headers() {
|
|
1775
|
+
return {
|
|
1776
|
+
"x-goog-api-key": this.opts.apiKey,
|
|
1777
|
+
"Content-Type": "application/json"
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
async request(path, init) {
|
|
1781
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
1782
|
+
method: init?.method ?? "GET",
|
|
1783
|
+
headers: this.headers,
|
|
1784
|
+
body: init ? JSON.stringify(init.body) : void 0
|
|
1785
|
+
});
|
|
1786
|
+
const payload = await response.json();
|
|
1787
|
+
if (payload.error) {
|
|
1788
|
+
throw new Error(payload.error.message ?? "Google rejected the request.");
|
|
1789
|
+
}
|
|
1790
|
+
if (!response.ok) {
|
|
1791
|
+
throw new Error(`Google returned ${response.status} for ${path}.`);
|
|
1792
|
+
}
|
|
1793
|
+
return payload;
|
|
1794
|
+
}
|
|
1795
|
+
// ------------------------------------------------------------------ images
|
|
1796
|
+
async generateImage(params) {
|
|
1797
|
+
const startedAt = Date.now();
|
|
1798
|
+
const modelId = params.model ?? "";
|
|
1799
|
+
const failure = (error) => ({
|
|
1800
|
+
success: false,
|
|
1801
|
+
outputs: [],
|
|
1802
|
+
creditsUsed: 0,
|
|
1803
|
+
provider: this.providerName,
|
|
1804
|
+
model: modelId,
|
|
1805
|
+
processingTimeMs: Date.now() - startedAt,
|
|
1806
|
+
error
|
|
1807
|
+
});
|
|
1808
|
+
const binding = GOOGLE_IMAGE_MODELS[modelId];
|
|
1809
|
+
if (!binding) {
|
|
1810
|
+
return failure(
|
|
1811
|
+
`"${modelId}" is not a Google image model. Known: ${Object.keys(GOOGLE_IMAGE_MODELS).join(", ")}.`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
if (params.resolution && !binding.sizes.includes(params.resolution)) {
|
|
1815
|
+
return failure(
|
|
1816
|
+
`Google "${modelId}" accepts ${binding.sizes.join(", ")}, not ${params.resolution}.`
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
try {
|
|
1820
|
+
const input = [
|
|
1821
|
+
{ type: "text", text: params.prompt }
|
|
1822
|
+
];
|
|
1823
|
+
for (const image of params.referenceImages ?? []) {
|
|
1824
|
+
const [meta, data] = image.split(",");
|
|
1825
|
+
input.push({
|
|
1826
|
+
type: "image",
|
|
1827
|
+
mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
|
|
1828
|
+
data: data ?? image
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
const payload = await this.request(
|
|
1832
|
+
INTERACTIONS_PATH,
|
|
1833
|
+
{
|
|
1834
|
+
method: "POST",
|
|
1835
|
+
body: {
|
|
1836
|
+
model: modelId,
|
|
1837
|
+
input,
|
|
1838
|
+
response_format: {
|
|
1839
|
+
type: "image",
|
|
1840
|
+
mime_type: `image/${params.outputFormat ?? "png"}`,
|
|
1841
|
+
// Size drives the bill here, since pricing is token-based.
|
|
1842
|
+
image_size: params.resolution ?? "1K",
|
|
1843
|
+
...params.aspectRatio && params.aspectRatio !== "auto" ? { aspect_ratio: params.aspectRatio } : {}
|
|
1844
|
+
},
|
|
1845
|
+
...params.providerOptions?.google ?? {}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
);
|
|
1849
|
+
const base64 = payload.output_image?.data ?? payload.steps?.flatMap((step) => step.content ?? []).find((part) => part.type === "image")?.data;
|
|
1850
|
+
if (!base64) return failure("Google returned no image.");
|
|
1851
|
+
const format = params.outputFormat ?? "png";
|
|
1852
|
+
return {
|
|
1853
|
+
success: true,
|
|
1854
|
+
outputs: [
|
|
1855
|
+
{
|
|
1856
|
+
url: `data:image/${format};base64,${base64}`,
|
|
1857
|
+
mimeType: `image/${format}`,
|
|
1858
|
+
raw: { inline: true }
|
|
1859
|
+
}
|
|
1860
|
+
],
|
|
1861
|
+
creditsUsed: 0,
|
|
1862
|
+
provider: this.providerName,
|
|
1863
|
+
model: modelId,
|
|
1864
|
+
processingTimeMs: Date.now() - startedAt
|
|
1865
|
+
};
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
// ------------------------------------------------------------------- video
|
|
1871
|
+
veoBody(params) {
|
|
1872
|
+
const hasImage = Boolean(params.imageUrl ?? params.referenceImages?.length);
|
|
1873
|
+
const instance = { prompt: params.prompt };
|
|
1874
|
+
const first = params.imageUrl ?? params.referenceImages?.[0];
|
|
1875
|
+
if (first) {
|
|
1876
|
+
const [meta, data] = first.split(",");
|
|
1877
|
+
instance.image = {
|
|
1878
|
+
inlineData: {
|
|
1879
|
+
mimeType: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
|
|
1880
|
+
data: data ?? first
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
const resolution = params.resolution ?? "720p";
|
|
1885
|
+
const forcesEight = resolution !== "720p";
|
|
1886
|
+
const duration = String(params.duration ?? (forcesEight ? 8 : 4));
|
|
1887
|
+
return {
|
|
1888
|
+
instances: [instance],
|
|
1889
|
+
parameters: {
|
|
1890
|
+
aspectRatio: params.aspectRatio === "9:16" ? "9:16" : "16:9",
|
|
1891
|
+
resolution,
|
|
1892
|
+
// A string, not a number. Serializing it as an int is a 400.
|
|
1893
|
+
durationSeconds: duration,
|
|
1894
|
+
// The accepted value narrows by mode: text-to-video takes allow_all,
|
|
1895
|
+
// anything driven by an image takes allow_adult and rejects the other.
|
|
1896
|
+
personGeneration: hasImage ? "allow_adult" : "allow_all",
|
|
1897
|
+
...params.seed !== void 0 ? { seed: params.seed } : {},
|
|
1898
|
+
...params.providerOptions?.google ?? {}
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Omni speaks the Interactions API, so it gets its own builder — snake_case
|
|
1904
|
+
* `input`/`response_format`, against Veo's camelCase predict envelope.
|
|
1905
|
+
*/
|
|
1906
|
+
omniBody(modelId, params) {
|
|
1907
|
+
const input = [
|
|
1908
|
+
{ type: "text", text: params.prompt }
|
|
1909
|
+
];
|
|
1910
|
+
const attach = (type, value) => {
|
|
1911
|
+
const [meta, data] = value.split(",");
|
|
1912
|
+
input.push({
|
|
1913
|
+
type,
|
|
1914
|
+
mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? (type === "video" ? "video/mp4" : "image/png"),
|
|
1915
|
+
data: data ?? value
|
|
1916
|
+
});
|
|
1917
|
+
};
|
|
1918
|
+
const first = params.imageUrl ?? params.referenceImages?.[0];
|
|
1919
|
+
if (first) attach("image", first);
|
|
1920
|
+
for (const image of params.imageUrls ?? []) attach("image", image);
|
|
1921
|
+
const video = params.videoUrl ?? params.videoUrls?.[0];
|
|
1922
|
+
if (video) attach("video", video);
|
|
1923
|
+
const task = video ? "edit" : params.imageUrls?.length ? "reference_to_video" : first ? "image_to_video" : "text_to_video";
|
|
1924
|
+
return {
|
|
1925
|
+
model: modelId,
|
|
1926
|
+
input,
|
|
1927
|
+
// Continues a previous result instead of starting over.
|
|
1928
|
+
...params.previousInteractionId ? { previous_interaction_id: params.previousInteractionId } : {},
|
|
1929
|
+
response_format: {
|
|
1930
|
+
type: "video",
|
|
1931
|
+
aspect_ratio: params.aspectRatio === "9:16" ? "9:16" : "16:9",
|
|
1932
|
+
// A base64 video over ~4MB has to come back as a URI instead.
|
|
1933
|
+
delivery: "uri"
|
|
1934
|
+
},
|
|
1935
|
+
generation_config: { video_config: { task } },
|
|
1936
|
+
...params.providerOptions?.google ?? {}
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
/** Generate or refine a video conversationally. Returns the interaction. */
|
|
1940
|
+
async omniVideo(params) {
|
|
1941
|
+
const modelId = params.model ?? "gemini-omni-flash-preview";
|
|
1942
|
+
const payload = await this.request(INTERACTIONS_PATH, {
|
|
1943
|
+
method: "POST",
|
|
1944
|
+
body: this.omniBody(modelId, params)
|
|
1945
|
+
});
|
|
1946
|
+
const found = payload.output_video ?? payload.steps?.flatMap((step) => step.content ?? []).find((part) => part.type === "video");
|
|
1947
|
+
const url = found?.uri ?? (found?.data ? `data:video/mp4;base64,${found.data}` : void 0);
|
|
1948
|
+
return {
|
|
1949
|
+
// Hand this back to keep refining the same video.
|
|
1950
|
+
interactionId: payload.id,
|
|
1951
|
+
outputs: url ? [
|
|
1952
|
+
{
|
|
1953
|
+
url,
|
|
1954
|
+
mimeType: "video/mp4",
|
|
1955
|
+
taskId: payload.id,
|
|
1956
|
+
expiresAt: expiresInHours(48),
|
|
1957
|
+
raw: found?.uri ? { requiresApiKeyHeader: true } : { inline: true }
|
|
1958
|
+
}
|
|
1959
|
+
] : []
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
async runVideo(params) {
|
|
1963
|
+
const startedAt = Date.now();
|
|
1964
|
+
const modelId = params.model ?? "";
|
|
1965
|
+
const failure = (error) => ({
|
|
1966
|
+
success: false,
|
|
1967
|
+
outputs: [],
|
|
1968
|
+
creditsUsed: 0,
|
|
1969
|
+
provider: this.providerName,
|
|
1970
|
+
model: modelId,
|
|
1971
|
+
processingTimeMs: Date.now() - startedAt,
|
|
1972
|
+
error
|
|
1973
|
+
});
|
|
1974
|
+
const binding = GOOGLE_VIDEO_MODELS[modelId];
|
|
1975
|
+
if (!binding) {
|
|
1976
|
+
return failure(
|
|
1977
|
+
`"${modelId}" is not a Veo model. Known: ${Object.keys(GOOGLE_VIDEO_MODELS).join(", ")}.`
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
if (params.resolution && !binding.resolutions.includes(params.resolution)) {
|
|
1981
|
+
return failure(
|
|
1982
|
+
`Veo "${modelId}" supports ${binding.resolutions.join(", ")}, not ${params.resolution}.`
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
if (params.imageUrls?.length && !binding.referenceImages) {
|
|
1986
|
+
return failure(`Veo "${modelId}" does not take reference images.`);
|
|
1987
|
+
}
|
|
1988
|
+
if (binding.api === "interactions") {
|
|
1989
|
+
try {
|
|
1990
|
+
const { outputs, interactionId } = await this.omniVideo(params);
|
|
1991
|
+
if (outputs.length === 0) return failure("Omni returned no video.");
|
|
1992
|
+
return {
|
|
1993
|
+
success: true,
|
|
1994
|
+
outputs,
|
|
1995
|
+
creditsUsed: 0,
|
|
1996
|
+
provider: this.providerName,
|
|
1997
|
+
model: modelId,
|
|
1998
|
+
processingTimeMs: Date.now() - startedAt,
|
|
1999
|
+
// Carried so the caller can keep refining this result.
|
|
2000
|
+
error: void 0,
|
|
2001
|
+
...interactionId ? {} : {}
|
|
2002
|
+
};
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
let operation;
|
|
2008
|
+
try {
|
|
2009
|
+
const submitted = await this.request(
|
|
2010
|
+
`/v1beta/models/${modelId}:predictLongRunning`,
|
|
2011
|
+
{ method: "POST", body: this.veoBody(params) }
|
|
2012
|
+
);
|
|
2013
|
+
if (!submitted.name) {
|
|
2014
|
+
throw new Error("Veo accepted the request but returned no operation.");
|
|
2015
|
+
}
|
|
2016
|
+
operation = submitted.name;
|
|
2017
|
+
} catch (error) {
|
|
2018
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2019
|
+
}
|
|
2020
|
+
if (isSubmitMode()) throw new PendingJob(operation, "/v1beta");
|
|
2021
|
+
const job = {
|
|
2022
|
+
id: operation,
|
|
2023
|
+
provider: this.providerName,
|
|
2024
|
+
model: modelId,
|
|
2025
|
+
kind: "video",
|
|
2026
|
+
pollEndpoint: "/v1beta",
|
|
2027
|
+
params,
|
|
2028
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
2029
|
+
};
|
|
2030
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS2;
|
|
2031
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2032
|
+
const snapshot = await this.completeJob(job);
|
|
2033
|
+
if (snapshot.status === "failed") {
|
|
2034
|
+
return failure(snapshot.error ?? "Veo task failed.");
|
|
2035
|
+
}
|
|
2036
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2037
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2038
|
+
}
|
|
2039
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS2));
|
|
2040
|
+
}
|
|
2041
|
+
return failure(`Veo operation ${operation} did not finish in time.`);
|
|
2042
|
+
}
|
|
2043
|
+
/** Resume a Veo operation submitted earlier, possibly by another process. */
|
|
2044
|
+
async completeJob(job) {
|
|
2045
|
+
try {
|
|
2046
|
+
const operation = await this.request(
|
|
2047
|
+
`${job.pollEndpoint ?? "/v1beta"}/${job.id}`
|
|
2048
|
+
);
|
|
2049
|
+
if (!operation.done) return { status: "pending" };
|
|
2050
|
+
if (operation.error) {
|
|
2051
|
+
return { status: "failed", error: operation.error.message };
|
|
2052
|
+
}
|
|
2053
|
+
const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
|
|
2054
|
+
if (!uri) return { status: "pending" };
|
|
2055
|
+
const outputs = [
|
|
2056
|
+
{
|
|
2057
|
+
url: uri,
|
|
2058
|
+
mimeType: "video/mp4",
|
|
2059
|
+
taskId: job.id,
|
|
2060
|
+
// Google removes the file after two days.
|
|
2061
|
+
expiresAt: expiresInHours(48),
|
|
2062
|
+
// The URI is not public: fetching it needs the x-goog-api-key header,
|
|
2063
|
+
// and it redirects, so the header has to survive the redirect.
|
|
2064
|
+
raw: { requiresApiKeyHeader: true }
|
|
2065
|
+
}
|
|
2066
|
+
];
|
|
2067
|
+
return {
|
|
2068
|
+
status: "succeeded",
|
|
2069
|
+
result: {
|
|
2070
|
+
success: true,
|
|
2071
|
+
outputs,
|
|
2072
|
+
creditsUsed: 0,
|
|
2073
|
+
provider: this.providerName,
|
|
2074
|
+
model: job.model,
|
|
2075
|
+
processingTimeMs: 0
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
return {
|
|
2080
|
+
status: "failed",
|
|
2081
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
generateVideo(params) {
|
|
2086
|
+
return this.runVideo(params);
|
|
2087
|
+
}
|
|
2088
|
+
generateAudio(_params) {
|
|
2089
|
+
throw new Error("Google has no speech synthesis wired up here.");
|
|
2090
|
+
}
|
|
2091
|
+
generateText(_params) {
|
|
2092
|
+
throw new Error("The Google adapter here covers image and video only.");
|
|
2093
|
+
}
|
|
2094
|
+
async estimateCost(type, params) {
|
|
2095
|
+
return estimateFor(this.providerName, type, params);
|
|
2096
|
+
}
|
|
2097
|
+
supportsModel(model) {
|
|
2098
|
+
return model in GOOGLE_IMAGE_MODELS || model in GOOGLE_VIDEO_MODELS;
|
|
2099
|
+
}
|
|
2100
|
+
getAvailableModels() {
|
|
2101
|
+
return [
|
|
2102
|
+
...Object.keys(GOOGLE_IMAGE_MODELS).map((id) => ({
|
|
2103
|
+
id,
|
|
2104
|
+
name: id,
|
|
2105
|
+
type: "image"
|
|
2106
|
+
})),
|
|
2107
|
+
...Object.keys(GOOGLE_VIDEO_MODELS).map((id) => ({
|
|
2108
|
+
id,
|
|
2109
|
+
name: id,
|
|
2110
|
+
type: "video"
|
|
2111
|
+
}))
|
|
2112
|
+
];
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
|
|
2116
|
+
// src/adapters/kling.adapter.ts
|
|
2117
|
+
var DEFAULT_BASE_URL4 = "https://api-singapore.klingai.com";
|
|
2118
|
+
var POLL_INTERVAL_MS3 = 5e3;
|
|
2119
|
+
var DEFAULT_MAX_POLL_ATTEMPTS3 = 240;
|
|
2120
|
+
function stateOf(task) {
|
|
2121
|
+
const raw = (task.status ?? task.task_status ?? "").toLowerCase();
|
|
2122
|
+
if (raw === "failed") return "failed";
|
|
2123
|
+
if (raw === "succeed" || raw === "succeeded") return "succeeded";
|
|
2124
|
+
return "pending";
|
|
2125
|
+
}
|
|
2126
|
+
var KlingAdapter = class {
|
|
2127
|
+
providerName = "kling";
|
|
2128
|
+
supportedTypes = ["image", "video"];
|
|
2129
|
+
opts;
|
|
2130
|
+
constructor(opts) {
|
|
2131
|
+
this.opts = opts;
|
|
2132
|
+
}
|
|
2133
|
+
get baseUrl() {
|
|
2134
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL4).replace(/\/$/, "");
|
|
2135
|
+
}
|
|
2136
|
+
async request(path, init) {
|
|
2137
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
2138
|
+
method: init?.method ?? "GET",
|
|
2139
|
+
headers: {
|
|
2140
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
2141
|
+
"Content-Type": "application/json"
|
|
2142
|
+
},
|
|
2143
|
+
body: init ? JSON.stringify(init.body) : void 0
|
|
2144
|
+
});
|
|
2145
|
+
const envelope = await response.json();
|
|
2146
|
+
if (envelope.code !== 0) {
|
|
2147
|
+
throw new Error(`Kling ${envelope.code}: ${envelope.message}`);
|
|
2148
|
+
}
|
|
2149
|
+
if (envelope.data === void 0) {
|
|
2150
|
+
throw new Error(`Kling returned an empty body for ${path}.`);
|
|
2151
|
+
}
|
|
2152
|
+
return envelope.data;
|
|
2153
|
+
}
|
|
2154
|
+
binding(modelId) {
|
|
2155
|
+
const id = modelId ?? "";
|
|
2156
|
+
const found = KLING_MODELS[id];
|
|
2157
|
+
if (!found) {
|
|
2158
|
+
throw new Error(
|
|
2159
|
+
`"${id}" is not a Kling model. Known: ${Object.keys(KLING_MODELS).join(", ")}.`
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
return [id, found];
|
|
2163
|
+
}
|
|
2164
|
+
submitPath(binding, hasImage) {
|
|
2165
|
+
if (binding.api === "v1") {
|
|
2166
|
+
if (binding.kind === "image") return "/v1/images/generations";
|
|
2167
|
+
return hasImage ? "/v1/videos/image2video" : "/v1/videos/text2video";
|
|
2168
|
+
}
|
|
2169
|
+
return `/${hasImage ? "image-to-video" : "text-to-video"}/${binding.modelName}`;
|
|
2170
|
+
}
|
|
2171
|
+
legacyBody(binding, params, referenceImage) {
|
|
2172
|
+
const body = {
|
|
2173
|
+
model_name: binding.modelName,
|
|
2174
|
+
prompt: params.prompt
|
|
2175
|
+
};
|
|
2176
|
+
if (params.negativePrompt) body.negative_prompt = params.negativePrompt;
|
|
2177
|
+
if (params.aspectRatio && params.aspectRatio !== "auto") {
|
|
2178
|
+
body.aspect_ratio = params.aspectRatio;
|
|
2179
|
+
}
|
|
2180
|
+
if (binding.kind === "video") {
|
|
2181
|
+
body.duration = String(params.duration ?? 5);
|
|
2182
|
+
const wanted = params.mode === "pro" ? "pro" : "std";
|
|
2183
|
+
body.mode = binding.modes?.includes(wanted) ? wanted : binding.modes?.[0] ?? wanted;
|
|
2184
|
+
if (referenceImage) body.image = referenceImage;
|
|
2185
|
+
}
|
|
2186
|
+
return body;
|
|
2187
|
+
}
|
|
2188
|
+
nextBody(params, referenceImage) {
|
|
2189
|
+
const settings = {};
|
|
2190
|
+
if (params.duration) settings.duration = params.duration;
|
|
2191
|
+
if (params.resolution) settings.resolution = params.resolution;
|
|
2192
|
+
if (!referenceImage && params.aspectRatio && params.aspectRatio !== "auto") {
|
|
2193
|
+
settings.aspect_ratio = params.aspectRatio;
|
|
2194
|
+
}
|
|
2195
|
+
const body = referenceImage ? {
|
|
2196
|
+
contents: [
|
|
2197
|
+
{ type: "prompt", text: params.prompt },
|
|
2198
|
+
{ type: "first_frame", url: referenceImage }
|
|
2199
|
+
]
|
|
2200
|
+
} : { prompt: params.prompt };
|
|
2201
|
+
if (Object.keys(settings).length > 0) body.settings = settings;
|
|
2202
|
+
return { ...body, ...params.providerOptions?.kling ?? {} };
|
|
2203
|
+
}
|
|
2204
|
+
async submitTask(kind, params) {
|
|
2205
|
+
const [, binding] = this.binding(params.model);
|
|
2206
|
+
const referenceImage = params.imageUrl ?? params.referenceImages?.[0];
|
|
2207
|
+
const hasImage = Boolean(referenceImage);
|
|
2208
|
+
if (kind === "video" && !hasImage && !binding.textToVideo) {
|
|
2209
|
+
throw new Error(
|
|
2210
|
+
`Kling model "${params.model}" only runs image-to-video \u2014 pass imageUrl.`
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
if (binding.resolutions && params.resolution && !binding.resolutions.includes(params.resolution)) {
|
|
2214
|
+
throw new Error(
|
|
2215
|
+
`Kling "${params.model}" supports ${binding.resolutions.join(", ")}, not ${params.resolution}.`
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
if (binding.durations && params.duration && !binding.durations.includes(params.duration)) {
|
|
2219
|
+
throw new Error(
|
|
2220
|
+
`Kling "${params.model}" accepts durations ${binding.durations.join(", ")}s, not ${params.duration}s.`
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
const path = this.submitPath(binding, hasImage);
|
|
2224
|
+
const body = binding.api === "v1" ? this.legacyBody(binding, params, referenceImage) : this.nextBody(params, referenceImage);
|
|
2225
|
+
const task = await this.request(path, { method: "POST", body });
|
|
2226
|
+
const taskId = task.task_id ?? task.id;
|
|
2227
|
+
if (!taskId) {
|
|
2228
|
+
throw new Error("Kling accepted the request but returned no task id.");
|
|
2229
|
+
}
|
|
2230
|
+
return { taskId, pollEndpoint: binding.api === "v1" ? path : "/tasks" };
|
|
2231
|
+
}
|
|
2232
|
+
async queryTask(pollEndpoint, taskId) {
|
|
2233
|
+
if (pollEndpoint === "/tasks") {
|
|
2234
|
+
const tasks = await this.request(
|
|
2235
|
+
`/tasks?task_ids=${encodeURIComponent(taskId)}`
|
|
2236
|
+
);
|
|
2237
|
+
const task = tasks[0];
|
|
2238
|
+
if (!task) throw new Error(`Kling knows no task ${taskId}.`);
|
|
2239
|
+
return task;
|
|
2240
|
+
}
|
|
2241
|
+
return this.request(
|
|
2242
|
+
`${pollEndpoint}/${encodeURIComponent(taskId)}`
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
outputsFrom(task, kind, resultField) {
|
|
2246
|
+
const fromNext = task.outputs ?? [];
|
|
2247
|
+
const legacy = task.task_result;
|
|
2248
|
+
const fromLegacy = resultField ? resultField === "outputs" ? [] : legacy?.[resultField] ?? [] : kind === "video" ? legacy?.videos ?? [] : legacy?.images ?? [];
|
|
2249
|
+
return [...fromNext, ...fromLegacy].filter((item) => Boolean(item?.url)).map((item) => ({
|
|
2250
|
+
url: item.url,
|
|
2251
|
+
mimeType: kind === "video" ? "video/mp4" : "image/png",
|
|
2252
|
+
taskId: task.task_id ?? task.id,
|
|
2253
|
+
// Kling clears results after 30 days.
|
|
2254
|
+
expiresAt: expiresInHours(24 * 30)
|
|
2255
|
+
}));
|
|
2256
|
+
}
|
|
2257
|
+
/** A capability job carries `kling:<name>:<resultField>` in its model slot. */
|
|
2258
|
+
resultFieldOf(job) {
|
|
2259
|
+
if (!job.model.startsWith("kling:")) return void 0;
|
|
2260
|
+
return job.model.split(":")[2];
|
|
2261
|
+
}
|
|
2262
|
+
snapshot(task, job) {
|
|
2263
|
+
const state = stateOf(task);
|
|
2264
|
+
if (state === "failed") {
|
|
2265
|
+
return {
|
|
2266
|
+
status: "failed",
|
|
2267
|
+
error: task.task_status_msg ?? "Kling task failed."
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
if (state === "pending") return { status: "pending" };
|
|
2271
|
+
const outputs = this.outputsFrom(task, job.kind, this.resultFieldOf(job));
|
|
2272
|
+
if (outputs.length === 0) return { status: "pending" };
|
|
2273
|
+
return {
|
|
2274
|
+
status: "succeeded",
|
|
2275
|
+
result: {
|
|
2276
|
+
success: true,
|
|
2277
|
+
outputs,
|
|
2278
|
+
creditsUsed: 0,
|
|
2279
|
+
provider: this.providerName,
|
|
2280
|
+
model: job.model,
|
|
2281
|
+
processingTimeMs: 0
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
async run(kind, params) {
|
|
2286
|
+
const startedAt = Date.now();
|
|
2287
|
+
let modelId = params.model ?? "(none)";
|
|
2288
|
+
const failure = (error) => ({
|
|
2289
|
+
success: false,
|
|
2290
|
+
outputs: [],
|
|
2291
|
+
creditsUsed: 0,
|
|
2292
|
+
provider: this.providerName,
|
|
2293
|
+
model: modelId,
|
|
2294
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2295
|
+
error
|
|
2296
|
+
});
|
|
2297
|
+
let submitted;
|
|
2298
|
+
try {
|
|
2299
|
+
modelId = this.binding(params.model)[0];
|
|
2300
|
+
submitted = await this.submitTask(kind, params);
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2303
|
+
}
|
|
2304
|
+
if (isSubmitMode()) {
|
|
2305
|
+
throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
|
|
2306
|
+
}
|
|
2307
|
+
const job = {
|
|
2308
|
+
id: submitted.taskId,
|
|
2309
|
+
provider: this.providerName,
|
|
2310
|
+
model: modelId,
|
|
2311
|
+
kind,
|
|
2312
|
+
pollEndpoint: submitted.pollEndpoint,
|
|
2313
|
+
params,
|
|
2314
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
2315
|
+
};
|
|
2316
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS3;
|
|
2317
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2318
|
+
const snapshot = await this.completeJob(job);
|
|
2319
|
+
if (snapshot.status === "failed") {
|
|
2320
|
+
return failure(snapshot.error ?? "Kling task failed.");
|
|
2321
|
+
}
|
|
2322
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2323
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2324
|
+
}
|
|
2325
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS3));
|
|
2326
|
+
}
|
|
2327
|
+
return failure(
|
|
2328
|
+
`Kling task ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
/**
|
|
2332
|
+
* Submit any declared capability and hand back a job.
|
|
2333
|
+
*
|
|
2334
|
+
* The three axes that differ between them — API family, body, result field —
|
|
2335
|
+
* all come from the capability itself, so this runs every one of them.
|
|
2336
|
+
*/
|
|
2337
|
+
async submitCapability(name, capability, input) {
|
|
2338
|
+
const path = capability.path(input);
|
|
2339
|
+
const task = await this.request(path, {
|
|
2340
|
+
method: "POST",
|
|
2341
|
+
body: capability.body(input)
|
|
2342
|
+
});
|
|
2343
|
+
const taskId = task.task_id ?? task.id;
|
|
2344
|
+
if (!taskId) {
|
|
2345
|
+
throw new Error(`Kling ${name} returned no task id.`);
|
|
2346
|
+
}
|
|
2347
|
+
return {
|
|
2348
|
+
id: taskId,
|
|
2349
|
+
provider: this.providerName,
|
|
2350
|
+
// Not a catalog model: the capability plus its result field is what the
|
|
2351
|
+
// resume path needs, so it rides in the model slot.
|
|
2352
|
+
model: `kling:${name}:${capability.results}`,
|
|
2353
|
+
kind: capability.kind === "image" ? "image" : "video",
|
|
2354
|
+
// The legacy family polls its own path; the newer one has /tasks.
|
|
2355
|
+
pollEndpoint: capability.api === "v1" ? path : "/tasks",
|
|
2356
|
+
params: { prompt: "" },
|
|
2357
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
/** Resume a job submitted earlier, possibly by another process. */
|
|
2361
|
+
async completeJob(job) {
|
|
2362
|
+
if (!job.pollEndpoint) {
|
|
2363
|
+
return { status: "failed", error: "Kling job handle has no endpoint." };
|
|
2364
|
+
}
|
|
2365
|
+
try {
|
|
2366
|
+
return this.snapshot(await this.queryTask(job.pollEndpoint, job.id), job);
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
return {
|
|
2369
|
+
status: "failed",
|
|
2370
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
generateImage(params) {
|
|
2375
|
+
return this.run("image", params);
|
|
2376
|
+
}
|
|
2377
|
+
generateVideo(params) {
|
|
2378
|
+
return this.run("video", params);
|
|
2379
|
+
}
|
|
2380
|
+
/** Your own cloned voices. Presets are in KLING_VOICES. */
|
|
2381
|
+
async listVoices() {
|
|
2382
|
+
const voices = await this.request("/v1/general/custom-voices?pageNum=1&pageSize=100");
|
|
2383
|
+
return voices.filter((voice) => voice.voice_id).map((voice) => ({
|
|
2384
|
+
voiceId: voice.voice_id,
|
|
2385
|
+
name: voice.voice_name
|
|
2386
|
+
}));
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Speech. Synchronous, and snake_case — unlike both video APIs on this host.
|
|
2390
|
+
*/
|
|
2391
|
+
async generateAudio(params) {
|
|
2392
|
+
const startedAt = Date.now();
|
|
2393
|
+
const modelId = params.model ?? KLING_AUDIO_MODEL;
|
|
2394
|
+
const failure = (error) => ({
|
|
2395
|
+
success: false,
|
|
2396
|
+
outputs: [],
|
|
2397
|
+
creditsUsed: 0,
|
|
2398
|
+
provider: this.providerName,
|
|
2399
|
+
model: modelId,
|
|
2400
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2401
|
+
error
|
|
2402
|
+
});
|
|
2403
|
+
const voiceId = params.voice;
|
|
2404
|
+
if (!voiceId) {
|
|
2405
|
+
return failure(
|
|
2406
|
+
`Kling needs a voice id. Presets: ${KLING_VOICES.slice(0, 5).join(", ")}\u2026 or one of your own from listVoices().`
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
try {
|
|
2410
|
+
const data = await this.request("/v1/audio/tts", {
|
|
2411
|
+
method: "POST",
|
|
2412
|
+
body: {
|
|
2413
|
+
// snake_case: camelCase falls into another DTO and misreports.
|
|
2414
|
+
voice_id: voiceId,
|
|
2415
|
+
voice_language: params.language ?? "en",
|
|
2416
|
+
text: params.prompt,
|
|
2417
|
+
...params.speed !== void 0 ? { voice_speed: params.speed } : {},
|
|
2418
|
+
...params.providerOptions?.kling ?? {}
|
|
2419
|
+
}
|
|
2420
|
+
});
|
|
2421
|
+
const url = data.audio_url ?? data.url;
|
|
2422
|
+
if (!url) return failure("Kling returned no audio URL.");
|
|
2423
|
+
return {
|
|
2424
|
+
success: true,
|
|
2425
|
+
outputs: [
|
|
2426
|
+
{
|
|
2427
|
+
url,
|
|
2428
|
+
mimeType: "audio/mpeg",
|
|
2429
|
+
taskId: data.id,
|
|
2430
|
+
expiresAt: expiresInHours(24 * 30),
|
|
2431
|
+
raw: { voiceId, characters: params.prompt.length }
|
|
2432
|
+
}
|
|
2433
|
+
],
|
|
2434
|
+
creditsUsed: 0,
|
|
2435
|
+
provider: this.providerName,
|
|
2436
|
+
model: modelId,
|
|
2437
|
+
processingTimeMs: Date.now() - startedAt
|
|
2438
|
+
};
|
|
2439
|
+
} catch (error) {
|
|
2440
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
generateText(_params) {
|
|
2444
|
+
throw new Error("Kling does not generate text.");
|
|
2445
|
+
}
|
|
2446
|
+
async estimateCost(type, params) {
|
|
2447
|
+
return estimateFor(this.providerName, type, params);
|
|
2448
|
+
}
|
|
2449
|
+
supportsModel(model) {
|
|
2450
|
+
return model in KLING_MODELS;
|
|
2451
|
+
}
|
|
2452
|
+
getAvailableModels() {
|
|
2453
|
+
return Object.entries(KLING_MODELS).map(([id, binding]) => ({
|
|
2454
|
+
id,
|
|
2455
|
+
name: id,
|
|
2456
|
+
type: binding.kind
|
|
2457
|
+
}));
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
|
|
2461
|
+
// src/adapters/openai.adapter.ts
|
|
2462
|
+
var DEFAULT_BASE_URL5 = "https://api.openai.com";
|
|
2463
|
+
var IMAGES_PATH2 = "/v1/images/generations";
|
|
2464
|
+
var OpenAIAdapter = class {
|
|
2465
|
+
providerName = "openai";
|
|
2466
|
+
// No video: the Videos API shuts down on 24 September 2026.
|
|
2467
|
+
supportedTypes = ["image"];
|
|
2468
|
+
opts;
|
|
2469
|
+
constructor(opts) {
|
|
2470
|
+
this.opts = opts;
|
|
2471
|
+
}
|
|
2472
|
+
get baseUrl() {
|
|
2473
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL5).replace(/\/$/, "");
|
|
2474
|
+
}
|
|
2475
|
+
binding(modelId) {
|
|
2476
|
+
const id = modelId ?? "";
|
|
2477
|
+
const found = OPENAI_IMAGE_MODELS[id];
|
|
2478
|
+
if (!found) {
|
|
2479
|
+
throw new Error(
|
|
2480
|
+
`"${id}" is not an OpenAI image model. Known: ${Object.keys(OPENAI_IMAGE_MODELS).join(", ")}.`
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
return [id, found];
|
|
2484
|
+
}
|
|
2485
|
+
buildBody(modelId, binding, params) {
|
|
2486
|
+
const quality = params.quality ?? "medium";
|
|
2487
|
+
if (!binding.qualities.includes(quality)) {
|
|
2488
|
+
throw new Error(
|
|
2489
|
+
`OpenAI "${modelId}" offers ${binding.qualities.join(", ")}, not ${quality}.`
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
if (params.resolution && binding.sizes !== "any") {
|
|
2493
|
+
if (!binding.sizes.includes(params.resolution)) {
|
|
2494
|
+
throw new Error(
|
|
2495
|
+
`OpenAI "${modelId}" accepts ${binding.sizes.join(", ")}, not ${params.resolution}.`
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
if (params.resolution && binding.sizes === "any") {
|
|
2500
|
+
const [w, h] = params.resolution.split("x").map(Number);
|
|
2501
|
+
if (w && h && (w % 16 !== 0 || h % 16 !== 0)) {
|
|
2502
|
+
throw new Error(
|
|
2503
|
+
`OpenAI "${modelId}" needs both edges divisible by 16; got ${params.resolution}.`
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
const body = {
|
|
2508
|
+
// Omitting it historically defaulted to dall-e-2, which is shut down.
|
|
2509
|
+
model: modelId,
|
|
2510
|
+
prompt: params.prompt,
|
|
2511
|
+
n: 1,
|
|
2512
|
+
quality
|
|
2513
|
+
};
|
|
2514
|
+
if (params.resolution) body.size = params.resolution;
|
|
2515
|
+
if (params.outputFormat) body.output_format = params.outputFormat;
|
|
2516
|
+
return { ...body, ...params.providerOptions?.openai ?? {} };
|
|
2517
|
+
}
|
|
2518
|
+
async generateImage(params) {
|
|
2519
|
+
const startedAt = Date.now();
|
|
2520
|
+
let modelId = params.model ?? "";
|
|
2521
|
+
const failure = (error) => ({
|
|
2522
|
+
success: false,
|
|
2523
|
+
outputs: [],
|
|
2524
|
+
creditsUsed: 0,
|
|
2525
|
+
provider: this.providerName,
|
|
2526
|
+
model: modelId,
|
|
2527
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2528
|
+
error
|
|
2529
|
+
});
|
|
2530
|
+
try {
|
|
2531
|
+
const [id, binding] = this.binding(params.model);
|
|
2532
|
+
modelId = id;
|
|
2533
|
+
const response = await fetch(`${this.baseUrl}${IMAGES_PATH2}`, {
|
|
2534
|
+
method: "POST",
|
|
2535
|
+
headers: {
|
|
2536
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
2537
|
+
"Content-Type": "application/json",
|
|
2538
|
+
...this.opts.organization ? { "OpenAI-Organization": this.opts.organization } : {}
|
|
2539
|
+
},
|
|
2540
|
+
body: JSON.stringify(this.buildBody(id, binding, params))
|
|
2541
|
+
});
|
|
2542
|
+
const payload = await response.json();
|
|
2543
|
+
if (payload.error) throw new Error(payload.error.message);
|
|
2544
|
+
if (!response.ok) {
|
|
2545
|
+
throw new Error(`OpenAI returned ${response.status}.`);
|
|
2546
|
+
}
|
|
2547
|
+
const format = params.outputFormat ?? "png";
|
|
2548
|
+
const outputs = (payload.data ?? []).map((item) => {
|
|
2549
|
+
if (item.b64_json) {
|
|
2550
|
+
return {
|
|
2551
|
+
url: `data:image/${format};base64,${item.b64_json}`,
|
|
2552
|
+
mimeType: `image/${format}`,
|
|
2553
|
+
raw: { inline: true, revisedPrompt: item.revised_prompt }
|
|
2554
|
+
};
|
|
2555
|
+
}
|
|
2556
|
+
if (item.url) {
|
|
2557
|
+
return { url: item.url, mimeType: `image/${format}` };
|
|
2558
|
+
}
|
|
2559
|
+
return void 0;
|
|
2560
|
+
}).filter((output) => Boolean(output));
|
|
2561
|
+
if (outputs.length === 0) return failure("OpenAI returned no image.");
|
|
2562
|
+
return {
|
|
2563
|
+
success: true,
|
|
2564
|
+
outputs,
|
|
2565
|
+
creditsUsed: 0,
|
|
2566
|
+
provider: this.providerName,
|
|
2567
|
+
model: modelId,
|
|
2568
|
+
processingTimeMs: Date.now() - startedAt
|
|
2569
|
+
};
|
|
2570
|
+
} catch (error) {
|
|
2571
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
generateVideo(_params) {
|
|
2575
|
+
throw new Error(
|
|
2576
|
+
"OpenAI has no supported video API \u2014 Sora and the Videos API shut down on 24 September 2026."
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
generateAudio(_params) {
|
|
2580
|
+
throw new Error("OpenAI has no speech synthesis wired up here.");
|
|
2581
|
+
}
|
|
2582
|
+
generateText(_params) {
|
|
2583
|
+
throw new Error("The OpenAI adapter here covers images only.");
|
|
2584
|
+
}
|
|
2585
|
+
/** Synchronous provider: there is never a queued job to come back to. */
|
|
2586
|
+
async completeJob(job) {
|
|
2587
|
+
return {
|
|
2588
|
+
status: "failed",
|
|
2589
|
+
error: `OpenAI answers inline, so job ${job.id} has nothing to resume.`
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
async estimateCost(type, params) {
|
|
2593
|
+
const quality = params.quality === "low" ? "low" : params.quality === "high" ? "high" : "medium";
|
|
2594
|
+
const binding = OPENAI_IMAGE_MODELS[params.model ?? ""];
|
|
2595
|
+
const base = estimateFor(this.providerName, type, params);
|
|
2596
|
+
if (!binding) return base;
|
|
2597
|
+
return {
|
|
2598
|
+
...base,
|
|
2599
|
+
usd: binding.usd[quality],
|
|
2600
|
+
note: `At ${quality} quality and 1024x1024. Low is ${binding.usd.low}, high is ${binding.usd.high} \u2014 the tier moves the price by more than 30x.`
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
supportsModel(model) {
|
|
2604
|
+
return model in OPENAI_IMAGE_MODELS;
|
|
2605
|
+
}
|
|
2606
|
+
getAvailableModels() {
|
|
2607
|
+
return Object.keys(OPENAI_IMAGE_MODELS).map((id) => ({
|
|
2608
|
+
id,
|
|
2609
|
+
name: id,
|
|
2610
|
+
type: "image"
|
|
2611
|
+
}));
|
|
2612
|
+
}
|
|
2613
|
+
};
|
|
2614
|
+
|
|
2615
|
+
// src/adapters/qwen.adapter.ts
|
|
2616
|
+
var DEFAULT_BASE_URL6 = "https://dashscope-intl.aliyuncs.com";
|
|
2617
|
+
var TASKS_PATH2 = "/api/v1/tasks";
|
|
2618
|
+
var POLL_INTERVAL_MS4 = 5e3;
|
|
2619
|
+
var DEFAULT_MAX_POLL_ATTEMPTS4 = 240;
|
|
2620
|
+
var QwenAdapter = class {
|
|
2621
|
+
providerName = "qwen";
|
|
2622
|
+
supportedTypes = ["image", "video"];
|
|
2623
|
+
opts;
|
|
2624
|
+
constructor(opts) {
|
|
2625
|
+
this.opts = opts;
|
|
2626
|
+
}
|
|
2627
|
+
get baseUrl() {
|
|
2628
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL6).replace(/\/$/, "");
|
|
2629
|
+
}
|
|
2630
|
+
async request(path, init) {
|
|
2631
|
+
const headers = {
|
|
2632
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
2633
|
+
"Content-Type": "application/json"
|
|
2634
|
+
};
|
|
2635
|
+
if (init?.async) headers["X-DashScope-Async"] = "enable";
|
|
2636
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
2637
|
+
method: init?.method ?? "GET",
|
|
2638
|
+
headers,
|
|
2639
|
+
body: init ? JSON.stringify(init.body) : void 0
|
|
2640
|
+
});
|
|
2641
|
+
const payload = await response.json();
|
|
2642
|
+
if (payload.code) {
|
|
2643
|
+
throw new Error(`${payload.code}: ${payload.message ?? "unknown"}`);
|
|
2644
|
+
}
|
|
2645
|
+
return payload;
|
|
2646
|
+
}
|
|
2647
|
+
binding(modelId) {
|
|
2648
|
+
const id = modelId ?? "";
|
|
2649
|
+
const found = QWEN_VIDEO_MODELS[id];
|
|
2650
|
+
if (!found) {
|
|
2651
|
+
throw new Error(
|
|
2652
|
+
`"${id}" is not a Qwen video model. Known: ${Object.keys(QWEN_VIDEO_MODELS).join(", ")}.`
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
return [id, found];
|
|
2656
|
+
}
|
|
2657
|
+
videoBody(modelId, binding, params) {
|
|
2658
|
+
const input = { prompt: params.prompt };
|
|
2659
|
+
if (params.negativePrompt) input.negative_prompt = params.negativePrompt;
|
|
2660
|
+
const first = params.imageUrl ?? params.referenceImages?.[0];
|
|
2661
|
+
const last = params.referenceImages?.[1];
|
|
2662
|
+
const references = params.imageUrls ?? [];
|
|
2663
|
+
switch (binding.shape) {
|
|
2664
|
+
case "kf2v": {
|
|
2665
|
+
if (!first || !last) {
|
|
2666
|
+
throw new Error(
|
|
2667
|
+
`Qwen "${modelId}" needs a first and a last frame \u2014 pass both in referenceImages.`
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
input.first_frame_url = first;
|
|
2671
|
+
input.last_frame_url = last;
|
|
2672
|
+
break;
|
|
2673
|
+
}
|
|
2674
|
+
case "img_url": {
|
|
2675
|
+
if (!first) {
|
|
2676
|
+
throw new Error(`Qwen "${modelId}" needs an image \u2014 pass imageUrl.`);
|
|
2677
|
+
}
|
|
2678
|
+
input.img_url = first;
|
|
2679
|
+
break;
|
|
2680
|
+
}
|
|
2681
|
+
case "reference_urls": {
|
|
2682
|
+
if (references.length === 0) {
|
|
2683
|
+
throw new Error(
|
|
2684
|
+
`Qwen "${modelId}" needs reference images \u2014 pass imageUrls.`
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
input.reference_urls = references;
|
|
2688
|
+
break;
|
|
2689
|
+
}
|
|
2690
|
+
case "media": {
|
|
2691
|
+
const media = [];
|
|
2692
|
+
if (binding.mode === "video-edit") {
|
|
2693
|
+
const video = params.videoUrl ?? params.videoUrls?.[0];
|
|
2694
|
+
if (!video) {
|
|
2695
|
+
throw new Error(`Qwen "${modelId}" needs a video \u2014 pass videoUrl.`);
|
|
2696
|
+
}
|
|
2697
|
+
media.push({ type: "video", url: video });
|
|
2698
|
+
} else if (binding.mode === "r2v") {
|
|
2699
|
+
for (const url of references) {
|
|
2700
|
+
media.push({ type: "reference_image", url });
|
|
2701
|
+
}
|
|
2702
|
+
} else if (first) {
|
|
2703
|
+
media.push({ type: "first_frame", url: first });
|
|
2704
|
+
if (last) media.push({ type: "last_frame", url: last });
|
|
2705
|
+
}
|
|
2706
|
+
if (media.length > 0) input.media = media;
|
|
2707
|
+
break;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const parameters = {};
|
|
2711
|
+
if (binding.sizing === "size") {
|
|
2712
|
+
if (params.resolution) parameters.size = params.resolution;
|
|
2713
|
+
} else {
|
|
2714
|
+
parameters.resolution = params.resolution ?? "720P";
|
|
2715
|
+
if (params.aspectRatio && params.aspectRatio !== "auto") {
|
|
2716
|
+
parameters.ratio = params.aspectRatio;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
if (params.duration !== void 0) parameters.duration = params.duration;
|
|
2720
|
+
if (params.seed !== void 0) parameters.seed = params.seed;
|
|
2721
|
+
parameters.prompt_extend = false;
|
|
2722
|
+
if (binding.watermarkOnByDefault) parameters.watermark = false;
|
|
2723
|
+
return {
|
|
2724
|
+
model: modelId,
|
|
2725
|
+
input,
|
|
2726
|
+
// The caller's escape hatch wins, since they asked for it by name.
|
|
2727
|
+
parameters: { ...parameters, ...params.providerOptions?.qwen ?? {} }
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
outputsFrom(payload, kind) {
|
|
2731
|
+
const output = payload.output;
|
|
2732
|
+
if (!output) return [];
|
|
2733
|
+
const urls = [
|
|
2734
|
+
...output.video_url ? [output.video_url] : [],
|
|
2735
|
+
...(output.results ?? []).map((result) => result.url).filter((url) => Boolean(url)),
|
|
2736
|
+
...(output.choices ?? []).flatMap(
|
|
2737
|
+
(choice) => (choice.message?.content ?? []).map((part) => part.image).filter((image) => Boolean(image))
|
|
2738
|
+
)
|
|
2739
|
+
];
|
|
2740
|
+
return urls.map((url) => ({
|
|
2741
|
+
url,
|
|
2742
|
+
mimeType: kind === "video" ? "video/mp4" : "image/png",
|
|
2743
|
+
taskId: output.task_id,
|
|
2744
|
+
durationSeconds: payload.usage?.duration,
|
|
2745
|
+
// The OSS link is presigned and stated as valid for 24 hours.
|
|
2746
|
+
expiresAt: expiresInHours(24),
|
|
2747
|
+
raw: { size: payload.usage?.size }
|
|
2748
|
+
}));
|
|
2749
|
+
}
|
|
2750
|
+
snapshot(payload, job) {
|
|
2751
|
+
const status = payload.output?.task_status;
|
|
2752
|
+
if (status === "FAILED" || status === "CANCELED" || status === "CANCELLED" || // An unknown task id is expired or wrong: terminal, not worth retrying.
|
|
2753
|
+
status === "UNKNOWN") {
|
|
2754
|
+
const output = payload.output;
|
|
2755
|
+
const reason = output?.message ?? `DashScope reported ${status}.`;
|
|
2756
|
+
return {
|
|
2757
|
+
status: "failed",
|
|
2758
|
+
error: output?.code ? `${output.code}: ${reason}` : reason
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
if (status !== "SUCCEEDED") return { status: "pending" };
|
|
2762
|
+
const outputs = this.outputsFrom(payload, job.kind);
|
|
2763
|
+
if (outputs.length === 0) return { status: "pending" };
|
|
2764
|
+
return {
|
|
2765
|
+
status: "succeeded",
|
|
2766
|
+
result: {
|
|
2767
|
+
success: true,
|
|
2768
|
+
outputs,
|
|
2769
|
+
creditsUsed: 0,
|
|
2770
|
+
provider: this.providerName,
|
|
2771
|
+
model: job.model,
|
|
2772
|
+
processingTimeMs: 0
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
async run(kind, params) {
|
|
2777
|
+
const startedAt = Date.now();
|
|
2778
|
+
let modelId = params.model ?? "(none)";
|
|
2779
|
+
const failure = (error) => ({
|
|
2780
|
+
success: false,
|
|
2781
|
+
outputs: [],
|
|
2782
|
+
creditsUsed: 0,
|
|
2783
|
+
provider: this.providerName,
|
|
2784
|
+
model: modelId,
|
|
2785
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2786
|
+
error
|
|
2787
|
+
});
|
|
2788
|
+
let taskId;
|
|
2789
|
+
try {
|
|
2790
|
+
const [id, binding] = this.binding(params.model);
|
|
2791
|
+
modelId = id;
|
|
2792
|
+
if (binding.resolutions && params.resolution) {
|
|
2793
|
+
if (!binding.resolutions.includes(params.resolution)) {
|
|
2794
|
+
throw new Error(
|
|
2795
|
+
`Qwen "${id}" supports ${binding.resolutions.join(", ")}, not ${params.resolution}.`
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
const payload = await this.request(videoPathFor(binding), {
|
|
2800
|
+
method: "POST",
|
|
2801
|
+
body: this.videoBody(id, binding, params),
|
|
2802
|
+
async: true
|
|
2803
|
+
});
|
|
2804
|
+
const submitted = payload.output?.task_id;
|
|
2805
|
+
if (!submitted) {
|
|
2806
|
+
throw new Error(
|
|
2807
|
+
"DashScope accepted the request but returned no task id."
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
taskId = submitted;
|
|
2811
|
+
} catch (error) {
|
|
2812
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2813
|
+
}
|
|
2814
|
+
if (isSubmitMode()) {
|
|
2815
|
+
throw new PendingJob(taskId, TASKS_PATH2);
|
|
2816
|
+
}
|
|
2817
|
+
const job = {
|
|
2818
|
+
id: taskId,
|
|
2819
|
+
provider: this.providerName,
|
|
2820
|
+
model: modelId,
|
|
2821
|
+
kind,
|
|
2822
|
+
pollEndpoint: TASKS_PATH2,
|
|
2823
|
+
params,
|
|
2824
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
2825
|
+
};
|
|
2826
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS4;
|
|
2827
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2828
|
+
const snapshot = await this.completeJob(job);
|
|
2829
|
+
if (snapshot.status === "failed") {
|
|
2830
|
+
return failure(snapshot.error ?? "DashScope task failed.");
|
|
2831
|
+
}
|
|
2832
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2833
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2834
|
+
}
|
|
2835
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS4));
|
|
2836
|
+
}
|
|
2837
|
+
return failure(
|
|
2838
|
+
`DashScope task ${taskId} did not finish after ${maxAttempts} checks.`
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
/** Resume a job submitted earlier, possibly by another process. */
|
|
2842
|
+
async completeJob(job) {
|
|
2843
|
+
try {
|
|
2844
|
+
const payload = await this.request(
|
|
2845
|
+
`${job.pollEndpoint ?? TASKS_PATH2}/${encodeURIComponent(job.id)}`
|
|
2846
|
+
);
|
|
2847
|
+
return this.snapshot(payload, job);
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
return {
|
|
2850
|
+
status: "failed",
|
|
2851
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
generateVideo(params) {
|
|
2856
|
+
return this.run("video", params);
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Every image model wired up here answers inline, so this makes no task.
|
|
2860
|
+
*
|
|
2861
|
+
* That is not a style choice: sending `X-DashScope-Async: enable` to a
|
|
2862
|
+
* sync-only model returns 429, which reads as rate limiting and never clears
|
|
2863
|
+
* on retry. The header is gated on the model's own flag for that reason.
|
|
2864
|
+
*/
|
|
2865
|
+
async generateImage(params) {
|
|
2866
|
+
const startedAt = Date.now();
|
|
2867
|
+
const modelId = params.model ?? "";
|
|
2868
|
+
const failure = (error) => ({
|
|
2869
|
+
success: false,
|
|
2870
|
+
outputs: [],
|
|
2871
|
+
creditsUsed: 0,
|
|
2872
|
+
provider: this.providerName,
|
|
2873
|
+
model: modelId,
|
|
2874
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2875
|
+
error
|
|
2876
|
+
});
|
|
2877
|
+
const binding = QWEN_IMAGE_MODELS[modelId];
|
|
2878
|
+
if (!binding) {
|
|
2879
|
+
return failure(
|
|
2880
|
+
`"${modelId}" is not a Qwen image model. Known: ${Object.keys(QWEN_IMAGE_MODELS).join(", ")}.`
|
|
2881
|
+
);
|
|
2882
|
+
}
|
|
2883
|
+
try {
|
|
2884
|
+
const content = [];
|
|
2885
|
+
for (const image of params.referenceImages ?? []) {
|
|
2886
|
+
content.push({ image });
|
|
2887
|
+
}
|
|
2888
|
+
content.push({ text: params.prompt });
|
|
2889
|
+
const parameters = {
|
|
2890
|
+
// Several models default to 4, and billing is per image produced.
|
|
2891
|
+
n: 1,
|
|
2892
|
+
// Defaults to true and silently rewrites the prompt through an LLM.
|
|
2893
|
+
prompt_extend: false
|
|
2894
|
+
};
|
|
2895
|
+
if (params.resolution) parameters.size = params.resolution;
|
|
2896
|
+
const payload = await this.request(QWEN_IMAGE_PATHS[binding.path], {
|
|
2897
|
+
method: "POST",
|
|
2898
|
+
body: binding.body === "messages" ? {
|
|
2899
|
+
model: modelId,
|
|
2900
|
+
// The qwen-image family takes chat-style messages.
|
|
2901
|
+
input: { messages: [{ role: "user", content }] },
|
|
2902
|
+
parameters
|
|
2903
|
+
} : {
|
|
2904
|
+
model: modelId,
|
|
2905
|
+
input: { prompt: params.prompt },
|
|
2906
|
+
parameters
|
|
2907
|
+
},
|
|
2908
|
+
// Gated deliberately: see the note above.
|
|
2909
|
+
async: !binding.sync
|
|
2910
|
+
});
|
|
2911
|
+
const outputs = this.outputsFrom(payload, "image");
|
|
2912
|
+
if (outputs.length === 0) {
|
|
2913
|
+
const reason = payload.output?.message ?? payload.message;
|
|
2914
|
+
return failure(reason ?? "DashScope returned no image.");
|
|
2915
|
+
}
|
|
2916
|
+
return {
|
|
2917
|
+
success: true,
|
|
2918
|
+
outputs,
|
|
2919
|
+
creditsUsed: 0,
|
|
2920
|
+
provider: this.providerName,
|
|
2921
|
+
model: modelId,
|
|
2922
|
+
processingTimeMs: Date.now() - startedAt
|
|
2923
|
+
};
|
|
2924
|
+
} catch (error) {
|
|
2925
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Speech. Synchronous: the audio URL comes back on the same response, with a
|
|
2930
|
+
* real expiry attached rather than one we guess at.
|
|
2931
|
+
*/
|
|
2932
|
+
async generateAudio(params) {
|
|
2933
|
+
const startedAt = Date.now();
|
|
2934
|
+
const modelId = params.model ?? "";
|
|
2935
|
+
const failure = (error) => ({
|
|
2936
|
+
success: false,
|
|
2937
|
+
outputs: [],
|
|
2938
|
+
creditsUsed: 0,
|
|
2939
|
+
provider: this.providerName,
|
|
2940
|
+
model: modelId,
|
|
2941
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2942
|
+
error
|
|
2943
|
+
});
|
|
2944
|
+
const binding = QWEN_AUDIO_MODELS[modelId];
|
|
2945
|
+
if (!binding) {
|
|
2946
|
+
return failure(
|
|
2947
|
+
`"${modelId}" is not a Qwen speech model. Known: ${Object.keys(QWEN_AUDIO_MODELS).join(", ")}.`
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
const voice = params.voice ?? binding.voices[0];
|
|
2951
|
+
if (!binding.voices.includes(voice)) {
|
|
2952
|
+
return failure(
|
|
2953
|
+
`Qwen "${modelId}" offers ${binding.voices.join(", ")}, not "${voice}".`
|
|
2954
|
+
);
|
|
2955
|
+
}
|
|
2956
|
+
try {
|
|
2957
|
+
const payload = await this.request(
|
|
2958
|
+
"/api/v1/services/aigc/multimodal-generation/generation",
|
|
2959
|
+
{
|
|
2960
|
+
method: "POST",
|
|
2961
|
+
body: {
|
|
2962
|
+
model: modelId,
|
|
2963
|
+
input: { text: params.prompt },
|
|
2964
|
+
parameters: {
|
|
2965
|
+
voice,
|
|
2966
|
+
...params.outputFormat ? { format: params.outputFormat } : {},
|
|
2967
|
+
...params.providerOptions?.qwen ?? {}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
);
|
|
2972
|
+
const audio = payload.output?.audio;
|
|
2973
|
+
if (!audio?.url) return failure("Qwen returned no audio.");
|
|
2974
|
+
return {
|
|
2975
|
+
success: true,
|
|
2976
|
+
outputs: [
|
|
2977
|
+
{
|
|
2978
|
+
url: audio.url,
|
|
2979
|
+
mimeType: `audio/${params.outputFormat ?? "mp3"}`,
|
|
2980
|
+
taskId: audio.id,
|
|
2981
|
+
// A real timestamp from the provider, not an assumed window.
|
|
2982
|
+
expiresAt: audio.expires_at ? new Date(audio.expires_at * 1e3).toISOString() : void 0,
|
|
2983
|
+
raw: { characters: payload.usage?.characters }
|
|
2984
|
+
}
|
|
2985
|
+
],
|
|
2986
|
+
creditsUsed: 0,
|
|
2987
|
+
provider: this.providerName,
|
|
2988
|
+
model: modelId,
|
|
2989
|
+
processingTimeMs: Date.now() - startedAt
|
|
2990
|
+
};
|
|
2991
|
+
} catch (error) {
|
|
2992
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
/** Text, through the OpenAI-compatible surface. Synchronous. */
|
|
2996
|
+
async generateText(params) {
|
|
2997
|
+
const startedAt = Date.now();
|
|
2998
|
+
const modelId = params.model ?? "";
|
|
2999
|
+
const failure = (error) => ({
|
|
3000
|
+
success: false,
|
|
3001
|
+
outputs: [],
|
|
3002
|
+
creditsUsed: 0,
|
|
3003
|
+
provider: this.providerName,
|
|
3004
|
+
model: modelId,
|
|
3005
|
+
processingTimeMs: Date.now() - startedAt,
|
|
3006
|
+
error
|
|
3007
|
+
});
|
|
3008
|
+
if (!(modelId in QWEN_TEXT_MODELS)) {
|
|
3009
|
+
return failure(
|
|
3010
|
+
`"${modelId}" is not a Qwen text model. Known: ${Object.keys(QWEN_TEXT_MODELS).join(", ")}.`
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
try {
|
|
3014
|
+
const response = await fetch(
|
|
3015
|
+
`${this.baseUrl}/compatible-mode/v1/chat/completions`,
|
|
3016
|
+
{
|
|
3017
|
+
method: "POST",
|
|
3018
|
+
headers: {
|
|
3019
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
3020
|
+
"Content-Type": "application/json"
|
|
3021
|
+
},
|
|
3022
|
+
body: JSON.stringify({
|
|
3023
|
+
model: modelId,
|
|
3024
|
+
messages: [
|
|
3025
|
+
...params.systemPrompt ? [{ role: "system", content: params.systemPrompt }] : [],
|
|
3026
|
+
{ role: "user", content: params.prompt }
|
|
3027
|
+
],
|
|
3028
|
+
...params.maxTokens ? { max_tokens: params.maxTokens } : {},
|
|
3029
|
+
...params.temperature !== void 0 ? { temperature: params.temperature } : {},
|
|
3030
|
+
...params.providerOptions?.qwen ?? {}
|
|
3031
|
+
})
|
|
3032
|
+
}
|
|
3033
|
+
);
|
|
3034
|
+
const payload = await response.json();
|
|
3035
|
+
if (payload.error) throw new Error(payload.error.message ?? "rejected");
|
|
3036
|
+
const text = payload.choices?.[0]?.message?.content;
|
|
3037
|
+
if (!text) return failure("Qwen returned no text.");
|
|
3038
|
+
return {
|
|
3039
|
+
success: true,
|
|
3040
|
+
// Text has no file, so it rides as a data URI and stays uniform.
|
|
3041
|
+
outputs: [
|
|
3042
|
+
{
|
|
3043
|
+
url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
|
|
3044
|
+
mimeType: "text/plain",
|
|
3045
|
+
raw: { text, tokens: payload.usage?.completion_tokens }
|
|
3046
|
+
}
|
|
3047
|
+
],
|
|
3048
|
+
creditsUsed: 0,
|
|
3049
|
+
provider: this.providerName,
|
|
3050
|
+
model: modelId,
|
|
3051
|
+
processingTimeMs: Date.now() - startedAt
|
|
3052
|
+
};
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
async estimateCost(type, params) {
|
|
3058
|
+
return estimateFor(this.providerName, type, params);
|
|
3059
|
+
}
|
|
3060
|
+
supportsModel(model) {
|
|
3061
|
+
return model in QWEN_VIDEO_MODELS || model in QWEN_IMAGE_MODELS || model in QWEN_AUDIO_MODELS || model in QWEN_TEXT_MODELS;
|
|
3062
|
+
}
|
|
3063
|
+
getAvailableModels() {
|
|
3064
|
+
return [
|
|
3065
|
+
...Object.keys(QWEN_VIDEO_MODELS).map((id) => ({
|
|
3066
|
+
id,
|
|
3067
|
+
name: id,
|
|
3068
|
+
type: "video"
|
|
3069
|
+
})),
|
|
3070
|
+
...Object.keys(QWEN_IMAGE_MODELS).map((id) => ({
|
|
3071
|
+
id,
|
|
3072
|
+
name: id,
|
|
3073
|
+
type: "image"
|
|
3074
|
+
}))
|
|
3075
|
+
];
|
|
3076
|
+
}
|
|
3077
|
+
};
|
|
3078
|
+
|
|
3079
|
+
// src/helpers/result.ts
|
|
3080
|
+
function ok(data) {
|
|
3081
|
+
return { data, error: null };
|
|
3082
|
+
}
|
|
3083
|
+
function fail(error) {
|
|
3084
|
+
return { data: null, error };
|
|
3085
|
+
}
|
|
3086
|
+
function failFrom(code, cause, context) {
|
|
3087
|
+
return fail({
|
|
3088
|
+
code,
|
|
3089
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
3090
|
+
cause,
|
|
3091
|
+
...context
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// src/lib/storage.ts
|
|
3096
|
+
async function loadS3(config) {
|
|
3097
|
+
let s3;
|
|
3098
|
+
try {
|
|
3099
|
+
s3 = await import("@aws-sdk/client-s3");
|
|
3100
|
+
} catch {
|
|
3101
|
+
throw new Error(
|
|
3102
|
+
"Configuring `storage` needs @aws-sdk/client-s3. Install it: bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
|
|
3103
|
+
);
|
|
3104
|
+
}
|
|
3105
|
+
const client = new s3.S3Client({
|
|
3106
|
+
region: config.region ?? "us-east-1",
|
|
3107
|
+
endpoint: config.endpoint,
|
|
3108
|
+
// Custom hosts (R2, MinIO) serve one bucket per path, not per subdomain.
|
|
3109
|
+
forcePathStyle: Boolean(config.endpoint),
|
|
3110
|
+
credentials: {
|
|
3111
|
+
accessKeyId: config.accessKeyId,
|
|
3112
|
+
secretAccessKey: config.secretAccessKey
|
|
3113
|
+
}
|
|
3114
|
+
});
|
|
3115
|
+
return { client, ...s3 };
|
|
3116
|
+
}
|
|
3117
|
+
function keyFor(config, filename) {
|
|
3118
|
+
const prefix = config.prefix?.replace(/^\/|\/$/g, "");
|
|
3119
|
+
const stamp = Date.now();
|
|
3120
|
+
const safe = filename.replace(/[^\w.-]/g, "_");
|
|
3121
|
+
return prefix ? `${prefix}/${stamp}-${safe}` : `${stamp}-${safe}`;
|
|
3122
|
+
}
|
|
3123
|
+
function extensionFor(url, mimeType) {
|
|
3124
|
+
const fromUrl = new URL(url).pathname.split(".").pop();
|
|
3125
|
+
if (fromUrl && fromUrl.length <= 5 && /^[a-z0-9]+$/i.test(fromUrl)) {
|
|
3126
|
+
return fromUrl;
|
|
3127
|
+
}
|
|
3128
|
+
if (mimeType?.startsWith("video/")) return "mp4";
|
|
3129
|
+
if (mimeType === "image/jpeg") return "jpg";
|
|
3130
|
+
return "png";
|
|
3131
|
+
}
|
|
3132
|
+
function createS3Storage(config) {
|
|
3133
|
+
let loaded;
|
|
3134
|
+
async function s3() {
|
|
3135
|
+
loaded ??= await loadS3(config);
|
|
3136
|
+
return loaded;
|
|
3137
|
+
}
|
|
3138
|
+
async function urlFor(key) {
|
|
3139
|
+
if (config.publicUrl) {
|
|
3140
|
+
return `${config.publicUrl.replace(/\/$/, "")}/${key}`;
|
|
3141
|
+
}
|
|
3142
|
+
const { client, GetObjectCommand } = await s3();
|
|
3143
|
+
let presigner;
|
|
3144
|
+
try {
|
|
3145
|
+
presigner = await import("@aws-sdk/s3-request-presigner");
|
|
3146
|
+
} catch {
|
|
3147
|
+
throw new Error(
|
|
3148
|
+
"Signing a URL needs @aws-sdk/s3-request-presigner, or set storage.publicUrl to skip signing."
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
return presigner.getSignedUrl(
|
|
3152
|
+
client,
|
|
3153
|
+
new GetObjectCommand({ Bucket: config.bucket, Key: key }),
|
|
3154
|
+
{ expiresIn: config.signedUrlExpiresIn ?? 3600 }
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
return {
|
|
3158
|
+
async upload(buffer, filename, mimeType) {
|
|
3159
|
+
const { client, PutObjectCommand } = await s3();
|
|
3160
|
+
const key = keyFor(config, filename);
|
|
3161
|
+
await client.send(
|
|
3162
|
+
new PutObjectCommand({
|
|
3163
|
+
Bucket: config.bucket,
|
|
3164
|
+
Key: key,
|
|
3165
|
+
Body: buffer,
|
|
3166
|
+
ContentType: mimeType
|
|
3167
|
+
})
|
|
3168
|
+
);
|
|
3169
|
+
return urlFor(key);
|
|
3170
|
+
},
|
|
3171
|
+
async persist(url, mimeType) {
|
|
3172
|
+
const response = await fetch(url);
|
|
3173
|
+
if (!response.ok) {
|
|
3174
|
+
throw new Error(
|
|
3175
|
+
`Could not fetch ${url} to persist it: ${response.status} ${response.statusText}`
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3179
|
+
const contentType = mimeType ?? response.headers.get("content-type") ?? "application/octet-stream";
|
|
3180
|
+
const { client, PutObjectCommand } = await s3();
|
|
3181
|
+
const key = keyFor(config, `output.${extensionFor(url, contentType)}`);
|
|
3182
|
+
await client.send(
|
|
3183
|
+
new PutObjectCommand({
|
|
3184
|
+
Bucket: config.bucket,
|
|
3185
|
+
Key: key,
|
|
3186
|
+
Body: buffer,
|
|
3187
|
+
ContentType: contentType
|
|
3188
|
+
})
|
|
3189
|
+
);
|
|
3190
|
+
return urlFor(key);
|
|
3191
|
+
}
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
async function persistOutputs(storage, outputs) {
|
|
3195
|
+
return Promise.all(
|
|
3196
|
+
outputs.map(async (output) => {
|
|
3197
|
+
try {
|
|
3198
|
+
const url = await storage.persist(output.url, output.mimeType);
|
|
3199
|
+
return {
|
|
3200
|
+
...output,
|
|
3201
|
+
url,
|
|
3202
|
+
sourceUrl: output.url,
|
|
3203
|
+
// The copy is ours now, so the provider's clock no longer applies.
|
|
3204
|
+
expiresAt: void 0
|
|
3205
|
+
};
|
|
3206
|
+
} catch (error) {
|
|
3207
|
+
console.warn(
|
|
3208
|
+
`[storage] Kept the provider URL for ${output.url}:`,
|
|
3209
|
+
error
|
|
3210
|
+
);
|
|
3211
|
+
return output;
|
|
3212
|
+
}
|
|
3213
|
+
})
|
|
3214
|
+
);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
// src/client.ts
|
|
3218
|
+
var NATIVE_PROVIDERS = [
|
|
3219
|
+
"byteplus",
|
|
3220
|
+
"elevenlabs",
|
|
3221
|
+
"google",
|
|
3222
|
+
"kling",
|
|
3223
|
+
"openai",
|
|
3224
|
+
"qwen"
|
|
3225
|
+
];
|
|
3226
|
+
function brotuClient(options) {
|
|
3227
|
+
if (Object.keys(options.providers).length === 0) {
|
|
3228
|
+
throw new Error("brotuClient requires at least one provider API key.");
|
|
3229
|
+
}
|
|
3230
|
+
registerModels(options.models);
|
|
3231
|
+
const storage = options.storage ? createS3Storage(options.storage) : void 0;
|
|
3232
|
+
const shouldPersist = storage && options.storage?.persistOutputs !== false;
|
|
3233
|
+
const adapters = /* @__PURE__ */ new Map();
|
|
3234
|
+
function route(modelId) {
|
|
3235
|
+
if (!modelId) {
|
|
3236
|
+
return fail({
|
|
3237
|
+
code: "invalid_request",
|
|
3238
|
+
message: "Pass a model id \u2014 this SDK ships no implicit default."
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
const model = getModel(modelId);
|
|
3242
|
+
if (!model) {
|
|
3243
|
+
return fail({
|
|
3244
|
+
code: "unknown_model",
|
|
3245
|
+
message: `Unknown model "${modelId}".`,
|
|
3246
|
+
model: modelId
|
|
3247
|
+
});
|
|
3248
|
+
}
|
|
3249
|
+
let provider;
|
|
3250
|
+
try {
|
|
3251
|
+
provider = resolveProvider(modelId, options);
|
|
3252
|
+
} catch (error) {
|
|
3253
|
+
return failFrom("missing_key", error, { model: modelId });
|
|
3254
|
+
}
|
|
3255
|
+
let adapter = adapters.get(provider.id);
|
|
3256
|
+
if (!adapter) {
|
|
3257
|
+
const built = buildAdapter(provider);
|
|
3258
|
+
if (!built) {
|
|
3259
|
+
return fail({
|
|
3260
|
+
code: "unsupported_provider",
|
|
3261
|
+
message: `No adapter ships for provider "${provider.id}". Native providers: ${NATIVE_PROVIDERS.join(", ")}.`,
|
|
3262
|
+
provider: provider.id,
|
|
3263
|
+
model: modelId
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
adapter = built;
|
|
3267
|
+
adapters.set(provider.id, adapter);
|
|
3268
|
+
}
|
|
3269
|
+
return ok({ adapter, provider: provider.id, model: modelId });
|
|
3270
|
+
}
|
|
3271
|
+
function buildAdapter(provider) {
|
|
3272
|
+
if (provider.id === "kling") {
|
|
3273
|
+
return new KlingAdapter({
|
|
3274
|
+
apiKey: provider.apiKey,
|
|
3275
|
+
baseUrl: provider.baseUrl
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
if (provider.id === "byteplus") {
|
|
3279
|
+
return new BytePlusAdapter({
|
|
3280
|
+
apiKey: provider.apiKey,
|
|
3281
|
+
baseUrl: provider.baseUrl
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
if (provider.id === "qwen") {
|
|
3285
|
+
return new QwenAdapter({
|
|
3286
|
+
apiKey: provider.apiKey,
|
|
3287
|
+
baseUrl: provider.baseUrl
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
if (provider.id === "openai") {
|
|
3291
|
+
return new OpenAIAdapter({
|
|
3292
|
+
apiKey: provider.apiKey,
|
|
3293
|
+
baseUrl: provider.baseUrl
|
|
3294
|
+
});
|
|
3295
|
+
}
|
|
3296
|
+
if (provider.id === "elevenlabs") {
|
|
3297
|
+
return new ElevenLabsAdapter({
|
|
3298
|
+
apiKey: provider.apiKey,
|
|
3299
|
+
baseUrl: provider.baseUrl,
|
|
3300
|
+
defaultVoiceId: options.elevenLabsVoiceId
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
if (provider.id === "google") {
|
|
3304
|
+
return new GoogleAdapter({
|
|
3305
|
+
apiKey: provider.apiKey,
|
|
3306
|
+
baseUrl: provider.baseUrl
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
return void 0;
|
|
3310
|
+
}
|
|
3311
|
+
function generateWith(adapter, kind, params) {
|
|
3312
|
+
if (kind === "image")
|
|
3313
|
+
return adapter.generateImage(params);
|
|
3314
|
+
if (kind === "video")
|
|
3315
|
+
return adapter.generateVideo(params);
|
|
3316
|
+
if (kind === "audio")
|
|
3317
|
+
return adapter.generateAudio(params);
|
|
3318
|
+
return adapter.generateText(params);
|
|
3319
|
+
}
|
|
3320
|
+
async function toGeneration(raw, metadata) {
|
|
3321
|
+
if (!raw.success) {
|
|
3322
|
+
return fail({
|
|
3323
|
+
code: "provider_error",
|
|
3324
|
+
message: raw.error ?? "The provider failed without saying why.",
|
|
3325
|
+
provider: raw.provider,
|
|
3326
|
+
model: raw.model
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
const outputs = shouldPersist && storage ? await persistOutputs(storage, raw.outputs) : raw.outputs;
|
|
3330
|
+
return ok({
|
|
3331
|
+
outputs,
|
|
3332
|
+
provider: raw.provider,
|
|
3333
|
+
model: raw.model,
|
|
3334
|
+
processingTimeMs: raw.processingTimeMs,
|
|
3335
|
+
metadata
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
async function generate(kind, params) {
|
|
3339
|
+
const routed = route(params.model);
|
|
3340
|
+
if (routed.error) return fail(routed.error);
|
|
3341
|
+
try {
|
|
3342
|
+
return await toGeneration(
|
|
3343
|
+
await generateWith(routed.data.adapter, kind, params),
|
|
3344
|
+
params.metadata
|
|
3345
|
+
);
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
return failFrom("provider_error", error, {
|
|
3348
|
+
provider: routed.data.provider,
|
|
3349
|
+
model: routed.data.model
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
async function submit(kind, params) {
|
|
3354
|
+
const routed = route(params.model);
|
|
3355
|
+
if (routed.error) return fail(routed.error);
|
|
3356
|
+
const base = {
|
|
3357
|
+
provider: routed.data.provider,
|
|
3358
|
+
model: routed.data.model,
|
|
3359
|
+
kind,
|
|
3360
|
+
params,
|
|
3361
|
+
metadata: params.metadata,
|
|
3362
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3363
|
+
};
|
|
3364
|
+
try {
|
|
3365
|
+
const raw = await runInSubmitMode(
|
|
3366
|
+
() => generateWith(routed.data.adapter, kind, params)
|
|
3367
|
+
);
|
|
3368
|
+
if (!raw.success) {
|
|
3369
|
+
return fail({
|
|
3370
|
+
code: "provider_error",
|
|
3371
|
+
message: raw.error ?? "The provider rejected the request.",
|
|
3372
|
+
provider: raw.provider,
|
|
3373
|
+
model: raw.model
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
return ok({
|
|
3377
|
+
...base,
|
|
3378
|
+
id: `inline-${base.model}-${base.submittedAt}`,
|
|
3379
|
+
result: raw
|
|
3380
|
+
});
|
|
3381
|
+
} catch (error) {
|
|
3382
|
+
if (!isPendingJob(error)) {
|
|
3383
|
+
return failFrom("provider_error", error, {
|
|
3384
|
+
provider: base.provider,
|
|
3385
|
+
model: base.model
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
return ok({
|
|
3389
|
+
...base,
|
|
3390
|
+
id: error.taskId,
|
|
3391
|
+
pollEndpoint: error.pollEndpoint
|
|
3392
|
+
});
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
async function pollJob(job) {
|
|
3396
|
+
if (job.result) {
|
|
3397
|
+
return ok(
|
|
3398
|
+
job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }
|
|
3399
|
+
);
|
|
3400
|
+
}
|
|
3401
|
+
const routed = route(job.model);
|
|
3402
|
+
if (routed.error) return fail(routed.error);
|
|
3403
|
+
const adapter = routed.data.adapter;
|
|
3404
|
+
if (typeof adapter.completeJob !== "function") {
|
|
3405
|
+
return fail({
|
|
3406
|
+
code: "unsupported_provider",
|
|
3407
|
+
message: `Provider "${job.provider}" cannot resume a job by handle.`,
|
|
3408
|
+
provider: job.provider
|
|
3409
|
+
});
|
|
3410
|
+
}
|
|
3411
|
+
try {
|
|
3412
|
+
return ok(await adapter.completeJob(job));
|
|
3413
|
+
} catch (error) {
|
|
3414
|
+
return failFrom("provider_error", error, {
|
|
3415
|
+
provider: job.provider,
|
|
3416
|
+
model: job.model
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
function klingNamespace() {
|
|
3421
|
+
const configured = options.providers.kling;
|
|
3422
|
+
if (!configured) return void 0;
|
|
3423
|
+
const adapter = new KlingAdapter({
|
|
3424
|
+
apiKey: configured.apiKey,
|
|
3425
|
+
baseUrl: configured.baseUrl ?? "https://api-singapore.klingai.com"
|
|
3426
|
+
});
|
|
3427
|
+
function run(name) {
|
|
3428
|
+
return async (input) => {
|
|
3429
|
+
try {
|
|
3430
|
+
const capability = KLING_CAPABILITIES[name];
|
|
3431
|
+
return ok(await adapter.submitCapability(name, capability, input));
|
|
3432
|
+
} catch (error) {
|
|
3433
|
+
return failFrom("provider_error", error, { provider: "kling" });
|
|
3434
|
+
}
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
return {
|
|
3438
|
+
motionControl: run("motionControl"),
|
|
3439
|
+
omniVideo: run("omniVideo"),
|
|
3440
|
+
avatar: run("avatar"),
|
|
3441
|
+
outpainting: run("outpainting"),
|
|
3442
|
+
imageOmni: run("imageOmni")
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
function googleNamespace() {
|
|
3446
|
+
const configured = options.providers.google;
|
|
3447
|
+
if (!configured) return void 0;
|
|
3448
|
+
const adapter = new GoogleAdapter({
|
|
3449
|
+
apiKey: configured.apiKey,
|
|
3450
|
+
baseUrl: configured.baseUrl
|
|
3451
|
+
});
|
|
3452
|
+
return {
|
|
3453
|
+
omniVideo: async (input) => {
|
|
3454
|
+
try {
|
|
3455
|
+
return ok(await adapter.omniVideo(input));
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
return failFrom("provider_error", error, { provider: "google" });
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
return {
|
|
3463
|
+
google: googleNamespace(),
|
|
3464
|
+
kling: klingNamespace(),
|
|
3465
|
+
image: {
|
|
3466
|
+
submit: (params) => submit("image", params),
|
|
3467
|
+
generate: (params) => generate("image", params)
|
|
3468
|
+
},
|
|
3469
|
+
video: {
|
|
3470
|
+
submit: (params) => submit("video", params),
|
|
3471
|
+
generate: (params) => generate("video", params)
|
|
3472
|
+
},
|
|
3473
|
+
text: {
|
|
3474
|
+
submit: (params) => submit("text", params),
|
|
3475
|
+
generate: (params) => generate("text", params)
|
|
3476
|
+
},
|
|
3477
|
+
audio: {
|
|
3478
|
+
submit: (params) => submit("audio", params),
|
|
3479
|
+
generate: (params) => generate("audio", params)
|
|
3480
|
+
},
|
|
3481
|
+
jobs: {
|
|
3482
|
+
poll: pollJob,
|
|
3483
|
+
wait: async (job, waitOptions) => {
|
|
3484
|
+
const deadline = Date.now() + (waitOptions?.timeoutMs ?? 30 * 6e4);
|
|
3485
|
+
for (; ; ) {
|
|
3486
|
+
const polled = await pollJob(job);
|
|
3487
|
+
if (polled.error) return fail(polled.error);
|
|
3488
|
+
const snapshot = polled.data;
|
|
3489
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
3490
|
+
return toGeneration(snapshot.result, job.metadata);
|
|
3491
|
+
}
|
|
3492
|
+
if (snapshot.status === "failed") {
|
|
3493
|
+
return fail({
|
|
3494
|
+
code: "provider_error",
|
|
3495
|
+
message: snapshot.error ?? "The job failed.",
|
|
3496
|
+
provider: job.provider,
|
|
3497
|
+
model: job.model
|
|
3498
|
+
});
|
|
3499
|
+
}
|
|
3500
|
+
if (Date.now() >= deadline) {
|
|
3501
|
+
return fail({
|
|
3502
|
+
code: "timeout",
|
|
3503
|
+
message: `Job ${job.id} is still running. Poll it again later.`,
|
|
3504
|
+
provider: job.provider,
|
|
3505
|
+
model: job.model
|
|
3506
|
+
});
|
|
3507
|
+
}
|
|
3508
|
+
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
},
|
|
3512
|
+
models: () => getAvailableModels(options),
|
|
3513
|
+
estimateCost: async (type, params) => {
|
|
3514
|
+
const routed = route(params.model);
|
|
3515
|
+
if (routed.error) return fail(routed.error);
|
|
3516
|
+
try {
|
|
3517
|
+
return ok(await routed.data.adapter.estimateCost(type, params));
|
|
3518
|
+
} catch (error) {
|
|
3519
|
+
return failFrom("provider_error", error, {
|
|
3520
|
+
provider: routed.data.provider,
|
|
3521
|
+
model: routed.data.model
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
};
|
|
3526
|
+
}
|
|
3527
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3528
|
+
0 && (module.exports = {
|
|
3529
|
+
BYTEPLUS_CATALOG,
|
|
3530
|
+
BYTEPLUS_MODELS,
|
|
3531
|
+
BytePlusAdapter,
|
|
3532
|
+
ELEVENLABS_CATALOG,
|
|
3533
|
+
ELEVENLABS_MODELS,
|
|
3534
|
+
ELEVENLABS_OUTPUT_FORMATS,
|
|
3535
|
+
ElevenLabsAdapter,
|
|
3536
|
+
GOOGLE_CATALOG,
|
|
3537
|
+
GOOGLE_IMAGE_MODELS,
|
|
3538
|
+
GOOGLE_VIDEO_MODELS,
|
|
3539
|
+
GoogleAdapter,
|
|
3540
|
+
KLING_CAPABILITIES,
|
|
3541
|
+
KLING_CATALOG,
|
|
3542
|
+
KLING_MODELS,
|
|
3543
|
+
KlingAdapter,
|
|
3544
|
+
OPENAI_CATALOG,
|
|
3545
|
+
OPENAI_IMAGE_MODELS,
|
|
3546
|
+
OpenAIAdapter,
|
|
3547
|
+
PendingJob,
|
|
3548
|
+
QWEN_AUDIO_MODELS,
|
|
3549
|
+
QWEN_CATALOG,
|
|
3550
|
+
QWEN_IMAGE_MODELS,
|
|
3551
|
+
QWEN_TEXT_MODELS,
|
|
3552
|
+
QWEN_VIDEO_MODELS,
|
|
3553
|
+
QwenAdapter,
|
|
3554
|
+
brotuClient,
|
|
3555
|
+
createS3Storage,
|
|
3556
|
+
fail,
|
|
3557
|
+
getAvailableModels,
|
|
3558
|
+
getModel,
|
|
3559
|
+
getModels,
|
|
3560
|
+
getProviders,
|
|
3561
|
+
hasModel,
|
|
3562
|
+
isPendingJob,
|
|
3563
|
+
ok,
|
|
3564
|
+
persistOutputs,
|
|
3565
|
+
registerModels,
|
|
3566
|
+
resetCatalog,
|
|
3567
|
+
resolveProvider
|
|
3568
|
+
});
|