@openclaw/novita-provider 2026.9.5 → 2026.9.7

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/assets/icon.png CHANGED
Binary file
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { buildNovitaVideoGenerationProvider } from "./video-generation-provider.js";
1
2
  import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared";
2
3
  import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
3
4
  import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
@@ -13,6 +14,11 @@ var openclaw_plugin_default = {
13
14
  "novita-ai",
14
15
  "novitaai"
15
16
  ],
17
+ contracts: { "videoGenerationProviders": ["novita"] },
18
+ videoGenerationProviderMetadata: { "novita": {
19
+ "authSignals": [{ "provider": "novita" }],
20
+ "referenceAudioInputs": true
21
+ } },
16
22
  providerAuthAliases: {
17
23
  "novita-ai": "novita",
18
24
  "novitaai": "novita"
@@ -214,6 +220,9 @@ var novita_default = defineSingleProviderPluginEntry({
214
220
  dropReasoningFromHistory: false
215
221
  }),
216
222
  ...buildProviderToolCompatFamilyHooks("openai")
223
+ },
224
+ register(api) {
225
+ api.registerVideoGenerationProvider(buildNovitaVideoGenerationProvider());
217
226
  }
218
227
  });
219
228
  //#endregion
@@ -0,0 +1,342 @@
1
+ import { toImageDataUrl } from "openclaw/plugin-sdk/image-generation";
2
+ import { downloadGeneratedVideoAsset, resolveGeneratedMediaMaxBytes } from "openclaw/plugin-sdk/media-generation-runtime";
3
+ import { isProviderApiKeyConfigured } from "openclaw/plugin-sdk/provider-auth";
4
+ import { resolveApiKeyForProvider } from "openclaw/plugin-sdk/provider-auth-runtime";
5
+ import { assertOkOrThrowHttpError, createProviderOperationDeadline, createProviderOperationTimeoutResolver, pollProviderOperationJson, postJsonRequest, readProviderJsonObjectResponse, resolveProviderHttpRequestConfig, sanitizeConfiguredModelProviderRequest } from "openclaw/plugin-sdk/provider-http";
6
+ import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
7
+ //#region extensions/novita/video-generation-provider.ts
8
+ const DEFAULT_BASE_URL = "https://api.novita.ai";
9
+ const DEFAULT_MODEL = "wan2.6-t2v";
10
+ const DEFAULT_TIMEOUT_MS = 12e4;
11
+ const DEFAULT_OPERATION_TIMEOUT_MS = 6e5;
12
+ const WAN_SIZES = [
13
+ "1280x720",
14
+ "720x1280",
15
+ "960x960",
16
+ "1088x832",
17
+ "832x1088",
18
+ "1920x1080",
19
+ "1080x1920",
20
+ "1440x1440",
21
+ "1632x1248",
22
+ "1248x1632"
23
+ ];
24
+ const WAN_ASPECT_RATIOS = [
25
+ "16:9",
26
+ "9:16",
27
+ "1:1",
28
+ "4:3",
29
+ "3:4"
30
+ ];
31
+ const WAN_OPTIONS = {
32
+ negative_prompt: "string",
33
+ prompt_extend: "boolean",
34
+ shot_type: "string",
35
+ seed: "number"
36
+ };
37
+ const HAILUO_OPTIONS = {
38
+ enable_prompt_expansion: "boolean",
39
+ fast_pretreatment: "boolean"
40
+ };
41
+ const WAN_COMMON = {
42
+ maxVideos: 1,
43
+ maxDurationSeconds: 15,
44
+ supportedDurationSeconds: [
45
+ 5,
46
+ 10,
47
+ 15
48
+ ],
49
+ resolutions: ["720P", "1080P"],
50
+ supportsResolution: true,
51
+ supportsAudio: true,
52
+ supportsWatermark: true,
53
+ maxInputAudios: 1,
54
+ providerOptions: WAN_OPTIONS
55
+ };
56
+ const WAN_CAPABILITIES = {
57
+ generate: {
58
+ ...WAN_COMMON,
59
+ sizes: WAN_SIZES,
60
+ aspectRatios: WAN_ASPECT_RATIOS,
61
+ supportsSize: true,
62
+ supportsAspectRatio: true
63
+ },
64
+ imageToVideo: {
65
+ ...WAN_COMMON,
66
+ enabled: true,
67
+ maxInputImages: 1,
68
+ supportsSize: false,
69
+ supportsAspectRatio: false
70
+ },
71
+ videoToVideo: { enabled: false }
72
+ };
73
+ const HAILUO_COMMON = {
74
+ maxVideos: 1,
75
+ maxDurationSeconds: 10,
76
+ supportedDurationSeconds: [6, 10],
77
+ resolutions: ["768P", "1080P"],
78
+ supportsResolution: true,
79
+ supportsSize: false,
80
+ supportsAspectRatio: false,
81
+ supportsAudio: false,
82
+ supportsWatermark: false,
83
+ maxInputAudios: 0,
84
+ providerOptions: HAILUO_OPTIONS
85
+ };
86
+ const HAILUO_CAPABILITIES = {
87
+ generate: HAILUO_COMMON,
88
+ imageToVideo: {
89
+ ...HAILUO_COMMON,
90
+ enabled: true,
91
+ maxInputImages: 1
92
+ },
93
+ videoToVideo: { enabled: false }
94
+ };
95
+ const MODEL_CATALOG = {
96
+ "wan2.6-t2v": {
97
+ modes: ["generate", "imageToVideo"],
98
+ capabilities: WAN_CAPABILITIES
99
+ },
100
+ "wan2.6-i2v": {
101
+ modes: ["imageToVideo"],
102
+ capabilities: WAN_CAPABILITIES
103
+ },
104
+ "minimax-hailuo-2.3-t2v": {
105
+ modes: ["generate", "imageToVideo"],
106
+ capabilities: HAILUO_CAPABILITIES
107
+ },
108
+ "minimax-hailuo-2.3-i2v": {
109
+ modes: ["imageToVideo"],
110
+ capabilities: HAILUO_CAPABILITIES
111
+ },
112
+ "minimax-hailuo-2.3-fast-i2v": {
113
+ modes: ["imageToVideo"],
114
+ capabilities: {
115
+ ...HAILUO_CAPABILITIES,
116
+ imageToVideo: {
117
+ ...HAILUO_CAPABILITIES.imageToVideo,
118
+ enabled: true,
119
+ providerOptions: { enable_prompt_expansion: "boolean" }
120
+ }
121
+ }
122
+ }
123
+ };
124
+ function nearestDuration(value, allowed) {
125
+ const requested = typeof value === "number" && Number.isFinite(value) ? value : allowed[0];
126
+ return allowed.reduce((best, current) => Math.abs(current - requested) < Math.abs(best - requested) ? current : best);
127
+ }
128
+ function resolveModel(req) {
129
+ const model = normalizeOptionalString(req.model) ?? DEFAULT_MODEL;
130
+ if (!Object.hasOwn(MODEL_CATALOG, model)) throw new Error(`Novita video generation does not support model ${model}.`);
131
+ if ((req.inputVideos?.length ?? 0) > 0) throw new Error("Novita video generation does not support video reference inputs.");
132
+ const images = req.inputImages ?? [];
133
+ if (images.length > 1) throw new Error("Novita video generation supports at most one input image.");
134
+ if (images.length === 0 && model.endsWith("-i2v")) throw new Error(`Novita ${model} requires one input image.`);
135
+ if (images[0]?.role && images[0].role !== "first_frame") throw new Error("Novita image-to-video supports only an ordinary or first_frame image.");
136
+ return images.length > 0 ? model.replace(/-t2v$/, "-i2v") : model;
137
+ }
138
+ function resolveImage(req) {
139
+ const image = req.inputImages?.[0];
140
+ if (!image) return;
141
+ const url = normalizeOptionalString(image.url);
142
+ if (url) return url;
143
+ if (!image.buffer) throw new Error("Novita image-to-video input is missing image data.");
144
+ return toImageDataUrl({
145
+ ...image,
146
+ buffer: image.buffer,
147
+ defaultMimeType: "image/png"
148
+ });
149
+ }
150
+ function resolveResolution(req, wan) {
151
+ return normalizeOptionalString(req.resolution)?.toUpperCase() === "1080P" ? "1080P" : wan ? "720P" : "768P";
152
+ }
153
+ function resolveWanSize(req) {
154
+ const size = normalizeOptionalString(req.size)?.replace("*", "x");
155
+ const supported = WAN_SIZES.find((candidate) => candidate === size);
156
+ if (supported) return supported.replace("x", "*");
157
+ const aspectRatioIndex = WAN_ASPECT_RATIOS.findIndex((ratio) => ratio === req.aspectRatio);
158
+ const index = Math.max(0, aspectRatioIndex) + (resolveResolution(req, true) === "1080P" ? 5 : 0);
159
+ return (WAN_SIZES[index] ?? WAN_SIZES[0]).replace("x", "*");
160
+ }
161
+ function buildBody(req, model) {
162
+ const wan = model.startsWith("wan");
163
+ const image = resolveImage(req);
164
+ const options = req.providerOptions ?? {};
165
+ const mode = image ? "imageToVideo" : "generate";
166
+ const schema = MODEL_CATALOG[model]?.capabilities?.[mode]?.providerOptions ?? {};
167
+ for (const [key, value] of Object.entries(options)) if (typeof value !== schema[key]) throw new Error(`Novita ${model} does not support provider option ${key} with this value.`);
168
+ const audioInputs = req.inputAudios ?? [];
169
+ if (audioInputs.length > (wan ? 1 : 0)) throw new Error(`Novita ${model} supports ${wan ? "at most one" : "no"} audio reference input.`);
170
+ const duration = nearestDuration(req.durationSeconds, wan ? [
171
+ 5,
172
+ 10,
173
+ 15
174
+ ] : [6, 10]);
175
+ const resolution = resolveResolution(req, wan);
176
+ if (!wan) {
177
+ if (duration === 10 && resolution === "1080P") throw new Error("Novita Hailuo 1080P requires a 6-second duration; use 768P for 10 seconds.");
178
+ return {
179
+ prompt: req.prompt,
180
+ duration,
181
+ resolution,
182
+ ...options,
183
+ ...image ? { image } : {}
184
+ };
185
+ }
186
+ if (options.shot_type !== void 0 && options.shot_type !== "single" && options.shot_type !== "multi") throw new Error("Novita Wan shot_type must be single or multi.");
187
+ if (options.seed !== void 0 && (typeof options.seed !== "number" || !Number.isInteger(options.seed) || options.seed < 0 || options.seed > 2147483647)) throw new Error("Novita Wan seed must be an integer between 0 and 2147483647.");
188
+ const audioUrl = normalizeOptionalString(audioInputs[0]?.url);
189
+ if (audioInputs.length > 0 && (!audioUrl || !/^https?:\/\//i.test(audioUrl))) throw new Error("Novita Wan audio reference input requires a remote http(s) URL.");
190
+ return {
191
+ input: {
192
+ prompt: req.prompt,
193
+ ...image ? { img_url: image } : {},
194
+ ...audioUrl ? { audio_url: audioUrl } : {},
195
+ ...options.negative_prompt !== void 0 ? { negative_prompt: options.negative_prompt } : {}
196
+ },
197
+ parameters: {
198
+ ...image ? { resolution } : { size: resolveWanSize(req) },
199
+ duration,
200
+ audio: req.audio ?? false,
201
+ prompt_extend: options.prompt_extend ?? true,
202
+ shot_type: options.shot_type ?? "multi",
203
+ watermark: req.watermark ?? false,
204
+ ...options.seed !== void 0 ? { seed: options.seed } : {}
205
+ }
206
+ };
207
+ }
208
+ function readTask(payload) {
209
+ if (!isRecord(payload.task)) throw new Error("Novita video status response missing task.");
210
+ switch (payload.task.status) {
211
+ case "TASK_STATUS_QUEUED":
212
+ case "TASK_STATUS_PROCESSING":
213
+ case "TASK_STATUS_SUCCEED":
214
+ case "TASK_STATUS_FAILED": return payload.task;
215
+ default: throw new Error("Novita video status response returned an unknown task status.");
216
+ }
217
+ }
218
+ function buildNovitaVideoGenerationProvider() {
219
+ return {
220
+ id: "novita",
221
+ label: "NovitaAI",
222
+ defaultModel: DEFAULT_MODEL,
223
+ defaultTimeoutMs: DEFAULT_OPERATION_TIMEOUT_MS,
224
+ models: Object.keys(MODEL_CATALOG),
225
+ catalogByModel: MODEL_CATALOG,
226
+ capabilities: {
227
+ generate: { maxVideos: 1 },
228
+ imageToVideo: {
229
+ enabled: true,
230
+ maxVideos: 1,
231
+ maxInputImages: 1
232
+ },
233
+ videoToVideo: { enabled: false }
234
+ },
235
+ resolveModelCapabilities: ({ model }) => MODEL_CATALOG[model]?.capabilities,
236
+ isConfigured: (ctx) => isProviderApiKeyConfigured({
237
+ provider: "novita",
238
+ ...ctx
239
+ }),
240
+ async generateVideo(req) {
241
+ const model = resolveModel(req);
242
+ const body = buildBody(req, model);
243
+ const auth = await resolveApiKeyForProvider({
244
+ provider: "novita",
245
+ cfg: req.cfg,
246
+ agentDir: req.agentDir,
247
+ store: req.authStore
248
+ });
249
+ if (!auth.apiKey) throw new Error("Novita API key missing");
250
+ const deadline = createProviderOperationDeadline({
251
+ timeoutMs: req.timeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS,
252
+ label: "Novita video generation"
253
+ });
254
+ const timeoutMs = createProviderOperationTimeoutResolver({
255
+ deadline,
256
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS
257
+ });
258
+ const providerConfig = req.cfg.models?.providers?.novita;
259
+ const configuredBase = normalizeOptionalString(providerConfig?.baseUrl)?.replace(/\/+$/, "").replace(/\/openai\/v1$/, "");
260
+ const { baseUrl, headers, allowPrivateNetwork, dispatcherPolicy } = resolveProviderHttpRequestConfig({
261
+ baseUrl: configuredBase ?? DEFAULT_BASE_URL,
262
+ defaultBaseUrl: DEFAULT_BASE_URL,
263
+ defaultHeaders: {
264
+ Authorization: `Bearer ${auth.apiKey}`,
265
+ "Content-Type": "application/json"
266
+ },
267
+ provider: "novita",
268
+ capability: "video",
269
+ transport: "http",
270
+ request: sanitizeConfiguredModelProviderRequest(providerConfig?.request)
271
+ });
272
+ const endpoint = `/v3/async/${model}`;
273
+ const { response, release } = await postJsonRequest({
274
+ url: `${baseUrl}${endpoint}`,
275
+ headers,
276
+ body,
277
+ timeoutMs: timeoutMs(),
278
+ fetchFn: fetch,
279
+ allowPrivateNetwork,
280
+ dispatcherPolicy
281
+ });
282
+ let taskId;
283
+ try {
284
+ await assertOkOrThrowHttpError(response, "Novita video generation failed", {
285
+ bodyTimeoutMs: timeoutMs,
286
+ requestHeaders: headers
287
+ });
288
+ const submitted = await readProviderJsonObjectResponse(response, "Novita video generation failed", { timeoutMs });
289
+ const id = normalizeOptionalString(submitted.task_id);
290
+ if (!id) throw new Error("Novita video generation response missing task_id");
291
+ taskId = id;
292
+ } finally {
293
+ await release();
294
+ }
295
+ const pollUrl = new URL(`${baseUrl}/v3/async/task-result`);
296
+ pollUrl.searchParams.set("task_id", taskId);
297
+ const completed = await pollProviderOperationJson({
298
+ url: pollUrl.toString(),
299
+ headers,
300
+ deadline,
301
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
302
+ fetchFn: fetch,
303
+ allowPrivateNetwork,
304
+ dispatcherPolicy,
305
+ maxAttempts: 120,
306
+ pollIntervalMs: 5e3,
307
+ requestFailedMessage: "Novita video status request failed",
308
+ timeoutMessage: `Novita video generation task ${taskId} did not finish in time`,
309
+ isComplete: (payload) => readTask(payload).status === "TASK_STATUS_SUCCEED",
310
+ getFailureMessage: (payload) => {
311
+ const task = readTask(payload);
312
+ return task.status === "TASK_STATUS_FAILED" ? `Novita video generation failed: ${normalizeOptionalString(task.reason) ?? "task failed"}` : void 0;
313
+ }
314
+ });
315
+ const output = Array.isArray(completed.videos) ? completed.videos[0] : void 0;
316
+ const videoUrl = isRecord(output) ? normalizeOptionalString(output.video_url) : void 0;
317
+ if (!videoUrl) throw new Error("Novita video generation completed without a video URL");
318
+ return {
319
+ videos: [await downloadGeneratedVideoAsset({
320
+ url: videoUrl,
321
+ timeoutMs,
322
+ defaultTimeoutMs: DEFAULT_TIMEOUT_MS,
323
+ fetchFn: fetch,
324
+ provider: "novita",
325
+ label: "Novita generated video download",
326
+ requestFailedMessage: "Novita generated video download failed",
327
+ maxBytes: resolveGeneratedMediaMaxBytes(req.cfg, "video"),
328
+ validateBinaryResponse: true,
329
+ metadata: { sourceUrl: videoUrl }
330
+ })],
331
+ model,
332
+ metadata: {
333
+ taskId,
334
+ endpoint,
335
+ videoUrl
336
+ }
337
+ };
338
+ }
339
+ };
340
+ }
341
+ //#endregion
342
+ export { buildNovitaVideoGenerationProvider };
@@ -6,6 +6,15 @@
6
6
  },
7
7
  "enabledByDefault": true,
8
8
  "providers": ["novita", "novita-ai", "novitaai"],
9
+ "contracts": {
10
+ "videoGenerationProviders": ["novita"]
11
+ },
12
+ "videoGenerationProviderMetadata": {
13
+ "novita": {
14
+ "authSignals": [{ "provider": "novita" }],
15
+ "referenceAudioInputs": true
16
+ }
17
+ },
9
18
  "providerAuthAliases": {
10
19
  "novita-ai": "novita",
11
20
  "novitaai": "novita"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/novita-provider",
3
- "version": "2026.9.5",
3
+ "version": "2026.9.7",
4
4
  "description": "OpenClaw NovitaAI provider plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,10 +18,10 @@
18
18
  "minHostVersion": ">=2026.7.2"
19
19
  },
20
20
  "compat": {
21
- "pluginApi": ">=2026.9.5"
21
+ "pluginApi": ">=2026.9.7"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.9.5",
24
+ "openclawVersion": "2026.9.7",
25
25
  "bundledDist": false
26
26
  },
27
27
  "release": {
@@ -40,7 +40,7 @@
40
40
  "assets/activity.svg"
41
41
  ],
42
42
  "peerDependencies": {
43
- "openclaw": ">=2026.9.5"
43
+ "openclaw": ">=2026.9.7"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "openclaw": {