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

+100 -5
View File
@@ -7,12 +7,13 @@ export const meta = {
en: "Google Veo video generation on Vertex AI (text-to-video and image-to-video)",
zh: "Google Veo 视频生成(文生视频、图生视频),Vertex AI 版本",
},
version: "1.0.2",
version: "1.1.0",
channelTypes: [41],
author: { name: "QuantumNous" },
models: ["veo-3.0-generate-001", "veo-3.0-fast-generate-001", "veo-3.1-generate-preview", "veo-3.1-fast-generate-preview"],
fetchMode: "per_task",
auth: { type: "oauth2_jwt" },
upstreams: ["vendor", "new_api"],
usageSchema: {
// Requested video duration in seconds. Allowed values: 4, 6, 8.
seconds: {
@@ -38,12 +39,40 @@ export const meta = {
{ label: "8s 1080p audio", facts: { seconds: 8, resolution: "1080p", generate_audio: true } },
{ label: "8s 4k audio", facts: { seconds: 8, resolution: "4k", generate_audio: true } },
],
// Vertex AI request and response bodies, for clients and for a downstream New
// API gateway. The model segment carries the method: predictLongRunning
// submits and fetchPredictOperation polls. The gateway runs every request in
// its own channel's project; the operation names it returns echo the path's
// project and location and end with the gateway task id.
routes: [
{
method: "POST",
path: "/vertex-ai/v1/projects/:project/locations/:location/publishers/google/models/:model",
type: "dynamic",
decode: "modelMethod",
render: "operation",
},
],
protocols: [{ name: "openai_responses", supports: ["stream", "sync", "background"] }, "openai_video"],
};
function trimmed(value) {
return String(value || "").trim();
}
function plainObject(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
// On a New API channel the same plugin answers on another gateway, which
// serves the Vertex AI wire format on the prefixed native routes, takes its own
// token as a Bearer header and runs the request in its own project.
function viaGateway(ctx) {
return Boolean(ctx.upstream && ctx.upstream.kind === "new_api");
}
function requestHeaders(ctx) {
const headers = { "Content-Type": "application/json", Accept: "application/json", Authorization: ctx.authHeader };
if (!viaGateway(ctx)) headers["x-goog-user-project"] = ctx.auth.projectId;
return headers;
}
function responsesInput(req) {
const texts = [],
images = [];
@@ -125,7 +154,7 @@ function apiBase(baseUrl, project, location) {
return "https://" + (location === "global" ? "" : location + "-") + "aiplatform.googleapis.com/v1/projects/" + project + "/locations/" + location;
}
function modelURL(ctx, project, location, model, action) {
return apiBase(ctx.baseUrl, project, location) + "/publishers/google/models/" + model + ":" + action;
return apiBase(ctx.baseUrl + (viaGateway(ctx) ? "/vertex-ai" : ""), project, location) + "/publishers/google/models/" + model + ":" + action;
}
function decodeTaskId(value) {
return utils.base64URLDecode(value);
@@ -144,6 +173,71 @@ function dataVideo(response) {
return "data:" + (String(enc).includes("/") ? enc : "video/" + enc) + ";base64," + data;
}
export const native = {
modelMethod: function (ctx) {
const segment = String((ctx.params || {}).model || "");
const separator = segment.lastIndexOf(":");
const method = separator > 0 ? segment.slice(separator + 1) : "";
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");
if (method === "fetchPredictOperation") {
const name = String(body.operationName || "");
const marker = name.lastIndexOf("/operations/");
if (marker < 0) throw new Error("operationName must name an operation");
return { kind: "query", taskIds: [name.slice(marker + "/operations/".length)] };
}
if (method !== "predictLongRunning") throw new Error("the path must name models/{model}:predictLongRunning or :fetchPredictOperation");
const model = segment.slice(0, separator);
const instances = Array.isArray(body.instances) ? body.instances : [];
const instance = plainObject(instances[0]);
if (instances.length !== 1 || !instance) throw new Error("instances must hold exactly one instance");
// buildSubmitRequest sends only the prompt and the first frame.
for (const key of Object.keys(instance)) {
if (key !== "prompt" && key !== "image") throw new Error("instances[0]." + key + " is not supported");
}
const metadata = Object.assign({}, plainObject(body.parameters) || {});
const requestBody = { model: model, prompt: instance.prompt, metadata: metadata };
if (metadata.durationSeconds !== undefined) {
requestBody.duration = metadata.durationSeconds;
delete metadata.durationSeconds;
if ([4, 6, 8].indexOf(Number(requestBody.duration)) < 0) throw new Error("durationSeconds must be one of 4, 6, or 8");
}
if (metadata.resolution !== undefined && ["720p", "1080p", "4k"].indexOf(String(metadata.resolution).toLowerCase()) < 0)
throw new Error("resolution must be one of 720p, 1080p, or 4k");
if (instance.image !== undefined) {
const image = plainObject(instance.image) || {};
const mime = trimmed(image.mimeType).toLowerCase();
if (!trimmed(image.bytesBase64Encoded)) throw new Error("instances[0].image must carry bytesBase64Encoded");
if (mime !== "image/jpeg" && mime !== "image/png") throw new Error("input image must be image/jpeg or image/png");
requestBody.images = ["data:" + mime + ";base64," + image.bytesBase64Encoded];
}
return { kind: "submit", model: model, action: requestBody.images ? "image_to_video" : "text_to_video", requestBody: requestBody };
},
// Submission renders one task and fetchPredictOperation a list of one. The
// poller can fail a task whose operation was still running; that operation
// is rendered done with the failure.
operation: function (ctx, tasks) {
const task = Array.isArray(tasks) ? tasks[0] : tasks;
const params = ctx.params || {};
const data = plainObject(task.data) || {};
const named = /\/models\/([^/]+)\/operations\//.exec(String(data.name || ""));
const model = named ? named[1] : String(params.model || "").split(":")[0];
const output = Object.assign({}, data, {
name: "projects/" + params.project + "/locations/" + params.location + "/publishers/google/models/" + model + "/operations/" + task.task_id,
});
if (task.status === "FAILURE" && !(plainObject(data.error) && data.error.message)) {
output.done = true;
output.error = { message: task.fail_reason || "task failed" };
delete output.response;
}
return output;
},
error: function (ctx, error) {
return { error: { code: error.httpStatus, message: error.message } };
},
};
export function buildSubmitRequest(ctx) {
if (ctx.authError) throw new Error(ctx.authError);
const req = ctx.requestBody || {},
@@ -183,9 +277,10 @@ export function buildSubmitRequest(ctx) {
instance.image = { bytesBase64Encoded: data, mimeType: mime };
}
return {
url: modelURL(ctx, ctx.auth.projectId, region(ctx, model), model, "predictLongRunning"),
// A gateway runs the request in its own project, so the path names none.
url: modelURL(ctx, viaGateway(ctx) ? "-" : ctx.auth.projectId, region(ctx, model), model, "predictLongRunning"),
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: ctx.authHeader, "x-goog-user-project": ctx.auth.projectId },
headers: requestHeaders(ctx),
body: { instances: [instance], parameters: metadata },
action: image ? "image_to_video" : "text_to_video",
};
@@ -217,7 +312,7 @@ export function buildQueryRequest(ctx) {
return {
url: modelURL(ctx, project, location || "us-central1", model, "fetchPredictOperation"),
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: ctx.authHeader, "x-goog-user-project": ctx.auth.projectId },
headers: requestHeaders(ctx),
body: { operationName: name },
};
}