Skip to content

Commit d64fee8

Browse files
committed
fix(minimax): address video generation review
1 parent 8422a6e commit d64fee8

4 files changed

Lines changed: 250 additions & 84 deletions

File tree

src/providers/minimax/actions.ts

Lines changed: 32 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -215,31 +215,38 @@ const createResponseOutputSchema = s.looseRequiredObject(
215215
},
216216
);
217217

218-
const videoModels = [
218+
const textToVideoModels = ["MiniMax-Hailuo-2.3", "MiniMax-Hailuo-02", "T2V-01-Director", "T2V-01"];
219+
220+
const imageToVideoModels = [
219221
"MiniMax-Hailuo-2.3",
220222
"MiniMax-Hailuo-2.3-Fast",
221223
"MiniMax-Hailuo-02",
222-
"T2V-01-Director",
223-
"T2V-01",
224224
"I2V-01-Director",
225225
"I2V-01-live",
226226
"I2V-01",
227227
];
228228

229-
const videoRegionSchema = s.stringEnum(["global", "china"], {
230-
description:
231-
"MiniMax service region. Use global for the https://api.minimax.io host or china for the https://api.minimaxi.com host. Defaults to global.",
232-
default: "global",
229+
const textToVideoModelSchema = s.stringEnum(textToVideoModels, {
230+
description: "MiniMax text-to-video model to invoke, for example MiniMax-Hailuo-2.3.",
231+
default: "MiniMax-Hailuo-2.3",
233232
});
234233

235-
const videoModelSchema = s.stringEnum(videoModels, {
236-
description: "MiniMax video model to invoke, for example MiniMax-Hailuo-2.3.",
234+
const imageToVideoModelSchema = s.stringEnum(imageToVideoModels, {
235+
description: "MiniMax image-to-video model to invoke, for example MiniMax-Hailuo-2.3.",
237236
default: "MiniMax-Hailuo-2.3",
238237
});
239238

240-
const videoDurationSchema = s.integer("Length of the generated video in seconds.", { minimum: 1 });
241-
const videoResolutionSchema = trimmedNonEmptyString(
242-
"Resolution of the generated video, for example 512P, 768P, or 1080P.",
239+
const videoDurationSchema = s.anyOf([s.literal(6), s.literal(10)], {
240+
description: "Length of the generated video in seconds. Model and resolution determine whether 6 or 10 is valid.",
241+
default: 6,
242+
});
243+
const textToVideoResolutionSchema = s.stringEnum(
244+
"Resolution of the generated text-to-video result. Supported values depend on the model and duration.",
245+
["720P", "768P", "1080P"],
246+
);
247+
const imageToVideoResolutionSchema = s.stringEnum(
248+
"Resolution of the generated image-to-video result. Supported values depend on the model and duration.",
249+
["512P", "720P", "768P", "1080P"],
243250
);
244251
const videoPromptOptimizerSchema = s.boolean("Whether MiniMax may rewrite the prompt to improve the result.");
245252
const videoFastPretreatmentSchema = s.boolean("Whether MiniMax applies fast pre-processing to speed up generation.");
@@ -248,53 +255,39 @@ const videoCallbackUrlSchema = s.url("URL MiniMax calls with asynchronous task s
248255
const textToVideoInputSchema = s.object(
249256
"Request body for creating a MiniMax text-to-video generation task.",
250257
{
251-
model: videoModelSchema,
258+
model: textToVideoModelSchema,
252259
prompt: trimmedNonEmptyString("Text description of the video to generate."),
253260
prompt_optimizer: videoPromptOptimizerSchema,
254261
fast_pretreatment: videoFastPretreatmentSchema,
255262
duration: videoDurationSchema,
256-
resolution: videoResolutionSchema,
263+
resolution: textToVideoResolutionSchema,
257264
callback_url: videoCallbackUrlSchema,
258-
region: videoRegionSchema,
259265
},
260-
{ optional: ["prompt_optimizer", "fast_pretreatment", "duration", "resolution", "callback_url", "region"] },
266+
{ optional: ["prompt_optimizer", "fast_pretreatment", "duration", "resolution", "callback_url"] },
261267
);
262268

263269
const imageToVideoInputSchema = s.object(
264270
"Request body for creating a MiniMax image-to-video generation task from a first frame image.",
265271
{
266-
model: videoModelSchema,
272+
model: imageToVideoModelSchema,
267273
first_frame_image: trimmedNonEmptyString("First frame image as a public HTTPS URL or a data URI base64 string."),
268274
prompt: optionalTrimmedString("Text description that guides the generated video."),
269275
prompt_optimizer: videoPromptOptimizerSchema,
270276
fast_pretreatment: videoFastPretreatmentSchema,
271277
duration: videoDurationSchema,
272-
resolution: videoResolutionSchema,
278+
resolution: imageToVideoResolutionSchema,
273279
callback_url: videoCallbackUrlSchema,
274-
region: videoRegionSchema,
275-
},
276-
{
277-
optional: ["prompt", "prompt_optimizer", "fast_pretreatment", "duration", "resolution", "callback_url", "region"],
278280
},
281+
{ optional: ["prompt", "prompt_optimizer", "fast_pretreatment", "duration", "resolution", "callback_url"] },
279282
);
280283

281-
const queryVideoGenerationInputSchema = s.object(
282-
"Input parameters for querying a MiniMax video generation task.",
283-
{
284-
task_id: trimmedNonEmptyString("Identifier of the MiniMax video generation task to query."),
285-
region: videoRegionSchema,
286-
},
287-
{ optional: ["region"] },
288-
);
284+
const queryVideoGenerationInputSchema = s.object("Input parameters for querying a MiniMax video generation task.", {
285+
task_id: trimmedNonEmptyString("Identifier of the MiniMax video generation task to query."),
286+
});
289287

290-
const downloadVideoInputSchema = s.object(
291-
"Input parameters for retrieving a generated MiniMax video file.",
292-
{
293-
file_id: trimmedNonEmptyString("Identifier of the generated video file to retrieve."),
294-
region: videoRegionSchema,
295-
},
296-
{ optional: ["region"] },
297-
);
288+
const downloadVideoInputSchema = s.object("Input parameters for retrieving a generated MiniMax video file.", {
289+
file_id: trimmedNonEmptyString("Identifier of the generated video file to retrieve."),
290+
});
298291

299292
const minimaxBaseRespSchema = s.looseRequiredObject(
300293
"MiniMax base response wrapper.",
@@ -331,7 +324,7 @@ const videoFileOutputSchema = s.looseRequiredObject(
331324
file: s.looseRequiredObject(
332325
"MiniMax file object with download metadata.",
333326
{
334-
file_id: s.integer("MiniMax file identifier."),
327+
file_id: s.string("MiniMax file identifier."),
335328
bytes: s.integer("Size of the file in bytes."),
336329
created_at: s.integer("File creation time as Unix seconds."),
337330
filename: s.string("File name assigned by MiniMax."),

src/providers/minimax/definition.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,19 @@ export const provider: ProviderDefinition = {
1515
label: "API Key",
1616
placeholder: "MINIMAX_API_KEY",
1717
description:
18-
"MiniMax API key sent as an Authorization Bearer token. Create or view API keys in Account Management > API Keys: https://platform.minimax.io/user-center/basic-information/interface-key.",
18+
"MiniMax API key sent as an Authorization Bearer token. Create or view global keys at https://platform.minimax.io/user-center/basic-information/interface-key or China keys at https://platform.minimaxi.com/user-center/basic-information/interface-key.",
19+
extraFields: [
20+
{
21+
key: "region",
22+
label: "Region",
23+
inputType: "text",
24+
required: false,
25+
secret: false,
26+
placeholder: "global",
27+
description:
28+
"Optional MiniMax API region for this key. Use global for api.minimax.io or china for api.minimaxi.com.",
29+
},
30+
],
1931
},
2032
],
2133
homepageUrl: "https://www.minimax.io",
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, expect, it } from "vitest";
2+
import { validateActionInput } from "../../core/validation.ts";
3+
import { minimaxActions } from "./actions.ts";
4+
import { credentialValidators, minimaxActionHandlers } from "./executors.ts";
5+
6+
const textToVideo = minimaxActions.find((action) => action.name === "text_to_video")!;
7+
const imageToVideo = minimaxActions.find((action) => action.name === "image_to_video")!;
8+
const downloadVideo = minimaxActions.find((action) => action.name === "download_video")!;
9+
10+
describe("MiniMax video actions", () => {
11+
it("rejects models that do not support the selected generation mode", () => {
12+
expect(
13+
validateActionInput(textToVideo, {
14+
model: "I2V-01",
15+
prompt: "A calm lake at sunrise.",
16+
}).valid,
17+
).toBe(false);
18+
expect(
19+
validateActionInput(textToVideo, {
20+
model: "MiniMax-Hailuo-2.3-Fast",
21+
prompt: "A calm lake at sunrise.",
22+
}).valid,
23+
).toBe(false);
24+
expect(
25+
validateActionInput(imageToVideo, {
26+
model: "T2V-01",
27+
first_frame_image: "https://example.com/frame.png",
28+
}).valid,
29+
).toBe(false);
30+
});
31+
32+
it("rejects unsupported duration and resolution values", () => {
33+
expect(
34+
validateActionInput(textToVideo, {
35+
model: "MiniMax-Hailuo-2.3",
36+
prompt: "A calm lake at sunrise.",
37+
duration: 7,
38+
}).valid,
39+
).toBe(false);
40+
expect(
41+
validateActionInput(textToVideo, {
42+
model: "MiniMax-Hailuo-2.3",
43+
prompt: "A calm lake at sunrise.",
44+
resolution: "banana",
45+
}).valid,
46+
).toBe(false);
47+
expect(
48+
validateActionInput(imageToVideo, {
49+
model: "MiniMax-Hailuo-02",
50+
first_frame_image: "https://example.com/frame.png",
51+
duration: 10,
52+
resolution: "512P",
53+
}).valid,
54+
).toBe(true);
55+
});
56+
57+
it("declares retrieved file ids as strings", () => {
58+
expect(downloadVideo.outputSchema).toMatchObject({
59+
properties: {
60+
file: {
61+
properties: {
62+
file_id: { type: "string" },
63+
},
64+
},
65+
},
66+
});
67+
});
68+
69+
it("maps successful HTTP responses with MiniMax error status to failures", async () => {
70+
const fetcher: typeof fetch = async () =>
71+
new Response(
72+
JSON.stringify({
73+
base_resp: {
74+
status_code: 1004,
75+
status_msg: "invalid api key",
76+
},
77+
}),
78+
{ status: 200, headers: { "content-type": "application/json" } },
79+
);
80+
81+
await expect(
82+
minimaxActionHandlers.text_to_video(
83+
{
84+
model: "MiniMax-Hailuo-2.3",
85+
prompt: "A calm lake at sunrise.",
86+
},
87+
{
88+
apiKey: "invalid",
89+
apiBaseUrl: "https://api.minimax.io",
90+
fetcher,
91+
},
92+
),
93+
).rejects.toMatchObject({
94+
status: 401,
95+
message: "invalid api key",
96+
});
97+
});
98+
99+
it("validates China-region credentials against the China API host", async () => {
100+
const urls: string[] = [];
101+
const fetcher: typeof fetch = async (input) => {
102+
urls.push(String(input));
103+
return new Response(JSON.stringify({ data: [{ id: "MiniMax-M3" }] }), {
104+
status: 200,
105+
headers: { "content-type": "application/json" },
106+
});
107+
};
108+
109+
const result = await credentialValidators.apiKey!(
110+
{
111+
apiKey: "china-key",
112+
values: { apiKey: "china-key", region: "china" },
113+
},
114+
{ fetcher },
115+
);
116+
117+
expect(urls).toEqual(["https://api.minimaxi.com/v1/models"]);
118+
expect(result?.metadata).toMatchObject({
119+
apiBaseUrl: "https://api.minimaxi.com",
120+
});
121+
});
122+
123+
it("rejects unsupported credential regions", async () => {
124+
const fetcher: typeof fetch = async () => {
125+
throw new Error("unexpected request");
126+
};
127+
128+
await expect(
129+
credentialValidators.apiKey!(
130+
{
131+
apiKey: "test-key",
132+
values: { apiKey: "test-key", region: "europe" },
133+
},
134+
{ fetcher },
135+
),
136+
).rejects.toMatchObject({
137+
status: 400,
138+
message: "minimax region must be global or china",
139+
});
140+
});
141+
});

0 commit comments

Comments
 (0)