269 lines
15 KiB
JavaScript
269 lines
15 KiB
JavaScript
|
|
"use strict";
|
||
|
|
|
||
|
|
const DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com";
|
||
|
|
const KUAIKUAI_IMAGE_BASE_URL = process.env.KUAIKUAI_IMAGE_BASE_URL || "https://ai-api.kkidc.com";
|
||
|
|
const KUAIKUAI_GEMINI_ENDPOINT = "/v1beta/models/{model}:generateContent";
|
||
|
|
const KUAIKUAI_OPENAI_ENDPOINT = "/v1/chat/completions";
|
||
|
|
const GRSAI_IMAGE_BASE_URL = "https://grsai.dakka.com.cn";
|
||
|
|
const GRSAI_TEXT_BASE_URL = "https://grsai.dakka.com.cn";
|
||
|
|
const RIGHTCODE_IMAGE_BASE_URL = process.env.RIGHTCODE_IMAGE_BASE_URL || process.env.RIGHTCODE_BASE_URL || "https://www.right.codes/draw";
|
||
|
|
const SEEDANCE_BASE_URL = "https://ai-api.kkidc.com";
|
||
|
|
const SEEDANCE_ARK_BASE_URL = "https://ark.cn-beijing.volces.com";
|
||
|
|
const KLING_BASE_URL = "https://api-beijing.klingai.com";
|
||
|
|
|
||
|
|
const DASHSCOPE_IMAGE_ENDPOINT = "/api/v1/services/aigc/multimodal-generation/generation";
|
||
|
|
const DASHSCOPE_VIDEO_ENDPOINT = "/api/v1/services/aigc/video-generation/video-synthesis";
|
||
|
|
const DASHSCOPE_S2V_ENDPOINT = "/api/v1/services/aigc/image2video/video-synthesis";
|
||
|
|
const DASHSCOPE_S2V_DETECT_ENDPOINT = "/api/v1/services/aigc/image2video/face-detect";
|
||
|
|
const DASHSCOPE_ANIMATE_ENDPOINT = "/api/v1/services/aigc/image2video/video-synthesis";
|
||
|
|
const GRSAI_IMAGE_ENDPOINT = "/v1/api/generate";
|
||
|
|
const GRSAI_RESULT_ENDPOINT = "/v1/api/result";
|
||
|
|
const RIGHTCODE_IMAGE_ENDPOINT = process.env.RIGHTCODE_IMAGE_ENDPOINT || "/v1/images/generations";
|
||
|
|
|
||
|
|
const IMAGE_MODELS = {
|
||
|
|
"nano-banana-pro": { provider: "grsai", transport: "grsai-image", baseUrl: GRSAI_IMAGE_BASE_URL, endpoint: GRSAI_IMAGE_ENDPOINT, resultEndpoint: GRSAI_RESULT_ENDPOINT },
|
||
|
|
"nano-banana-2": { provider: "grsai", transport: "grsai-image", baseUrl: GRSAI_IMAGE_BASE_URL, endpoint: GRSAI_IMAGE_ENDPOINT, resultEndpoint: GRSAI_RESULT_ENDPOINT },
|
||
|
|
"nano-banana-fast": { provider: "grsai", transport: "grsai-image", baseUrl: GRSAI_IMAGE_BASE_URL, endpoint: GRSAI_IMAGE_ENDPOINT, resultEndpoint: GRSAI_RESULT_ENDPOINT },
|
||
|
|
"gpt-image-2": { provider: "grsai", transport: "grsai-image", baseUrl: GRSAI_IMAGE_BASE_URL, endpoint: GRSAI_IMAGE_ENDPOINT, resultEndpoint: GRSAI_RESULT_ENDPOINT },
|
||
|
|
"gpt-image-2-vip": { provider: "grsai", transport: "grsai-image", baseUrl: GRSAI_IMAGE_BASE_URL, endpoint: GRSAI_IMAGE_ENDPOINT, resultEndpoint: GRSAI_RESULT_ENDPOINT },
|
||
|
|
"wan2.7-image": { provider: "dashscope-wan2.7", transport: "dashscope-image", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_IMAGE_ENDPOINT },
|
||
|
|
"wan2.7-image-pro": { provider: "dashscope-wan2.7-pro", transport: "dashscope-image", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_IMAGE_ENDPOINT },
|
||
|
|
};
|
||
|
|
|
||
|
|
const RIGHTCODE_IMAGE_MODEL_MAP = {
|
||
|
|
"gpt-image-2": "gpt-image-2",
|
||
|
|
"gpt-image-2-vip": "gpt-image-2-vip",
|
||
|
|
"nano-banana-fast": "nano-banana",
|
||
|
|
"nano-banana-2": "nano-banana-2",
|
||
|
|
"nano-banana-pro": "nano-banana-pro",
|
||
|
|
};
|
||
|
|
|
||
|
|
const KUAIKUAI_IMAGE_MODEL_MAP = {
|
||
|
|
"nano-banana-fast": "gemini-2.5-flash-image",
|
||
|
|
"nano-banana-2": "gemini-3.1-flash-image-preview",
|
||
|
|
"nano-banana-pro": "gemini-3-pro-image-preview",
|
||
|
|
"gpt-image-2": "gpt-image-2",
|
||
|
|
"gpt-image-2-vip": "gpt-image-2-vip",
|
||
|
|
};
|
||
|
|
|
||
|
|
const VIDEO_MODELS = {
|
||
|
|
"seedance-2": { provider: "seedance-2.0", protocol: "seed-video", baseUrl: SEEDANCE_BASE_URL, endpoint: "/v1/video/generations", model: "seed-2" },
|
||
|
|
"seedance-2-fast": { provider: "seedance-2.0-fast", protocol: "seed-video", baseUrl: SEEDANCE_BASE_URL, endpoint: "/v1/video/generations", model: "seed-2-fast" },
|
||
|
|
"seedance-2-official": { provider: "seedance-2.0-ark", protocol: "seed-video-ark", baseUrl: SEEDANCE_ARK_BASE_URL, endpoint: "/api/v3/contents/generations/tasks", model: "doubao-seedance-2-0-260128" },
|
||
|
|
"seedance-2-fast-official": { provider: "seedance-2.0-fast-ark", protocol: "seed-video-ark", baseUrl: SEEDANCE_ARK_BASE_URL, endpoint: "/api/v3/contents/generations/tasks", model: "doubao-seedance-2-0-fast-260128" },
|
||
|
|
"happyhorse-1.0-t2v": { provider: "dashscope-happyhorse-t2v", protocol: "happyhorse-t2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "happyhorse-1.0-t2v" },
|
||
|
|
"happyhorse-1.0-i2v": { provider: "dashscope-happyhorse-i2v", protocol: "happyhorse-i2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "happyhorse-1.0-i2v" },
|
||
|
|
"happyhorse-1.0-r2v": { provider: "dashscope-happyhorse-r2v", protocol: "happyhorse-r2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "happyhorse-1.0-r2v" },
|
||
|
|
"wan2.7-i2v": { provider: "dashscope-wan-i2v", protocol: "wan-i2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "wan2.7-i2v" },
|
||
|
|
"wan2.7-t2v": { provider: "dashscope-wan-t2v", protocol: "wan-t2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "wan2.7-t2v" },
|
||
|
|
"wan2.2-s2v": { provider: "dashscope-wan-s2v", protocol: "wan-s2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_S2V_ENDPOINT, detectEndpoint: DASHSCOPE_S2V_DETECT_ENDPOINT, model: "wan2.2-s2v", detectModel: "wan2.2-s2v-detect" },
|
||
|
|
"wan2.2-animate-mix": { provider: "dashscope-wan-animate", protocol: "wan-animate-mix", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_ANIMATE_ENDPOINT, model: "wan2.2-animate-mix" },
|
||
|
|
"Kling-V3-Omni": { provider: "kling-official", protocol: "kling-omni", baseUrl: KLING_BASE_URL, endpoint: "/v1/videos/omni-video", model: "Kling-V3-Omni" },
|
||
|
|
"kling-2-1": { provider: "kling-official", protocol: "kling-omni", baseUrl: KLING_BASE_URL, endpoint: "/v1/videos/omni-video", model: "Kling-V3-Omni" },
|
||
|
|
"kling-3.0-dashscope": { provider: "dashscope-kling", protocol: "kling-dashscope", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "kling/kling-v3-omni-video-generation" },
|
||
|
|
"Kling-V3-Omni-dashscope": { provider: "dashscope-kling", protocol: "kling-dashscope", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "kling/kling-v3-omni-video-generation" },
|
||
|
|
"kling-v3-omni-dashscope": { provider: "dashscope-kling", protocol: "kling-dashscope", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "kling/kling-v3-omni-video-generation" },
|
||
|
|
"kling/kling-v3-omni-video-generation": { provider: "dashscope-kling", protocol: "kling-dashscope", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "kling/kling-v3-omni-video-generation" },
|
||
|
|
"vidu-q3-turbo-t2v": { provider: "dashscope-vidu-t2v", protocol: "vidu-t2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "vidu/viduq3-turbo_text2video" },
|
||
|
|
"vidu-q3-turbo-i2v": { provider: "dashscope-vidu-i2v", protocol: "vidu-i2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "vidu/viduq3-turbo_img2video" },
|
||
|
|
"pixverse-c1-t2v": { provider: "dashscope-pixverse-t2v", protocol: "pixverse-t2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "pixverse/pixverse-c1-t2v" },
|
||
|
|
"pixverse-c1-i2v": { provider: "dashscope-pixverse-i2v", protocol: "pixverse-i2v", baseUrl: DASHSCOPE_BASE_URL, endpoint: DASHSCOPE_VIDEO_ENDPOINT, model: "pixverse/pixverse-c1-it2v" },
|
||
|
|
};
|
||
|
|
|
||
|
|
const TEXT_MODELS = {
|
||
|
|
"gemini-3.1-pro": { provider: "grsai", transport: "openai-chat", baseUrl: GRSAI_TEXT_BASE_URL, endpoint: "/v1/chat/completions" },
|
||
|
|
"gemini-2.5-pro": { provider: "grsai", transport: "openai-chat", baseUrl: GRSAI_TEXT_BASE_URL, endpoint: "/v1/chat/completions" },
|
||
|
|
"gpt-4o": { provider: "grsai", transport: "openai-chat", baseUrl: GRSAI_TEXT_BASE_URL, endpoint: "/v1/chat/completions" },
|
||
|
|
};
|
||
|
|
|
||
|
|
const DASHSCOPE_TEXT_PREFIXES = ["qwen", "qwq", "deepseek-"];
|
||
|
|
const DASHSCOPE_TEXT_CONFIG = { provider: "dashscope-text", transport: "dashscope-chat", baseUrl: DASHSCOPE_BASE_URL, endpoint: "/compatible-mode/v1/chat/completions" };
|
||
|
|
|
||
|
|
function normalizeModel(model, fallback) {
|
||
|
|
return String(model || fallback || "").trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
function withImageBillingProvider(config, billingProvider) {
|
||
|
|
return { ...config, billingProvider };
|
||
|
|
}
|
||
|
|
|
||
|
|
function createUnsupportedImageModelError(model) {
|
||
|
|
const error = new Error(`Unsupported image model: ${model}`);
|
||
|
|
error.status = 400;
|
||
|
|
return error;
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveImageProvider(model) {
|
||
|
|
const requestedModel = normalizeModel(model, "nano-banana-pro");
|
||
|
|
const lower = requestedModel.toLowerCase();
|
||
|
|
const config = IMAGE_MODELS[requestedModel] || IMAGE_MODELS[lower];
|
||
|
|
if (config) return { ...config, model: requestedModel, requestedModel };
|
||
|
|
|
||
|
|
throw createUnsupportedImageModelError(requestedModel);
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveImageProviderCandidates(model) {
|
||
|
|
const primary = resolveImageProvider(model);
|
||
|
|
const billingProvider = primary.billingProvider || primary.provider;
|
||
|
|
if (primary.transport !== "grsai-image" || primary.provider !== "grsai") {
|
||
|
|
return [withImageBillingProvider(primary, billingProvider)];
|
||
|
|
}
|
||
|
|
|
||
|
|
const rightcodeModel = RIGHTCODE_IMAGE_MODEL_MAP[String(primary.requestedModel || primary.model || "").toLowerCase()];
|
||
|
|
const fallbackModel = rightcodeModel || primary.requestedModel || primary.model;
|
||
|
|
const kuaikuaiModel = KUAIKUAI_IMAGE_MODEL_MAP[String(primary.requestedModel || primary.model || "").toLowerCase()] || primary.requestedModel || primary.model;
|
||
|
|
const modelKey = String(primary.requestedModel || primary.model || "").toLowerCase();
|
||
|
|
const isGptModel = modelKey.startsWith("gpt");
|
||
|
|
|
||
|
|
return [
|
||
|
|
withImageBillingProvider(primary, billingProvider),
|
||
|
|
{
|
||
|
|
...primary,
|
||
|
|
provider: "rightcode",
|
||
|
|
transport: "rightcode-image",
|
||
|
|
baseUrl: RIGHTCODE_IMAGE_BASE_URL,
|
||
|
|
endpoint: RIGHTCODE_IMAGE_ENDPOINT,
|
||
|
|
model: fallbackModel,
|
||
|
|
billingProvider,
|
||
|
|
fallbackOf: primary.provider,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
...primary,
|
||
|
|
provider: isGptModel ? "kuaikuai-gpt" : "kuaikuai-nano",
|
||
|
|
transport: isGptModel ? "openai-image" : "gemini-image",
|
||
|
|
baseUrl: KUAIKUAI_IMAGE_BASE_URL,
|
||
|
|
endpoint: isGptModel ? KUAIKUAI_OPENAI_ENDPOINT : KUAIKUAI_GEMINI_ENDPOINT,
|
||
|
|
model: kuaikuaiModel,
|
||
|
|
billingProvider,
|
||
|
|
fallbackOf: primary.provider,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
function sanitizeImageProviderCandidate(config) {
|
||
|
|
return {
|
||
|
|
provider: config.provider,
|
||
|
|
transport: config.transport,
|
||
|
|
model: config.model,
|
||
|
|
requestedModel: config.requestedModel,
|
||
|
|
billingProvider: config.billingProvider,
|
||
|
|
fallbackOf: config.fallbackOf,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildImageProviderDebug(model) {
|
||
|
|
const candidates = resolveImageProviderCandidates(model).map(sanitizeImageProviderCandidate);
|
||
|
|
return {
|
||
|
|
requestedModel: candidates[0]?.requestedModel || normalizeModel(model, "nano-banana-pro"),
|
||
|
|
effectiveModel: candidates[0]?.model,
|
||
|
|
primaryProvider: candidates[0]?.provider,
|
||
|
|
fallbackProviders: candidates.slice(1).map((candidate) => candidate.provider).filter(Boolean),
|
||
|
|
route: candidates.map((candidate) => candidate.provider).filter(Boolean),
|
||
|
|
candidates,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveVideoProvider(model) {
|
||
|
|
const requestedModel = normalizeModel(model, "seedance-2");
|
||
|
|
const lower = requestedModel.toLowerCase();
|
||
|
|
const config = VIDEO_MODELS[requestedModel] || VIDEO_MODELS[lower];
|
||
|
|
if (config) return { ...config, requestedModel };
|
||
|
|
|
||
|
|
if (lower.includes("happyhorse") && lower.includes("i2v")) {
|
||
|
|
return { ...VIDEO_MODELS["happyhorse-1.0-i2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.includes("happyhorse") && lower.includes("r2v")) {
|
||
|
|
return { ...VIDEO_MODELS["happyhorse-1.0-r2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.includes("happyhorse")) {
|
||
|
|
return { ...VIDEO_MODELS["happyhorse-1.0-t2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.startsWith("wan2.") && lower.includes("i2v")) {
|
||
|
|
return { ...VIDEO_MODELS["wan2.7-i2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.startsWith("wan2.")) {
|
||
|
|
return { ...VIDEO_MODELS["wan2.7-t2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.startsWith("kling/")) {
|
||
|
|
return { ...VIDEO_MODELS["kling/kling-v3-omni-video-generation"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.includes("kling")) {
|
||
|
|
return { ...VIDEO_MODELS["Kling-V3-Omni"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.includes("vidu") && lower.includes("i2v")) {
|
||
|
|
return { ...VIDEO_MODELS["vidu-q3-turbo-i2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.includes("vidu")) {
|
||
|
|
return { ...VIDEO_MODELS["vidu-q3-turbo-t2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.includes("pixverse") && lower.includes("i2v")) {
|
||
|
|
return { ...VIDEO_MODELS["pixverse-c1-i2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
if (lower.includes("pixverse")) {
|
||
|
|
return { ...VIDEO_MODELS["pixverse-c1-t2v"], model: requestedModel, requestedModel };
|
||
|
|
}
|
||
|
|
const error = new Error(`不支持的视频模型: ${requestedModel}`);
|
||
|
|
error.status = 400;
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
|
||
|
|
function resolveTextProvider(model) {
|
||
|
|
const m = normalizeModel(model, "gemini-3.1-pro").toLowerCase();
|
||
|
|
if (DASHSCOPE_TEXT_PREFIXES.some((p) => m.startsWith(p))) {
|
||
|
|
return { ...DASHSCOPE_TEXT_CONFIG, model: m, requestedModel: m };
|
||
|
|
}
|
||
|
|
const config = TEXT_MODELS[m];
|
||
|
|
if (config) return { ...config, model: m, requestedModel: m };
|
||
|
|
return { ...TEXT_MODELS["gemini-3.1-pro"], model: "gemini-3.1-pro", requestedModel: m };
|
||
|
|
}
|
||
|
|
|
||
|
|
function listKnownRoutes() {
|
||
|
|
return [
|
||
|
|
...Object.keys(IMAGE_MODELS).flatMap((model) =>
|
||
|
|
resolveImageProviderCandidates(model).map((config) => ({ type: "image", ...config })),
|
||
|
|
),
|
||
|
|
...Object.keys(VIDEO_MODELS).map((model) => ({ type: "video", ...resolveVideoProvider(model) })),
|
||
|
|
...Object.keys(TEXT_MODELS).map((model) => ({ type: "text", ...resolveTextProvider(model) })),
|
||
|
|
{ type: "text", ...resolveTextProvider("qwen-plus") },
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPostUrl(config) {
|
||
|
|
const base = `${config.baseUrl || ""}`;
|
||
|
|
const endpoint = String(config.endpoint || "").replace("{model}", encodeURIComponent(config.model || ""));
|
||
|
|
return `${base}${endpoint}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPollUrl(config) {
|
||
|
|
if (config.transport === "dashscope-image") return `${DASHSCOPE_BASE_URL}/api/v1/tasks/{task_id}`;
|
||
|
|
if (config.transport === "grsai-image") return `${config.baseUrl}${config.resultEndpoint || GRSAI_RESULT_ENDPOINT}?id={task_id}`;
|
||
|
|
if (config.transport === "rightcode-image" || config.transport === "gemini-image" || config.transport === "openai-image") return "";
|
||
|
|
if (
|
||
|
|
config.protocol === "wan-i2v" ||
|
||
|
|
config.protocol === "wan-t2v" ||
|
||
|
|
config.protocol === "wan-s2v" ||
|
||
|
|
config.protocol === "wan-animate-mix" ||
|
||
|
|
config.protocol === "kling-dashscope" ||
|
||
|
|
String(config.protocol || "").startsWith("happyhorse-") ||
|
||
|
|
String(config.protocol || "").startsWith("vidu-") ||
|
||
|
|
String(config.protocol || "").startsWith("pixverse-")
|
||
|
|
) return `${config.baseUrl}/api/v1/tasks/{task_id}`;
|
||
|
|
if (config.protocol === "kling-omni") return `${config.baseUrl}/v1/videos/omni-video/{task_id}`;
|
||
|
|
if (config.protocol === "seed-video" || config.protocol === "seed-video-ark") return `${config.baseUrl}${config.endpoint}/{task_id}`;
|
||
|
|
return "";
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = {
|
||
|
|
resolveImageProvider,
|
||
|
|
resolveImageProviderCandidates,
|
||
|
|
buildImageProviderDebug,
|
||
|
|
resolveVideoProvider,
|
||
|
|
resolveTextProvider,
|
||
|
|
listKnownRoutes,
|
||
|
|
getPostUrl,
|
||
|
|
getPollUrl,
|
||
|
|
IMAGE_MODELS,
|
||
|
|
VIDEO_MODELS,
|
||
|
|
TEXT_MODELS,
|
||
|
|
};
|