@ai-sdk/minimax 3.0.0 → 3.0.2

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,657 @@
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
+ createJsonErrorResponseHandler,
11
+ createJsonResponseHandler,
12
+ delay,
13
+ getFromApi,
14
+ getTopLevelMediaType,
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
+ minimaxVideoModelOptionsSchema,
24
+ minimaxVideoRatios,
25
+ minimaxVideoResolutions,
26
+ type MiniMaxVideoModelOptions,
27
+ } from './minimax-video-model-options';
28
+ import type { MiniMaxVideoModelId } from './minimax-video-settings';
29
+
30
+ interface MiniMaxVideoModelConfig {
31
+ provider: string;
32
+ // API root without a version suffix, e.g. `https://api.minimax.io`.
33
+ baseURL: string;
34
+ headers: Resolvable<Record<string, string | undefined>>;
35
+ fetch?: FetchFunction;
36
+ _internal?: {
37
+ currentDate?: () => Date;
38
+ };
39
+ }
40
+
41
+ type MiniMaxVideoDoGenerateOptions = Parameters<
42
+ Experimental_VideoModelV4['doGenerate']
43
+ >[0];
44
+
45
+ const DEFAULT_RESOLUTION = '2K';
46
+ const DEFAULT_POLL_INTERVAL_MS = 10_000;
47
+ const DEFAULT_POLL_TIMEOUT_MS = 600_000;
48
+ const MIN_DURATION_SECONDS = 5;
49
+ const MAX_DURATION_SECONDS = 15;
50
+ const MAX_REFERENCE_IMAGES = 9;
51
+ const MAX_REFERENCE_VIDEOS = 3;
52
+ const MAX_REFERENCE_AUDIOS = 3;
53
+
54
+ const allowedRatios = new Set<string>(minimaxVideoRatios);
55
+ const allowedResolutions = new Set<string>(minimaxVideoResolutions);
56
+
57
+ // The top-level `resolution` is `{width}x{height}`, but the API takes a named
58
+ // tier, so map the canonical 2K frame sizes onto the single tier H3 supports.
59
+ // Anything else can't be honored and warns.
60
+ // One canonical frame size per ratio H3 supports, so a caller who pairs a
61
+ // resolution with any supported ratio resolves to the tier instead of being
62
+ // told 2K is unsupported.
63
+ const RESOLUTION_MAP: Record<string, string> = {
64
+ // Square
65
+ '2048x2048': '2K',
66
+ // Landscape
67
+ '2560x1080': '2K',
68
+ '2560x1440': '2K',
69
+ '2048x1536': '2K',
70
+ // Portrait
71
+ '1440x2560': '2K',
72
+ '1536x2048': '2K',
73
+ };
74
+
75
+ // A caller may pass the named tier itself rather than a frame size — `2K` is
76
+ // what the MiniMax API takes, so it is the natural thing to send. Accept it
77
+ // case-insensitively instead of reporting it as unrecognized.
78
+ function resolveTopLevelResolution(resolution: string): string | undefined {
79
+ const named = resolution.toUpperCase();
80
+ return allowedResolutions.has(named) ? named : RESOLUTION_MAP[resolution];
81
+ }
82
+
83
+ // Frame images must be images. Returns the offending top-level media type when
84
+ // a frame is something else, so it can be rejected the way a reference input of
85
+ // the wrong kind is. A frame with no media type is left alone: the core emits
86
+ // URL inputs without one, and unlike a reference there is no routing decision
87
+ // riding on it.
88
+ function nonImageFrameMediaType(
89
+ file: Experimental_VideoModelV4File,
90
+ ): string | undefined {
91
+ if (file.mediaType == null) {
92
+ return undefined;
93
+ }
94
+
95
+ const topLevelMediaType = getTopLevelMediaType(file.mediaType);
96
+ return topLevelMediaType === 'image' ? undefined : topLevelMediaType;
97
+ }
98
+
99
+ export class MiniMaxVideoModel implements Experimental_VideoModelV4 {
100
+ readonly specificationVersion = 'v4';
101
+ readonly maxVideosPerCall = 1;
102
+
103
+ get provider(): string {
104
+ return this.config.provider;
105
+ }
106
+
107
+ constructor(
108
+ readonly modelId: MiniMaxVideoModelId,
109
+ private config: MiniMaxVideoModelConfig,
110
+ ) {}
111
+
112
+ async doGenerate(
113
+ options: MiniMaxVideoDoGenerateOptions,
114
+ ): Promise<Awaited<ReturnType<Experimental_VideoModelV4['doGenerate']>>> {
115
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
116
+ const warnings: SharedV4Warning[] = [];
117
+
118
+ const minimaxOptions = (await parseProviderOptions({
119
+ provider: 'minimax',
120
+ providerOptions: options.providerOptions,
121
+ schema: minimaxVideoModelOptionsSchema,
122
+ })) as MiniMaxVideoModelOptions | undefined;
123
+
124
+ if (options.fps != null) {
125
+ warnings.push({
126
+ type: 'unsupported',
127
+ feature: 'fps',
128
+ details: 'MiniMax-H3 does not support a custom frame rate.',
129
+ });
130
+ }
131
+
132
+ if (options.seed != null) {
133
+ warnings.push({
134
+ type: 'unsupported',
135
+ feature: 'seed',
136
+ details: 'MiniMax-H3 does not support a seed.',
137
+ });
138
+ }
139
+
140
+ if (options.n != null && options.n > 1) {
141
+ warnings.push({
142
+ type: 'unsupported',
143
+ feature: 'n',
144
+ details:
145
+ 'MiniMax-H3 generates a single video per call. Only 1 video will be generated.',
146
+ });
147
+ }
148
+
149
+ if (options.generateAudio != null) {
150
+ warnings.push({
151
+ type: 'unsupported',
152
+ feature: 'generateAudio',
153
+ details:
154
+ 'The MiniMax-H3 API does not expose an audio parameter. The generateAudio option was ignored.',
155
+ });
156
+ }
157
+
158
+ // Resolution: the API takes a named tier, so an explicit provider option
159
+ // wins and a top-level value is resolved to a tier.
160
+ let resolution: string | undefined = minimaxOptions?.resolution;
161
+ if (options.resolution != null) {
162
+ const mapped = resolveTopLevelResolution(options.resolution);
163
+ if (resolution != null) {
164
+ // The provider option is already a tier, so the only way the two can
165
+ // disagree is a top-level value that maps to no tier at all.
166
+ if (mapped == null) {
167
+ warnings.push({
168
+ type: 'unsupported',
169
+ feature: 'resolution',
170
+ details:
171
+ `Unrecognized resolution "${options.resolution}". MiniMax-H3 only supports "2K", ` +
172
+ `so providerOptions.minimax.resolution ("${resolution}") was used instead.`,
173
+ });
174
+ }
175
+ } else if (mapped != null) {
176
+ resolution = mapped;
177
+ } else {
178
+ warnings.push({
179
+ type: 'unsupported',
180
+ feature: 'resolution',
181
+ details:
182
+ `Unrecognized resolution "${options.resolution}". MiniMax-H3 only ` +
183
+ 'supports "2K".',
184
+ });
185
+ }
186
+ }
187
+ resolution ??= DEFAULT_RESOLUTION;
188
+
189
+ // `convertImageModelFileToDataUri` passes `url` files through unchanged and
190
+ // encodes inline data as a data URI. MiniMax `mm_file://…` handles survive
191
+ // that pass-through, but not the core `generateVideo` path: it base64-decodes
192
+ // any string that is not `http(s)://` or `data:`, so `mm_file://` only
193
+ // reaches the API through `providerOptions.minimax.referenceAudioUrls`,
194
+ // which is forwarded raw — not through `image`, `frameImages`, or
195
+ // `inputReferences`.
196
+ const content: Array<Record<string, unknown>> = [
197
+ { type: 'text', text: options.prompt ?? '' },
198
+ ];
199
+
200
+ // The inputs that actually make it into `content` below, after the caps and
201
+ // rejections H3 imposes. Reported as `providerMetadata.minimax
202
+ // .resolvedInputs`: the warnings say an input was dropped, but not how many
203
+ // survived, which callers that meter usage need. Accumulated at the push
204
+ // sites rather than re-derived, so the two cannot disagree.
205
+ let sentImageCount = 0;
206
+ const sentReferenceVideoUrls: string[] = [];
207
+
208
+ // Resolve first/last frame inputs. A standalone `image` is treated as the
209
+ // first frame (image-to-video). The core sets `image` to the `first_frame`
210
+ // frame image when there is one, so the frame-image entry is tracked
211
+ // separately to report which input a warning came from.
212
+ const firstFrameImage = options.frameImages?.find(
213
+ frame => frame.frameType === 'first_frame',
214
+ )?.image;
215
+ let firstFrame = firstFrameImage ?? options.image;
216
+ let lastFrame = options.frameImages?.find(
217
+ frame => frame.frameType === 'last_frame',
218
+ )?.image;
219
+
220
+ // Reject unusable frames before deciding the generation mode.
221
+ const firstFrameMediaType =
222
+ firstFrame != null ? nonImageFrameMediaType(firstFrame) : undefined;
223
+ if (firstFrame != null && firstFrameMediaType != null) {
224
+ warnings.push({
225
+ type: 'unsupported',
226
+ feature: firstFrameImage != null ? 'frameImages' : 'image',
227
+ details:
228
+ firstFrameMediaType === 'video'
229
+ ? 'MiniMax-H3 does not accept a video as a frame image. The video was ignored.'
230
+ : `MiniMax-H3 only accepts an image as a frame image; the "${firstFrame.mediaType}" file was ignored.`,
231
+ });
232
+ firstFrame = undefined;
233
+ }
234
+
235
+ if (lastFrame != null) {
236
+ if (firstFrame == null) {
237
+ warnings.push({
238
+ type: 'unsupported',
239
+ feature: 'frameImages',
240
+ details:
241
+ 'MiniMax-H3 requires a first_frame when a last_frame is provided. The last_frame was ignored.',
242
+ });
243
+ lastFrame = undefined;
244
+ } else {
245
+ const lastFrameMediaType = nonImageFrameMediaType(lastFrame);
246
+ if (lastFrameMediaType != null) {
247
+ warnings.push({
248
+ type: 'unsupported',
249
+ feature: 'frameImages',
250
+ details:
251
+ lastFrameMediaType === 'video'
252
+ ? 'MiniMax-H3 does not accept a video as a frame image. The last_frame video was ignored.'
253
+ : `MiniMax-H3 only accepts an image as a frame image; the "${lastFrame.mediaType}" last_frame was ignored.`,
254
+ });
255
+ lastFrame = undefined;
256
+ }
257
+ }
258
+ }
259
+
260
+ const usesFrameImages = firstFrame != null || lastFrame != null;
261
+
262
+ const referenceFiles = options.inputReferences ?? [];
263
+ const referenceAudioUrls = minimaxOptions?.referenceAudioUrls ?? [];
264
+ const usesReferences =
265
+ referenceFiles.length > 0 || referenceAudioUrls.length > 0;
266
+
267
+ if (usesFrameImages) {
268
+ if (firstFrame != null) {
269
+ content.push({
270
+ type: 'image_url',
271
+ image_url: { url: convertImageModelFileToDataUri(firstFrame) },
272
+ role: 'first_frame',
273
+ });
274
+ sentImageCount++;
275
+ }
276
+
277
+ if (lastFrame != null) {
278
+ content.push({
279
+ type: 'image_url',
280
+ image_url: { url: convertImageModelFileToDataUri(lastFrame) },
281
+ role: 'last_frame',
282
+ });
283
+ sentImageCount++;
284
+ }
285
+
286
+ // Frame images and reference inputs are mutually exclusive.
287
+ if (usesReferences) {
288
+ warnings.push({
289
+ type: 'unsupported',
290
+ feature: 'inputReferences',
291
+ details:
292
+ 'MiniMax-H3 cannot combine frame images with reference inputs. The references were ignored.',
293
+ });
294
+ }
295
+ } else if (usesReferences) {
296
+ const referenceImages: Experimental_VideoModelV4File[] = [];
297
+ const referenceVideos: Experimental_VideoModelV4File[] = [];
298
+
299
+ for (const file of referenceFiles) {
300
+ const topLevelMediaType =
301
+ file.mediaType != null
302
+ ? getTopLevelMediaType(file.mediaType)
303
+ : undefined;
304
+
305
+ if (topLevelMediaType === 'video') {
306
+ referenceVideos.push(file);
307
+ } else if (topLevelMediaType === 'image') {
308
+ referenceImages.push(file);
309
+ } else if (topLevelMediaType == null) {
310
+ // Only URL references can omit a media type.
311
+ // Fall back to an image and warn
312
+ warnings.push({
313
+ type: 'unsupported',
314
+ feature: 'inputReferences',
315
+ details:
316
+ 'MiniMax-H3 requires an explicit mediaType to route URL references as ' +
317
+ 'video or image. Pass { data: url, mediaType: "video/mp4" } for video ' +
318
+ 'references. The reference was treated as an image.',
319
+ });
320
+ referenceImages.push(file);
321
+ } else {
322
+ warnings.push({
323
+ type: 'unsupported',
324
+ feature: 'inputReferences',
325
+ details:
326
+ `MiniMax-H3 only accepts image and video references; the "${file.mediaType}" ` +
327
+ 'reference was ignored. Pass reference audio via ' +
328
+ 'providerOptions.minimax.referenceAudioUrls.',
329
+ });
330
+ }
331
+ }
332
+
333
+ for (const image of referenceImages.slice(0, MAX_REFERENCE_IMAGES)) {
334
+ content.push({
335
+ type: 'image_url',
336
+ image_url: { url: convertImageModelFileToDataUri(image) },
337
+ role: 'reference_image',
338
+ });
339
+ sentImageCount++;
340
+ }
341
+ if (referenceImages.length > MAX_REFERENCE_IMAGES) {
342
+ warnings.push({
343
+ type: 'unsupported',
344
+ feature: 'inputReferences',
345
+ details: `MiniMax-H3 accepts at most ${MAX_REFERENCE_IMAGES} reference images. Extra images were ignored.`,
346
+ });
347
+ }
348
+
349
+ for (const video of referenceVideos.slice(0, MAX_REFERENCE_VIDEOS)) {
350
+ const url = convertImageModelFileToDataUri(video);
351
+ content.push({
352
+ type: 'video_url',
353
+ video_url: { url },
354
+ role: 'reference_video',
355
+ });
356
+ // Inline files become data URIs, which are not worth echoing back.
357
+ if (video.type === 'url') {
358
+ sentReferenceVideoUrls.push(url);
359
+ }
360
+ }
361
+ if (referenceVideos.length > MAX_REFERENCE_VIDEOS) {
362
+ warnings.push({
363
+ type: 'unsupported',
364
+ feature: 'inputReferences',
365
+ details: `MiniMax-H3 accepts at most ${MAX_REFERENCE_VIDEOS} reference videos. Extra videos were ignored.`,
366
+ });
367
+ }
368
+
369
+ // Reference audio must accompany at least one reference image or video.
370
+ if (referenceAudioUrls.length > 0) {
371
+ if (referenceImages.length === 0 && referenceVideos.length === 0) {
372
+ warnings.push({
373
+ type: 'unsupported',
374
+ feature: 'referenceAudioUrls',
375
+ details:
376
+ 'MiniMax-H3 reference audio must be paired with at least one reference image or video. The audio was ignored.',
377
+ });
378
+ } else {
379
+ for (const url of referenceAudioUrls.slice(0, MAX_REFERENCE_AUDIOS)) {
380
+ content.push({
381
+ type: 'audio_url',
382
+ audio_url: { url },
383
+ role: 'reference_audio',
384
+ });
385
+ }
386
+ if (referenceAudioUrls.length > MAX_REFERENCE_AUDIOS) {
387
+ warnings.push({
388
+ type: 'unsupported',
389
+ feature: 'referenceAudioUrls',
390
+ details: `MiniMax-H3 accepts at most ${MAX_REFERENCE_AUDIOS} reference audios. Extra audios were ignored.`,
391
+ });
392
+ }
393
+ }
394
+ }
395
+ }
396
+
397
+ // Aspect ratio. In frame-image mode the ratio follows the supplied image,
398
+ // so an explicit ratio is ignored.
399
+ let ratio = minimaxOptions?.ratio as string | undefined;
400
+ if (ratio == null && options.aspectRatio != null) {
401
+ if (allowedRatios.has(options.aspectRatio)) {
402
+ ratio = options.aspectRatio;
403
+ } else {
404
+ warnings.push({
405
+ type: 'unsupported',
406
+ feature: 'aspectRatio',
407
+ details:
408
+ `MiniMax-H3 does not support the aspect ratio "${options.aspectRatio}". ` +
409
+ 'Using the provider default (adaptive).',
410
+ });
411
+ }
412
+ }
413
+ if (usesFrameImages && ratio != null) {
414
+ warnings.push({
415
+ type: 'unsupported',
416
+ feature: 'aspectRatio',
417
+ details:
418
+ 'MiniMax-H3 derives the aspect ratio from the frame image; the requested ratio was ignored.',
419
+ });
420
+ ratio = undefined;
421
+ }
422
+
423
+ // H3 takes an integer between 5 and 15, so round before clamping: a
424
+ // fractional duration is rejected by the API just as an out-of-range one is.
425
+ let duration = options.duration ?? MIN_DURATION_SECONDS;
426
+ if (options.duration != null) {
427
+ if (!Number.isInteger(duration)) {
428
+ duration = Math.round(duration);
429
+ warnings.push({
430
+ type: 'unsupported',
431
+ feature: 'duration',
432
+ details: `MiniMax-H3 requires a whole number of seconds. The requested duration of ${options.duration} was rounded to ${duration}.`,
433
+ });
434
+ }
435
+
436
+ if (duration > MAX_DURATION_SECONDS) {
437
+ warnings.push({
438
+ type: 'unsupported',
439
+ feature: 'duration',
440
+ details: `MiniMax-H3 supports at most ${MAX_DURATION_SECONDS} seconds. The requested duration of ${options.duration} was clamped to ${MAX_DURATION_SECONDS}.`,
441
+ });
442
+ duration = MAX_DURATION_SECONDS;
443
+ } else if (duration < MIN_DURATION_SECONDS) {
444
+ warnings.push({
445
+ type: 'unsupported',
446
+ feature: 'duration',
447
+ details: `MiniMax-H3 requires at least ${MIN_DURATION_SECONDS} seconds. The requested duration of ${options.duration} was clamped to ${MIN_DURATION_SECONDS}.`,
448
+ });
449
+ duration = MIN_DURATION_SECONDS;
450
+ }
451
+ }
452
+
453
+ const body: Record<string, unknown> = {
454
+ model: this.modelId,
455
+ content,
456
+ resolution,
457
+ duration,
458
+ };
459
+ if (ratio != null) {
460
+ body.ratio = ratio;
461
+ }
462
+ if (minimaxOptions?.aigcWatermark != null) {
463
+ body.aigc_watermark = minimaxOptions.aigcWatermark;
464
+ }
465
+
466
+ const baseURL = this.config.baseURL;
467
+
468
+ const { value: createResponse } = await postJsonToApi({
469
+ url: `${baseURL}/v2/video_generation`,
470
+ headers: combineHeaders(
471
+ await resolve(this.config.headers),
472
+ options.headers,
473
+ ),
474
+ body,
475
+ failedResponseHandler: minimaxVideoFailedResponseHandler,
476
+ successfulResponseHandler: createJsonResponseHandler(
477
+ minimaxCreateVideoResponseSchema,
478
+ ),
479
+ abortSignal: options.abortSignal,
480
+ fetch: this.config.fetch,
481
+ });
482
+
483
+ const taskId = createResponse.task_id;
484
+ if (!taskId) {
485
+ throw new AISDKError({
486
+ name: 'MINIMAX_VIDEO_GENERATION_ERROR',
487
+ message: `No task_id returned from the MiniMax API. Response: ${JSON.stringify(createResponse)}`,
488
+ });
489
+ }
490
+
491
+ const pollIntervalMs =
492
+ minimaxOptions?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
493
+ const pollTimeoutMs =
494
+ minimaxOptions?.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
495
+ const startTime = Date.now();
496
+ let responseHeaders: Record<string, string> | undefined;
497
+
498
+ while (true) {
499
+ await delay(pollIntervalMs, { abortSignal: options.abortSignal });
500
+
501
+ if (Date.now() - startTime > pollTimeoutMs) {
502
+ throw new AISDKError({
503
+ name: 'MINIMAX_VIDEO_GENERATION_TIMEOUT',
504
+ message: `MiniMax video generation timed out after ${pollTimeoutMs}ms. Task ID: ${taskId}`,
505
+ });
506
+ }
507
+
508
+ const { value: statusResponse, responseHeaders: pollHeaders } =
509
+ await getFromApi({
510
+ url: `${baseURL}/v2/query/video_generation/${taskId}`,
511
+ validateUrl: false,
512
+ headers: combineHeaders(
513
+ await resolve(this.config.headers),
514
+ options.headers,
515
+ ),
516
+ successfulResponseHandler: createJsonResponseHandler(
517
+ minimaxVideoStatusResponseSchema,
518
+ ),
519
+ failedResponseHandler: minimaxVideoFailedResponseHandler,
520
+ abortSignal: options.abortSignal,
521
+ fetch: this.config.fetch,
522
+ });
523
+
524
+ responseHeaders = pollHeaders;
525
+ const task = statusResponse.task;
526
+
527
+ switch (task.status) {
528
+ case 'succeeded': {
529
+ const url = task.content?.url;
530
+ if (!url) {
531
+ throw new AISDKError({
532
+ name: 'MINIMAX_VIDEO_GENERATION_ERROR',
533
+ message: `MiniMax video generation completed but no video URL was returned. Task ID: ${taskId}`,
534
+ });
535
+ }
536
+
537
+ return {
538
+ videos: [
539
+ {
540
+ type: 'url' as const,
541
+ url,
542
+ mediaType: 'video/mp4',
543
+ },
544
+ ],
545
+ warnings,
546
+ response: {
547
+ timestamp: currentDate,
548
+ modelId: this.modelId,
549
+ headers: responseHeaders,
550
+ },
551
+ providerMetadata: {
552
+ minimax: {
553
+ taskId,
554
+ videoUrl: url,
555
+ resolvedInputs: {
556
+ imageCount: sentImageCount,
557
+ referenceVideoUrls: sentReferenceVideoUrls,
558
+ },
559
+ ...(task.duration != null ? { duration: task.duration } : {}),
560
+ ...(task.ratio != null ? { ratio: task.ratio } : {}),
561
+ ...(task.resolution != null
562
+ ? { resolution: task.resolution }
563
+ : {}),
564
+ ...(task.usage != null
565
+ ? {
566
+ usage: {
567
+ totalSeconds: task.usage.total_seconds,
568
+ inputSeconds: task.usage.input_seconds,
569
+ outputSeconds: task.usage.output_seconds,
570
+ },
571
+ }
572
+ : {}),
573
+ },
574
+ },
575
+ };
576
+ }
577
+
578
+ case 'failed': {
579
+ throw new AISDKError({
580
+ name: 'MINIMAX_VIDEO_GENERATION_FAILED',
581
+ message: `MiniMax video generation failed${
582
+ task.error?.message ? `: ${task.error.message}` : ''
583
+ }${task.error?.code != null ? ` (${task.error.code})` : ''}. Task ID: ${taskId}`,
584
+ });
585
+ }
586
+
587
+ case 'cancelled': {
588
+ throw new AISDKError({
589
+ name: 'MINIMAX_VIDEO_GENERATION_CANCELLED',
590
+ message: `MiniMax video generation was cancelled. Task ID: ${taskId}`,
591
+ });
592
+ }
593
+
594
+ case 'expired': {
595
+ throw new AISDKError({
596
+ name: 'MINIMAX_VIDEO_GENERATION_EXPIRED',
597
+ message: `MiniMax video generation request expired. Task ID: ${taskId}`,
598
+ });
599
+ }
600
+
601
+ // 'queued' | 'running' | unknown → keep polling.
602
+ default:
603
+ break;
604
+ }
605
+ }
606
+ }
607
+ }
608
+
609
+ const minimaxCreateVideoResponseSchema = z.object({
610
+ task_id: z.string().nullish(),
611
+ });
612
+
613
+ const minimaxVideoStatusResponseSchema = z.object({
614
+ task: z.object({
615
+ id: z.string().nullish(),
616
+ status: z.string().nullish(),
617
+ content: z
618
+ .object({
619
+ url: z.string().nullish(),
620
+ })
621
+ .nullish(),
622
+ resolution: z.string().nullish(),
623
+ duration: z.number().nullish(),
624
+ ratio: z.string().nullish(),
625
+ usage: z
626
+ .object({
627
+ total_seconds: z.number().nullish(),
628
+ input_seconds: z.number().nullish(),
629
+ output_seconds: z.number().nullish(),
630
+ })
631
+ .nullish(),
632
+ error: z
633
+ .object({
634
+ code: z.union([z.string(), z.number()]).nullish(),
635
+ message: z.string().nullish(),
636
+ })
637
+ .nullish(),
638
+ }),
639
+ });
640
+
641
+ const minimaxVideoErrorSchema = z.object({
642
+ type: z.string().nullish(),
643
+ error: z
644
+ .object({
645
+ type: z.string().nullish(),
646
+ message: z.string().nullish(),
647
+ http_code: z.union([z.string(), z.number()]).nullish(),
648
+ })
649
+ .nullish(),
650
+ request_id: z.string().nullish(),
651
+ });
652
+
653
+ const minimaxVideoFailedResponseHandler = createJsonErrorResponseHandler({
654
+ errorSchema: minimaxVideoErrorSchema,
655
+ errorToMessage: data =>
656
+ data.error?.message ?? 'MiniMax video generation error',
657
+ });
@@ -0,0 +1,2 @@
1
+ // https://platform.minimax.io/docs
2
+ export type MiniMaxVideoModelId = 'MiniMax-H3' | (string & {});