feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+178 -45
View File
@@ -53,6 +53,33 @@ const HAILUO_01_USAGE_SCHEMA = {
seconds: VIDEO_SECONDS_FIELD,
};
// MiniMax serves MiniMax-H3 on its /v2 video API and every other model on /v1.
const H3_MODEL = "MiniMax-H3";
const V1_MODELS = [
"MiniMax-Hailuo-2.3",
"MiniMax-Hailuo-2.3-Fast",
"MiniMax-Hailuo-02",
"T2V-01-Director",
"T2V-01",
"I2V-01-Director",
"I2V-01-live",
"I2V-01",
"S2V-01",
];
// Vendor fields the OpenAI-style requests carry in metadata; the MiniMax body
// has them at the top level.
const V1_METADATA_FIELDS = [
"prompt_optimizer",
"fast_pretreatment",
"callback_url",
"aigc_watermark",
"first_frame_image",
"last_frame_image",
"subject_reference",
];
const H3_METADATA_FIELDS = ["callback_url", "aigc_watermark"];
export const meta = {
apiVersion: 1,
key: "hailuo",
@@ -62,22 +89,12 @@ export const meta = {
en: "MiniMax Hailuo video generation (text-to-video, image-to-video, and MiniMax-H3 multimodal reference)",
zh: "MiniMax 海螺视频生成(文生视频、图生视频、MiniMax-H3 多模态参考生视频)",
},
version: "1.2.0",
version: "1.3.0",
author: { name: "QuantumNous" },
channelTypes: [35],
models: [
"MiniMax-H3",
"MiniMax-Hailuo-2.3",
"MiniMax-Hailuo-2.3-Fast",
"MiniMax-Hailuo-02",
"T2V-01-Director",
"T2V-01",
"I2V-01-Director",
"I2V-01-live",
"I2V-01",
"S2V-01",
],
models: [H3_MODEL].concat(V1_MODELS),
fetchMode: "per_task",
upstreams: ["vendor", "new_api"],
// Fallback for a channel alias that resolves to more than one model: the
// union of every family's fields and resolutions.
usageSchema: Object.assign({}, H3_USAGE_SCHEMA, {
@@ -134,6 +151,15 @@ export const meta = {
examples: [{ label: "01-series 720P 6s", facts: { seconds: 6 } }],
},
],
// MiniMax request and response bodies, for clients and for a downstream New
// API gateway. The task id of a query is a path parameter on both versions,
// where MiniMax /v1 reads it from the query string.
routes: [
{ method: "POST", path: "/hailuo/v1/video_generation", type: "submit", decode: "createTask", render: "taskCreated", models: V1_MODELS },
{ method: "GET", path: "/hailuo/v1/query/video_generation/:task_id", type: "query", render: "taskStatus" },
{ method: "POST", path: "/hailuo/v2/video_generation", type: "submit", decode: "createH3Task", render: "taskCreated", models: [H3_MODEL] },
{ method: "GET", path: "/hailuo/v2/query/video_generation/:task_id", type: "query", render: "h3TaskStatus" },
],
protocols: [{ name: "openai_responses", supports: ["stream", "sync", "background"] }, "openai_video"],
};
@@ -141,6 +167,29 @@ function trimmed(value) {
return String(value || "").trim();
}
function plainObject(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
// The members of source that hold a value.
function definedFields(source, keys) {
const fields = {};
for (const key of keys) {
if (source[key] !== undefined && source[key] !== null) fields[key] = source[key];
}
return fields;
}
// On a New API channel the same plugin answers on another gateway, which
// serves the MiniMax wire format only on the prefixed native routes.
function viaGateway(ctx) {
return Boolean(ctx.upstream && ctx.upstream.kind === "new_api");
}
function apiRoot(ctx) {
return ctx.baseUrl + (viaGateway(ctx) ? "/hailuo" : "");
}
function isModernHailuo(model) {
return model === "MiniMax-Hailuo-2.3" || model === "MiniMax-Hailuo-2.3-Fast" || model === "MiniMax-Hailuo-02";
}
@@ -186,7 +235,6 @@ function hasHailuoImage(req, hasInputReferenceFile) {
);
}
const H3_MODEL = "MiniMax-H3";
const H3_MIN_DURATION = 4;
const H3_MAX_DURATION = 15;
const H3_DEFAULT_DURATION = 5;
@@ -437,43 +485,106 @@ function responsesVideoText(ctx) {
return '<video controls src="' + escaped + '"></video>';
}
function nativeBody(ctx) {
if (!ctx.body || ctx.body.kind !== "json") throw new Error("JSON body required");
const body = plainObject(ctx.body.value);
if (!body) throw new Error("request body must be an object");
return body;
}
// The decoders move the vendor fields to where the OpenAI-style requests carry
// them, so buildSubmitRequest sends MiniMax the body the client sent.
export const native = {
createTask: function (ctx) {
const body = nativeBody(ctx);
const requestBody = Object.assign(definedFields(body, ["prompt", "duration", "resolution"]), {
model: body.model,
metadata: definedFields(body, V1_METADATA_FIELDS),
});
const hasImage = hasHailuoImage(requestBody, false);
const duration = requestBody.duration === undefined ? undefined : Number(requestBody.duration);
validateHailuoCombo(body.model, duration, outboundResolution(requestBody, body.model), hasImage);
return { kind: "submit", model: body.model, action: hasImage ? "image_to_video" : "text_to_video", requestBody: requestBody };
},
createH3Task: function (ctx) {
const body = nativeBody(ctx);
if (!Array.isArray(body.content)) throw new Error("content must be an array");
const content = validateH3Content(body.content);
const requestBody = Object.assign(definedFields(body, ["duration", "resolution"]), {
model: body.model,
metadata: definedFields(body, ["content", "ratio"].concat(H3_METADATA_FIELDS)),
});
return { kind: "submit", model: body.model, action: h3HasVisualContent(content) ? "image_to_video" : "text_to_video", requestBody: requestBody };
},
// MiniMax answers /v1 with a base_resp envelope and /v2 with a bare task_id.
taskCreated: function (ctx, task) {
return Object.assign({}, plainObject(task.data) || {}, { task_id: task.task_id });
},
// A status route answers with the latest MiniMax snapshot. Before the first
// poll that is the create response, which has no status, and the poller can
// fail a task whose snapshot was still running; both take their status from
// the task, so a downstream gateway reads the same state.
taskStatus: function (ctx, task) {
const data = plainObject(task.data) || {};
const output = Object.assign({}, data, { task_id: task.task_id });
if (task.status === "FAILURE") output.status = "Fail";
else if (typeof data.status !== "string") output.status = task.status === "IN_PROGRESS" ? "Processing" : "Queueing";
return output;
},
h3TaskStatus: function (ctx, task) {
const data = plainObject(task.data) || {};
if (h3APIError(data)) return data;
const current = h3QueryTask(data);
const output = Object.assign({}, current || {}, { id: task.task_id });
if (task.status === "FAILURE" && output.status !== "failed" && output.status !== "cancelled") {
output.status = "failed";
output.error = { message: task.fail_reason || "task failed" };
} else if (!current) {
output.status = task.status === "IN_PROGRESS" ? "running" : "queued";
}
return Object.assign({}, data, { task: output });
},
// MiniMax reports a rejected call in base_resp on both versions.
error: function (ctx, error) {
return { base_resp: { status_code: error.httpStatus, status_msg: error.message } };
},
};
export function buildSubmitRequest(ctx) {
const req = ctx.requestBody || {};
const model = ctx.upstreamModel;
const metadata = req.metadata || {};
if (isH3(model)) {
const content = h3Content(req);
const h3Body = {
model: model,
content: content,
resolution: h3Resolution(req),
duration: h3Duration(req),
ratio: h3Ratio(req, content),
};
["callback_url", "aigc_watermark"].forEach(function (key) {
if (metadata[key] !== undefined && metadata[key] !== null) h3Body[key] = metadata[key];
});
const h3Body = Object.assign(
{
model: model,
content: content,
resolution: h3Resolution(req),
duration: h3Duration(req),
ratio: h3Ratio(req, content),
},
definedFields(metadata, H3_METADATA_FIELDS)
);
return {
url: ctx.baseUrl + "/v2/video_generation",
url: apiRoot(ctx) + "/v2/video_generation",
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: "Bearer " + ctx.apiKey },
body: h3Body,
action: h3HasVisualContent(content) ? "image_to_video" : "text_to_video",
};
}
const body = {
model: model,
prompt: req.prompt || undefined,
duration: outboundDuration(req),
resolution: outboundResolution(req, model),
};
["prompt_optimizer", "fast_pretreatment", "callback_url", "aigc_watermark", "first_frame_image", "last_frame_image", "subject_reference"].forEach(
function (key) {
if (metadata[key] !== undefined && metadata[key] !== null) body[key] = metadata[key];
}
const body = Object.assign(
{
model: model,
prompt: req.prompt || undefined,
duration: outboundDuration(req),
resolution: outboundResolution(req, model),
},
definedFields(metadata, V1_METADATA_FIELDS)
);
return {
url: ctx.baseUrl + "/v1/video_generation",
url: apiRoot(ctx) + "/v1/video_generation",
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: "Bearer " + ctx.apiKey },
body: body,
@@ -524,11 +635,12 @@ export function extractUsage(ctx) {
export function buildQueryRequest(ctx) {
// Polling carries no relay info; the host fills these identities from the
// persisted task properties.
const path = isH3(ctx.upstreamModel || ctx.model)
? "/v2/query/video_generation/" + encodeURIComponent(ctx.taskId)
: "/v1/query/video_generation?task_id=" + encodeURIComponent(ctx.taskId);
const taskID = encodeURIComponent(ctx.taskId);
let path = "/v1/query/video_generation?task_id=" + taskID;
if (isH3(ctx.upstreamModel || ctx.model)) path = "/v2/query/video_generation/" + taskID;
else if (viaGateway(ctx)) path = "/v1/query/video_generation/" + taskID;
return {
url: ctx.baseUrl + path,
url: apiRoot(ctx) + path,
method: "GET",
headers: { Accept: "application/json", Authorization: "Bearer " + ctx.apiKey },
};
@@ -600,11 +712,32 @@ export function buildContentRequest(ctx) {
if (!url) throw new Error("artifact_not_found");
return { url: url, method: ctx.clientRequest.method, credentialless: true };
}
return {
url: ctx.baseUrl + "/v1/files/download?file_id=" + encodeURIComponent(fileID),
method: ctx.clientRequest.method,
headers: { Accept: "video/*", Authorization: "Bearer " + ctx.apiKey },
};
// The gateway resolves its MiniMax file itself and serves the video as the
// artifact of its own task.
if (viaGateway(ctx)) {
return {
url: ctx.baseUrl + "/v1/tasks/" + encodeURIComponent(ctx.upstreamTaskId) + "/artifacts/video/content",
method: ctx.clientRequest.method,
headers: { Authorization: "Bearer " + ctx.apiKey },
};
}
// MiniMax serves v1 videos only through the signed download_url that
// /v1/files/retrieve returns. The link is signed for GET, so a HEAD client
// is served with GET as well; the host sends that client the headers only.
const response = utils.fetch({
url: ctx.baseUrl + "/v1/files/retrieve?file_id=" + encodeURIComponent(fileID),
headers: { Accept: "application/json", Authorization: "Bearer " + ctx.apiKey },
});
if (!response.body || typeof response.body !== "object") {
throw new Error("MiniMax file retrieve returned an unexpected response: HTTP " + response.status);
}
const base = response.body.base_resp || {};
if (response.status !== 200 || base.status_code) {
throw new Error("MiniMax file retrieve failed: " + (base.status_msg || "HTTP " + response.status));
}
const url = trimmed(response.body.file && response.body.file.download_url);
if (!url) throw new Error("artifact_not_found");
return { url: url, method: "GET", credentialless: true };
}
export function extractUsageOnComplete(_task, _taskResult, body) {