@ai-sdk/alibaba 0.0.0-6b196531-20260710185421

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,587 @@
1
+ import {
2
+ AISDKError,
3
+ type Experimental_VideoModelV4,
4
+ type Experimental_VideoModelV4File,
5
+ type SharedV4Warning,
6
+ } from '@ai-sdk/provider';
7
+ import {
8
+ combineHeaders,
9
+ convertImageModelFileToDataUri,
10
+ convertUint8ArrayToBase64,
11
+ createJsonErrorResponseHandler,
12
+ createJsonResponseHandler,
13
+ delay,
14
+ getFromApi,
15
+ parseProviderOptions,
16
+ postJsonToApi,
17
+ resolve,
18
+ type FetchFunction,
19
+ type Resolvable,
20
+ } from '@ai-sdk/provider-utils';
21
+ import { z } from 'zod/v4';
22
+ import {
23
+ alibabaVideoModelOptionsSchema,
24
+ type AlibabaVideoModelOptions,
25
+ } from './alibaba-video-model-options';
26
+ import type { AlibabaVideoModelId } from './alibaba-video-settings';
27
+
28
+ interface AlibabaVideoModelConfig {
29
+ provider: string;
30
+ baseURL: string;
31
+ headers: Resolvable<Record<string, string | undefined>>;
32
+ fetch?: FetchFunction;
33
+ _internal?: {
34
+ currentDate?: () => Date;
35
+ };
36
+ }
37
+
38
+ // DashScope native API error format (different from OpenAI-compatible endpoint)
39
+ const alibabaVideoErrorSchema = z.object({
40
+ code: z.string().nullish(),
41
+ message: z.string(),
42
+ request_id: z.string().nullish(),
43
+ });
44
+
45
+ const alibabaVideoFailedResponseHandler = createJsonErrorResponseHandler({
46
+ errorSchema: alibabaVideoErrorSchema,
47
+ errorToMessage: data => data.message,
48
+ });
49
+
50
+ const alibabaVideoCreateTaskSchema = z.object({
51
+ output: z
52
+ .object({
53
+ task_status: z.string(),
54
+ task_id: z.string(),
55
+ })
56
+ .nullish(),
57
+ request_id: z.string().nullish(),
58
+ });
59
+
60
+ const alibabaVideoTaskStatusSchema = z.object({
61
+ output: z
62
+ .object({
63
+ task_id: z.string(),
64
+ task_status: z.string(),
65
+ video_url: z.string().nullish(),
66
+ submit_time: z.string().nullish(),
67
+ scheduled_time: z.string().nullish(),
68
+ end_time: z.string().nullish(),
69
+ orig_prompt: z.string().nullish(),
70
+ actual_prompt: z.string().nullish(),
71
+ code: z.string().nullish(),
72
+ message: z.string().nullish(),
73
+ })
74
+ .nullish(),
75
+ usage: z
76
+ .object({
77
+ duration: z.number().nullish(),
78
+ output_video_duration: z.number().nullish(),
79
+ SR: z.number().nullish(),
80
+ size: z.string().nullish(),
81
+ })
82
+ .nullish(),
83
+ request_id: z.string().nullish(),
84
+ });
85
+
86
+ type AlibabaVideoTaskStatusResponse = z.infer<
87
+ typeof alibabaVideoTaskStatusSchema
88
+ >;
89
+
90
+ function detectMode(modelId: string): 't2v' | 'i2v' | 'r2v' {
91
+ if (modelId.includes('-i2v')) return 'i2v';
92
+ if (modelId.includes('-r2v')) return 'r2v';
93
+ return 't2v';
94
+ }
95
+
96
+ // wan2.7 models use a different protocol than earlier wan models:
97
+ // resolution tiers + ratio instead of size, input.media instead of
98
+ // input.reference_urls (R2V), and no shot_type or audio parameters.
99
+ function isWan27Model(modelId: string): boolean {
100
+ return modelId.startsWith('wan2.7');
101
+ }
102
+
103
+ // Maps SDK "WIDTHxHEIGHT" resolutions to Alibaba resolution tiers.
104
+ const resolutionTierMap: Record<string, string> = {
105
+ '1280x720': '720P',
106
+ '720x1280': '720P',
107
+ '960x960': '720P',
108
+ '1088x832': '720P',
109
+ '832x1088': '720P',
110
+ '1920x1080': '1080P',
111
+ '1080x1920': '1080P',
112
+ '1440x1440': '1080P',
113
+ '1632x1248': '1080P',
114
+ '1248x1632': '1080P',
115
+ '832x480': '480P',
116
+ '480x832': '480P',
117
+ '624x624': '480P',
118
+ };
119
+
120
+ const supportedRatios = new Set(['16:9', '9:16', '1:1', '4:3', '3:4']);
121
+
122
+ function deriveRatioFromResolution(
123
+ resolution: `${number}x${number}`,
124
+ ): string | undefined {
125
+ const [width, height] = resolution.split('x').map(Number);
126
+ if (
127
+ !Number.isInteger(width) ||
128
+ !Number.isInteger(height) ||
129
+ width <= 0 ||
130
+ height <= 0
131
+ ) {
132
+ return undefined;
133
+ }
134
+ let a = width;
135
+ let b = height;
136
+ while (b !== 0) {
137
+ [a, b] = [b, a % b];
138
+ }
139
+ const ratio = `${width / a}:${height / a}`;
140
+ return supportedRatios.has(ratio) ? ratio : undefined;
141
+ }
142
+
143
+ function fileToImageString(file: Experimental_VideoModelV4File): string {
144
+ if (file.type === 'url') {
145
+ return file.url;
146
+ }
147
+ return typeof file.data === 'string'
148
+ ? file.data
149
+ : convertUint8ArrayToBase64(file.data);
150
+ }
151
+
152
+ function getFirstFrameImage(
153
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
154
+ ): Experimental_VideoModelV4File | undefined {
155
+ return options.frameImages?.find(frame => frame.frameType === 'first_frame')
156
+ ?.image;
157
+ }
158
+
159
+ function resolveStartImage(
160
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
161
+ ): Experimental_VideoModelV4File | undefined {
162
+ return getFirstFrameImage(options) ?? options.image;
163
+ }
164
+
165
+ function isVideoUrl(url: string): boolean {
166
+ return /\.(mp4|mov)([?#]|$)/i.test(url);
167
+ }
168
+
169
+ // Builds the wan2.7 input.media array from inputReferences and frameImages.
170
+ function resolveMedia(
171
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
172
+ alibabaOptions: AlibabaVideoModelOptions | undefined,
173
+ warnings: SharedV4Warning[],
174
+ ): Array<Record<string, unknown>> | undefined {
175
+ if (alibabaOptions?.media != null && alibabaOptions.media.length > 0) {
176
+ return alibabaOptions.media.map(item => ({
177
+ type: item.type,
178
+ url: item.url,
179
+ ...(item.referenceVoice != null
180
+ ? { reference_voice: item.referenceVoice }
181
+ : {}),
182
+ }));
183
+ }
184
+
185
+ const media: Array<Record<string, unknown>> = [];
186
+
187
+ for (const reference of options.inputReferences ?? []) {
188
+ if (reference.type === 'url') {
189
+ media.push({
190
+ type: isVideoUrl(reference.url) ? 'reference_video' : 'reference_image',
191
+ url: reference.url,
192
+ });
193
+ } else if (reference.mediaType.startsWith('image/')) {
194
+ media.push({
195
+ type: 'reference_image',
196
+ url: convertImageModelFileToDataUri(reference),
197
+ });
198
+ } else {
199
+ warnings.push({
200
+ type: 'unsupported',
201
+ feature: 'inputReferences',
202
+ details:
203
+ 'Alibaba reference-to-video requires URL references for videos. ' +
204
+ 'Non-URL video reference was skipped.',
205
+ });
206
+ }
207
+ }
208
+
209
+ const firstFrame = getFirstFrameImage(options);
210
+ if (firstFrame != null) {
211
+ media.push({
212
+ type: 'first_frame',
213
+ url: convertImageModelFileToDataUri(firstFrame),
214
+ });
215
+ }
216
+
217
+ return media.length > 0 ? media : undefined;
218
+ }
219
+
220
+ function resolveReferenceUrls(
221
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
222
+ alibabaOptions: AlibabaVideoModelOptions | undefined,
223
+ warnings: SharedV4Warning[],
224
+ ): string[] | undefined {
225
+ if (options.frameImages != null && options.frameImages.length > 0) {
226
+ return undefined;
227
+ }
228
+
229
+ if (options.inputReferences != null && options.inputReferences.length > 0) {
230
+ const urls: string[] = [];
231
+
232
+ for (const reference of options.inputReferences) {
233
+ if (reference.type === 'url') {
234
+ urls.push(reference.url);
235
+ } else {
236
+ warnings.push({
237
+ type: 'unsupported',
238
+ feature: 'inputReferences',
239
+ details:
240
+ 'Alibaba reference-to-video requires URL references. ' +
241
+ 'Non-URL reference was skipped.',
242
+ });
243
+ }
244
+ }
245
+
246
+ return urls.length > 0 ? urls : undefined;
247
+ }
248
+
249
+ return alibabaOptions?.referenceUrls ?? undefined;
250
+ }
251
+
252
+ export class AlibabaVideoModel implements Experimental_VideoModelV4 {
253
+ readonly specificationVersion = 'v4';
254
+ readonly maxVideosPerCall = 1;
255
+
256
+ get provider(): string {
257
+ return this.config.provider;
258
+ }
259
+
260
+ constructor(
261
+ readonly modelId: AlibabaVideoModelId,
262
+ private readonly config: AlibabaVideoModelConfig,
263
+ ) {}
264
+
265
+ async doGenerate(
266
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
267
+ ): Promise<Awaited<ReturnType<Experimental_VideoModelV4['doGenerate']>>> {
268
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
269
+ const warnings: SharedV4Warning[] = [];
270
+ const mode = detectMode(this.modelId);
271
+
272
+ const alibabaOptions = (await parseProviderOptions({
273
+ provider: 'alibaba',
274
+ providerOptions: options.providerOptions,
275
+ schema: alibabaVideoModelOptionsSchema,
276
+ })) as AlibabaVideoModelOptions | undefined;
277
+
278
+ // Build input object
279
+ const input: Record<string, unknown> = {};
280
+
281
+ if (options.prompt != null) {
282
+ input.prompt = options.prompt;
283
+ }
284
+
285
+ if (alibabaOptions?.negativePrompt != null) {
286
+ input.negative_prompt = alibabaOptions.negativePrompt;
287
+ }
288
+
289
+ if (alibabaOptions?.audioUrl != null) {
290
+ input.audio_url = alibabaOptions.audioUrl;
291
+ }
292
+
293
+ const startImage = resolveStartImage(options);
294
+ const wan27 = isWan27Model(this.modelId);
295
+ // wan2.7 T2V and R2V take an explicit aspect ratio (I2V follows the input image)
296
+ const supportsRatio = wan27 && mode !== 'i2v';
297
+
298
+ // Handle image input for I2V mode
299
+ if (mode === 'i2v' && startImage != null) {
300
+ input.img_url = fileToImageString(startImage);
301
+ }
302
+
303
+ // Handle references for R2V mode
304
+ if (mode === 'r2v') {
305
+ if (wan27) {
306
+ // wan2.7: input.media
307
+ const media = resolveMedia(options, alibabaOptions, warnings);
308
+ if (media != null) {
309
+ input.media = media;
310
+ }
311
+ } else {
312
+ // wan2.6: legacy protocol with input.reference_urls
313
+ const referenceUrls = resolveReferenceUrls(
314
+ options,
315
+ alibabaOptions,
316
+ warnings,
317
+ );
318
+ if (referenceUrls != null && referenceUrls.length > 0) {
319
+ input.reference_urls = referenceUrls;
320
+ }
321
+ }
322
+ }
323
+
324
+ const lastFrame = options.frameImages?.find(
325
+ frame => frame.frameType === 'last_frame',
326
+ )?.image;
327
+
328
+ if (lastFrame != null) {
329
+ warnings.push({
330
+ type: 'unsupported',
331
+ feature: 'frameImages',
332
+ details:
333
+ 'This model does not support last_frame. ' +
334
+ 'The last frame image was ignored.',
335
+ });
336
+ }
337
+
338
+ if (
339
+ options.inputReferences != null &&
340
+ options.inputReferences.length > 0 &&
341
+ mode !== 'r2v'
342
+ ) {
343
+ warnings.push({
344
+ type: 'unsupported',
345
+ feature: 'inputReferences',
346
+ details:
347
+ 'Alibaba only supports inputReferences (reference-to-video) on ' +
348
+ 'reference-to-video models. The reference images were ignored.',
349
+ });
350
+ }
351
+
352
+ // Build parameters object
353
+ const parameters: Record<string, unknown> = {};
354
+
355
+ if (options.duration != null) {
356
+ parameters.duration = options.duration;
357
+ }
358
+
359
+ if (options.seed != null) {
360
+ parameters.seed = options.seed;
361
+ }
362
+
363
+ // Resolution / Size mapping
364
+ if (options.resolution != null) {
365
+ if (mode === 'i2v' || wan27) {
366
+ // I2V and wan2.7 models use "720P" / "1080P" format
367
+ const resolutionTier =
368
+ resolutionTierMap[options.resolution] || options.resolution;
369
+ if (wan27 && resolutionTier !== '720P' && resolutionTier !== '1080P') {
370
+ warnings.push({
371
+ type: 'unsupported',
372
+ feature: 'resolution',
373
+ details:
374
+ 'wan2.7 models only support 720P and 1080P ' +
375
+ `resolutions. The resolution "${options.resolution}" was ignored.`,
376
+ });
377
+ } else {
378
+ parameters.resolution = resolutionTier;
379
+ }
380
+ } else {
381
+ // wan2.6 T2V and R2V use "WIDTH*HEIGHT" format for the size parameter
382
+ // Convert "WIDTHxHEIGHT" (SDK standard) to "WIDTH*HEIGHT" (Alibaba API)
383
+ parameters.size = options.resolution.replace('x', '*');
384
+ }
385
+ }
386
+
387
+ // wan2.7 T2V and R2V support an explicit aspect ratio parameter
388
+ if (supportsRatio) {
389
+ const ratio =
390
+ alibabaOptions?.ratio ??
391
+ options.aspectRatio ??
392
+ (options.resolution != null
393
+ ? deriveRatioFromResolution(options.resolution)
394
+ : undefined);
395
+ if (ratio != null) {
396
+ parameters.ratio = ratio;
397
+ }
398
+ }
399
+
400
+ // Provider-specific parameters
401
+ if (alibabaOptions?.promptExtend != null) {
402
+ parameters.prompt_extend = alibabaOptions.promptExtend;
403
+ }
404
+ if (alibabaOptions?.shotType != null) {
405
+ if (wan27) {
406
+ // wan2.7 removed shot_type; shot structure is described in the prompt
407
+ warnings.push({
408
+ type: 'unsupported',
409
+ feature: 'shotType',
410
+ details:
411
+ 'wan2.7 models do not support the shotType option. ' +
412
+ 'Describe the shot structure in the prompt instead.',
413
+ });
414
+ } else {
415
+ parameters.shot_type = alibabaOptions.shotType;
416
+ }
417
+ }
418
+ if (alibabaOptions?.watermark != null) {
419
+ parameters.watermark = alibabaOptions.watermark;
420
+ }
421
+ const audio = options.generateAudio ?? alibabaOptions?.audio;
422
+ if (audio != null) {
423
+ if (wan27) {
424
+ // wan2.7 does not have an audio parameter (audio is always generated)
425
+ warnings.push({
426
+ type: 'unsupported',
427
+ feature: 'generateAudio',
428
+ details:
429
+ 'wan2.7 models always generate audio. ' +
430
+ 'The audio option was ignored.',
431
+ });
432
+ } else {
433
+ parameters.audio = audio;
434
+ }
435
+ }
436
+
437
+ // Warn about unsupported standard options
438
+ if (options.aspectRatio && !supportsRatio) {
439
+ warnings.push({
440
+ type: 'unsupported',
441
+ feature: 'aspectRatio',
442
+ details:
443
+ 'Alibaba video models use explicit size/resolution dimensions. Use the resolution option or providerOptions.alibaba for size control.',
444
+ });
445
+ }
446
+ if (options.fps) {
447
+ warnings.push({
448
+ type: 'unsupported',
449
+ feature: 'fps',
450
+ details: 'Alibaba video models do not support custom FPS.',
451
+ });
452
+ }
453
+ if (options.n != null && options.n > 1) {
454
+ warnings.push({
455
+ type: 'unsupported',
456
+ feature: 'n',
457
+ details:
458
+ 'Alibaba video models only support generating 1 video per call.',
459
+ });
460
+ }
461
+
462
+ // Step 1: Create task
463
+ const { value: createResponse } = await postJsonToApi({
464
+ url: `${this.config.baseURL}/api/v1/services/aigc/video-generation/video-synthesis`,
465
+ headers: combineHeaders(
466
+ await resolve(this.config.headers),
467
+ options.headers,
468
+ {
469
+ 'X-DashScope-Async': 'enable',
470
+ },
471
+ ),
472
+ body: {
473
+ model: this.modelId,
474
+ input,
475
+ parameters,
476
+ },
477
+ successfulResponseHandler: createJsonResponseHandler(
478
+ alibabaVideoCreateTaskSchema,
479
+ ),
480
+ failedResponseHandler: alibabaVideoFailedResponseHandler,
481
+ abortSignal: options.abortSignal,
482
+ fetch: this.config.fetch,
483
+ });
484
+
485
+ const taskId = createResponse.output?.task_id;
486
+ if (!taskId) {
487
+ throw new AISDKError({
488
+ name: 'ALIBABA_VIDEO_GENERATION_ERROR',
489
+ message: `No task_id returned from Alibaba API. Response: ${JSON.stringify(createResponse)}`,
490
+ });
491
+ }
492
+
493
+ // Step 2: Poll for task completion
494
+ const pollIntervalMs = alibabaOptions?.pollIntervalMs ?? 5000;
495
+ const pollTimeoutMs = alibabaOptions?.pollTimeoutMs ?? 600000;
496
+ const startTime = Date.now();
497
+ let finalResponse: AlibabaVideoTaskStatusResponse | undefined;
498
+ let responseHeaders: Record<string, string> | undefined;
499
+
500
+ while (true) {
501
+ await delay(pollIntervalMs, { abortSignal: options.abortSignal });
502
+
503
+ if (Date.now() - startTime > pollTimeoutMs) {
504
+ throw new AISDKError({
505
+ name: 'ALIBABA_VIDEO_GENERATION_TIMEOUT',
506
+ message: `Video generation timed out after ${pollTimeoutMs}ms`,
507
+ });
508
+ }
509
+
510
+ const { value: statusResponse, responseHeaders: pollHeaders } =
511
+ await getFromApi({
512
+ url: `${this.config.baseURL}/api/v1/tasks/${taskId}`,
513
+ headers: combineHeaders(
514
+ await resolve(this.config.headers),
515
+ options.headers,
516
+ ),
517
+ successfulResponseHandler: createJsonResponseHandler(
518
+ alibabaVideoTaskStatusSchema,
519
+ ),
520
+ failedResponseHandler: alibabaVideoFailedResponseHandler,
521
+ abortSignal: options.abortSignal,
522
+ fetch: this.config.fetch,
523
+ });
524
+
525
+ responseHeaders = pollHeaders;
526
+ const taskStatus = statusResponse.output?.task_status;
527
+
528
+ if (taskStatus === 'SUCCEEDED') {
529
+ finalResponse = statusResponse;
530
+ break;
531
+ }
532
+
533
+ if (taskStatus === 'FAILED' || taskStatus === 'CANCELED') {
534
+ throw new AISDKError({
535
+ name: 'ALIBABA_VIDEO_GENERATION_FAILED',
536
+ message: `Video generation ${taskStatus.toLowerCase()}. Task ID: ${taskId}. ${statusResponse.output?.message ?? ''}`,
537
+ });
538
+ }
539
+
540
+ // Continue polling for PENDING, RUNNING, UNKNOWN statuses
541
+ }
542
+
543
+ const videoUrl = finalResponse?.output?.video_url;
544
+ if (!videoUrl) {
545
+ throw new AISDKError({
546
+ name: 'ALIBABA_VIDEO_GENERATION_ERROR',
547
+ message: `No video URL in response. Task ID: ${taskId}`,
548
+ });
549
+ }
550
+
551
+ return {
552
+ videos: [
553
+ {
554
+ type: 'url',
555
+ url: videoUrl,
556
+ mediaType: 'video/mp4',
557
+ },
558
+ ],
559
+ warnings,
560
+ response: {
561
+ timestamp: currentDate,
562
+ modelId: this.modelId,
563
+ headers: responseHeaders,
564
+ },
565
+ providerMetadata: {
566
+ alibaba: {
567
+ taskId,
568
+ videoUrl,
569
+ ...(finalResponse?.output?.actual_prompt
570
+ ? { actualPrompt: finalResponse.output.actual_prompt }
571
+ : {}),
572
+ ...(finalResponse?.usage
573
+ ? {
574
+ usage: {
575
+ duration: finalResponse.usage.duration,
576
+ outputVideoDuration:
577
+ finalResponse.usage.output_video_duration,
578
+ resolution: finalResponse.usage.SR,
579
+ size: finalResponse.usage.size,
580
+ },
581
+ }
582
+ : {}),
583
+ },
584
+ },
585
+ };
586
+ }
587
+ }
@@ -0,0 +1,16 @@
1
+ // https://www.alibabacloud.com/help/en/model-studio/use-video-generation
2
+ export type AlibabaVideoModelId =
3
+ // Text-to-Video
4
+ | 'wan2.6-t2v'
5
+ | 'wan2.5-t2v-preview'
6
+ | 'wan2.7-t2v'
7
+ | 'wan2.7-t2v-2026-06-12'
8
+ // Image-to-Video (first frame)
9
+ | 'wan2.6-i2v'
10
+ | 'wan2.6-i2v-flash'
11
+ // Reference-to-Video
12
+ | 'wan2.6-r2v'
13
+ | 'wan2.6-r2v-flash'
14
+ | 'wan2.7-r2v'
15
+ | 'wan2.7-r2v-2026-06-12'
16
+ | (string & {});
@@ -0,0 +1,37 @@
1
+ import { convertOpenAICompatibleChatUsage } from '@ai-sdk/openai-compatible/internal';
2
+ import type { LanguageModelV4Usage } from '@ai-sdk/provider';
3
+
4
+ export type AlibabaUsage = {
5
+ prompt_tokens?: number | null;
6
+ completion_tokens?: number | null;
7
+ total_tokens?: number | null;
8
+ prompt_tokens_details?: {
9
+ cached_tokens?: number | null;
10
+ cache_creation_input_tokens?: number | null;
11
+ } | null;
12
+ completion_tokens_details?: {
13
+ reasoning_tokens?: number | null;
14
+ } | null;
15
+ };
16
+
17
+ export function convertAlibabaUsage(
18
+ usage: AlibabaUsage | undefined | null,
19
+ ): LanguageModelV4Usage {
20
+ const baseUsage = convertOpenAICompatibleChatUsage(usage);
21
+
22
+ const cacheWriteTokens =
23
+ usage?.prompt_tokens_details?.cache_creation_input_tokens ?? 0;
24
+ const noCacheTokens =
25
+ (baseUsage.inputTokens.total ?? 0) -
26
+ (baseUsage.inputTokens.cacheRead ?? 0) -
27
+ cacheWriteTokens;
28
+
29
+ return {
30
+ ...baseUsage,
31
+ inputTokens: {
32
+ ...baseUsage.inputTokens,
33
+ cacheWrite: cacheWriteTokens,
34
+ noCache: noCacheTokens,
35
+ },
36
+ };
37
+ }