Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 156 additions & 1 deletion src/providers/minimax/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import { defineProviderAction } from "../../core/provider-definition.ts";

const service = "minimax";

export type MinimaxActionName = "list_models" | "retrieve_model" | "create_response" | "estimate_input_tokens";
export type MinimaxActionName =
| "list_models"
| "retrieve_model"
| "create_response"
| "estimate_input_tokens"
| "text_to_video"
| "image_to_video"
| "query_video_generation"
| "download_video";

const trimmedNonEmptyString = (description: string) => s.string({ description, minLength: 1, pattern: "\\S" });

Expand Down Expand Up @@ -207,6 +215,129 @@ const createResponseOutputSchema = s.looseRequiredObject(
},
);

const textToVideoModels = ["MiniMax-Hailuo-2.3", "MiniMax-Hailuo-02", "T2V-01-Director", "T2V-01"];

const imageToVideoModels = [
"MiniMax-Hailuo-2.3",
"MiniMax-Hailuo-2.3-Fast",
"MiniMax-Hailuo-02",
"I2V-01-Director",
"I2V-01-live",
"I2V-01",
];

const textToVideoModelSchema = s.stringEnum(textToVideoModels, {
description: "MiniMax text-to-video model to invoke, for example MiniMax-Hailuo-2.3.",
default: "MiniMax-Hailuo-2.3",
});

const imageToVideoModelSchema = s.stringEnum(imageToVideoModels, {
description: "MiniMax image-to-video model to invoke, for example MiniMax-Hailuo-2.3.",
default: "MiniMax-Hailuo-2.3",
});

const videoDurationSchema = s.anyOf([s.literal(6), s.literal(10)], {
description: "Length of the generated video in seconds. Model and resolution determine whether 6 or 10 is valid.",
default: 6,
});
const textToVideoResolutionSchema = s.stringEnum(
"Resolution of the generated text-to-video result. Supported values depend on the model and duration.",
["720P", "768P", "1080P"],
);
const imageToVideoResolutionSchema = s.stringEnum(
"Resolution of the generated image-to-video result. Supported values depend on the model and duration.",
["512P", "720P", "768P", "1080P"],
);
const videoPromptOptimizerSchema = s.boolean("Whether MiniMax may rewrite the prompt to improve the result.");
const videoFastPretreatmentSchema = s.boolean("Whether MiniMax applies fast pre-processing to speed up generation.");
const videoCallbackUrlSchema = s.url("URL MiniMax calls with asynchronous task status updates.");

const textToVideoInputSchema = s.object(
"Request body for creating a MiniMax text-to-video generation task.",
{
model: textToVideoModelSchema,
prompt: trimmedNonEmptyString("Text description of the video to generate."),
prompt_optimizer: videoPromptOptimizerSchema,
fast_pretreatment: videoFastPretreatmentSchema,
duration: videoDurationSchema,
resolution: textToVideoResolutionSchema,
callback_url: videoCallbackUrlSchema,
},
{ optional: ["prompt_optimizer", "fast_pretreatment", "duration", "resolution", "callback_url"] },
);

const imageToVideoInputSchema = s.object(
"Request body for creating a MiniMax image-to-video generation task from a first frame image.",
{
model: imageToVideoModelSchema,
first_frame_image: trimmedNonEmptyString("First frame image as a public HTTPS URL or a data URI base64 string."),
prompt: optionalTrimmedString("Text description that guides the generated video."),
prompt_optimizer: videoPromptOptimizerSchema,
fast_pretreatment: videoFastPretreatmentSchema,
duration: videoDurationSchema,
resolution: imageToVideoResolutionSchema,
callback_url: videoCallbackUrlSchema,
},
{ optional: ["prompt", "prompt_optimizer", "fast_pretreatment", "duration", "resolution", "callback_url"] },
);

const queryVideoGenerationInputSchema = s.object("Input parameters for querying a MiniMax video generation task.", {
task_id: trimmedNonEmptyString("Identifier of the MiniMax video generation task to query."),
});

const downloadVideoInputSchema = s.object("Input parameters for retrieving a generated MiniMax video file.", {
file_id: trimmedNonEmptyString("Identifier of the generated video file to retrieve."),
});

const minimaxBaseRespSchema = s.looseRequiredObject(
"MiniMax base response wrapper.",
{
status_code: s.integer("MiniMax status code where 0 indicates success."),
status_msg: s.string("Human-readable MiniMax status message."),
},
{ optional: ["status_code", "status_msg"] },
);

const videoTaskCreatedOutputSchema = s.looseRequiredObject(
"MiniMax asynchronous video generation task creation response.",
{
task_id: s.string("Identifier of the asynchronous MiniMax video generation task."),
base_resp: minimaxBaseRespSchema,
},
{ optional: ["task_id", "base_resp"] },
);

const videoTaskStatusOutputSchema = s.looseRequiredObject(
"MiniMax video generation task status response.",
{
task_id: s.string("Identifier of the queried MiniMax video generation task."),
status: s.string("Current task status, for example Preparing, Queueing, Processing, Success, or Fail."),
file_id: s.string("Identifier of the generated video file, present once the task succeeds."),
base_resp: minimaxBaseRespSchema,
},
{ optional: ["task_id", "status", "file_id", "base_resp"] },
);

const videoFileOutputSchema = s.looseRequiredObject(
"MiniMax file retrieval response for a generated video.",
{
file: s.looseRequiredObject(
"MiniMax file object with download metadata.",
{
file_id: s.string("MiniMax file identifier."),
bytes: s.integer("Size of the file in bytes."),
created_at: s.integer("File creation time as Unix seconds."),
filename: s.string("File name assigned by MiniMax."),
purpose: s.string("Purpose associated with the file."),
download_url: s.string("Temporary URL to download the generated video."),
},
{ optional: ["file_id", "bytes", "created_at", "filename", "purpose", "download_url"] },
),
base_resp: minimaxBaseRespSchema,
},
{ optional: ["file", "base_resp"] },
);

export const minimaxActions: ActionDefinition[] = [
defineProviderAction(service, {
name: "list_models",
Expand Down Expand Up @@ -238,4 +369,28 @@ export const minimaxActions: ActionDefinition[] = [
input_tokens: s.integer("Estimated input token count."),
}),
}),
defineProviderAction(service, {
name: "text_to_video",
description: "Create a MiniMax asynchronous text-to-video generation task.",
inputSchema: textToVideoInputSchema,
outputSchema: videoTaskCreatedOutputSchema,
}),
defineProviderAction(service, {
name: "image_to_video",
description: "Create a MiniMax asynchronous image-to-video generation task from a first frame image.",
inputSchema: imageToVideoInputSchema,
outputSchema: videoTaskCreatedOutputSchema,
}),
defineProviderAction(service, {
name: "query_video_generation",
description: "Query the status of a MiniMax video generation task and read its file id when it completes.",
inputSchema: queryVideoGenerationInputSchema,
outputSchema: videoTaskStatusOutputSchema,
}),
defineProviderAction(service, {
name: "download_video",
description: "Retrieve the download URL and metadata for a generated MiniMax video file.",
inputSchema: downloadVideoInputSchema,
outputSchema: videoFileOutputSchema,
}),
];
14 changes: 13 additions & 1 deletion src/providers/minimax/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,19 @@ export const provider: ProviderDefinition = {
label: "API Key",
placeholder: "MINIMAX_API_KEY",
description:
"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.",
"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.",
extraFields: [
{
key: "region",
label: "Region",
inputType: "text",
required: false,
secret: false,
placeholder: "global",
description:
"Optional MiniMax API region for this key. Use global for api.minimax.io or china for api.minimaxi.com.",
},
],
},
],
homepageUrl: "https://www.minimax.io",
Expand Down
141 changes: 141 additions & 0 deletions src/providers/minimax/executors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { describe, expect, it } from "vitest";
import { validateActionInput } from "../../core/validation.ts";
import { minimaxActions } from "./actions.ts";
import { credentialValidators, minimaxActionHandlers } from "./executors.ts";

const textToVideo = minimaxActions.find((action) => action.name === "text_to_video")!;
const imageToVideo = minimaxActions.find((action) => action.name === "image_to_video")!;
const downloadVideo = minimaxActions.find((action) => action.name === "download_video")!;

describe("MiniMax video actions", () => {
it("rejects models that do not support the selected generation mode", () => {
expect(
validateActionInput(textToVideo, {
model: "I2V-01",
prompt: "A calm lake at sunrise.",
}).valid,
).toBe(false);
expect(
validateActionInput(textToVideo, {
model: "MiniMax-Hailuo-2.3-Fast",
prompt: "A calm lake at sunrise.",
}).valid,
).toBe(false);
expect(
validateActionInput(imageToVideo, {
model: "T2V-01",
first_frame_image: "https://example.com/frame.png",
}).valid,
).toBe(false);
});

it("rejects unsupported duration and resolution values", () => {
expect(
validateActionInput(textToVideo, {
model: "MiniMax-Hailuo-2.3",
prompt: "A calm lake at sunrise.",
duration: 7,
}).valid,
).toBe(false);
expect(
validateActionInput(textToVideo, {
model: "MiniMax-Hailuo-2.3",
prompt: "A calm lake at sunrise.",
resolution: "banana",
}).valid,
).toBe(false);
expect(
validateActionInput(imageToVideo, {
model: "MiniMax-Hailuo-02",
first_frame_image: "https://example.com/frame.png",
duration: 10,
resolution: "512P",
}).valid,
).toBe(true);
});

it("declares retrieved file ids as strings", () => {
expect(downloadVideo.outputSchema).toMatchObject({
properties: {
file: {
properties: {
file_id: { type: "string" },
},
},
},
});
});

it("maps successful HTTP responses with MiniMax error status to failures", async () => {
const fetcher: typeof fetch = async () =>
new Response(
JSON.stringify({
base_resp: {
status_code: 1004,
status_msg: "invalid api key",
},
}),
{ status: 200, headers: { "content-type": "application/json" } },
);

await expect(
minimaxActionHandlers.text_to_video(
{
model: "MiniMax-Hailuo-2.3",
prompt: "A calm lake at sunrise.",
},
{
apiKey: "invalid",
apiBaseUrl: "https://api.minimax.io",
fetcher,
},
),
).rejects.toMatchObject({
status: 401,
message: "invalid api key",
});
});

it("validates China-region credentials against the China API host", async () => {
const urls: string[] = [];
const fetcher: typeof fetch = async (input) => {
urls.push(String(input));
return new Response(JSON.stringify({ data: [{ id: "MiniMax-M3" }] }), {
status: 200,
headers: { "content-type": "application/json" },
});
};

const result = await credentialValidators.apiKey!(
{
apiKey: "china-key",
values: { apiKey: "china-key", region: "china" },
},
{ fetcher },
);

expect(urls).toEqual(["https://api.minimaxi.com/v1/models"]);
expect(result?.metadata).toMatchObject({
apiBaseUrl: "https://api.minimaxi.com",
});
});

it("rejects unsupported credential regions", async () => {
const fetcher: typeof fetch = async () => {
throw new Error("unexpected request");
};

await expect(
credentialValidators.apiKey!(
{
apiKey: "test-key",
values: { apiKey: "test-key", region: "europe" },
},
{ fetcher },
),
).rejects.toMatchObject({
status: 400,
message: "minimax region must be global or china",
});
});
});
Loading
Loading