464 lines
15 KiB
TypeScript
464 lines
15 KiB
TypeScript
/**
|
||
* 智能模型路由器
|
||
* 根据用户意图选择最佳AI模型
|
||
*
|
||
* 核心逻辑:图片编辑场景优先使用 alibaba-image (灵魂4.5)
|
||
*/
|
||
|
||
import { isAiModelMultimodal } from "@/lib/db/ai-model-flags";
|
||
import {
|
||
isImageGenerationModelDescriptor,
|
||
modelSupportsTextToImageDescriptor,
|
||
isVideoModelDescriptor,
|
||
} from "@/lib/ai/model-classification";
|
||
import { isKnownReferenceImageModel } from "@/lib/ai/reference-image-capability";
|
||
|
||
type EnabledModelWithProvider = Awaited<
|
||
ReturnType<typeof import("@/lib/db/queries")["getEnabledAiModelsWithProvider"]>
|
||
>[number];
|
||
|
||
// 使用动态导入避免循环依赖
|
||
// import { getEnabledAiModelsWithProvider } from "@/lib/db/queries";
|
||
|
||
// 路由结果类型
|
||
export interface RouteResult {
|
||
id: string;
|
||
displayName: string;
|
||
providerType: string;
|
||
modelId: string;
|
||
reason: string;
|
||
}
|
||
|
||
function hasVisionCapability(model: {
|
||
supportsVision?: boolean | null;
|
||
capabilities?: unknown;
|
||
defaultParams?: unknown;
|
||
modelId?: unknown;
|
||
}, providerType?: string | null) {
|
||
return (
|
||
Boolean(model.supportsVision) ||
|
||
isKnownReferenceImageModel(model, { type: providerType }) ||
|
||
isAiModelMultimodal(model)
|
||
);
|
||
}
|
||
|
||
const CHAT_PROVIDER_TYPES = [
|
||
"alibaba",
|
||
"zhipu",
|
||
"openai",
|
||
"custom",
|
||
"local",
|
||
"gemini",
|
||
"huanke-gemini",
|
||
"volcano-multimodal",
|
||
] as const;
|
||
|
||
const IMAGE_ONLY_PROVIDER_TYPES = new Set([
|
||
"alibaba-image",
|
||
"bfl-image",
|
||
"ruxa-image",
|
||
"fal-image",
|
||
"snapgen-image",
|
||
"openai-image",
|
||
"bananaimage",
|
||
"visionary",
|
||
"atlascloud",
|
||
"stepfun-image",
|
||
"midjourney",
|
||
"gemini-image",
|
||
]);
|
||
|
||
function isVideoProviderType(
|
||
providerType: string,
|
||
model?: {
|
||
modelId?: string | null;
|
||
capabilities?: unknown;
|
||
supportsImageGen?: boolean | null;
|
||
}
|
||
) {
|
||
return isVideoModelDescriptor({
|
||
providerType,
|
||
modelId: model?.modelId,
|
||
capabilities: model?.capabilities,
|
||
supportsImageGen: model?.supportsImageGen,
|
||
});
|
||
}
|
||
|
||
function isConversationalCandidate(
|
||
item: EnabledModelWithProvider,
|
||
options?: { requireVision?: boolean },
|
||
) {
|
||
const providerType = String(item.provider.type || "");
|
||
if (!item.model.isEnabled || item.model.isSubModel) return false;
|
||
if (isVideoProviderType(providerType, item.model)) return false;
|
||
|
||
const requireVision = Boolean(options?.requireVision);
|
||
const matchesPreferredProvider = CHAT_PROVIDER_TYPES.includes(
|
||
providerType as (typeof CHAT_PROVIDER_TYPES)[number],
|
||
);
|
||
const hasVision = hasVisionCapability(item.model, providerType);
|
||
|
||
if (matchesPreferredProvider) {
|
||
return requireVision ? hasVision : true;
|
||
}
|
||
|
||
if (IMAGE_ONLY_PROVIDER_TYPES.has(providerType)) {
|
||
return false;
|
||
}
|
||
|
||
if (requireVision) {
|
||
return hasVision;
|
||
}
|
||
|
||
return !item.model.supportsImageGen;
|
||
}
|
||
|
||
function matchesIntentRequirements(
|
||
item: EnabledModelWithProvider,
|
||
params: {
|
||
intent: string;
|
||
requireVision?: boolean;
|
||
requireImageGen?: boolean;
|
||
},
|
||
) {
|
||
if (!item.model.isEnabled || item.model.isSubModel) return false;
|
||
|
||
if (params.intent === "chat") {
|
||
return isConversationalCandidate(item);
|
||
}
|
||
|
||
if (params.intent === "vision") {
|
||
return (
|
||
!isVideoProviderType(String(item.provider.type || ""), item.model) &&
|
||
hasVisionCapability(item.model, item.provider.type)
|
||
);
|
||
}
|
||
|
||
if (params.intent === "video_gen") {
|
||
return isVideoProviderType(String(item.provider.type || ""), item.model);
|
||
}
|
||
|
||
if (params.requireVision && !hasVisionCapability(item.model, item.provider.type)) return false;
|
||
if (
|
||
params.requireImageGen &&
|
||
!(params.intent === "image_gen"
|
||
? modelSupportsTextToImageDescriptor({
|
||
providerType: item.provider.type,
|
||
modelId: item.model.modelId,
|
||
capabilities: item.model.capabilities,
|
||
defaultParams: item.model.defaultParams,
|
||
supportsImageGen: item.model.supportsImageGen,
|
||
})
|
||
: isImageGenerationModelDescriptor({
|
||
providerType: item.provider.type,
|
||
modelId: item.model.modelId,
|
||
capabilities: item.model.capabilities,
|
||
supportsImageGen: item.model.supportsImageGen,
|
||
}))
|
||
) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// 意图到模型的映射配置
|
||
// providerTypes 按优先级排序,第一个是首选
|
||
const INTENT_MODEL_MAP: Record<
|
||
string,
|
||
{
|
||
providerTypes: string[];
|
||
requireVision?: boolean;
|
||
requireImageGen?: boolean;
|
||
description: string;
|
||
}
|
||
> = {
|
||
// 图片编辑:优先 alibaba-image(灵魂4.5,支持提示词扩展)
|
||
image_edit: {
|
||
providerTypes: ["alibaba-image", "bfl-image", "ruxa-image", "fal-image", "snapgen-image", "openai-image", "bananaimage", "visionary", "atlascloud", "stepfun-image", "runway", "krea", "gemini", "huanke-gemini"],
|
||
requireVision: true,
|
||
requireImageGen: true,
|
||
description: "图片编辑",
|
||
},
|
||
|
||
// 视觉理解:优先千问3 VL(成本低效果好)
|
||
vision: {
|
||
providerTypes: ["alibaba", "zhipu", "openai", "custom", "gemini", "huanke-gemini", "volcano-multimodal"],
|
||
requireVision: true,
|
||
description: "图片理解",
|
||
},
|
||
|
||
// 图片生成:优先灵魂4.5
|
||
image_gen: {
|
||
providerTypes: ["alibaba-image", "bfl-image", "ruxa-image", "fal-image", "snapgen-image", "openai-image", "bananaimage", "visionary", "atlascloud", "stepfun-image", "runway", "krea", "gemini", "huanke-gemini", "volcano-vision"],
|
||
requireImageGen: true,
|
||
description: "图片生成",
|
||
},
|
||
|
||
// 视频生成
|
||
video_gen: {
|
||
providerTypes: ["doubao-video", "volcano-video", "qianwen-video", "fal-video", "ruxa-video", "runway", "krea", "custom"],
|
||
description: "视频生成",
|
||
},
|
||
|
||
// 普通对话:优先千问3
|
||
chat: {
|
||
providerTypes: ["alibaba", "zhipu", "openai", "custom", "local", "gemini", "huanke-gemini", "volcano-multimodal"],
|
||
description: "对话",
|
||
},
|
||
};
|
||
|
||
// 子意图的特殊路由配置(覆盖默认配置)
|
||
const SUB_INTENT_OVERRIDES: Record<
|
||
string,
|
||
{
|
||
providerTypes?: string[];
|
||
reason?: string;
|
||
}
|
||
> = {
|
||
// 抠图:alibaba-image 效果最好
|
||
remove_background: {
|
||
providerTypes: ["alibaba-image", "bfl-image", "ruxa-image", "fal-image", "snapgen-image", "openai-image", "bananaimage", "visionary", "atlascloud", "stepfun-image", "runway", "krea", "gemini"],
|
||
reason: "抠图首选灵魂4.5",
|
||
},
|
||
|
||
// 换背景:alibaba-image 支持场景生成
|
||
replace_background: {
|
||
providerTypes: ["alibaba-image", "bfl-image", "ruxa-image", "fal-image", "snapgen-image", "openai-image", "bananaimage", "visionary", "atlascloud", "stepfun-image", "runway", "krea", "gemini"],
|
||
reason: "换背景首选灵魂4.5",
|
||
},
|
||
|
||
// 图片增强
|
||
enhance: {
|
||
providerTypes: ["alibaba-image", "bfl-image", "ruxa-image", "fal-image", "snapgen-image", "openai-image", "bananaimage", "visionary", "atlascloud", "stepfun-image", "runway", "krea", "gemini", "volcano-vision"],
|
||
reason: "增强首选灵魂4.5",
|
||
},
|
||
|
||
// 风格迁移:gemini 可能更有创意
|
||
style_transfer: {
|
||
providerTypes: ["gemini", "alibaba-image", "bfl-image", "ruxa-image", "fal-image", "snapgen-image", "openai-image", "bananaimage", "visionary", "atlascloud", "stepfun-image", "runway", "krea"],
|
||
reason: "风格迁移优选Gemini",
|
||
},
|
||
};
|
||
|
||
/**
|
||
* 路由到最佳模型
|
||
*/
|
||
export async function routeToModel(params: {
|
||
intent: string;
|
||
subIntent?: string;
|
||
hasImage: boolean;
|
||
}): Promise<RouteResult> {
|
||
const { intent, subIntent, hasImage } = params;
|
||
|
||
console.log("[Model Router] Routing:", { intent, subIntent, hasImage });
|
||
|
||
// 使用动态导入避免循环依赖
|
||
const { getEnabledAiModelsWithProvider } = await import("@/lib/db/queries");
|
||
const { isAiModelAgentAuto } = await import("@/lib/db/ai-model-flags");
|
||
const { db } = await import("@/lib/db");
|
||
const { siteSettings } = await import("@/lib/db/schema");
|
||
|
||
// 获取所有可用模型
|
||
const allModelsRaw = await getEnabledAiModelsWithProvider();
|
||
const nonAgentModels = allModelsRaw.filter(
|
||
(item) => !isAiModelAgentAuto(item.model)
|
||
);
|
||
const allModels = nonAgentModels.length > 0 ? nonAgentModels : allModelsRaw;
|
||
console.log("[Model Router] Available models:", allModels.length);
|
||
|
||
// 获取意图对应的配置
|
||
let config = INTENT_MODEL_MAP[intent] || INTENT_MODEL_MAP["chat"];
|
||
|
||
// 检查是否有子意图的特殊配置
|
||
if (subIntent && SUB_INTENT_OVERRIDES[subIntent]) {
|
||
const override = SUB_INTENT_OVERRIDES[subIntent];
|
||
if (override.providerTypes) {
|
||
config = { ...config, providerTypes: override.providerTypes };
|
||
}
|
||
}
|
||
|
||
// 🆕 检查是否有配置的默认生图模型(仅在能力匹配时使用)
|
||
const [settings] = await db.select().from(siteSettings).limit(1);
|
||
const configuredDefaultModelId = settings?.canvasAutoModeDefaultModelId;
|
||
|
||
if (configuredDefaultModelId && intent !== "chat") {
|
||
const configuredModel = allModels.find(
|
||
(m) => m.model.id === configuredDefaultModelId && m.model.isEnabled && !m.model.isSubModel
|
||
);
|
||
|
||
if (configuredModel) {
|
||
const isImageIntent = intent === "image_edit" || intent === "image_gen";
|
||
const matchesVision =
|
||
!config.requireVision ||
|
||
hasVisionCapability(configuredModel.model, configuredModel.provider.type);
|
||
const configuredModelSupportsRequestedImageGeneration =
|
||
intent === "image_gen"
|
||
? modelSupportsTextToImageDescriptor({
|
||
providerType: configuredModel.provider.type,
|
||
modelId: configuredModel.model.modelId,
|
||
capabilities: configuredModel.model.capabilities,
|
||
defaultParams: configuredModel.model.defaultParams,
|
||
supportsImageGen: configuredModel.model.supportsImageGen,
|
||
})
|
||
: configuredModel.model.supportsImageGen;
|
||
const matchesImageGen =
|
||
!config.requireImageGen || configuredModelSupportsRequestedImageGeneration;
|
||
const allowImageIntentWithImageGenOnly =
|
||
isImageIntent && configuredModelSupportsRequestedImageGeneration;
|
||
|
||
if ((matchesVision && matchesImageGen) || allowImageIntentWithImageGenOnly) {
|
||
const reason = allowImageIntentWithImageGenOnly && !matchesVision
|
||
? `管理员配置的默认模型(按生图能力优先)→ ${configuredModel.model.displayName}`
|
||
: `管理员配置的默认模型 → ${configuredModel.model.displayName}`;
|
||
console.log("[Model Router] ✅ Using configured default model:", configuredModel.model.displayName);
|
||
if (allowImageIntentWithImageGenOnly && !matchesVision) {
|
||
console.log(
|
||
"[Model Router] ℹ️ Configured default model does not support vision, but image intent can reuse Agent analysis and keep admin default image model."
|
||
);
|
||
}
|
||
return {
|
||
id: configuredModel.model.id,
|
||
displayName: configuredModel.model.displayName,
|
||
providerType: configuredModel.provider.type,
|
||
modelId: configuredModel.model.modelId,
|
||
reason,
|
||
};
|
||
}
|
||
|
||
console.log("[Model Router] ⚠️ Configured default model does not match intent requirements, falling back");
|
||
} else {
|
||
console.log("[Model Router] ⚠️ Configured default model not found or disabled, falling back to intent routing");
|
||
}
|
||
} else if (configuredDefaultModelId && intent === "chat") {
|
||
console.log("[Model Router] ℹ️ Skip configured default image model for chat intent");
|
||
}
|
||
|
||
// 按优先级查找匹配的模型
|
||
for (const providerType of config.providerTypes) {
|
||
const candidates = allModels.filter((m) => {
|
||
// 匹配 provider 类型
|
||
if (m.provider.type !== providerType) return false;
|
||
|
||
return matchesIntentRequirements(m, {
|
||
intent,
|
||
requireVision: config.requireVision,
|
||
requireImageGen: config.requireImageGen,
|
||
});
|
||
});
|
||
|
||
if (candidates.length > 0) {
|
||
// 优先选择默认模型
|
||
const defaultModel = candidates.find((c) => c.model.isDefault);
|
||
const selected = defaultModel || candidates[0];
|
||
|
||
const result: RouteResult = {
|
||
id: selected.model.id,
|
||
displayName: selected.model.displayName,
|
||
providerType: selected.provider.type,
|
||
modelId: selected.model.modelId,
|
||
reason: subIntent
|
||
? `${config.description}/${subIntent} → ${selected.model.displayName}`
|
||
: `${config.description} → ${selected.model.displayName}`,
|
||
};
|
||
|
||
console.log("[Model Router] ✅ Selected:", result);
|
||
return result;
|
||
}
|
||
}
|
||
|
||
const fallbackCandidates = allModels.filter((item) => {
|
||
if (intent === "chat") {
|
||
return isConversationalCandidate(item, { requireVision: false });
|
||
}
|
||
if (intent === "vision") {
|
||
return isConversationalCandidate(item, { requireVision: true });
|
||
}
|
||
return (
|
||
!item.model.isSubModel &&
|
||
item.model.isEnabled &&
|
||
!isVideoProviderType(String(item.provider.type || ""), item.model) &&
|
||
((intent === "image_gen"
|
||
? modelSupportsTextToImageDescriptor({
|
||
providerType: item.provider.type,
|
||
modelId: item.model.modelId,
|
||
capabilities: item.model.capabilities,
|
||
defaultParams: item.model.defaultParams,
|
||
supportsImageGen: item.model.supportsImageGen,
|
||
})
|
||
: item.model.supportsImageGen) ||
|
||
hasVisionCapability(item.model, item.provider.type))
|
||
);
|
||
});
|
||
|
||
if (fallbackCandidates.length > 0) {
|
||
const fallback = fallbackCandidates[0];
|
||
console.log("[Model Router] Using intent-safe fallback:", fallback.model.displayName);
|
||
return {
|
||
id: fallback.model.id,
|
||
displayName: fallback.model.displayName,
|
||
providerType: fallback.provider.type,
|
||
modelId: fallback.model.modelId,
|
||
reason: `fallback (${config.description})`,
|
||
};
|
||
}
|
||
|
||
// 最终兜底:返回任意可用模型
|
||
const anyModel =
|
||
(intent === "chat"
|
||
? allModels.find(
|
||
(m) =>
|
||
!m.model.isSubModel &&
|
||
m.model.isEnabled &&
|
||
!isVideoProviderType(String(m.provider.type || ""), m.model) &&
|
||
!m.model.supportsImageGen,
|
||
)
|
||
: undefined) ||
|
||
allModels.find((m) => !m.model.isSubModel && m.model.isEnabled);
|
||
if (anyModel) {
|
||
console.log("[Model Router] Using any available model:", anyModel.model.displayName);
|
||
return {
|
||
id: anyModel.model.id,
|
||
displayName: anyModel.model.displayName,
|
||
providerType: anyModel.provider.type,
|
||
modelId: anyModel.model.modelId,
|
||
reason: "fallback (any)",
|
||
};
|
||
}
|
||
|
||
// 如果没有任何模型,抛出错误
|
||
throw new Error("No available models for routing");
|
||
}
|
||
|
||
/**
|
||
* 获取意图的推荐模型列表(用于 UI 展示)
|
||
*/
|
||
export function getRecommendedModelsForIntent(intent: string): string[] {
|
||
const config = INTENT_MODEL_MAP[intent];
|
||
return config ? config.providerTypes : INTENT_MODEL_MAP["chat"].providerTypes;
|
||
}
|
||
|
||
/**
|
||
* 检查模型是否适合指定意图
|
||
*/
|
||
export function isModelSuitableForIntent(
|
||
providerType: string,
|
||
intent: string,
|
||
supportsVision: boolean,
|
||
supportsImageGen: boolean
|
||
): boolean {
|
||
const config = INTENT_MODEL_MAP[intent];
|
||
if (!config) return true; // 未知意图,允许任何模型
|
||
|
||
// 检查 provider 类型是否匹配
|
||
if (!config.providerTypes.includes(providerType)) {
|
||
return false;
|
||
}
|
||
|
||
// 检查能力要求
|
||
if (config.requireVision && !supportsVision) return false;
|
||
if (config.requireImageGen && !supportsImageGen) return false;
|
||
|
||
return true;
|
||
}
|