Files
omniai-server/src/aiProviderRouter.js
T

269 lines
15 KiB
JavaScript
Raw Normal View History

2026-06-02 13:14:10 +08:00
"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,
};