@ai-sdk/klingai 3.0.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.
@@ -0,0 +1,116 @@
1
+ import {
2
+ type Experimental_VideoModelV3 as VideoModelV3,
3
+ type ProviderV3,
4
+ NoSuchModelError,
5
+ } from '@ai-sdk/provider';
6
+ import {
7
+ type FetchFunction,
8
+ withoutTrailingSlash,
9
+ withUserAgentSuffix,
10
+ } from '@ai-sdk/provider-utils';
11
+ import { generateKlingAIAuthToken } from './klingai-auth';
12
+ import { KlingAIVideoModel } from './klingai-video-model';
13
+ import type { KlingAIVideoModelId } from './klingai-video-settings';
14
+ import { VERSION } from './version';
15
+
16
+ export interface KlingAIProviderSettings {
17
+ /**
18
+ * KlingAI Access key. Default value is taken from the `KLINGAI_ACCESS_KEY`
19
+ * environment variable.
20
+ */
21
+ accessKey?: string;
22
+ /**
23
+ * KlingAI Secret key. Default value is taken from the `KLINGAI_SECRET_KEY`
24
+ * environment variable.
25
+ */
26
+ secretKey?: string;
27
+ /**
28
+ * Base URL for the API calls.
29
+ */
30
+ baseURL?: string;
31
+ /**
32
+ * Custom headers to include in the requests.
33
+ */
34
+ headers?: Record<string, string>;
35
+ /**
36
+ * Custom fetch implementation. You can use it as a middleware to intercept
37
+ * requests, or to provide a custom fetch implementation for e.g. testing.
38
+ */
39
+ fetch?: FetchFunction;
40
+ }
41
+
42
+ export interface KlingAIProvider extends ProviderV3 {
43
+ /**
44
+ * Creates a model for video generation.
45
+ */
46
+ video(modelId: KlingAIVideoModelId): VideoModelV3;
47
+
48
+ /**
49
+ * Creates a model for video generation.
50
+ */
51
+ videoModel(modelId: KlingAIVideoModelId): VideoModelV3;
52
+ }
53
+
54
+ const defaultBaseURL = 'https://api-singapore.klingai.com';
55
+
56
+ /**
57
+ * Create a KlingAI provider instance.
58
+ */
59
+ export function createKlingAI(
60
+ options: KlingAIProviderSettings = {},
61
+ ): KlingAIProvider {
62
+ const baseURL =
63
+ withoutTrailingSlash(options.baseURL ?? defaultBaseURL) ?? defaultBaseURL;
64
+
65
+ const getHeaders = async () => {
66
+ const token = await generateKlingAIAuthToken({
67
+ accessKey: options.accessKey,
68
+ secretKey: options.secretKey,
69
+ });
70
+
71
+ return withUserAgentSuffix(
72
+ {
73
+ Authorization: `Bearer ${token}`,
74
+ ...options.headers,
75
+ },
76
+ `ai-sdk/klingai/${VERSION}`,
77
+ );
78
+ };
79
+
80
+ const createVideoModel = (modelId: KlingAIVideoModelId): VideoModelV3 =>
81
+ new KlingAIVideoModel(modelId, {
82
+ provider: 'klingai.video',
83
+ baseURL,
84
+ headers: getHeaders,
85
+ fetch: options.fetch,
86
+ });
87
+
88
+ const noSuchModel = (
89
+ modelId: string,
90
+ modelType:
91
+ | 'languageModel'
92
+ | 'embeddingModel'
93
+ | 'imageModel'
94
+ | 'transcriptionModel'
95
+ | 'speechModel'
96
+ | 'rerankingModel',
97
+ ): never => {
98
+ throw new NoSuchModelError({ modelId, modelType });
99
+ };
100
+
101
+ const provider: KlingAIProvider = {
102
+ specificationVersion: 'v3' as const,
103
+ video: createVideoModel,
104
+ videoModel: createVideoModel,
105
+ languageModel: (modelId: string) => noSuchModel(modelId, 'languageModel'),
106
+ embeddingModel: (modelId: string) => noSuchModel(modelId, 'embeddingModel'),
107
+ imageModel: (modelId: string) => noSuchModel(modelId, 'imageModel'),
108
+ };
109
+
110
+ return provider;
111
+ }
112
+
113
+ /**
114
+ * Default KlingAI provider instance.
115
+ */
116
+ export const klingai = createKlingAI();
@@ -0,0 +1,467 @@
1
+ import {
2
+ AISDKError,
3
+ type Experimental_VideoModelV3,
4
+ NoSuchModelError,
5
+ type SharedV3Warning,
6
+ } from '@ai-sdk/provider';
7
+ import {
8
+ combineHeaders,
9
+ convertUint8ArrayToBase64,
10
+ createJsonResponseHandler,
11
+ delay,
12
+ type FetchFunction,
13
+ getFromApi,
14
+ lazySchema,
15
+ parseProviderOptions,
16
+ postJsonToApi,
17
+ type Resolvable,
18
+ resolve,
19
+ zodSchema,
20
+ } from '@ai-sdk/provider-utils';
21
+ import { z } from 'zod/v4';
22
+ import { klingaiFailedResponseHandler } from './klingai-error';
23
+ import type { KlingAIVideoModelId } from './klingai-video-settings';
24
+
25
+ /**
26
+ * Maps known model IDs to their API endpoint paths.
27
+ */
28
+ const modelEndpointMap: Record<string, string> = {
29
+ 'kling-v2.6-motion-control': '/v1/videos/motion-control',
30
+ };
31
+
32
+ function getEndpointPath(modelId: string): string {
33
+ const endpoint = modelEndpointMap[modelId];
34
+ if (!endpoint) {
35
+ throw new NoSuchModelError({
36
+ modelId,
37
+ modelType: 'videoModel',
38
+ });
39
+ }
40
+ return endpoint;
41
+ }
42
+
43
+ export type KlingAIVideoProviderOptions = {
44
+ /**
45
+ * URL of the reference video. The character actions in the generated video
46
+ * are consistent with the reference video.
47
+ *
48
+ * Supports .mp4/.mov, max 100MB, side lengths 340px–3850px,
49
+ * duration 3–30 seconds (depends on `characterOrientation`).
50
+ */
51
+ videoUrl: string;
52
+
53
+ /**
54
+ * Orientation of the characters in the generated video.
55
+ *
56
+ * - `'image'`: Same orientation as the person in the image.
57
+ * Reference video duration max 10 seconds.
58
+ * - `'video'`: Same orientation as the person in the video.
59
+ * Reference video duration max 30 seconds.
60
+ */
61
+ characterOrientation: 'image' | 'video';
62
+
63
+ /**
64
+ * Video generation mode.
65
+ *
66
+ * - `'std'`: Standard mode — cost-effective.
67
+ * - `'pro'`: Professional mode — higher quality but longer generation time.
68
+ */
69
+ mode: 'std' | 'pro';
70
+
71
+ /**
72
+ * Whether to keep the original sound of the reference video.
73
+ * Default: `'yes'`.
74
+ */
75
+ keepOriginalSound?: 'yes' | 'no' | null;
76
+
77
+ /**
78
+ * Whether to generate watermarked results simultaneously.
79
+ */
80
+ watermarkEnabled?: boolean | null;
81
+
82
+ /**
83
+ * Polling interval in milliseconds for checking task status.
84
+ * Default: 5000 (5 seconds).
85
+ */
86
+ pollIntervalMs?: number | null;
87
+
88
+ /**
89
+ * Maximum time in milliseconds to wait for video generation.
90
+ * Default: 600000 (10 minutes).
91
+ */
92
+ pollTimeoutMs?: number | null;
93
+
94
+ [key: string]: unknown; // For passthrough
95
+ };
96
+
97
+ const klingaiVideoProviderOptionsSchema = lazySchema(() =>
98
+ zodSchema(
99
+ z
100
+ .object({
101
+ videoUrl: z.string(),
102
+ characterOrientation: z.enum(['image', 'video']),
103
+ mode: z.enum(['std', 'pro']),
104
+ keepOriginalSound: z.enum(['yes', 'no']).nullish(),
105
+ watermarkEnabled: z.boolean().nullish(),
106
+ pollIntervalMs: z.number().positive().nullish(),
107
+ pollTimeoutMs: z.number().positive().nullish(),
108
+ })
109
+ .passthrough(),
110
+ ),
111
+ );
112
+
113
+ interface KlingAIVideoModelConfig {
114
+ provider: string;
115
+ baseURL: string;
116
+ headers?: Resolvable<Record<string, string | undefined>>;
117
+ fetch?: FetchFunction;
118
+ _internal?: {
119
+ currentDate?: () => Date;
120
+ };
121
+ }
122
+
123
+ export class KlingAIVideoModel implements Experimental_VideoModelV3 {
124
+ readonly specificationVersion = 'v3';
125
+ readonly maxVideosPerCall = 1;
126
+
127
+ get provider(): string {
128
+ return this.config.provider;
129
+ }
130
+
131
+ constructor(
132
+ readonly modelId: KlingAIVideoModelId,
133
+ private readonly config: KlingAIVideoModelConfig,
134
+ ) {}
135
+
136
+ async doGenerate(
137
+ options: Parameters<Experimental_VideoModelV3['doGenerate']>[0],
138
+ ): Promise<Awaited<ReturnType<Experimental_VideoModelV3['doGenerate']>>> {
139
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
140
+ const warnings: SharedV3Warning[] = [];
141
+
142
+ const klingaiOptions = (await parseProviderOptions({
143
+ provider: 'klingai',
144
+ providerOptions: options.providerOptions,
145
+ schema: klingaiVideoProviderOptionsSchema,
146
+ })) as KlingAIVideoProviderOptions | undefined;
147
+
148
+ if (!klingaiOptions) {
149
+ throw new AISDKError({
150
+ name: 'KLINGAI_VIDEO_MISSING_OPTIONS',
151
+ message:
152
+ 'KlingAI requires providerOptions.klingai with videoUrl, characterOrientation, and mode.',
153
+ });
154
+ }
155
+
156
+ // Build the request body for the KlingAI Motion Control endpoint
157
+ const body: Record<string, unknown> = {
158
+ video_url: klingaiOptions.videoUrl,
159
+ character_orientation: klingaiOptions.characterOrientation,
160
+ mode: klingaiOptions.mode,
161
+ };
162
+
163
+ // Map standard SDK prompt option
164
+ if (options.prompt != null) {
165
+ body.prompt = options.prompt;
166
+ }
167
+
168
+ // Map standard SDK image option to KlingAI's image_url
169
+ if (options.image != null) {
170
+ if (options.image.type === 'url') {
171
+ body.image_url = options.image.url;
172
+ } else {
173
+ // KlingAI accepts raw base64 without the data: prefix
174
+ const base64Data =
175
+ typeof options.image.data === 'string'
176
+ ? options.image.data
177
+ : convertUint8ArrayToBase64(options.image.data);
178
+ body.image_url = base64Data;
179
+ }
180
+ }
181
+
182
+ // Map KlingAI-specific options
183
+ if (
184
+ klingaiOptions.keepOriginalSound !== undefined &&
185
+ klingaiOptions.keepOriginalSound !== null
186
+ ) {
187
+ body.keep_original_sound = klingaiOptions.keepOriginalSound;
188
+ }
189
+
190
+ if (
191
+ klingaiOptions.watermarkEnabled !== undefined &&
192
+ klingaiOptions.watermarkEnabled !== null
193
+ ) {
194
+ body.watermark_info = { enabled: klingaiOptions.watermarkEnabled };
195
+ }
196
+
197
+ // Pass through any additional provider-specific options
198
+ for (const [key, value] of Object.entries(klingaiOptions)) {
199
+ if (
200
+ ![
201
+ 'videoUrl',
202
+ 'characterOrientation',
203
+ 'mode',
204
+ 'keepOriginalSound',
205
+ 'watermarkEnabled',
206
+ 'pollIntervalMs',
207
+ 'pollTimeoutMs',
208
+ ].includes(key)
209
+ ) {
210
+ body[key] = value;
211
+ }
212
+ }
213
+
214
+ // Warn about unsupported standard options
215
+ if (options.aspectRatio) {
216
+ warnings.push({
217
+ type: 'unsupported',
218
+ feature: 'aspectRatio',
219
+ details:
220
+ 'KlingAI Motion Control does not support aspectRatio. ' +
221
+ 'The output dimensions are determined by the reference image/video.',
222
+ });
223
+ }
224
+
225
+ if (options.resolution) {
226
+ warnings.push({
227
+ type: 'unsupported',
228
+ feature: 'resolution',
229
+ details:
230
+ 'KlingAI Motion Control does not support resolution. ' +
231
+ 'The output resolution is determined by the reference image/video.',
232
+ });
233
+ }
234
+
235
+ if (options.seed) {
236
+ warnings.push({
237
+ type: 'unsupported',
238
+ feature: 'seed',
239
+ details:
240
+ 'KlingAI Motion Control does not support seed for deterministic generation.',
241
+ });
242
+ }
243
+
244
+ if (options.duration) {
245
+ warnings.push({
246
+ type: 'unsupported',
247
+ feature: 'duration',
248
+ details:
249
+ 'KlingAI Motion Control does not support custom duration. ' +
250
+ 'The output duration matches the reference video duration.',
251
+ });
252
+ }
253
+
254
+ if (options.fps) {
255
+ warnings.push({
256
+ type: 'unsupported',
257
+ feature: 'fps',
258
+ details: 'KlingAI Motion Control does not support custom FPS.',
259
+ });
260
+ }
261
+
262
+ if (options.n != null && options.n > 1) {
263
+ warnings.push({
264
+ type: 'unsupported',
265
+ feature: 'n',
266
+ details:
267
+ 'KlingAI Motion Control does not support generating multiple videos per call. ' +
268
+ 'Only 1 video will be generated.',
269
+ });
270
+ }
271
+
272
+ // Resolve the API endpoint path for this model
273
+ const endpointPath = getEndpointPath(this.modelId);
274
+
275
+ // Step 1: Create the task
276
+ const { value: createResponse, responseHeaders: createHeaders } =
277
+ await postJsonToApi({
278
+ url: `${this.config.baseURL}${endpointPath}`,
279
+ headers: combineHeaders(
280
+ await resolve(this.config.headers),
281
+ options.headers,
282
+ ),
283
+ body,
284
+ successfulResponseHandler: createJsonResponseHandler(
285
+ klingaiCreateTaskSchema,
286
+ ),
287
+ failedResponseHandler: klingaiFailedResponseHandler,
288
+ abortSignal: options.abortSignal,
289
+ fetch: this.config.fetch,
290
+ });
291
+
292
+ const taskId = createResponse.data?.task_id;
293
+ if (!taskId) {
294
+ throw new AISDKError({
295
+ name: 'KLINGAI_VIDEO_GENERATION_ERROR',
296
+ message: `No task_id returned from KlingAI API. Response: ${JSON.stringify(createResponse)}`,
297
+ });
298
+ }
299
+
300
+ // Step 2: Poll for task completion
301
+ const pollIntervalMs = klingaiOptions.pollIntervalMs ?? 5000; // 5 seconds
302
+ const pollTimeoutMs = klingaiOptions.pollTimeoutMs ?? 600000; // 10 minutes
303
+ const startTime = Date.now();
304
+ let finalResponse: KlingAITaskResponse | undefined;
305
+ let responseHeaders: Record<string, string> | undefined = createHeaders;
306
+
307
+ while (true) {
308
+ await delay(pollIntervalMs, { abortSignal: options.abortSignal });
309
+
310
+ if (Date.now() - startTime > pollTimeoutMs) {
311
+ throw new AISDKError({
312
+ name: 'KLINGAI_VIDEO_GENERATION_TIMEOUT',
313
+ message: `Video generation timed out after ${pollTimeoutMs}ms`,
314
+ });
315
+ }
316
+
317
+ const { value: statusResponse, responseHeaders: pollHeaders } =
318
+ await getFromApi({
319
+ url: `${this.config.baseURL}${endpointPath}/${taskId}`,
320
+ headers: combineHeaders(
321
+ await resolve(this.config.headers),
322
+ options.headers,
323
+ ),
324
+ successfulResponseHandler: createJsonResponseHandler(
325
+ klingaiTaskStatusSchema,
326
+ ),
327
+ failedResponseHandler: klingaiFailedResponseHandler,
328
+ abortSignal: options.abortSignal,
329
+ fetch: this.config.fetch,
330
+ });
331
+
332
+ responseHeaders = pollHeaders;
333
+ const taskStatus = statusResponse.data?.task_status;
334
+
335
+ if (taskStatus === 'succeed') {
336
+ finalResponse = statusResponse;
337
+ break;
338
+ }
339
+
340
+ if (taskStatus === 'failed') {
341
+ throw new AISDKError({
342
+ name: 'KLINGAI_VIDEO_GENERATION_FAILED',
343
+ message: `Video generation failed: ${statusResponse.data?.task_status_msg ?? 'Unknown error'}`,
344
+ });
345
+ }
346
+
347
+ // Continue polling for 'submitted' and 'processing' statuses
348
+ }
349
+
350
+ if (!finalResponse?.data?.task_result?.videos?.length) {
351
+ throw new AISDKError({
352
+ name: 'KLINGAI_VIDEO_GENERATION_ERROR',
353
+ message: `No videos in response. Response: ${JSON.stringify(finalResponse)}`,
354
+ });
355
+ }
356
+
357
+ const videos: Array<{ type: 'url'; url: string; mediaType: string }> = [];
358
+ const videoMetadata: Array<{
359
+ id: string;
360
+ url: string;
361
+ watermarkUrl?: string;
362
+ duration?: string;
363
+ }> = [];
364
+
365
+ for (const video of finalResponse.data.task_result.videos) {
366
+ if (video.url) {
367
+ videos.push({
368
+ type: 'url',
369
+ url: video.url,
370
+ mediaType: 'video/mp4',
371
+ });
372
+ videoMetadata.push({
373
+ id: video.id ?? '',
374
+ url: video.url,
375
+ ...(video.watermark_url ? { watermarkUrl: video.watermark_url } : {}),
376
+ ...(video.duration ? { duration: video.duration } : {}),
377
+ });
378
+ }
379
+ }
380
+
381
+ if (videos.length === 0) {
382
+ throw new AISDKError({
383
+ name: 'KLINGAI_VIDEO_GENERATION_ERROR',
384
+ message: 'No valid video URLs in response',
385
+ });
386
+ }
387
+
388
+ return {
389
+ videos,
390
+ warnings,
391
+ response: {
392
+ timestamp: currentDate,
393
+ modelId: this.modelId,
394
+ headers: responseHeaders,
395
+ },
396
+ providerMetadata: {
397
+ klingai: {
398
+ taskId,
399
+ videos: videoMetadata,
400
+ },
401
+ },
402
+ };
403
+ }
404
+ }
405
+
406
+ // Response schema for task creation (POST)
407
+ const klingaiCreateTaskSchema = z.object({
408
+ code: z.number(),
409
+ message: z.string(),
410
+ request_id: z.string().nullish(),
411
+ data: z
412
+ .object({
413
+ task_id: z.string(),
414
+ task_status: z.string().nullish(),
415
+ task_info: z
416
+ .object({
417
+ external_task_id: z.string().nullish(),
418
+ })
419
+ .nullish(),
420
+ created_at: z.number().nullish(),
421
+ updated_at: z.number().nullish(),
422
+ })
423
+ .nullish(),
424
+ });
425
+
426
+ // Response schema for task status query (GET)
427
+ const klingaiTaskStatusSchema = z.object({
428
+ code: z.number(),
429
+ message: z.string(),
430
+ request_id: z.string().nullish(),
431
+ data: z
432
+ .object({
433
+ task_id: z.string(),
434
+ task_status: z.string(),
435
+ task_status_msg: z.string().nullish(),
436
+ task_info: z
437
+ .object({
438
+ external_task_id: z.string().nullish(),
439
+ })
440
+ .nullish(),
441
+ watermark_info: z
442
+ .object({
443
+ enabled: z.boolean().nullish(),
444
+ })
445
+ .nullish(),
446
+ final_unit_deduction: z.string().nullish(),
447
+ created_at: z.number().nullish(),
448
+ updated_at: z.number().nullish(),
449
+ task_result: z
450
+ .object({
451
+ videos: z
452
+ .array(
453
+ z.object({
454
+ id: z.string().nullish(),
455
+ url: z.string().nullish(),
456
+ watermark_url: z.string().nullish(),
457
+ duration: z.string().nullish(),
458
+ }),
459
+ )
460
+ .nullish(),
461
+ })
462
+ .nullish(),
463
+ })
464
+ .nullish(),
465
+ });
466
+
467
+ type KlingAITaskResponse = z.infer<typeof klingaiTaskStatusSchema>;
@@ -0,0 +1 @@
1
+ export type KlingAIVideoModelId = 'kling-v2.6-motion-control' | (string & {});
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare const __PACKAGE_VERSION__: string;
2
+
3
+ export const VERSION = __PACKAGE_VERSION__;