Files
ttstd-infinite-canvas/plugin-packages/generate-catalog.mjs
米罗 4290af0c29 feat(provider): 模型协议 - 新增千问 AI 平台(QianWenAI MAAS)图像协议插件
千问 AI 平台的网关域名 maas.qianwenaiapi.com 与阿里云百炼的
dashscope.aliyuncs.com 不是同一个入口,现有模板无法直接复用:把渠道
Base URL 改过去后,create 端点仍是百炼的 image-generation 路径。

单独提供 qianwenai-maas-image 插件,走平台自己的多模态端点
POST /api/v1/services/aigc/multimodal-generation/generation。报文形状与
百炼多模态一致(参考图放 input.messages[].content[].image,任务结果在
output.choices[0].message.content[].image),size 用“宽*高”,
enable_thinking 固定 false。
2026-10-13 20:56:49 +08:00

1362 lines
101 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = dirname(fileURLToPath(import.meta.url));
const scopes = ["admin.system-channel", "user.custom-channel", "canvas", "creation", "agent"];
const ref = (path) => ({ $ref: path });
const omit = (value) => ({ $omitEmpty: value });
const coalesce = (...values) => ({ $coalesce: values });
const addNumbers = (...values) => ({ $add: values });
const multiply = (...values) => ({ $multiply: values });
const map = (from, as, body) => ({ $map: { from, as, in: body } });
const filter = (from, as, where) => ({ $filter: { from, as, where } });
const eq = (left, right) => ({ $eq: [left, right] });
const ne = (left, right) => ({ $ne: [left, right] });
const gt = (left, right) => ({ $gt: [left, right] });
const gte = (left, right) => ({ $gte: [left, right] });
const lt = (left, right) => ({ $lt: [left, right] });
const lte = (left, right) => ({ $lte: [left, right] });
const and = (...values) => ({ $and: values });
const len = (value) => ({ $len: value });
const lower = (value) => ({ $lower: value });
const trim = (value) => ({ $trim: value });
const toFloat = (value) => ({ $toFloat: value });
const toInt = (value) => ({ $toInt: value });
const split = (value, separator) => ({ $split: [value, separator] });
const at = (value, index) => ({ $at: [value, index] });
const divide = (left, right) => ({ $divide: [left, right] });
const conditional = (condition, thenValue, elseValue = null) => ({ $if: { condition, then: thenValue, else: elseValue } });
const nonZeroFloat = (value) => conditional(ne(toFloat(value), 0), toFloat(value));
const first = (value) => ({ $first: value });
const sorted = (value) => ({ $sortByOrder: value });
const mediaWithRoles = (path, roles) => filter(sorted(ref(path)), "media", { $in: [ref("media.role"), roles] });
const firstMediaWithRoles = (path, roles) => first(mediaWithRoles(path, roles));
const firstMediaFieldWithRoles = (path, roles, field) => first(map(mediaWithRoles(path, roles), "media", ref(`media.${field}`)));
const textParams = [
["model", "string", true, "model", "上游模型 ID。"],
["messages", "message[]", true, "provider message container", "包含历史消息和当前用户输入。"],
["instructions", "string", false, "system/instructions", "系统指令。"],
["temperature", "number", false, "temperature", "采样温度。"],
["top_p", "number", false, "top_p", "核采样参数。"],
["max_tokens", "integer", false, "max_tokens/max_output_tokens", "最大输出 token。"],
["tools", "array", false, "tools/toolConfig", "工具定义。"],
["tool_choice", "object|string", false, "tool_choice", "工具选择策略。"],
["response_format", "object", false, "response_format/text", "结构化输出配置。"],
["stream", "boolean", false, "stream", "流式开关;后台任务当前以最终响应归一。"]
];
const imageParams = [
["model", "string", true, "model", "图片模型 ID。"],
["prompt", "string", true, "prompt", "图片提示词。"],
["images", "media[]", false, "provider image/reference fields", "参考图或编辑源图,role 由业务层确定。"],
["imageCount", "integer", false, "n/sample_count", "输出数量。"],
["aspectRatio", "string", false, "size/aspect_ratio", "比例或尺寸,语义按协议说明。"],
["resolution", "string", false, "resolution/imageSize", "分辨率档位。"],
["quality", "string", false, "quality", "质量档位。"],
["providerOptions", "object", false, "provider-specific fields", "插件命名空间内的厂商扩展字段。"]
];
const videoParams = [
["model", "string", true, "model", "视频模型 ID。"],
["prompt", "string", true, "prompt/content/input", "视频提示词。"],
["images", "media[]", false, "first/last/reference image", "显式 role 图片输入。"],
["videos", "media[]", false, "reference video", "参考视频。"],
["audios", "media[]", false, "reference audio/voice", "参考音频或音色。"],
["duration", "integer", false, "duration/seconds", "时长秒数。"],
["aspectRatio", "string", false, "ratio/aspect_ratio/size", "画幅比例或尺寸。"],
["resolution", "string", false, "resolution", "分辨率档位。"],
["generateAudio", "boolean", false, "generate_audio", "是否生成音频。"],
["watermark", "boolean", false, "watermark", "水印开关。"],
["providerOptions", "object", false, "provider-specific fields", "插件命名空间内的厂商扩展字段。"]
];
const audioParams = [
["model", "string", true, "model", "音频模型 ID。"],
["prompt", "string", true, "input", "待合成文本。"],
["providerOptions", "object", false, "provider-specific fields", "插件命名空间内的厂商扩展字段。"]
];
const parameters = (items) => items.map(([name, type, required, mapping, description]) => ({ name, type, required, mapping, description }));
const config = (extra = []) => ({
fields: [
{ name: "apiKey", type: "secret", label: "API Key", required: true },
...extra
]
});
const bearer = { type: "bearer", field: "apiKey" };
const jsonCreate = (path, body, extra = {}) => ({ method: "POST", path, contentType: "application/json", body, ...extra });
const asyncResponse = (kind, overrides = {}) => ({
taskId: coalesce(ref("response.id"), ref("response.task_id"), ref("response.taskId"), ref("response.data.id"), ref("taskId")),
status: coalesce(ref("response.status"), ref("response.state"), ref("response.data.status"), "pending"),
message: coalesce(ref("response.error.message"), ref("response.message"), ref("response.fail_reason")),
[kind + "s"]: coalesce(ref(`response.${kind}_url`), ref(`response.${kind}Url`), ref("response.result_url"), ref("response.url"), ref(`response.data.${kind}_url`), ref("response.output.url")),
errorPaths: ["error.code"],
resultEphemeral: true,
...overrides
});
const specs = [];
function add(spec) {
specs.push(spec);
}
add({
id: "openai-chat-completions", providerId: "chat-completion", name: "OpenAI Chat Completions", vendor: "OpenAI", capability: "text",
baseUrl: "https://api.openai.com", auth: bearer, params: textParams,
create: jsonCreate("/chat/completions", {
model: ref("request.model"), messages: ref("request.messages"),
temperature: omit(ref("request.providerOptions.chat-completion.temperature")),
top_p: omit(ref("request.providerOptions.chat-completion.top_p")),
max_tokens: omit(coalesce(ref("request.extra.max_tokens"), ref("request.providerOptions.chat-completion.max_tokens"))),
tools: omit(ref("request.providerOptions.chat-completion.tools")),
tool_choice: omit(ref("request.providerOptions.chat-completion.tool_choice")),
response_format: omit(ref("request.providerOptions.chat-completion.response_format")),
stream: omit(ref("request.providerOptions.chat-completion.stream")),
stop: omit(ref("request.providerOptions.chat-completion.stop")),
seed: omit(ref("request.providerOptions.chat-completion.seed")),
frequency_penalty: omit(ref("request.providerOptions.chat-completion.frequency_penalty")),
presence_penalty: omit(ref("request.providerOptions.chat-completion.presence_penalty")),
logprobs: omit(ref("request.providerOptions.chat-completion.logprobs")),
top_logprobs: omit(ref("request.providerOptions.chat-completion.top_logprobs")),
user: omit(ref("request.providerOptions.chat-completion.user"))
}),
agent: jsonCreate("/chat/completions", { $merge: [ref("request.extra.agent.chatCompletion"), { model: ref("request.model") }] }),
agentResponse: { textPaths: ["choices.0.message.content", "choices.0.text"], reasoningPaths: ["choices.0.message.reasoning_content"], toolCallsPath: "choices.0.message.tool_calls", toolCallIdPaths: ["id"], toolCallNamePaths: ["function.name"], toolCallArgumentsPaths: ["function.arguments"] },
response: { status: "succeeded", textPaths: ["choices.0.message.content", "choices.0.text"], reasoningPaths: ["choices.0.message.reasoning_content"], usage: ref("response.usage"), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
add({
id: "openai-responses", providerId: "openai-response", name: "OpenAI Responses", vendor: "OpenAI", capability: "text",
baseUrl: "https://api.openai.com", auth: bearer, params: textParams,
create: jsonCreate("/responses", {
model: ref("request.model"),
input: filter(ref("request.messages"), "message", ne(ref("message.role"), "system")),
instructions: omit(ref("request.instructions")),
temperature: omit(ref("request.providerOptions.openai-response.temperature")),
top_p: omit(ref("request.providerOptions.openai-response.top_p")),
max_output_tokens: omit(coalesce(ref("request.extra.max_output_tokens"), ref("request.providerOptions.openai-response.max_output_tokens"))),
tools: omit(ref("request.providerOptions.openai-response.tools")), tool_choice: omit(ref("request.providerOptions.openai-response.tool_choice")),
text: omit(ref("request.providerOptions.openai-response.text")), reasoning: omit(ref("request.providerOptions.openai-response.reasoning")),
previous_response_id: omit(ref("request.providerOptions.openai-response.previous_response_id")),
store: omit(ref("request.providerOptions.openai-response.store")), metadata: omit(ref("request.providerOptions.openai-response.metadata")),
stream: omit(ref("request.providerOptions.openai-response.stream")), truncation: omit(ref("request.providerOptions.openai-response.truncation")),
user: omit(ref("request.providerOptions.openai-response.user"))
}),
agent: jsonCreate("/responses", { $merge: [ref("request.extra.agent.responses"), { model: ref("request.model") }] }),
agentResponse: { textPaths: ["output_text"], reasoningPaths: ["reasoning.summary.0.text"], toolCallsPath: "output", toolCallIdPaths: ["call_id", "id"], toolCallNamePaths: ["name"], toolCallArgumentsPaths: ["arguments"] },
response: { status: "succeeded", textPaths: ["output_text"], reasoningPaths: ["reasoning.summary.0.text"], usage: ref("response.usage"), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
add({
id: "anthropic-messages", providerId: "claude-api", name: "Anthropic Messages", vendor: "Anthropic", capability: "text",
baseUrl: "https://api.anthropic.com", auth: { type: "anthropic", field: "apiKey" }, params: textParams,
create: jsonCreate("/v1/messages", {
model: ref("request.model"),
max_tokens: coalesce(ref("request.extra.max_tokens"), ref("request.providerOptions.claude-api.max_tokens"), 4096),
system: omit(coalesce(ref("request.instructions"), ref("request.providerOptions.claude-api.system"))),
messages: filter(ref("request.messages"), "message", ne(ref("message.role"), "system")),
temperature: omit(ref("request.providerOptions.claude-api.temperature")), top_p: omit(ref("request.providerOptions.claude-api.top_p")),
top_k: omit(ref("request.providerOptions.claude-api.top_k")), stop_sequences: omit(ref("request.providerOptions.claude-api.stop_sequences")),
tools: omit(ref("request.providerOptions.claude-api.tools")), tool_choice: omit(ref("request.providerOptions.claude-api.tool_choice")),
metadata: omit(ref("request.providerOptions.claude-api.metadata")), stream: omit(ref("request.providerOptions.claude-api.stream")),
thinking: omit(ref("request.providerOptions.claude-api.thinking")), service_tier: omit(ref("request.providerOptions.claude-api.service_tier"))
}, { headers: { "anthropic-version": coalesce(ref("request.providerOptions.claude-api.anthropic-version"), "2023-06-01"), "anthropic-beta": omit(ref("request.providerOptions.claude-api.anthropic-beta")) } }),
agent: jsonCreate("/v1/messages", { $merge: [ref("request.extra.agent.claude"), { model: ref("request.model") }] }, { headers: { "anthropic-version": "2023-06-01" } }),
agentResponse: { textPaths: ["content.0.text"], reasoningPaths: ["content.0.thinking"], toolCallsPath: "content", toolCallIdPaths: ["id"], toolCallNamePaths: ["name"], toolCallArgumentsPaths: ["input"] },
response: {
status: "succeeded",
text: map(filter(ref("response.content"), "part", eq(ref("part.type"), "text")), "part", ref("part.text")),
reasoning: map(filter(ref("response.content"), "part", eq(ref("part.type"), "thinking")), "part", ref("part.thinking")),
usage: ref("response.usage"), errorPaths: ["error.type"], messagePaths: ["error.message"]
}
});
add({
id: "google-gemini-generate-content", providerId: "gemini-generate-content", name: "Google Gemini generateContent", vendor: "Google", capability: "text",
baseUrl: "https://generativelanguage.googleapis.com", auth: { type: "google-api-key", field: "apiKey" }, params: textParams,
create: jsonCreate("/v1beta/models/{{model}}:generateContent", {
contents: map(filter(ref("request.messages"), "message", ne(ref("message.role"), "system")), "message", {
role: conditional(eq(ref("message.role"), "assistant"), "model", "user"),
parts: [{ text: ref("message.content") }]
}),
systemInstruction: conditional(ref("request.instructions"), { parts: [{ text: ref("request.instructions") }] }),
generationConfig: omit(ref("request.providerOptions.gemini-generate-content.generationConfig")),
safetySettings: omit(ref("request.providerOptions.gemini-generate-content.safetySettings")),
tools: omit(ref("request.providerOptions.gemini-generate-content.tools")),
toolConfig: omit(ref("request.providerOptions.gemini-generate-content.toolConfig")),
cachedContent: omit(ref("request.providerOptions.gemini-generate-content.cachedContent"))
}),
agent: jsonCreate("/v1beta/models/{{model}}:generateContent", ref("request.extra.agent.gemini")),
agentResponse: { textPaths: ["candidates.0.content.parts.0.text"], toolCallsPath: "candidates.0.content.parts", toolCallIdPaths: ["functionCall.id"], toolCallNamePaths: ["functionCall.name"], toolCallArgumentsPaths: ["functionCall.args"], toolCallThoughtSignaturePaths: ["thoughtSignature", "thought_signature"] },
response: { status: "succeeded", text: map(ref("response.candidates.0.content.parts"), "part", omit(ref("part.text"))), usage: ref("response.usageMetadata"), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
add({
id: "cohere-chat-v2", providerId: "cohere-chat-v2", name: "Cohere Chat v2", vendor: "Cohere", capability: "text",
baseUrl: "https://api.cohere.com", auth: bearer, params: textParams,
create: jsonCreate("/v2/chat", {
model: ref("request.model"), messages: ref("request.messages"),
temperature: omit(ref("request.providerOptions.cohere-chat-v2.temperature")), max_tokens: omit(ref("request.extra.max_tokens")),
stop_sequences: omit(ref("request.providerOptions.cohere-chat-v2.stop_sequences")), tools: omit(ref("request.providerOptions.cohere-chat-v2.tools")),
tool_choice: omit(ref("request.providerOptions.cohere-chat-v2.tool_choice")), response_format: omit(ref("request.providerOptions.cohere-chat-v2.response_format")),
documents: omit(ref("request.providerOptions.cohere-chat-v2.documents")), citation_options: omit(ref("request.providerOptions.cohere-chat-v2.citation_options")),
safety_mode: omit(ref("request.providerOptions.cohere-chat-v2.safety_mode")), seed: omit(ref("request.providerOptions.cohere-chat-v2.seed")), stream: omit(ref("request.providerOptions.cohere-chat-v2.stream"))
}),
response: { status: "succeeded", textPaths: ["message.content.0.text", "text"], usage: ref("response.usage"), errorPaths: ["error.type"], messagePaths: ["message"] }
});
add({
id: "ollama-chat", providerId: "ollama-chat", name: "Ollama Chat", vendor: "Ollama", capability: "text",
baseUrl: "http://127.0.0.1:11434", auth: { type: "none", field: "apiKey" }, params: textParams,
create: jsonCreate("/api/chat", {
model: ref("request.model"), messages: ref("request.messages"), stream: false,
format: omit(ref("request.providerOptions.ollama-chat.format")), options: omit(ref("request.providerOptions.ollama-chat.options")),
keep_alive: omit(ref("request.providerOptions.ollama-chat.keep_alive")), tools: omit(ref("request.providerOptions.ollama-chat.tools")),
think: omit(ref("request.providerOptions.ollama-chat.think"))
}),
response: { status: "succeeded", textPaths: ["message.content", "response"], reasoningPaths: ["message.thinking", "thinking"], errorPaths: ["error"], messagePaths: ["error"] }
});
for (const [id, name, vendor, baseUrl] of [
["xai-grok-chat", "xAI Grok Chat", "xAI", "https://api.x.ai"],
["deepseek-chat", "DeepSeek Chat", "DeepSeek", "https://api.deepseek.com"],
["mistral-chat", "Mistral Chat", "Mistral AI", "https://api.mistral.ai"],
["kimi-chat", "Kimi OpenAI-Compatible", "Moonshot AI", "https://api.moonshot.cn"],
["zhipu-glm-chat", "智谱 GLM Chat", "Zhipu AI", "https://open.bigmodel.cn/api/paas"],
["baichuan-chat", "百川 Chat", "Baichuan", "https://api.baichuan-ai.com"],
["yi-chat", "零一万物 Yi Chat", "01.AI", "https://api.lingyiwanwu.com"],
["siliconflow-chat", "SiliconFlow Chat", "SiliconFlow", "https://api.siliconflow.cn"],
["together-chat", "Together AI Chat", "Together AI", "https://api.together.xyz"],
["groq-chat", "Groq Chat", "Groq", "https://api.groq.com/openai"],
["fireworks-chat", "Fireworks Chat", "Fireworks AI", "https://api.fireworks.ai/inference"],
["nvidia-nim-chat", "NVIDIA NIM Chat", "NVIDIA", "https://integrate.api.nvidia.com"],
["openrouter-chat", "OpenRouter Chat", "OpenRouter", "https://openrouter.ai/api"],
["atlascloud-chat", "Atlas Cloud Chat", "Atlas Cloud", "https://api.atlascloud.ai"],
["litellm-proxy-chat", "LiteLLM Proxy Chat", "LiteLLM", "http://127.0.0.1:4000"],
["newapi-chat", "NewAPI Chat", "NewAPI", "http://127.0.0.1:3000"],
["vllm-chat", "vLLM OpenAI-Compatible", "vLLM", "http://127.0.0.1:8000"],
["localai-chat", "LocalAI Chat", "LocalAI", "http://127.0.0.1:8080"]
]) {
add({
id, providerId: id, name, vendor, capability: "text", baseUrl, auth: bearer, params: textParams,
notes: "该插件实现该平台公开的 OpenAI Chat Completions 线协议 profile;平台专有字段通过 providerOptions 命名空间透传,未声明支持的 OpenAI 字段仍由模型 capability profile 校验。",
create: jsonCreate("/v1/chat/completions", {
model: ref("request.model"), messages: ref("request.messages"),
temperature: omit(ref(`request.providerOptions.${id}.temperature`)), top_p: omit(ref(`request.providerOptions.${id}.top_p`)),
max_tokens: omit(coalesce(ref("request.extra.max_tokens"), ref(`request.providerOptions.${id}.max_tokens`))),
tools: omit(ref(`request.providerOptions.${id}.tools`)), tool_choice: omit(ref(`request.providerOptions.${id}.tool_choice`)),
response_format: omit(ref(`request.providerOptions.${id}.response_format`)), stream: omit(ref(`request.providerOptions.${id}.stream`)),
provider: omit(ref(`request.providerOptions.${id}.provider`)), transforms: omit(ref(`request.providerOptions.${id}.transforms`)),
extra_body: omit(ref(`request.providerOptions.${id}.extra_body`))
}),
agent: jsonCreate("/v1/chat/completions", { $merge: [ref("request.extra.agent.chatCompletion"), { model: ref("request.model") }] }),
agentResponse: { textPaths: ["choices.0.message.content", "choices.0.text"], reasoningPaths: ["choices.0.message.reasoning_content"], toolCallsPath: "choices.0.message.tool_calls", toolCallIdPaths: ["id"], toolCallNamePaths: ["function.name"], toolCallArgumentsPaths: ["function.arguments"] },
response: { status: "succeeded", textPaths: ["choices.0.message.content", "choices.0.text"], reasoningPaths: ["choices.0.message.reasoning_content"], usage: ref("response.usage"), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
}
add({
id: "openai-images", providerId: "openai-image", name: "OpenAI Images", vendor: "OpenAI", capability: "image",
baseUrl: "https://api.openai.com", auth: bearer, params: imageParams, requiresPublicMediaUrls: true,
notes: "无参考图走 JSON generations;有参考图或蒙版走 JSON edits,并按官方 images 数组传入多张 image_url。quality 的 1k/2k/4k 映射为 OpenAI low/medium/high。",
create: {
method: "POST",
path: "/v1/images/generations",
pathTemplate: conditional(gt(len(ref("request.images")), 0), "/v1/images/edits", "/v1/images/generations"),
contentType: "application/json",
body: {
model: ref("request.model"),
prompt: ref("request.prompt"),
images: omit(conditional(gt(len(ref("request.images")), 0), map(
filter(sorted(ref("request.images")), "media", ne(ref("media.role"), "mask")),
"media",
{ image_url: ref("media.value") }
))),
mask: omit(conditional(gt(len(filter(ref("request.images"), "media", eq(ref("media.role"), "mask"))), 0), {
image_url: first(map(filter(sorted(ref("request.images")), "media", eq(ref("media.role"), "mask")), "media", ref("media.value")))
})),
n: omit(conditional(gt(ref("request.imageCount"), 0), ref("request.imageCount"), 1)),
size: omit(conditional({ $in: [lower(trim(ref("request.aspectRatio"))), ["", "auto"]] }, null, ref("request.aspectRatio"))),
quality: omit({
$switch: {
cases: [
{ when: { $in: [lower(trim(ref("request.quality"))), ["1k"]] }, then: "low" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["2k"]] }, then: "medium" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["4k"]] }, then: "high" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["", "auto"]] }, then: null }
],
default: ref("request.quality")
}
}),
background: omit(coalesce(
ref("request.providerOptions.openai-image.background"),
conditional(eq(ref("request.extra.transparentBackground"), "true"), "transparent", null)
)),
output_format: omit(coalesce(ref("request.providerOptions.openai-image.output_format"), "png")),
output_compression: omit(ref("request.providerOptions.openai-image.output_compression")),
moderation: omit(ref("request.providerOptions.openai-image.moderation")),
response_format: omit(ref("request.providerOptions.openai-image.response_format")),
style: omit(ref("request.providerOptions.openai-image.style")),
user: omit(ref("request.providerOptions.openai-image.user"))
}
},
response: {
status: "succeeded",
images: map(ref("response.data"), "item", {
url: omit(ref("item.url")),
dataUrl: conditional(ref("item.b64_json"), { $concat: ["data:image/png;base64,", ref("item.b64_json")] })
}),
usage: ref("response.usage"), errorPaths: ["error.code"], messagePaths: ["error.message"]
}
});
const grokAspectSource = lower(trim(ref("request.aspectRatio")));
const grokSizeParts = split(grokAspectSource, "x");
const grokSizeWidth = toFloat(at(grokSizeParts, 0));
const grokSizeHeight = toFloat(at(grokSizeParts, 1));
const grokSizeRatio = divide(grokSizeWidth, grokSizeHeight);
const grokHasPixelSize = and(eq(len(grokSizeParts), 2), gt(grokSizeWidth, 0), gt(grokSizeHeight, 0));
const grokAspectRatio = omit({
$switch: {
cases: [
{ when: { $in: [grokAspectSource, ["", "auto"]] }, then: null },
{ when: { $in: [grokAspectSource, ["1:1", "3:4", "4:3", "9:16", "16:9", "2:3", "3:2", "9:19.5", "19.5:9", "1:2", "2:1"]] }, then: grokAspectSource },
{ when: and(grokHasPixelSize, eq(grokSizeWidth, grokSizeHeight)), then: "1:1" },
{ when: and(grokHasPixelSize, gte(grokSizeRatio, 1.7), lte(grokSizeRatio, 1.8)), then: "16:9" },
{ when: and(grokHasPixelSize, gte(grokSizeRatio, 1.0 / 1.8), lte(grokSizeRatio, 1.0 / 1.7)), then: "9:16" },
{ when: and(grokHasPixelSize, gt(grokSizeRatio, 1.2), lt(grokSizeRatio, 1.4)), then: "4:3" },
{ when: and(grokHasPixelSize, gt(grokSizeRatio, 0.7), lt(grokSizeRatio, 0.85)), then: "3:4" },
{ when: and(grokHasPixelSize, gte(grokSizeRatio, 0.6), lt(grokSizeRatio, 0.72)), then: "2:3" },
{ when: and(grokHasPixelSize, gt(grokSizeRatio, 1.35), lt(grokSizeRatio, 1.6)), then: "3:2" },
{ when: and(grokHasPixelSize, gt(grokSizeRatio, 0.45), lt(grokSizeRatio, 0.55)), then: "1:2" },
{ when: and(grokHasPixelSize, gt(grokSizeRatio, 1.85), lt(grokSizeRatio, 2.2)), then: "2:1" },
{ when: and(grokHasPixelSize, gt(grokSizeWidth, grokSizeHeight)), then: "16:9" },
{ when: grokHasPixelSize, then: "9:16" }
],
default: null
}
});
const grokResolutionSource = lower(trim(coalesce(ref("request.resolution"), ref("request.quality"))));
const grokResolution = omit({
$switch: {
cases: [
{ when: { $in: [grokResolutionSource, ["1k", "low", "standard"]] }, then: "1k" },
{ when: { $in: [grokResolutionSource, ["2k", "medium", "hd", "high", "4k"]] }, then: "2k" }
],
default: null
}
});
add({
id: "xai-grok-images", providerId: "grok-image", name: "xAI Grok Images", vendor: "xAI", capability: "image",
baseUrl: "https://api.x.ai", auth: bearer, params: imageParams,
notes: "有参考图时改走 /v1/images/edits;resolution 只接受 1k/2k;像素尺寸会换算为 aspect_ratio。",
create: {
method: "POST",
path: "/v1/images/generations",
pathTemplate: conditional(gt(len(ref("request.images")), 0), "/v1/images/edits", "/v1/images/generations"),
contentType: "application/json",
body: {
model: ref("request.model"), prompt: ref("request.prompt"),
image: conditional(gt(len(ref("request.images")), 0), { url: firstMediaFieldWithRoles("request.images", ["edit_source", "reference_image", ""], "value") }),
n: conditional(gt(ref("request.imageCount"), 0), ref("request.imageCount"), 1),
response_format: coalesce(ref("request.providerOptions.grok-image.response_format"), "url"),
aspect_ratio: grokAspectRatio, resolution: grokResolution,
user: omit(ref("request.providerOptions.grok-image.user"))
}
},
response: { status: "succeeded", images: map(ref("response.data"), "item", { url: omit(ref("item.url")), dataUrl: conditional(ref("item.b64_json"), { $concat: ["data:image/png;base64,", ref("item.b64_json")] }) }), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
const arkSeedreamRatioSizes = [
["auto", "2k"], ["1:1", "2048x2048"], ["4:3", "2304x1728"], ["3:4", "1728x2304"], ["16:9", "2560x1440"], ["9:16", "1440x2560"],
["3:2", "2496x1664"], ["2:3", "1664x2496"], ["21:9", "3024x1296"]
];
const arkSeedreamImageBody = (optionNamespace) => ({
model: ref("request.model"), prompt: ref("request.prompt"),
// Ark 的 size 只接受 WIDTHxHEIGHT 或 2k/3k/4k;统一层的比例值在这里换算成 2K 档像素尺寸,像素或档位值原样透传。
size: omit({ $switch: { cases: arkSeedreamRatioSizes.map(([ratio, size]) => ({ when: eq(ref("request.aspectRatio"), ratio), then: size })), default: ref("request.aspectRatio") } }),
image: omit(conditional(
eq(len(ref("request.images")), 1),
first(map(ref("request.images"), "media", ref("media.value"))),
conditional(gt(len(ref("request.images")), 1), map(ref("request.images"), "media", ref("media.value")), null)
)),
sequential_image_generation: omit(ref(`request.providerOptions.${optionNamespace}.sequential_image_generation`)),
sequential_image_generation_options: omit(ref(`request.providerOptions.${optionNamespace}.sequential_image_generation_options`)),
watermark: coalesce(ref(`request.providerOptions.${optionNamespace}.watermark`), ref("request.watermark"), false),
seed: omit(ref(`request.providerOptions.${optionNamespace}.seed`)),
response_format: coalesce(ref(`request.providerOptions.${optionNamespace}.response_format`), "b64_json")
});
add({
id: "volcengine-ark-seedream", providerId: "volcengine-ark-image", name: "Volcengine Ark Seedream Images", vendor: "Volcengine", capability: "image",
baseUrl: "https://ark.cn-beijing.volces.com", auth: bearer, params: imageParams,
notes: "官方 Ark 推理接入:Base URL 使用 /api/v3,API Key 来自方舟推理接入控制台,不可与 Agent Plan 专属 Key 混用。",
create: jsonCreate("/api/v3/images/generations", arkSeedreamImageBody("volcengine-ark-image")),
response: { status: "succeeded", images: ref("response.data"), usage: ref("response.usage"), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
add({
id: "volcengine-ark-agent-plan-seedream", providerId: "volcengine-ark-agent-plan-image", name: "Volcengine Ark Agent Plan Seedream Images", vendor: "Volcengine", capability: "image",
baseUrl: "https://ark.cn-beijing.volces.com", auth: bearer, params: imageParams,
notes: "Agent Plan 专属接入:请求路径为 /api/plan/v3/images/generations,必须使用 Agent Plan 控制台专属 API Key;请求体与官方 Seedream 协议一致,但凭证与额度按 AFP 套餐结算,不能与 /api/v3 官方 Key 混用。",
create: jsonCreate("/api/plan/v3/images/generations", arkSeedreamImageBody("volcengine-ark-agent-plan-image")),
response: { status: "succeeded", images: ref("response.data"), usage: ref("response.usage"), errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
const arkSeedanceValidations = [
{ assert: { $lte: [len(ref("request.images")), 9] }, message: "Seedance 最多支持 9 张参考图片" },
{ assert: { $lte: [len(ref("request.videos")), 3] }, message: "Seedance 最多支持 3 个参考视频" },
{ assert: { $lte: [len(ref("request.audios")), 3] }, message: "Seedance 最多支持 3 个参考音频" },
{ assert: { $or: [eq(len(ref("request.audios")), 0), gt({ $add: [len(ref("request.images")), len(ref("request.videos"))] }, 0)] }, message: "Seedance 不支持纯音频或文本+音频,请同时添加参考图片或参考视频" }
];
const arkSeedanceParams = [
["model", "string", true, "model", "Ark endpoint/model ID。"],
["prompt", "string", true, "content[type=text].text", "视频提示词。"],
["images", "media[]", false, "content[type=image_url]", "first_frame、last_frame、reference_image 等 role 原样映射。"],
["videos", "media[]", false, "content[type=video_url]", "reference_video。"],
["audios", "media[]", false, "content[type=audio_url]", "reference_audio/reference_voice。"],
["aspectRatio", "string", false, "ratio", "输出画幅。"],
["resolution", "string", false, "resolution", "输出分辨率档位。"],
["duration", "integer", false, "duration", "输出时长秒数。"],
["generateAudio", "boolean", false, "generate_audio", "是否生成音频。"],
["watermark", "boolean", false, "watermark", "是否带水印。"],
["seed", "integer", false, "seed", "providerOptions seed。"],
["camera_fixed", "boolean", false, "camera_fixed", "providerOptions camera_fixed。"]
];
const arkSeedanceBody = (optionNamespace) => ({
model: ref("request.model"),
content: {
$concatArrays: [
[{ type: "text", text: ref("request.prompt") }],
map(sorted(ref("request.images")), "media", {
type: "image_url",
image_url: { url: ref("media.value") },
role: coalesce(ref("media.role"), "reference_image")
}),
map(sorted(ref("request.videos")), "media", {
type: "video_url",
video_url: { url: ref("media.value") },
role: coalesce(ref("media.role"), "reference_video")
}),
map(sorted(ref("request.audios")), "media", {
type: "audio_url",
audio_url: { url: ref("media.value") },
role: coalesce(ref("media.role"), "reference_audio")
})
]
},
ratio: coalesce(ref("request.aspectRatio"), "16:9"),
resolution: coalesce(ref("request.resolution"), "720p"),
duration: conditional(gt(ref("request.duration"), 0), ref("request.duration"), 5),
generate_audio: ref("request.generateAudio"),
watermark: ref("request.watermark"),
seed: omit(ref(`request.providerOptions.${optionNamespace}.seed`)),
camera_fixed: omit(ref(`request.providerOptions.${optionNamespace}.camera_fixed`))
});
const arkSeedanceResponse = {
taskId: coalesce(ref("response.id"), ref("response.task_id"), ref("response.data.id"), ref("taskId")),
status: coalesce(ref("response.status"), ref("response.data.status"), "pending"),
message: coalesce(ref("response.error.message"), ref("response.message"), ref("response.fail_reason")),
videos: coalesce(ref("response.content.video_url"), ref("response.video_url"), ref("response.output.video_url"), ref("response.data.video_url")),
usage: ref("response.usage"),
errorPaths: ["error.code"],
resultEphemeral: true
};
add({
id: "volcengine-ark-seedance", providerId: "volcengine-ark-video", name: "Volcengine Ark Seedance", vendor: "Volcengine", capability: "video",
baseUrl: "https://ark.cn-beijing.volces.com", auth: bearer, params: arkSeedanceParams, requiresPublicMediaUrls: true,
validations: arkSeedanceValidations,
notes: "官方 Ark 推理接入:创建/查询/取消走 /api/v3/contents/generations/tasks;插件不根据图片下标推断首尾帧,role 由业务层确定。API Key 来自方舟推理接入控制台。",
create: jsonCreate("/api/v3/contents/generations/tasks", arkSeedanceBody("volcengine-ark-video")),
poll: { method: "GET", path: "/api/v3/contents/generations/tasks/{{taskId}}" },
cancel: { method: "DELETE", path: "/api/v3/contents/generations/tasks/{{taskId}}" },
response: arkSeedanceResponse
});
add({
id: "volcengine-ark-agent-plan-seedance", providerId: "volcengine-ark-agent-plan-video", name: "Volcengine Ark Agent Plan Seedance", vendor: "Volcengine", capability: "video",
baseUrl: "https://ark.cn-beijing.volces.com", auth: bearer, params: arkSeedanceParams, requiresPublicMediaUrls: true,
validations: arkSeedanceValidations,
notes: "Agent Plan 专属接入:创建/查询/取消走 /api/plan/v3/contents/generations/tasks;请求体与官方 Seedance 协议一致,但必须使用 Agent Plan 专属 API Key 与 AFP 额度,不能与 /api/v3 官方 Key 混用。",
create: jsonCreate("/api/plan/v3/contents/generations/tasks", arkSeedanceBody("volcengine-ark-agent-plan-video")),
poll: { method: "GET", path: "/api/plan/v3/contents/generations/tasks/{{taskId}}" },
cancel: { method: "DELETE", path: "/api/plan/v3/contents/generations/tasks/{{taskId}}" },
response: arkSeedanceResponse
});
// Gemini imageConfig.imageSize 只接受 1K/2K/4K;画布统一层用 1k/2k/4k 或 low/medium/high。
const geminiImageSize = omit({
$coalesce: [
{
$switch: {
cases: [
{ when: { $in: [lower(ref("request.quality")), ["1k", "low"]] }, then: "1K" },
{ when: { $in: [lower(ref("request.quality")), ["2k", "medium"]] }, then: "2K" },
{ when: { $in: [lower(ref("request.quality")), ["4k", "high"]] }, then: "4K" }
],
default: null
}
},
{
$switch: {
cases: [
{ when: { $in: [lower(ref("request.resolution")), ["1k", "low"]] }, then: "1K" },
{ when: { $in: [lower(ref("request.resolution")), ["2k", "medium"]] }, then: "2K" },
{ when: { $in: [lower(ref("request.resolution")), ["4k", "high"]] }, then: "4K" }
],
default: null
}
}
]
});
add({
id: "google-gemini-image", providerId: "gemini-image", name: "Google Gemini Image", vendor: "Google", capability: "image",
baseUrl: "https://generativelanguage.googleapis.com", auth: { type: "google-api-key", field: "apiKey" }, params: imageParams,
notes: "imageSize 只映射 1K/2K/4K;未知质量值(如视频清晰度 720)必须省略。多图输出由宿主按次创建,不映射 candidateCount。",
create: jsonCreate("/v1beta/models/{{model}}:generateContent", {
contents: [{ role: "user", parts: { $concatArrays: [
[{ text: ref("request.prompt") }],
map(ref("request.images"), "media", conditional(ref("media.dataUrl"), { inlineData: { mimeType: { $dataMime: ref("media.dataUrl") }, data: { $dataPayload: ref("media.dataUrl") } } }, { fileData: { mimeType: omit(ref("media.mimeType")), fileUri: ref("media.url") } }))
] } }],
generationConfig: {
responseModalities: coalesce(ref("request.providerOptions.gemini-image.responseModalities"), ["TEXT", "IMAGE"]),
imageConfig: { aspectRatio: omit(ref("request.aspectRatio")), imageSize: geminiImageSize },
temperature: omit(ref("request.providerOptions.gemini-image.temperature")), topP: omit(ref("request.providerOptions.gemini-image.topP")), topK: omit(ref("request.providerOptions.gemini-image.topK")), seed: omit(ref("request.providerOptions.gemini-image.seed"))
},
safetySettings: omit(ref("request.providerOptions.gemini-image.safetySettings")),
systemInstruction: omit(coalesce(ref("request.providerOptions.gemini-image.systemInstruction"), conditional(ref("request.instructions"), { parts: [{ text: ref("request.instructions") }] }, null)))
}),
response: {
status: "succeeded",
images: map(filter(ref("response.candidates.0.content.parts"), "part", { $or: [ref("part.inlineData"), ref("part.inline_data")] }), "part", {
dataUrl: { $concat: ["data:", coalesce(ref("part.inlineData.mimeType"), ref("part.inline_data.mime_type"), "image/png"), ";base64,", coalesce(ref("part.inlineData.data"), ref("part.inline_data.data"))] }
}),
text: map(filter(ref("response.candidates.0.content.parts"), "part", ref("part.text")), "part", ref("part.text")),
usage: ref("response.usageMetadata"), errorPaths: ["error.code"], messagePaths: ["error.message"]
}
});
const jimengSizeParts = split(lower(trim(ref("request.aspectRatio"))), "x");
const jimengHasPixelSize = and(eq(len(jimengSizeParts), 2), gt(toFloat(at(jimengSizeParts, 0)), 0), gt(toFloat(at(jimengSizeParts, 1)), 0));
add({
id: "volcengine-jimeng-image", providerId: "volcengine-jimeng-image", name: "Volcengine Jimeng Image", vendor: "Volcengine", capability: "image",
baseUrl: "https://visual.volcengineapi.com", auth: { type: "volcengine-v4", field: "apiKey", secretField: "secretKey", service: "cv", region: "cn-north-1" }, params: imageParams,
configuration: config([{ name: "secretKey", type: "secret", label: "Secret Key", required: true }]),
notes: "本地参考图走 binary_data_base64;公网 URL 走 image_urls。WxH 尺寸拆成 width/height。",
create: jsonCreate("/", {
req_key: ref("request.model"), prompt: ref("request.prompt"), force_single: true,
binary_data_base64: omit(map(filter(ref("request.images"), "media", ref("media.dataUrl")), "media", { $dataPayload: ref("media.dataUrl") })),
image_urls: omit(map(filter(ref("request.images"), "media", and({ $not: ref("media.dataUrl") }, ref("media.url"))), "media", ref("media.url"))),
seed: omit(ref("request.providerOptions.volcengine-jimeng-image.seed")),
width: omit(conditional(jimengHasPixelSize, toInt(at(jimengSizeParts, 0)), ref("request.output.width"))),
height: omit(conditional(jimengHasPixelSize, toInt(at(jimengSizeParts, 1)), ref("request.output.height"))),
req_json: omit(ref("request.providerOptions.volcengine-jimeng-image.req_json"))
}, { originPath: true, query: { Action: "CVSync2AsyncSubmitTask", Version: "2022-08-31" } }),
poll: { method: "POST", path: "/", originPath: true, contentType: "application/json", query: { Action: "CVSync2AsyncGetResult", Version: "2022-08-31" }, body: { req_key: ref("request.model"), task_id: ref("taskId"), req_json: "{\"return_url\":true}" } },
response: asyncResponse("image", { taskId: coalesce(ref("response.data.task_id"), ref("response.task_id"), ref("taskId")), status: coalesce(ref("response.data.status"), ref("response.status"), "pending"), images: coalesce(ref("response.data.image_urls"), ref("response.data.binary_data_base64")), errorPaths: ["code"], messagePaths: ["message"] })
});
add({
id: "doubao-streaming-tts", providerId: "doubao-streaming-tts", name: "豆包音频生成", vendor: "Volcengine", capability: "audio",
baseUrl: "https://openspeech.bytedance.com", auth: { type: "header", field: "apiKey", header: "X-Api-Key" }, params: audioParams,
notes: "火山引擎 seed-audio-1.0 非流式音频生成。POST /api/v3/tts/create。不传 references 为纯文本生成;连接音频时写入 references 的 audio_url 或 audio_data,最多 3 段,参考音频少于 3 段且指定了音色时再追加一个 speaker;连接图片时写入 image_url 或 image_data,最多 1 张。图片和音频不能同时使用。没有参考素材时,speaker 只在用户明确指定音色时发送,且官方仅接受语音合成 2.0 音色或复刻音色。text_prompt 引用参考音频时使用 @Audio1、@Audio2。响应是单个 JSON,audio 为 Base64,url 两小时过期。",
create: jsonCreate("/api/v3/tts/create", {
model: ref("request.model"),
text_prompt: ref("request.prompt"),
references: omit(conditional(
gt(len(ref("request.images")), 0),
map(ref("request.images"), "media", {
image_url: omit(conditional(eq(ref("media.source.type"), "url"), ref("media.url"))),
image_data: omit(conditional(eq(ref("media.source.type"), "data"), { $dataPayload: ref("media.value") }))
}),
conditional(
gt(len(ref("request.audios")), 0),
{
$concatArrays: [
map(ref("request.audios"), "media", {
audio_url: omit(conditional(eq(ref("media.source.type"), "url"), ref("media.url"))),
audio_data: omit(conditional(eq(ref("media.source.type"), "data"), { $dataPayload: ref("media.value") }))
}),
conditional(and(ref("request.extra.audioVoice"), lt(len(ref("request.audios")), 3)), [{ speaker: ref("request.extra.audioVoice") }], [])
]
},
conditional(ref("request.extra.audioVoice"), [{ speaker: ref("request.extra.audioVoice") }], null)
)
)),
audio_config: {
format: coalesce(ref("request.extra.audioFormat"), "mp3"),
speech_rate: omit(conditional(ne(toFloat(ref("request.extra.audioSpeed")), 0), addNumbers(multiply(toFloat(ref("request.extra.audioSpeed")), 100), -100)))
}
}, {
headers: {
"X-Api-Request-Id": ref("request.extra.idempotencyKey")
}
}),
response: {
errorPaths: ["code"],
messagePaths: ["message"],
status: "succeeded",
resultKind: "audio",
audios: conditional(
ref("response.url"),
{ url: ref("response.url"), ephemeral: true },
conditional(ref("response.audio"), {
dataUrl: ref("response.audio"),
mimeType: conditional(eq(ref("request.extra.audioFormat"), "wav"), "audio/wav", conditional(eq(ref("request.extra.audioFormat"), "pcm"), "audio/pcm", conditional(eq(ref("request.extra.audioFormat"), "ogg_opus"), "audio/ogg", "audio/mpeg")))
})
)
}
});
add({
id: "openai-audio", providerId: "openai-audio", name: "OpenAI Audio Speech", vendor: "OpenAI", capability: "audio",
baseUrl: "https://api.openai.com", auth: bearer, params: audioParams,
notes: "同步 /v1/audio/speech 返回原始音频流,由 binaryPayload 包装为统一音频结果。",
create: jsonCreate("/v1/audio/speech", {
model: ref("request.model"),
input: ref("request.prompt"),
voice: coalesce(ref("request.extra.audioVoice"), ref("request.providerOptions.openai-audio.voice"), "alloy"),
response_format: coalesce(ref("request.extra.audioFormat"), ref("request.providerOptions.openai-audio.response_format"), "mp3"),
speed: coalesce(nonZeroFloat(ref("request.extra.audioSpeed")), ref("request.providerOptions.openai-audio.speed"), 1),
instructions: omit(coalesce(ref("request.extra.audioInstructions"), ref("request.providerOptions.openai-audio.instructions")))
}),
response: { binaryPayload: true, resultKind: "audio", status: "succeeded", errorPaths: ["error.code"], messagePaths: ["error.message"] }
});
add({
id: "async-audio", providerId: "async-audio", name: "Async Audio Tasks", vendor: "OpenAI compatible", capability: "audio",
baseUrl: "https://api.openai.com", auth: bearer, params: audioParams,
notes: "异步音频任务:创建 /v1/audio/tasks,轮询同一路径,结果优先 URL,否则下载 /content。",
create: jsonCreate("/v1/audio/tasks", {
model: ref("request.model"),
input: ref("request.prompt"),
voice: coalesce(ref("request.extra.audioVoice"), ref("request.providerOptions.async-audio.voice"), "alloy"),
response_format: coalesce(ref("request.extra.audioFormat"), ref("request.providerOptions.async-audio.response_format"), "mp3"),
speed: coalesce(nonZeroFloat(ref("request.extra.audioSpeed")), ref("request.providerOptions.async-audio.speed"), 1),
instructions: omit(coalesce(ref("request.extra.audioInstructions"), ref("request.providerOptions.async-audio.instructions")))
}),
poll: { method: "GET", path: "/v1/audio/tasks/{{taskId}}" },
result: { method: "GET", path: "/v1/audio/tasks/{{taskId}}/content", headers: { Accept: "audio/*" } },
response: asyncResponse("audio", {
taskId: coalesce(ref("response.id"), ref("response.task_id"), ref("response.data.id"), ref("taskId")),
status: coalesce(ref("response.status"), ref("response.data.status"), "pending"),
audios: coalesce(ref("response.audio_url"), ref("response.audioUrl"), ref("response.result_url"), ref("response.url"), ref("response.data.audio_url"), ref("response.output.url")),
errorPaths: ["error.code"], messagePaths: ["error.message"]
})
});
add({
id: "openai-videos", providerId: "newapi", name: "OpenAI Videos / Sora", vendor: "OpenAI", capability: "video",
baseUrl: "https://api.openai.com", auth: bearer, params: videoParams, requiresPublicMediaUrls: false,
create: {
method: "POST", path: "/v1/videos", contentType: "multipart/form-data",
body: {
model: ref("request.model"), prompt: ref("request.prompt"), seconds: { $toString: ref("request.duration") },
size: omit(ref("request.aspectRatio")), resolution_name: omit(ref("request.resolution")),
variants: omit(ref("request.providerOptions.newapi.variants"))
},
files: [{ name: "input_reference", source: first(filter(sorted(ref("request.images")), "media", ne(ref("media.role"), "mask"))), filename: "input-reference.png" }]
},
poll: { method: "GET", path: "/v1/videos/{{taskId}}" },
cancel: { method: "DELETE", path: "/v1/videos/{{taskId}}" },
result: { method: "GET", path: "/v1/videos/{{taskId}}/content", headers: { Accept: "video/mp4" } },
response: asyncResponse("video", { videos: coalesce(ref("response.url"), ref("response.video_url"), ref("response.output.url")), errorPaths: ["error.code"], messagePaths: ["error.message"] })
});
add({
id: "newapi-media-task-v1", providerId: "newapi-channel-1", name: "NewAPI Media Task Channel 1", vendor: "NewAPI", capability: "video",
baseUrl: "http://127.0.0.1:3000", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
create: jsonCreate("/v1/videos", {
model: ref("request.model"),
input: {
prompt: ref("request.prompt"),
media: omit({ $concatArrays: [
map(sorted(ref("request.images")), "media", { type: coalesce(ref("media.role"), "reference_image"), url: ref("media.value") }),
map(sorted(ref("request.videos")), "media", { type: coalesce(ref("media.role"), "reference_video"), url: ref("media.value") }),
map(sorted(ref("request.audios")), "media", { type: coalesce(ref("media.role"), "reference_voice"), url: ref("media.value") })
] })
},
parameters: {
resolution: coalesce(ref("request.resolution"), "720P"), ratio: coalesce(ref("request.aspectRatio"), "16:9"),
duration: conditional(gt(ref("request.duration"), 0), ref("request.duration"), 5), watermark: ref("request.watermark"),
prompt_extend: coalesce(ref("request.providerOptions.newapi-channel-1.prompt_extend"), false)
}
}),
poll: { method: "GET", path: "/v1/videos/{{taskId}}" }, response: asyncResponse("video")
});
add({
id: "newapi-video-generations-v1", providerId: "newapi-channel-2", name: "NewAPI Video Generations Channel 2", vendor: "NewAPI", capability: "video",
baseUrl: "http://127.0.0.1:3000", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
create: jsonCreate("/v1/video/generations", {
model: ref("request.model"), prompt: ref("request.prompt"), seconds: { $toString: ref("request.duration") },
aspect_ratio: coalesce(ref("request.aspectRatio"), "16:9"), resolution: omit(ref("request.resolution")), generate_audio: ref("request.generateAudio"),
image_urls: omit(map({ $sortByOrder: ref("request.images") }, "media", ref("media.value"))),
video_urls: omit(map({ $sortByOrder: ref("request.videos") }, "media", ref("media.value"))),
audio_urls: omit(map({ $sortByOrder: ref("request.audios") }, "media", ref("media.value")))
}),
poll: { method: "GET", path: "/v1/video/generations/{{taskId}}" },
response: asyncResponse("video", {
taskId: coalesce(ref("response.data.task_id"), ref("response.data.taskId"), ref("response.task_id"), ref("response.taskId"), ref("response.data.id"), ref("response.id"), ref("taskId")),
videos: coalesce(
ref("response.data.result_url"), ref("response.data.video_url"), ref("response.data.output_url"), ref("response.data.url"), ref("response.data.metadata.url"),
ref("response.data.data.video_url"), ref("response.data.data.output_url"), ref("response.data.data.result_url"), ref("response.data.data.url"), ref("response.data.data.metadata.url"),
ref("response.video_url"), ref("response.videoUrl"), ref("response.result_url"), ref("response.output_url"), ref("response.url"), ref("response.metadata.url"), ref("response.output.url")
)
})
});
add({
id: "rolldek-wan-video", providerId: "rolldek-wan-video", name: "RollDek WAN 3.0 Video", vendor: "RollDek", capability: "video",
baseUrl: "https://rolldek.com", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
notes: "该协议严格对应 RollDek WAN 3.0 的 JSON /v1/videos 合同。RollDek 同时暴露的其他兼容创建入口不共享任务查询路径,不能与 NewAPI Video Generations Channel 2 混用。",
create: jsonCreate("/v1/videos", {
model: ref("request.model"), prompt: ref("request.prompt"), seconds: { $toString: ref("request.duration") },
size: omit({ $upper: ref("request.resolution") }), aspect_ratio: omit(ref("request.aspectRatio")),
reference_images: omit(map(sorted(ref("request.images")), "media", {
url: ref("media.value"), role: coalesce(ref("media.role"), "reference_image")
})),
reference_videos: omit(map(sorted(ref("request.videos")), "media", {
url: ref("media.value"), duration: omit({ $multiply: [ref("media.metadata.durationMs"), 0.001] })
})),
reference_audios: omit(map(sorted(ref("request.audios")), "media", { url: ref("media.value") }))
}),
poll: { method: "GET", path: "/v1/videos/{{taskId}}" },
result: { method: "GET", path: "/v1/videos/{{taskId}}/content", headers: { Accept: "video/mp4" } },
response: asyncResponse("video", {
videos: coalesce(ref("response.metadata.url"), ref("response.data.metadata.url"), ref("response.video_url"), ref("response.url")),
errorPaths: ["error.code", "code"], messagePaths: ["error.message", "message", "fail_reason"]
})
});
add({
id: "xai-video", providerId: "xai-video", name: "xAI Video", vendor: "xAI", capability: "video",
baseUrl: "https://api.x.ai", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
validations: [
{ assert: { $lte: [len(filter(ref("request.images"), "media", { $in: [ref("media.role"), ["first_frame", "last_frame"]] })), 1] }, message: "xAI Video 最多支持一个帧输入,当前 profile 不支持尾帧" },
{ assert: { $not: { $and: [gt(len(filter(ref("request.images"), "media", { $in: [ref("media.role"), ["first_frame", "last_frame"]] })), 0), gt(len(filter(ref("request.images"), "media", { $in: [ref("media.role"), ["reference_image", "subject_reference", "style_reference"]] })), 0)] } }, message: "xAI Video 不能同时混用起始帧和角色参考图" }
],
create: jsonCreate("/v1/videos/generations", {
model: ref("request.model"), prompt: ref("request.prompt"), duration: conditional(gt(ref("request.duration"), 0), ref("request.duration"), 6),
aspect_ratio: coalesce(ref("request.aspectRatio"), "16:9"), resolution: coalesce(ref("request.resolution"), "720p"),
image: conditional(gt(len(mediaWithRoles("request.images", ["first_frame", ""])), 0), { url: firstMediaFieldWithRoles("request.images", ["first_frame", ""], "value") }),
reference_images: omit(map(filter(ref("request.images"), "media", { $in: [ref("media.role"), ["reference_image", "subject_reference", "style_reference"]] }), "media", { url: ref("media.value") }))
}),
poll: { method: "GET", path: "/v1/videos/{{taskId}}" }, response: asyncResponse("video")
});
add({
id: "google-gemini-veo", providerId: "gemini-veo", name: "Google Gemini Veo", vendor: "Google", capability: "video",
baseUrl: "https://generativelanguage.googleapis.com", auth: { type: "google-api-key", field: "apiKey" }, params: videoParams,
validations: [{ assert: { $eq: [len(filter(ref("request.images"), "media", eq(ref("media.role"), "reference_image"))), 0] }, message: "Gemini Veo 当前 profile 不支持角色参考图,请使用首帧输入或支持 reference_to_video 的协议" }],
create: jsonCreate("/v1beta/models/{{model}}:predictLongRunning", {
instances: [{
prompt: ref("request.prompt"),
image: conditional(gt(len(mediaWithRoles("request.images", ["first_frame", ""])), 0), conditional(firstMediaFieldWithRoles("request.images", ["first_frame", ""], "dataUrl"), { inlineData: { mimeType: { $dataMime: firstMediaFieldWithRoles("request.images", ["first_frame", ""], "dataUrl") }, data: { $dataPayload: firstMediaFieldWithRoles("request.images", ["first_frame", ""], "dataUrl") } } }, { fileUri: firstMediaFieldWithRoles("request.images", ["first_frame", ""], "url"), mimeType: omit(firstMediaFieldWithRoles("request.images", ["first_frame", ""], "mimeType")) }))
}],
parameters: {
aspectRatio: omit(ref("request.aspectRatio")), durationSeconds: omit(ref("request.duration")), resolution: omit(ref("request.resolution")),
generateAudio: ref("request.generateAudio"), sampleCount: omit(ref("request.imageCount")), negativePrompt: omit(ref("request.providerOptions.gemini-veo.negativePrompt")),
personGeneration: omit(ref("request.providerOptions.gemini-veo.personGeneration")), seed: omit(ref("request.providerOptions.gemini-veo.seed"))
}
}),
poll: { method: "GET", path: "/v1beta/{{taskId}}" },
response: asyncResponse("video", { taskId: coalesce(ref("response.name"), ref("taskId")), status: conditional(ref("response.error"), "failed", conditional(ref("response.done"), "succeeded", "processing")), videos: coalesce(ref("response.response.generateVideoResponse.generatedSamples"), ref("response.response.generatedVideos"), ref("response.response.videos")), message: ref("response.error.message") })
});
add({
id: "novita-video", providerId: "novita-video", name: "Novita Video", vendor: "Novita", capability: "video",
baseUrl: "https://api.novita.ai", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
validations: [{ assert: { $eq: [len(filter(ref("request.images"), "media", eq(ref("media.role"), "reference_image"))), 0] }, message: "Novita Video 当前 profile 只支持起始图,不支持角色参考图" }],
create: jsonCreate("/v3/video/create", {
model_name: ref("request.model"), prompt: ref("request.prompt"), duration: conditional(gt(ref("request.duration"), 0), ref("request.duration"), 5),
aspect_ratio: coalesce(ref("request.aspectRatio"), "16:9"), image_url: omit(firstMediaFieldWithRoles("request.images", ["first_frame", ""], "value")),
seed: omit(ref("request.providerOptions.novita-video.seed")), negative_prompt: omit(ref("request.providerOptions.novita-video.negative_prompt"))
}),
poll: { method: "GET", path: "/v3/async/task-result", query: { task_id: ref("taskId") } }, response: asyncResponse("video")
});
add({
id: "volcengine-jimeng-video", providerId: "volcengine-jimeng-video", name: "Volcengine Jimeng Video", vendor: "Volcengine", capability: "video",
baseUrl: "https://visual.volcengineapi.com", auth: { type: "volcengine-v4", field: "apiKey", secretField: "secretKey", service: "cv", region: "cn-north-1" }, params: videoParams,
configuration: config([{ name: "secretKey", type: "secret", label: "Secret Key", required: true }]),
create: jsonCreate("/", {
req_key: ref("request.model"), prompt: ref("request.prompt"), image_urls: omit(map({ $sortByOrder: ref("request.images") }, "media", ref("media.value"))),
duration: omit(ref("request.duration")), ratio: omit(ref("request.aspectRatio")), resolution: omit(ref("request.resolution")),
req_json: omit(ref("request.providerOptions.volcengine-jimeng-video.req_json"))
}, { originPath: true, query: { Action: "CVSync2AsyncSubmitTask", Version: "2022-08-31" } }),
poll: { method: "POST", path: "/", originPath: true, contentType: "application/json", query: { Action: "CVSync2AsyncGetResult", Version: "2022-08-31" }, body: { req_key: ref("request.model"), task_id: ref("taskId") } },
response: asyncResponse("video", { taskId: coalesce(ref("response.data.task_id"), ref("response.task_id"), ref("taskId")), status: coalesce(ref("response.data.status"), ref("response.status"), "pending"), videos: coalesce(ref("response.data.video_urls"), ref("response.data.video_url")), errorPaths: ["code"], messagePaths: ["message"] })
});
const agnesFrameImages = mediaWithRoles("request.images", ["first_frame", "last_frame"]);
const agnesReferenceImages = mediaWithRoles("request.images", ["reference_image", "subject_reference", "style_reference"]);
const agnesReferenceMode = { $or: [gt(len(agnesReferenceImages), 0), gt(len(ref("request.videos")), 0), gt(len(ref("request.audios")), 0)] };
add({
id: "agnes-video-25", providerId: "agnes-video", name: "Agnes Video 2.5 / 2.5 Flash", vendor: "Agnes AI", capability: "video",
baseUrl: "https://apihub.agnes-ai.com/v1", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
validations: [
{ assert: { $in: [ref("request.model"), ["agnes-video-2.5", "agnes-video-2.5-flash"]] }, message: "Agnes 2.5 插件仅支持 agnes-video-2.5 与 agnes-video-2.5-flash" },
{ assert: { $and: [{ $gte: [ref("request.duration"), 4] }, { $lte: [ref("request.duration"), 12] }] }, message: "Agnes Video 2.5 时长必须在 4–12 秒之间" },
{ assert: { $in: [{ $upper: coalesce(ref("request.resolution"), "720P") }, ["720P", "960P", "2K"]] }, message: "Agnes Video 2.5 分辨率必须是 720P、960P 或 2K" },
{ assert: { $not: { $and: [gt(len(agnesFrameImages), 0), agnesReferenceMode] } }, message: "Agnes Video 2.5 不能同时混用首尾帧和角色、视频或音频参考素材" },
{ assert: { $or: [ne(ref("request.model"), "agnes-video-2.5-flash"), eq({ $upper: coalesce(ref("request.resolution"), "720P") }, "720P")] }, message: "Agnes Video 2.5 Flash 仅支持 720P" },
{ assert: { $or: [ne(ref("request.model"), "agnes-video-2.5-flash"), { $lte: [len(ref("request.images")), 5] }] }, message: "Agnes Video 2.5 Flash 最多支持 5 张参考图" },
{ assert: { $or: [ne(ref("request.model"), "agnes-video-2.5-flash"), eq(len(ref("request.videos")), 0)] }, message: "Agnes Video 2.5 Flash 不支持参考视频" }
],
create: jsonCreate("/videos", {
model: ref("request.model"), prompt: ref("request.prompt"),
mode: conditional(agnesReferenceMode, "reference", conditional(gt(len(ref("request.images")), 0), "keyframe", "text")),
seconds: { $toString: ref("request.duration") }, size: { $upper: coalesce(ref("request.resolution"), "720P") },
aspect_ratio: coalesce(ref("request.aspectRatio"), "16:9"), n: coalesce(ref("request.output.count"), 1),
first_frame: conditional({ $not: agnesReferenceMode }, omit(coalesce(firstMediaFieldWithRoles("request.images", ["first_frame"], "value"), first(map(sorted(ref("request.images")), "media", ref("media.value"))))), null),
last_frame: conditional({ $not: agnesReferenceMode }, omit(firstMediaFieldWithRoles("request.images", ["last_frame"], "value")), null),
images: conditional(agnesReferenceMode, omit(map(sorted(ref("request.images")), "media", ref("media.value"))), null),
videos: conditional(agnesReferenceMode, omit(map(sorted(ref("request.videos")), "media", { url: ref("media.value") })), null),
audios: conditional(agnesReferenceMode, omit(map(sorted(ref("request.audios")), "media", ref("media.value"))), null)
}),
poll: { method: "GET", path: "/agnesapi", originPath: true, query: { video_id: ref("taskId"), model_name: ref("request.model") } },
response: asyncResponse("video", {
taskId: coalesce(ref("response.data.video_id"), ref("response.video_id"), ref("response.data.id"), ref("response.id"), ref("taskId")),
status: coalesce(ref("response.data.status"), ref("response.status"), "pending"),
videos: coalesce(ref("response.data.metadata.url"), ref("response.metadata.url"), ref("response.data.url"), ref("response.url")),
message: coalesce(ref("response.data.error.message"), ref("response.error.message"), ref("response.message"), ref("response.detail"))
})
});
// Agnes 图像是同步端点,文生图与图生图共用 POST /v1/images/generations:
// 不传 image 为文生图,传 image 为图生图或多图合成。官方参数表只承认
// model、prompt、size(必填,档位 1K/2K/3K/4K 或 WxH 精确尺寸)、ratio、image、
// return_base64 和 extra_body,因此这里不发送 n、output_format、quality、
// background 等 OpenAI 兼容字段。顶层 response_format 是官方明确列出的错误写法,
// 输出格式只能声明在 extra_body 内。参考图放 extra_body.image,支持公共 HTTPS URL
// 和 Data URI Base64,所以不强制公共媒体 URL,本地开发也能直接联调。
const agnesImageRatios = ["1:1", "3:4", "4:3", "16:9", "9:16", "2:3", "3:2", "21:9"];
const agnesImageAspect = trim(ref("request.aspectRatio"));
const agnesImageIsRatio = { $in: [agnesImageAspect, agnesImageRatios] };
const agnesImageIsPixel = eq(len(split(agnesImageAspect, "x")), 2);
// size 是官方必填项:像素尺寸原样透传,其余情况(比例、auto、空值)一律落到分辨率档位,不能省略。
const agnesImageSize = conditional(agnesImageIsPixel, agnesImageAspect, {
$switch: {
cases: [
{ when: { $in: [lower(trim(ref("request.quality"))), ["1k", "low", "standard"]] }, then: "1K" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["2k", "medium", "hd", "high"]] }, then: "2K" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["3k"]] }, then: "3K" },
{ when: { $in: [lower(trim(ref("request.quality"))), ["4k"]] }, then: "4K" }
],
default: "1K"
}
});
add({
id: "agnes-image", providerId: "agnes-image", name: "Agnes Image", vendor: "Agnes AI", capability: "image",
baseUrl: "https://api.agnes-ai.cn", auth: bearer, params: imageParams, requiresPublicMediaUrls: false,
notes: "Agnes 官方图像端点,同步返回。文生图与图生图共用 /v1/images/generations:不传 image 为文生图,传 image 为图生图或多图合成。size 必填,取 1K/2K/3K/4K 档位或 WxH 精确尺寸;画面比例走独立的 ratio 字段。参考图放在 extra_body.image,支持公共 HTTPS URL 或 Data URI Base64。顶层 response_format 是官方明确列出的错误写法,输出格式只能声明在 extra_body.response_format。该端点不接受 n,单次请求固定返回一张图片,需要多张时由上层拆分为多个任务。",
validations: [
{ assert: { $or: [{ $in: [agnesImageAspect, ["", "auto", ...agnesImageRatios]] }, eq(len(split(agnesImageAspect, "x")), 2)] }, message: "Agnes 图像只支持 1:1、3:4、4:3、16:9、9:16、2:3、3:2、21:9 比例或 WxH 像素尺寸" }
],
create: jsonCreate("/v1/images/generations", {
model: ref("request.model"),
prompt: ref("request.prompt"),
size: omit(agnesImageSize),
ratio: omit(conditional(agnesImageIsRatio, agnesImageAspect)),
extra_body: omit({
image: omit(map(sorted(ref("request.images")), "media", coalesce(ref("media.dataUrl"), ref("media.url")))),
response_format: coalesce(ref("request.providerOptions.agnes-image.response_format"), "url")
})
}, { originPath: true }),
response: {
status: "succeeded",
images: map(ref("response.data"), "item", {
url: omit(ref("item.url")),
dataUrl: conditional(ref("item.b64_json"), { $concat: ["data:image/png;base64,", ref("item.b64_json")] })
}),
errorPaths: ["error.code", "code"], messagePaths: ["error.message", "message", "msg"]
}
});
add({
id: "agnes-video-v20", providerId: "agnes-video-v20", name: "Agnes Video V2.0", vendor: "Agnes AI", capability: "video",
baseUrl: "https://apihub.agnes-ai.com/v1", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
validations: [
{ assert: eq(ref("request.model"), "agnes-video-v2.0"), message: "Agnes Video V2.0 插件仅支持 agnes-video-v2.0" },
{ assert: eq(len(ref("request.videos")), 0), message: "Agnes Video V2.0 不支持参考视频" },
{ assert: eq(len(ref("request.audios")), 0), message: "Agnes Video V2.0 不支持参考音频" },
{ assert: eq(len(agnesReferenceImages), 0), message: "Agnes Video V2.0 不支持角色或风格参考图" },
{ assert: { $lte: [len(agnesFrameImages), 2] }, message: "Agnes Video V2.0 最多支持首帧和尾帧各一张" }
],
create: jsonCreate("/videos", {
model: ref("request.model"), prompt: ref("request.prompt"), frame_rate: 24,
num_frames: { $min: [441, { $add: [{ $ceilStep: [{ $multiply: [coalesce(ref("request.duration"), 5), 24] }, 8] }, 1] }] },
image: conditional(eq(len(agnesFrameImages), 1), firstMediaFieldWithRoles("request.images", ["first_frame", "last_frame"], "value")),
extra_body: conditional(gt(len(agnesFrameImages), 1), { image: map(agnesFrameImages, "media", ref("media.value")), mode: "keyframes" })
}),
poll: { method: "GET", path: "/agnesapi", originPath: true, query: { video_id: ref("taskId"), model_name: ref("request.model") } },
response: asyncResponse("video", {
taskId: coalesce(ref("response.data.video_id"), ref("response.video_id"), ref("response.data.id"), ref("response.id"), ref("taskId")),
status: coalesce(ref("response.data.status"), ref("response.status"), "pending"),
videos: coalesce(ref("response.data.metadata.url"), ref("response.metadata.url"), ref("response.data.url"), ref("response.url")),
message: coalesce(ref("response.data.error.message"), ref("response.error.message"), ref("response.message"), ref("response.detail"))
})
});
add({
id: "stability-image", providerId: "stability-image", name: "Stability AI Image", vendor: "Stability AI", capability: "image",
baseUrl: "https://api.stability.ai", auth: bearer, params: imageParams,
create: {
method: "POST",
pathTemplate: coalesce(ref("request.providerOptions.stability-image.endpoint"), "/v2beta/stable-image/generate/core"),
contentType: "multipart/form-data",
headers: { Accept: "application/json" },
body: {
prompt: ref("request.prompt"), negative_prompt: omit(ref("request.providerOptions.stability-image.negative_prompt")),
aspect_ratio: omit(ref("request.aspectRatio")), output_format: coalesce(ref("request.output.format"), ref("request.providerOptions.stability-image.output_format"), "png"),
seed: omit(ref("request.providerOptions.stability-image.seed")), style_preset: omit(ref("request.providerOptions.stability-image.style_preset")),
strength: omit(ref("request.providerOptions.stability-image.strength")), cfg_scale: omit(ref("request.providerOptions.stability-image.cfg_scale")),
grow_mask: omit(ref("request.providerOptions.stability-image.grow_mask")), creativity: omit(ref("request.providerOptions.stability-image.creativity"))
},
files: [
{ name: "image", source: first(mediaWithRoles("request.images", ["edit_source", "reference_image", ""])), filename: "image.png" },
{ name: "mask", source: first(mediaWithRoles("request.images", ["mask"])), filename: "mask.png" }
]
},
response: {
status: conditional(ref("response.errors"), "failed", "succeeded"),
message: coalesce(ref("response.errors"), ref("response.message")),
images: conditional(ref("response.image"), [
{ dataUrl: { $concat: ["data:", coalesce(ref("response.mime_type"), "image/png"), ";base64,", ref("response.image")] } }
]),
errorPaths: ["errors"]
}
});
add({
id: "ideogram-image", providerId: "ideogram-image", name: "Ideogram Image", vendor: "Ideogram", capability: "image",
baseUrl: "https://api.ideogram.ai", auth: { type: "header", field: "apiKey", header: "Api-Key" }, params: imageParams,
create: {
method: "POST", pathTemplate: coalesce(ref("request.providerOptions.ideogram-image.endpoint"), "/v1/ideogram-v3/generate"), contentType: "multipart/form-data",
body: {
prompt: ref("request.prompt"), negative_prompt: omit(ref("request.providerOptions.ideogram-image.negative_prompt")),
aspect_ratio: omit(ref("request.aspectRatio")), resolution: omit(ref("request.resolution")), num_images: coalesce(ref("request.imageCount"), 1),
rendering_speed: omit(ref("request.providerOptions.ideogram-image.rendering_speed")), style_type: omit(ref("request.providerOptions.ideogram-image.style_type")),
magic_prompt: omit(ref("request.providerOptions.ideogram-image.magic_prompt")), seed: omit(ref("request.providerOptions.ideogram-image.seed")),
color_palette: omit(ref("request.providerOptions.ideogram-image.color_palette")), character_reference_images_mask: omit(ref("request.providerOptions.ideogram-image.character_reference_images_mask"))
},
files: [
{ name: "image", source: first(mediaWithRoles("request.images", ["edit_source", "reference_image", "subject_reference", "style_reference", ""])), filename: "reference.png" },
{ name: "mask", source: first(mediaWithRoles("request.images", ["mask"])), filename: "mask.png" }
]
},
response: { status: "succeeded", images: coalesce(ref("response.data"), ref("response.images")), errorPaths: ["error.code"], messagePaths: ["error.message", "message"] }
});
const wan3MediaType = coalesce(
ref("media.role"),
conditional(eq(ref("media.kind"), "image"), "reference_image", conditional(
eq(ref("media.kind"), "video"), "reference_video", conditional(
eq(ref("media.kind"), "audio"), "reference_audio", ref("media.kind")
)
))
);
add({
id: "dashscope-wan3-video", providerId: "dashscope-wan3-video", name: "DashScope Wan 3.0 Video", vendor: "Alibaba Cloud", capability: "video",
baseUrl: "https://dashscope.aliyuncs.com", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
validations: [
{ assert: { $in: [lower(ref("request.model")), ["wan3.0-video-prime", "wan3.0-video"]] }, message: "Wan 3.0 Video 仅支持 wan3.0-video-prime 或 wan3.0-video" },
{ assert: { $lte: [len(mediaWithRoles("request.images", ["first_frame"])), 1] }, message: "Wan 3.0 Video 最多只能有一个 first_frame" },
{ assert: { $lte: [len(mediaWithRoles("request.images", ["last_frame"])), 1] }, message: "Wan 3.0 Video 最多只能有一个 last_frame" }
],
create: jsonCreate("/api/v1/services/aigc/video-generation/video-synthesis", {
model: ref("request.model"),
input: {
prompt: omit(ref("request.prompt")),
media: omit(coalesce(
ref("request.providerOptions.dashscope-wan3-video.media"),
map(sorted(ref("request.inputs")), "media", { type: wan3MediaType, url: ref("media.value") })
))
},
parameters: {
resolution: omit(ref("request.resolution")),
ratio: omit(ref("request.aspectRatio")),
duration: omit(ref("request.duration")),
audio: coalesce(ref("request.providerOptions.dashscope-wan3-video.audio"), ref("request.generateAudio")),
seed: omit(ref("request.providerOptions.dashscope-wan3-video.seed")),
prompt_extend: coalesce(ref("request.providerOptions.dashscope-wan3-video.prompt_extend"), true),
watermark: ref("request.watermark")
}
}, { headers: { "X-DashScope-Async": "enable" }, originPath: true }),
poll: { method: "GET", path: "/api/v1/tasks/{{taskId}}", originPath: true },
response: asyncResponse("video", {
taskId: coalesce(ref("response.output.task_id"), ref("response.task_id"), ref("taskId")),
status: conditional(eq(lower(ref("response.output.task_status")), "unknown"), "failed", coalesce(ref("response.output.task_status"), ref("response.status"), "pending")),
videos: ref("response.output.video_url"),
usage: ref("response.usage"), errorPaths: ["code", "output.code"], messagePaths: ["message", "output.message"]
})
});
add({
id: "dashscope-wan-video", providerId: "dashscope-wan-video", name: "DashScope Wan Video", vendor: "Alibaba Cloud", capability: "video",
baseUrl: "https://dashscope.aliyuncs.com", auth: bearer, params: videoParams, requiresPublicMediaUrls: true,
validations: [
{ assert: { $lte: [len(mediaWithRoles("request.images", ["first_frame"])), 1] }, message: "Wan 视频最多只能有一个首帧" },
{ assert: { $lte: [len(mediaWithRoles("request.images", ["last_frame"])), 1] }, message: "Wan 视频最多只能有一个尾帧" },
{ assert: { $or: [eq(len(mediaWithRoles("request.images", ["last_frame"])), 0), eq(len(mediaWithRoles("request.images", ["first_frame"])), 1)] }, message: "Wan 视频使用尾帧时必须同时提供首帧" }
],
create: jsonCreate("/api/v1/services/aigc/video-generation/video-synthesis", {
model: ref("request.model"),
input: {
prompt: ref("request.prompt"),
negative_prompt: omit(ref("request.providerOptions.dashscope-wan-video.negative_prompt")),
img_url: omit(conditional({ $and: [ne(ref("request.operation"), "reference_to_video"), eq(len(mediaWithRoles("request.images", ["last_frame"])), 0)] }, coalesce(firstMediaFieldWithRoles("request.images", ["first_frame"], "value"), firstMediaFieldWithRoles("request.images", ["reference_image", "edit_source", ""], "value")))),
first_frame_url: omit(conditional(gt(len(mediaWithRoles("request.images", ["last_frame"])), 0), firstMediaFieldWithRoles("request.images", ["first_frame"], "value"))),
last_frame_url: omit(firstMediaFieldWithRoles("request.images", ["last_frame"], "value")),
reference_images: omit(conditional(eq(ref("request.operation"), "reference_to_video"), map(mediaWithRoles("request.images", ["reference_image", "subject_reference", "style_reference"]), "media", ref("media.value")))),
video_url: omit(firstMediaFieldWithRoles("request.videos", ["reference_video", ""], "value")),
audio_url: omit(firstMediaFieldWithRoles("request.audios", ["reference_audio", "reference_voice", ""], "value"))
},
parameters: {
size: omit(ref("request.resolution")), duration: omit(ref("request.duration")),
prompt_extend: omit(ref("request.providerOptions.dashscope-wan-video.prompt_extend")),
watermark: ref("request.watermark"), seed: omit(ref("request.providerOptions.dashscope-wan-video.seed")),
shot_type: omit(ref("request.providerOptions.dashscope-wan-video.shot_type")),
audio: ref("request.generateAudio"), template: omit(ref("request.providerOptions.dashscope-wan-video.template"))
}
}, { headers: { "X-DashScope-Async": "enable" } }),
poll: { method: "GET", path: "/api/v1/tasks/{{taskId}}" },
response: asyncResponse("video", {
taskId: coalesce(ref("response.output.task_id"), ref("response.task_id"), ref("taskId")),
status: coalesce(ref("response.output.task_status"), ref("response.status"), "pending"),
videos: coalesce(ref("response.output.video_url"), ref("response.output.results"), ref("response.video_url")),
usage: ref("response.usage"), errorPaths: ["code"], messagePaths: ["message", "output.message"]
})
});
add({
id: "dashscope-wanx-image", providerId: "dashscope-wanx-image", name: "DashScope Wanx Image", vendor: "Alibaba Cloud", capability: "image",
baseUrl: "https://dashscope.aliyuncs.com", auth: bearer, params: imageParams, requiresPublicMediaUrls: true,
create: jsonCreate("/api/v1/services/aigc/text2image/image-synthesis", {
model: ref("request.model"),
input: {
prompt: ref("request.prompt"), negative_prompt: omit(ref("request.providerOptions.dashscope-wanx-image.negative_prompt")),
ref_img: omit(conditional(eq(len(ref("request.images")), 1), firstMediaFieldWithRoles("request.images", ["edit_source", "reference_image", "subject_reference", "style_reference", ""], "value"))),
ref_images: omit(conditional(gt(len(ref("request.images")), 1), map(sorted(ref("request.images")), "media", ref("media.value"))))
},
parameters: {
size: omit(ref("request.aspectRatio")), n: coalesce(ref("request.imageCount"), 1),
seed: omit(ref("request.providerOptions.dashscope-wanx-image.seed")), style: omit(ref("request.providerOptions.dashscope-wanx-image.style")),
prompt_extend: omit(ref("request.providerOptions.dashscope-wanx-image.prompt_extend")), watermark: ref("request.watermark")
}
}, { headers: { "X-DashScope-Async": "enable" } }),
poll: { method: "GET", path: "/api/v1/tasks/{{taskId}}" },
response: asyncResponse("image", {
taskId: coalesce(ref("response.output.task_id"), ref("response.task_id"), ref("taskId")),
status: coalesce(ref("response.output.task_status"), ref("response.status"), "pending"),
images: coalesce(ref("response.output.results"), ref("response.output.image_url")),
usage: ref("response.usage"), errorPaths: ["code"], messagePaths: ["message", "output.message"]
})
});
// Qwen-Image 3.0 / Wan 2.7 图像属于百炼多模态模型,必须走多模态端点:
// 用 OpenAI 的 /v1/images/generations 或 /v1/images/edits 会被网关判为“模型与端点不匹配”,
// 官方错误码把它表现为 url error;参考图只能放在 input.messages[].content[].image。
// size 使用 DashScope 的 “宽*高”(星号),未登记的档位直接省略,由模型按提示词自动推荐分辨率。
const dashscopeMultimodalImageSizes = [
["1:1", "1024*1024"], ["3:4", "960*1280"], ["4:3", "1280*960"], ["2:3", "1024*1536"], ["3:2", "1536*1024"],
["9:16", "864*1536"], ["16:9", "1536*864"],
["1024x1024", "1024*1024"], ["1024x1536", "1024*1536"], ["1536x1024", "1536*1024"],
["1024x1280", "1024*1280"], ["1280x1024", "1280*1024"], ["960x1280", "960*1280"], ["1280x960", "1280*960"]
];
add({
id: "dashscope-qwen-image", providerId: "dashscope-qwen-image", name: "DashScope Qwen / Wan Image", vendor: "Alibaba Cloud", capability: "image",
baseUrl: "https://dashscope.aliyuncs.com", auth: bearer, params: imageParams, requiresPublicMediaUrls: false,
notes: "百炼多模态图像端点(image-generation 异步任务)。文生图与图生图共用同一入口:不传 image 为文生图,传 1-3 张 image 为图生图。参考图通过 input.messages[].content[].image 传输(优先 Base64 data URL),不使用 OpenAI 的 /v1/images/edits multipart。size 采用“宽*高”,未登记的档位省略并由模型自动推荐;enable_thinking 固定为 false,因为官方要求非流式调用关闭思考模式。",
create: jsonCreate("/api/v1/services/aigc/image-generation/generation", {
model: ref("request.model"),
input: {
messages: [{
role: "user",
content: {
$concatArrays: [
map(sorted(ref("request.images")), "media", { image: coalesce(ref("media.dataUrl"), ref("media.url")) }),
[{ text: ref("request.prompt") }]
]
}
}]
},
parameters: {
size: omit({ $switch: { cases: dashscopeMultimodalImageSizes.map(([ratio, size]) => ({ when: eq(ref("request.aspectRatio"), ratio), then: size })), default: null } }),
n: conditional(gt({ $toInt: ref("request.imageCount") }, 0), { $toInt: ref("request.imageCount") }, 1),
prompt_extend: true,
enable_thinking: false,
negative_prompt: omit(ref("request.providerOptions.dashscope-qwen-image.negative_prompt")),
seed: omit(ref("request.providerOptions.dashscope-qwen-image.seed")),
watermark: omit(ref("request.watermark"))
}
}, { headers: { "X-DashScope-Async": "enable" }, originPath: true }),
poll: { method: "GET", path: "/api/v1/tasks/{{taskId}}", originPath: true },
response: asyncResponse("image", {
taskId: coalesce(ref("response.output.task_id"), ref("response.task_id"), ref("taskId")),
status: coalesce(ref("response.output.task_status"), ref("response.status"), "pending"),
message: coalesce(ref("response.output.message"), ref("response.message")),
images: map(ref("response.output.choices.0.message.content"), "item", { url: omit(ref("item.image")) }),
errorPaths: ["code", "output.code"], messagePaths: ["message", "output.message"]
})
});
// 千问 AI 平台(QianWenAI MAAS)是阿里独立的 MaaS 站点,网关域名是
// maas.qianwenaiapi.com,与百炼的 dashscope.aliyuncs.com 不是同一入口,因此单列一个插件。
// 它同样走多模态图像端点 multimodal-generation/generation,报文形状与百炼一致
// (input.messages[].content[].image + text),但不能复用百炼插件:改 Base URL 后仍指向
// dashscope.aliyuncs.com,请求会打到没有开通该模型的站点。
const qianwenaiMaasImageSizes = [
["1:1", "1024*1024"], ["3:4", "960*1280"], ["4:3", "1280*960"], ["2:3", "1024*1536"], ["3:2", "1536*1024"],
["9:16", "864*1536"], ["16:9", "1536*864"],
["1024x1024", "1024*1024"], ["1024x1536", "1024*1536"], ["1536x1024", "1536*1024"],
["1024x1280", "1024*1280"], ["1280x1024", "1280*1024"], ["960x1280", "960*1280"], ["1280x960", "1280*960"]
];
add({
id: "qianwenai-maas-image", providerId: "qianwenai-maas-image", name: "QianWenAI MAAS", vendor: "Alibaba QianWenAI", capability: "image",
baseUrl: "https://maas.qianwenaiapi.com", auth: bearer, params: imageParams, requiresPublicMediaUrls: false,
notes: "千问 AI 平台(QianWenAI MAAS)多模态图像端点(multimodal-generation 异步任务)。文生图与图生图共用同一入口:不传 image 为文生图,传参考图为图生图。参考图通过 input.messages[].content[].image 传输(优先 Base64 data URL)。size 采用“宽*高”,未登记的档位省略并由模型自动推荐;enable_thinking 固定为 false,因为官方要求非流式调用关闭思考模式。",
create: jsonCreate("/api/v1/services/aigc/multimodal-generation/generation", {
model: ref("request.model"),
input: {
messages: [{
role: "user",
content: {
$concatArrays: [
map(sorted(ref("request.images")), "media", { image: coalesce(ref("media.dataUrl"), ref("media.url")) }),
[{ text: ref("request.prompt") }]
]
}
}]
},
parameters: {
size: omit({ $switch: { cases: qianwenaiMaasImageSizes.map(([ratio, size]) => ({ when: eq(ref("request.aspectRatio"), ratio), then: size })), default: null } }),
n: conditional(gt({ $toInt: ref("request.imageCount") }, 0), { $toInt: ref("request.imageCount") }, 1),
prompt_extend: true,
enable_thinking: false,
negative_prompt: omit(ref("request.providerOptions.qianwenai-maas-image.negative_prompt")),
seed: omit(ref("request.providerOptions.qianwenai-maas-image.seed")),
watermark: omit(ref("request.watermark"))
}
}, { headers: { "X-DashScope-Async": "enable" }, originPath: true }),
poll: { method: "GET", path: "/api/v1/tasks/{{taskId}}", originPath: true },
response: asyncResponse("image", {
taskId: coalesce(ref("response.output.task_id"), ref("response.task_id"), ref("taskId")),
status: coalesce(ref("response.output.task_status"), ref("response.status"), "pending"),
message: coalesce(ref("response.output.message"), ref("response.message")),
images: map(ref("response.output.choices.0.message.content"), "item", { url: omit(ref("item.image")) }),
errorPaths: ["code", "output.code"], messagePaths: ["message", "output.message"]
})
});
for (const [id, name, vendor, capability, baseUrl, createPath, pollPath, requestShape] of [
["dashscope-qwen-native", "DashScope Qwen Native", "Alibaba Cloud", "text", "https://dashscope.aliyuncs.com", "/api/v1/services/aigc/text-generation/generation", null, "input+parameters"],
["minimax-text-native", "MiniMax Text Native", "MiniMax", "text", "https://api.minimax.chat", "/v1/text/chatcompletion_v2", null, "messages+tokens_to_generate"],
["azure-openai", "Azure OpenAI", "Microsoft Azure", "text", "https://{resource}.openai.azure.com", "/openai/deployments/{{model}}/chat/completions", null, "OpenAI body + api-version query"],
["vertex-gemini", "Vertex AI Gemini", "Google Cloud", "text", "https://{location}-aiplatform.googleapis.com", "/v1/projects/{{request.providerOptions.vertex-gemini.project}}/locations/{{request.providerOptions.vertex-gemini.location}}/publishers/google/models/{{model}}:generateContent", null, "Gemini contents/parts"],
["aws-bedrock-converse", "AWS Bedrock Converse", "AWS", "text", "https://bedrock-runtime.{region}.amazonaws.com", "/model/{{model}}/converse", null, "messages+system+inferenceConfig+toolConfig"],
["aws-bedrock-invoke-model", "AWS Bedrock InvokeModel", "AWS", "text", "https://bedrock-runtime.{region}.amazonaws.com", "/model/{{model}}/invoke", null, "model-specific arbitrary body"],
["vertex-imagen", "Vertex Imagen", "Google Cloud", "image", "https://{location}-aiplatform.googleapis.com", "/v1/projects/{{request.providerOptions.vertex-imagen.project}}/locations/{{request.providerOptions.vertex-imagen.location}}/publishers/google/models/{{model}}:predict", null, "instances+parameters"],
["minimax-image", "MiniMax Image", "MiniMax", "image", "https://api.minimax.io", "/v1/image_generation", null, "model-specific image body"],
["kling-image", "Kling Image", "Kuaishou", "image", "https://api.klingai.com", "/v1/images/generations", "/v1/images/generations/{{taskId}}", "task JSON"],
["zhipu-cogview", "智谱 CogView", "Zhipu AI", "image", "https://open.bigmodel.cn/api/paas", "/v4/images/generations", null, "OpenAI-like image JSON"],
["bfl-flux", "Black Forest Labs FLUX", "Black Forest Labs", "image", "https://api.bfl.ai", "/v1/{{model}}", "/v1/get_result", "async model schema"],
["recraft-image", "Recraft Image", "Recraft", "image", "https://external.api.recraft.ai", "/v1/images/generations", null, "raster/vector image JSON"],
["adobe-firefly", "Adobe Firefly", "Adobe", "image", "https://firefly-api.adobe.io", "/v3/images/generate", null, "versioned Firefly request"],
["runway-image", "Runway Image", "Runway", "image", "https://api.dev.runwayml.com", "/v1/text_to_image", "/v1/tasks/{{taskId}}", "async Runway image schema"],
["vertex-veo", "Vertex AI Veo", "Google Cloud", "video", "https://{location}-aiplatform.googleapis.com", "/v1/projects/{{request.providerOptions.vertex-veo.project}}/locations/{{request.providerOptions.vertex-veo.location}}/publishers/google/models/{{model}}:predictLongRunning", "/v1/{{taskId}}", "Vertex long-running operation"],
["seedance-videos-compatible", "Seedance Compatible /videos", "Gateway profile", "video", "http://127.0.0.1:3000", "/v1/videos", "/v1/videos/{{taskId}}", "gateway-specific videos JSON"],
["kling-video", "Kling Video", "Kuaishou", "video", "https://api.klingai.com", "/v1/videos/generations", "/v1/videos/generations/{{taskId}}", "text/image/multi-image task"],
["runway-video", "Runway Video", "Runway", "video", "https://api.dev.runwayml.com", "/v1/image_to_video", "/v1/tasks/{{taskId}}", "promptImage+promptText+ratio+duration"],
["luma-dream-machine", "Luma Dream Machine", "Luma AI", "video", "https://api.lumalabs.ai", "/dream-machine/v1/generations", "/dream-machine/v1/generations/{{taskId}}", "prompt+keyframes"],
["vidu-video", "Vidu Video", "Vidu", "video", "https://api.vidu.com", "/v1/videos", "/v1/tasks/{{taskId}}", "text/image/reference video task"],
["pixverse-video", "PixVerse Video", "PixVerse", "video", "https://app-api.pixverse.ai", "/openapi/v2/video/text/generate", "/openapi/v2/video/result/{{taskId}}", "model+duration+quality+effect"],
["zhipu-cogvideox", "智谱 CogVideoX", "Zhipu AI", "video", "https://open.bigmodel.cn/api/paas", "/v4/videos/generations", "/v4/async-result/{{taskId}}", "model+prompt+image_url+quality+with_audio"],
["baidu-video", "百度千帆视频", "Baidu AI Cloud", "video", "https://qianfan.baidubce.com", "/v2/video/generations", "/v2/video/generations/{{taskId}}", "model-specific input"],
["tencent-hunyuan-video", "腾讯混元视频", "Tencent Cloud", "video", "https://hunyuan.tencentcloudapi.com", "/", "/", "TC3 Action task"],
["tencent-hunyuan-image", "腾讯混元图片", "Tencent Cloud", "image", "https://hunyuan.tencentcloudapi.com", "/", "/", "TC3 Action task"],
["baidu-qianfan-image", "百度千帆图片", "Baidu AI Cloud", "image", "https://qianfan.baidubce.com", "/v2/images/generations", null, "model-specific image input"]
]) {
const params = capability === "text" ? textParams : capability === "image" ? imageParams : videoParams;
const optionPath = `request.providerOptions.${id}`;
const standardFields = capability === "text"
? { model: ref("request.model"), messages: ref("request.messages"), input: omit(ref("request.prompt")), parameters: omit(ref(`${optionPath}.parameters`)) }
: capability === "image"
? { model: ref("request.model"), prompt: ref("request.prompt"), images: omit(map(ref("request.images"), "media", ref("media.value"))), parameters: omit(ref(`${optionPath}.parameters`)) }
: { model: ref("request.model"), prompt: ref("request.prompt"), images: omit(map(ref("request.images"), "media", ref("media.value"))), videos: omit(map(ref("request.videos"), "media", ref("media.value"))), audios: omit(map(ref("request.audios"), "media", ref("media.value"))), duration: omit(ref("request.duration")), aspect_ratio: omit(ref("request.aspectRatio")), resolution: omit(ref("request.resolution")), parameters: omit(ref(`${optionPath}.parameters`)) };
const standard = { $merge: [standardFields, coalesce(ref(`${optionPath}.body`), ref(`${optionPath}.extra_body`), {})] };
let auth = bearer;
let configuration;
let operationExtra = id.startsWith("dashscope-") ? { headers: { "X-DashScope-Async": "enable" } } : {};
if (id.startsWith("aws-bedrock-")) {
auth = { type: "aws-sigv4", field: "apiKey", secretField: "secretKey", service: "bedrock" };
configuration = { fields: [{ name: "apiKey", type: "secret", label: "Access Key ID", required: true }, { name: "secretKey", type: "secret", label: "Secret Access Key", required: true }] };
} else if (id.startsWith("tencent-")) {
auth = { type: "tc3", field: "apiKey", secretField: "secretKey", service: "hunyuan" };
configuration = { fields: [{ name: "apiKey", type: "secret", label: "SecretId", required: true }, { name: "secretKey", type: "secret", label: "SecretKey", required: true }] };
operationExtra = { headers: { "X-TC-Action": ref(`${optionPath}.action`), "X-TC-Version": ref(`${optionPath}.version`) } };
} else if (id === "azure-openai") {
auth = { type: "header", field: "apiKey", header: "api-key" };
operationExtra = { query: { "api-version": coalesce(ref(`${optionPath}.api-version`), "2024-10-21") } };
}
add({
id, providerId: id, name, vendor, capability, baseUrl, auth, params, configuration,
notes: `该协议的模型级字段变化快或依赖云资源配置。插件固定线协议入口和统一字段,完整厂商对象通过 providerOptions.${id} 的 parameters/input/extra_body 传入;${requestShape}。云签名型入口在未配置对应鉴权驱动时会明确失败,不会伪装成 Bearer 成功。`,
create: jsonCreate(createPath, standard, operationExtra),
poll: pollPath ? { method: "GET", path: pollPath } : undefined,
response: capability === "text"
? { status: "succeeded", textPaths: ["output.text", "output_text", "choices.0.message.content", "result"], reasoningPaths: ["reasoning_content"], usage: ref("response.usage"), errorPaths: ["error.code", "code"], messagePaths: ["error.message", "message"] }
: pollPath ? asyncResponse(capability) : { status: "succeeded", [capability + "s"]: coalesce(ref("response.data"), ref("response.output"), ref("response.url")), errorPaths: ["error.code", "code"], messagePaths: ["error.message", "message"] }
});
}
for (const [id, name, capability, createPath, pollPath, resultPath] of [
["fal-queue-image", "fal.ai Queue Image", "image", "/{{model}}", "/{{request.providerOptions.fal-queue-image.statusPath}}", "response.images"],
["fal-queue-video", "fal.ai Queue Video", "video", "/{{model}}", "/{{request.providerOptions.fal-queue-video.statusPath}}", "response.video"],
["replicate-prediction-image", "Replicate Predictions Image", "image", "/v1/predictions", "/v1/predictions/{{taskId}}", "response.output"],
["replicate-prediction-video", "Replicate Predictions Video", "video", "/v1/predictions", "/v1/predictions/{{taskId}}", "response.output"],
["runninghub-workflow", "RunningHub Workflow", "video", "/task/openapi/create", "/task/openapi/status", "response.data"],
["pika-via-fal", "Pika via fal.ai", "video", "/{{model}}", "/{{request.providerOptions.pika-via-fal.statusPath}}", "response.video"]
]) {
const optionPath = `request.providerOptions.${id}`;
add({
id, providerId: id, name, vendor: id.includes("replicate") ? "Replicate" : id.includes("comfy") ? "ComfyUI" : id.includes("runninghub") ? "RunningHub" : "fal.ai", capability,
baseUrl: id.includes("replicate") ? "https://api.replicate.com" : id.includes("fal") || id.includes("pika") ? "https://queue.fal.run" : "http://127.0.0.1:8188",
auth: id.includes("comfy") ? { type: "none", field: "apiKey" } : bearer,
params: capability === "image" ? imageParams : videoParams,
notes: "这是运行时/工作流协议,模型字段由 endpoint、version 或 workflow schema 决定。插件不伪造固定模型字段;providerOptions.input/workflow/prompt 是完整请求对象,并由 conformance fixture 锁定实际接入版本。",
create: jsonCreate(createPath, {
version: omit(ref(`${optionPath}.version`)), model: omit(coalesce(ref(`${optionPath}.model`), ref("request.model"))),
input: omit(coalesce(ref(`${optionPath}.input`), { prompt: ref("request.prompt"), images: map(ref("request.images"), "media", ref("media.value")), videos: map(ref("request.videos"), "media", ref("media.value")), audios: map(ref("request.audios"), "media", ref("media.value")) })),
prompt: omit(ref(`${optionPath}.workflow`)), client_id: omit(ref(`${optionPath}.client_id`)), webhook: omit(ref(`${optionPath}.webhook`)), webhook_events_filter: omit(ref(`${optionPath}.webhook_events_filter`))
}),
poll: pollPath ? { method: "GET", path: pollPath } : undefined,
cancel: id.includes("replicate") ? { method: "POST", path: "/v1/predictions/{{taskId}}/cancel" } : undefined,
response: asyncResponse(capability, { [capability + "s"]: ref(resultPath), taskId: coalesce(ref("response.id"), ref("response.request_id"), ref("response.prompt_id"), ref("taskId")) })
});
}
function manifestFor(spec) {
return {
apiVersion: "yingce.plugin/v2",
id: spec.id,
name: spec.name,
version: "2.0.0",
author: `${spec.vendor} / 影策`,
description: `${spec.name} 独立请求协议插件。`,
documentation: `# ${spec.name}\n\n完整字段、映射、响应、鉴权和兼容边界见包内 README.md 与 docs/interface.md。\n\n## 影策运行时合同\n\n用户只操作统一的文本、图片或视频能力;插件负责把统一请求转换为 ${spec.name} 上游协议。`,
permissions: ["generation.run", "media.read"],
configuration: spec.configuration || config(),
contributes: {
providers: [{
id: spec.providerId,
label: spec.name,
capabilities: [spec.capability],
scopes,
baseUrl: spec.baseUrl,
requiresPublicMediaUrls: spec.requiresPublicMediaUrls === true,
auth: spec.auth,
parameters: parameters(spec.params),
...(spec.validations ? { validations: spec.validations } : {}),
create: spec.create,
...(spec.agent ? { agent: spec.agent } : {}),
...(spec.poll ? { poll: spec.poll } : {}),
...(spec.cancel ? { cancel: spec.cancel } : {}),
...(spec.result ? { result: spec.result } : {}),
response: spec.response,
...(spec.agentResponse ? { agentResponse: spec.agentResponse } : {})
}]
}
};
}
function collectTemplateFields(value, prefix, rows) {
if (value === undefined || value === null) return;
if (Array.isArray(value)) {
if (value.length === 0) rows.push([prefix, "[]"]);
value.forEach((item, index) => collectTemplateFields(item, `${prefix}[${index}]`, rows));
return;
}
if (typeof value === "object") {
const keys = Object.keys(value);
if (keys.length === 1 && keys[0].startsWith("$")) {
rows.push([prefix, JSON.stringify(value)]);
return;
}
if (keys.length === 0) rows.push([prefix, "{}"]);
for (const key of keys) collectTemplateFields(value[key], prefix ? `${prefix}.${key}` : key, rows);
return;
}
rows.push([prefix, JSON.stringify(value)]);
}
function operationFieldRows(operation, label) {
if (!operation) return [];
const rows = [
[`${label}.method`, JSON.stringify(operation.method)],
[`${label}.path`, JSON.stringify(operation.path || "dynamic")],
[`${label}.contentType`, JSON.stringify(operation.contentType || "application/json")]
];
collectTemplateFields(operation.body, `${label}.body`, rows);
collectTemplateFields(operation.query, `${label}.query`, rows);
collectTemplateFields(operation.headers, `${label}.headers`, rows);
collectTemplateFields(operation.files, `${label}.files`, rows);
return rows;
}
function docsFor(spec) {
const rows = spec.params.map(([name, type, required, mapping, description]) => `| \`${name}\` | ${type} | ${required ? "是" : "否"} | \`${mapping}\` | ${description} |`).join("\n");
const operationRows = [
...operationFieldRows(spec.create, "create"),
...operationFieldRows(spec.agent, "agent"),
...operationFieldRows(spec.poll, "poll"),
...operationFieldRows(spec.cancel, "cancel"),
...operationFieldRows(spec.result, "result")
].map(([path, expression]) => `| \`${path}\` | \`${String(expression).replaceAll("|", "\\|")}\` |`).join("\n");
const responseRows = [];
collectTemplateFields(spec.response, "response", responseRows);
collectTemplateFields(spec.agentResponse, "agentResponse", responseRows);
const mappedResponseRows = responseRows.map(([path, expression]) => `| \`${path}\` | \`${String(expression).replaceAll("|", "\\|")}\` |`).join("\n");
const manifestJSON = JSON.stringify({ create: spec.create, agent: spec.agent, poll: spec.poll, cancel: spec.cancel, result: spec.result, response: spec.response, agentResponse: spec.agentResponse });
const optionRefs = [...new Set([...manifestJSON.matchAll(new RegExp(`request\\.providerOptions\\.${spec.providerId.replaceAll("-", "\\-")}\\.([A-Za-z0-9_.-]+)`, "g"))].map((match) => match[1]))].sort();
const optionRows = optionRefs.length ? optionRefs.map((name) => `- \`providerOptions.${spec.providerId}.${name}\``).join("\n") : "- 无额外扩展键。";
const configRows = (spec.configuration || config()).fields.map((field) => `| \`${field.name}\` | ${field.type} | ${field.required ? "是" : "否"} | ${field.label || ""} |`).join("\n");
return `# ${spec.name} 接口字段\n\n## 协议身份\n\n- 插件 ID:\`${spec.id}\`。\n- Provider ID:\`${spec.providerId}\`。\n- 能力:\`${spec.capability}\`。\n- 默认 Base URL:\`${spec.baseUrl}\`。\n- 鉴权驱动:\`${spec.auth?.type || "默认"}\`。\n- 创建:\`${spec.create.method} ${spec.create.path || "动态路径"}\`。\n${spec.agent ? `- Agent:\`${spec.agent.method} ${spec.agent.path || "动态路径"}\`。\n` : ""}${spec.poll ? `- 查询:\`${spec.poll.method} ${spec.poll.path}\`。\n` : "- 生命周期:同步响应。\n"}${spec.cancel ? `- 取消:\`${spec.cancel.method} ${spec.cancel.path}\`。\n` : ""}\n## 配置字段\n\n| 字段 | 类型 | 必填 | 含义 |\n| --- | --- | --- | --- |\n${configRows}\n\n## 统一字段映射\n\n| 统一字段 | 类型 | 必填 | 上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n${rows}\n\n## 上游请求模板逐字段清单\n\n下表由插件请求模板生成,覆盖 body、query、headers 和 multipart 文件声明中的每个字段。\n\n| 上游位置 | 值或转换表达式 |\n| --- | --- |\n${operationRows || "| `create` | 无请求字段 |"}\n\n## Provider 扩展键\n\n${optionRows}\n\n动态模型或工作流允许使用文档声明的完整 \`parameters/input/extra_body\` 对象;该对象是协议本身的开放 schema,不会被宿主裁剪。\n\n## 响应映射逐字段清单\n\n| 映射位置 | 上游路径或转换表达式 |\n| --- | --- |\n${mappedResponseRows || "| `response` | 无显式映射 |"}\n\n## 响应与错误\n\n插件把上游 task/status/text/media/usage 映射为统一结果。临时媒体 URL 标记为 ephemeral,由宿主立即下载持久化。HTTP 错误、业务 code 和 error object 保持失败语义,不包装成成功。\n\n## 兼容边界\n\n${spec.notes || "该包只代表上述线协议 profile;同一品牌的其他 endpoint、云区域或网关包装必须使用独立插件,不能根据模型名猜测。"}\n`;
}
const requestedPackageIDs = new Set(process.argv.slice(2).map((value) => value.trim()).filter(Boolean));
const selectedSpecs = requestedPackageIDs.size > 0 ? specs.filter((spec) => requestedPackageIDs.has(spec.id)) : specs;
if (requestedPackageIDs.size > 0 && selectedSpecs.length !== requestedPackageIDs.size) {
const found = new Set(selectedSpecs.map((spec) => spec.id));
const missing = [...requestedPackageIDs].filter((id) => !found.has(id));
throw new Error(`unknown protocol package id(s): ${missing.join(", ")}`);
}
for (const spec of selectedSpecs) {
const dir = join(root, spec.id);
await mkdir(join(dir, "docs"), { recursive: true });
await writeFile(join(dir, "manifest.json"), JSON.stringify(manifestFor(spec), null, 2) + "\n");
await writeFile(join(dir, "README.md"), `# ${spec.name}\n\n该目录是独立官方协议插件源码。后端从生成的 \`${spec.id}.yingce-plugin\` 包加载,不依赖系统内置 \`host:\` 适配器。\n\n完整接口见 [docs/interface.md](docs/interface.md)。\n`);
await writeFile(join(dir, "docs", "interface.md"), docsFor(spec));
}
console.log(`generated ${selectedSpecs.length} protocol packages`);
await import("./embed-documentation.mjs");