@ai-sdk/alibaba 1.0.34 → 1.0.35

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.
@@ -6,6 +6,7 @@ import {
6
6
  } from '@ai-sdk/provider';
7
7
  import {
8
8
  combineHeaders,
9
+ convertImageModelFileToDataUri,
9
10
  convertUint8ArrayToBase64,
10
11
  createJsonErrorResponseHandler,
11
12
  createJsonResponseHandler,
@@ -41,6 +42,23 @@ export type AlibabaVideoModelOptions = {
41
42
  * Use character identifiers (character1, character2) in prompts to reference them.
42
43
  */
43
44
  referenceUrls?: string[] | null;
45
+ /**
46
+ * Explicit media array for reference-to-video mode (wan2.7 models).
47
+ * Overrides the automatic mapping from `inputReferences` and `frameImages`.
48
+ * Use `Image 1`, `Video 1`, etc. in prompts to reference media items
49
+ * (images and videos are counted separately, in array order).
50
+ */
51
+ media?: Array<{
52
+ type: 'reference_image' | 'reference_video' | 'first_frame';
53
+ /** Public URL, or a `data:{mime};base64,{data}` URI for images. */
54
+ url: string;
55
+ /** URL to an audio file used as voice reference for this media item. */
56
+ referenceVoice?: string | null;
57
+ }> | null;
58
+ /**
59
+ * Aspect ratio (wan2.7 text-to-video and reference-to-video models).
60
+ */
61
+ ratio?: '16:9' | '9:16' | '1:1' | '4:3' | '3:4' | null;
44
62
  /** Polling interval in milliseconds. Defaults to 5000 (5 seconds). */
45
63
  pollIntervalMs?: number | null;
46
64
  /** Maximum wait time in milliseconds for video generation. Defaults to 600000 (10 minutes). */
@@ -59,6 +77,20 @@ const alibabaVideoModelOptionsSchema = lazySchema(() =>
59
77
  watermark: z.boolean().nullish(),
60
78
  audio: z.boolean().nullish(),
61
79
  referenceUrls: z.array(z.string()).nullish(),
80
+ media: z
81
+ .array(
82
+ z.object({
83
+ type: z.enum([
84
+ 'reference_image',
85
+ 'reference_video',
86
+ 'first_frame',
87
+ ]),
88
+ url: z.string(),
89
+ referenceVoice: z.string().nullish(),
90
+ }),
91
+ )
92
+ .nullish(),
93
+ ratio: z.enum(['16:9', '9:16', '1:1', '4:3', '3:4']).nullish(),
62
94
  pollIntervalMs: z.number().positive().nullish(),
63
95
  pollTimeoutMs: z.number().positive().nullish(),
64
96
  })
@@ -134,6 +166,53 @@ function detectMode(modelId: string): 't2v' | 'i2v' | 'r2v' {
134
166
  return 't2v';
135
167
  }
136
168
 
169
+ // wan2.7 models use a different protocol than earlier wan models:
170
+ // resolution tiers + ratio instead of size, input.media instead of
171
+ // input.reference_urls (R2V), and no shot_type or audio parameters.
172
+ function isWan27Model(modelId: string): boolean {
173
+ return modelId.startsWith('wan2.7');
174
+ }
175
+
176
+ // Maps SDK "WIDTHxHEIGHT" resolutions to Alibaba resolution tiers.
177
+ const resolutionTierMap: Record<string, string> = {
178
+ '1280x720': '720P',
179
+ '720x1280': '720P',
180
+ '960x960': '720P',
181
+ '1088x832': '720P',
182
+ '832x1088': '720P',
183
+ '1920x1080': '1080P',
184
+ '1080x1920': '1080P',
185
+ '1440x1440': '1080P',
186
+ '1632x1248': '1080P',
187
+ '1248x1632': '1080P',
188
+ '832x480': '480P',
189
+ '480x832': '480P',
190
+ '624x624': '480P',
191
+ };
192
+
193
+ const supportedRatios = new Set(['16:9', '9:16', '1:1', '4:3', '3:4']);
194
+
195
+ function deriveRatioFromResolution(
196
+ resolution: `${number}x${number}`,
197
+ ): string | undefined {
198
+ const [width, height] = resolution.split('x').map(Number);
199
+ if (
200
+ !Number.isInteger(width) ||
201
+ !Number.isInteger(height) ||
202
+ width <= 0 ||
203
+ height <= 0
204
+ ) {
205
+ return undefined;
206
+ }
207
+ let a = width;
208
+ let b = height;
209
+ while (b !== 0) {
210
+ [a, b] = [b, a % b];
211
+ }
212
+ const ratio = `${width / a}:${height / a}`;
213
+ return supportedRatios.has(ratio) ? ratio : undefined;
214
+ }
215
+
137
216
  function fileToImageString(file: Experimental_VideoModelV3File): string {
138
217
  if (file.type === 'url') {
139
218
  return file.url;
@@ -156,6 +235,61 @@ function resolveStartImage(
156
235
  return getFirstFrameImage(options) ?? options.image;
157
236
  }
158
237
 
238
+ function isVideoUrl(url: string): boolean {
239
+ return /\.(mp4|mov)([?#]|$)/i.test(url);
240
+ }
241
+
242
+ // Builds the wan2.7 input.media array from inputReferences and frameImages.
243
+ function resolveMedia(
244
+ options: Parameters<Experimental_VideoModelV3['doGenerate']>[0],
245
+ alibabaOptions: AlibabaVideoModelOptions | undefined,
246
+ warnings: SharedV3Warning[],
247
+ ): Array<Record<string, unknown>> | undefined {
248
+ if (alibabaOptions?.media != null && alibabaOptions.media.length > 0) {
249
+ return alibabaOptions.media.map(item => ({
250
+ type: item.type,
251
+ url: item.url,
252
+ ...(item.referenceVoice != null
253
+ ? { reference_voice: item.referenceVoice }
254
+ : {}),
255
+ }));
256
+ }
257
+
258
+ const media: Array<Record<string, unknown>> = [];
259
+
260
+ for (const reference of options.inputReferences ?? []) {
261
+ if (reference.type === 'url') {
262
+ media.push({
263
+ type: isVideoUrl(reference.url) ? 'reference_video' : 'reference_image',
264
+ url: reference.url,
265
+ });
266
+ } else if (reference.mediaType.startsWith('image/')) {
267
+ media.push({
268
+ type: 'reference_image',
269
+ url: convertImageModelFileToDataUri(reference),
270
+ });
271
+ } else {
272
+ warnings.push({
273
+ type: 'unsupported',
274
+ feature: 'inputReferences',
275
+ details:
276
+ 'Alibaba reference-to-video requires URL references for videos. ' +
277
+ 'Non-URL video reference was skipped.',
278
+ });
279
+ }
280
+ }
281
+
282
+ const firstFrame = getFirstFrameImage(options);
283
+ if (firstFrame != null) {
284
+ media.push({
285
+ type: 'first_frame',
286
+ url: convertImageModelFileToDataUri(firstFrame),
287
+ });
288
+ }
289
+
290
+ return media.length > 0 ? media : undefined;
291
+ }
292
+
159
293
  function resolveReferenceUrls(
160
294
  options: Parameters<Experimental_VideoModelV3['doGenerate']>[0],
161
295
  alibabaOptions: AlibabaVideoModelOptions | undefined,
@@ -230,20 +364,34 @@ export class AlibabaVideoModel implements Experimental_VideoModelV3 {
230
364
  }
231
365
 
232
366
  const startImage = resolveStartImage(options);
233
- const referenceUrls = resolveReferenceUrls(
234
- options,
235
- alibabaOptions,
236
- warnings,
237
- );
367
+ const wan27 = isWan27Model(this.modelId);
368
+ // wan2.7 T2V and R2V take an explicit aspect ratio (I2V follows the input image)
369
+ const supportsRatio = wan27 && mode !== 'i2v';
238
370
 
239
371
  // Handle image input for I2V mode
240
372
  if (mode === 'i2v' && startImage != null) {
241
373
  input.img_url = fileToImageString(startImage);
242
374
  }
243
375
 
244
- // Handle reference URLs for R2V mode
245
- if (mode === 'r2v' && referenceUrls != null && referenceUrls.length > 0) {
246
- input.reference_urls = referenceUrls;
376
+ // Handle references for R2V mode
377
+ if (mode === 'r2v') {
378
+ if (wan27) {
379
+ // wan2.7: input.media
380
+ const media = resolveMedia(options, alibabaOptions, warnings);
381
+ if (media != null) {
382
+ input.media = media;
383
+ }
384
+ } else {
385
+ // wan2.6: legacy protocol with input.reference_urls
386
+ const referenceUrls = resolveReferenceUrls(
387
+ options,
388
+ alibabaOptions,
389
+ warnings,
390
+ );
391
+ if (referenceUrls != null && referenceUrls.length > 0) {
392
+ input.reference_urls = referenceUrls;
393
+ }
394
+ }
247
395
  }
248
396
 
249
397
  const lastFrame = options.frameImages?.find(
@@ -287,49 +435,80 @@ export class AlibabaVideoModel implements Experimental_VideoModelV3 {
287
435
 
288
436
  // Resolution / Size mapping
289
437
  if (options.resolution != null) {
290
- if (mode === 'i2v') {
291
- // I2V uses "720P" / "1080P" format
292
- const resolutionMap: Record<string, string> = {
293
- '1280x720': '720P',
294
- '720x1280': '720P',
295
- '960x960': '720P',
296
- '1088x832': '720P',
297
- '832x1088': '720P',
298
- '1920x1080': '1080P',
299
- '1080x1920': '1080P',
300
- '1440x1440': '1080P',
301
- '1632x1248': '1080P',
302
- '1248x1632': '1080P',
303
- '832x480': '480P',
304
- '480x832': '480P',
305
- '624x624': '480P',
306
- };
307
- parameters.resolution =
308
- resolutionMap[options.resolution] || options.resolution;
438
+ if (mode === 'i2v' || wan27) {
439
+ // I2V and wan2.7 models use "720P" / "1080P" format
440
+ const resolutionTier =
441
+ resolutionTierMap[options.resolution] || options.resolution;
442
+ if (wan27 && resolutionTier !== '720P' && resolutionTier !== '1080P') {
443
+ warnings.push({
444
+ type: 'unsupported',
445
+ feature: 'resolution',
446
+ details:
447
+ 'wan2.7 models only support 720P and 1080P ' +
448
+ `resolutions. The resolution "${options.resolution}" was ignored.`,
449
+ });
450
+ } else {
451
+ parameters.resolution = resolutionTier;
452
+ }
309
453
  } else {
310
- // T2V and R2V use "WIDTH*HEIGHT" format for the size parameter
454
+ // wan2.6 T2V and R2V use "WIDTH*HEIGHT" format for the size parameter
311
455
  // Convert "WIDTHxHEIGHT" (SDK standard) to "WIDTH*HEIGHT" (Alibaba API)
312
456
  parameters.size = options.resolution.replace('x', '*');
313
457
  }
314
458
  }
315
459
 
460
+ // wan2.7 T2V and R2V support an explicit aspect ratio parameter
461
+ if (supportsRatio) {
462
+ const ratio =
463
+ alibabaOptions?.ratio ??
464
+ options.aspectRatio ??
465
+ (options.resolution != null
466
+ ? deriveRatioFromResolution(options.resolution)
467
+ : undefined);
468
+ if (ratio != null) {
469
+ parameters.ratio = ratio;
470
+ }
471
+ }
472
+
316
473
  // Provider-specific parameters
317
474
  if (alibabaOptions?.promptExtend != null) {
318
475
  parameters.prompt_extend = alibabaOptions.promptExtend;
319
476
  }
320
477
  if (alibabaOptions?.shotType != null) {
321
- parameters.shot_type = alibabaOptions.shotType;
478
+ if (wan27) {
479
+ // wan2.7 removed shot_type; shot structure is described in the prompt
480
+ warnings.push({
481
+ type: 'unsupported',
482
+ feature: 'shotType',
483
+ details:
484
+ 'wan2.7 models do not support the shotType option. ' +
485
+ 'Describe the shot structure in the prompt instead.',
486
+ });
487
+ } else {
488
+ parameters.shot_type = alibabaOptions.shotType;
489
+ }
322
490
  }
323
491
  if (alibabaOptions?.watermark != null) {
324
492
  parameters.watermark = alibabaOptions.watermark;
325
493
  }
326
494
  const audio = options.generateAudio ?? alibabaOptions?.audio;
327
495
  if (audio != null) {
328
- parameters.audio = audio;
496
+ if (wan27) {
497
+ // wan2.7 does not have an audio parameter (audio is always generated)
498
+ warnings.push({
499
+ type: 'unsupported',
500
+ feature: 'generateAudio',
501
+ details:
502
+ 'wan2.7 models always generate audio. ' +
503
+ 'The audio option was ignored.',
504
+ });
505
+ } else {
506
+ parameters.audio = audio;
507
+ }
329
508
  }
330
509
 
331
510
  // Warn about unsupported standard options
332
- if (options.aspectRatio) {
511
+ if (options.aspectRatio && !supportsRatio) {
333
512
  warnings.push({
334
513
  type: 'unsupported',
335
514
  feature: 'aspectRatio',
@@ -3,10 +3,14 @@ export type AlibabaVideoModelId =
3
3
  // Text-to-Video
4
4
  | 'wan2.6-t2v'
5
5
  | 'wan2.5-t2v-preview'
6
+ | 'wan2.7-t2v'
7
+ | 'wan2.7-t2v-2026-06-12'
6
8
  // Image-to-Video (first frame)
7
9
  | 'wan2.6-i2v'
8
10
  | 'wan2.6-i2v-flash'
9
11
  // Reference-to-Video
10
12
  | 'wan2.6-r2v'
11
13
  | 'wan2.6-r2v-flash'
14
+ | 'wan2.7-r2v'
15
+ | 'wan2.7-r2v-2026-06-12'
12
16
  | (string & {});