ai-chatbot/lib/ai/model-router.ts
Your Name ccc2e45b12
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Add BFL image runtime provider
2026-03-21 01:36:53 +08:00

422 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

/**
* 智能模型路由器
* 根据用户意图选择最佳AI模型
*
* 核心逻辑:图片编辑场景优先使用 alibaba-image (灵魂4.5)
*/
import { isAiModelMultimodal } from "@/lib/db/ai-model-flags";
import {
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
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;
}) {
return Boolean(model.supportsVision) || 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",
"openai-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);
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)
);
}
if (params.intent === "video_gen") {
return isVideoProviderType(String(item.provider.type || ""), item.model);
}
if (params.requireVision && !hasVisionCapability(item.model)) return false;
if (
params.requireImageGen &&
!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", "openai-image", "runway", "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", "openai-image", "runway", "gemini", "huanke-gemini", "volcano-vision"],
requireImageGen: true,
description: "图片生成",
},
// 视频生成
video_gen: {
providerTypes: ["doubao-video", "volcano-video", "qianwen-video", "fal-video", "ruxa-video", "runway"],
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", "openai-image", "runway", "gemini"],
reason: "抠图首选灵魂4.5",
},
// 换背景:alibaba-image 支持场景生成
replace_background: {
providerTypes: ["alibaba-image", "bfl-image", "ruxa-image", "fal-image", "openai-image", "runway", "gemini"],
reason: "换背景首选灵魂4.5",
},
// 图片增强
enhance: {
providerTypes: ["alibaba-image", "bfl-image", "ruxa-image", "fal-image", "openai-image", "runway", "gemini", "volcano-vision"],
reason: "增强首选灵魂4.5",
},
// 风格迁移:gemini 可能更有创意
style_transfer: {
providerTypes: ["gemini", "alibaba-image", "bfl-image", "ruxa-image", "fal-image", "openai-image", "runway"],
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);
const matchesImageGen = !config.requireImageGen || configuredModel.model.supportsImageGen;
const allowImageIntentWithImageGenOnly =
isImageIntent && configuredModel.model.supportsImageGen;
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) &&
(item.model.supportsImageGen || hasVisionCapability(item.model))
);
});
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;
}