167 lines
6.2 KiB
TypeScript
167 lines
6.2 KiB
TypeScript
import { auth } from "@/app/(auth)/auth";
|
|
import {
|
|
getAgentAiModelWithProvider,
|
|
getDefaultAiModel,
|
|
getEnabledAiModelsWithProvider,
|
|
getUserById,
|
|
} from "@/lib/db/queries";
|
|
import { buildVideoToolCapability } from "@/lib/ai/video-tool-capability";
|
|
import {
|
|
isImageGenerationModelDescriptor,
|
|
isVideoModelDescriptor,
|
|
} from "@/lib/ai/model-classification";
|
|
import { ChatSDKError } from "@/lib/errors";
|
|
import { isAiModelAgentAuto, isAiModelMultimodal } from "@/lib/db/ai-model-flags";
|
|
|
|
function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) {
|
|
const displayName = String(model.displayName || "").toLowerCase();
|
|
const modelId = String(model.modelId || "").toLowerCase();
|
|
return (
|
|
displayName.includes("nano banana pro") ||
|
|
displayName.includes("nano-banana-pro") ||
|
|
modelId.includes("nano-banana-pro") ||
|
|
modelId.includes("nano_banana_pro") ||
|
|
modelId.includes("nano-banana")
|
|
);
|
|
}
|
|
|
|
function sortModelsForDisplay<T extends { sortOrder?: number | null; displayName?: string | null }>(
|
|
models: T[]
|
|
) {
|
|
return [...models].sort((a, b) => {
|
|
const leftOrder = Number.isFinite(a.sortOrder as number) ? Number(a.sortOrder) : 0;
|
|
const rightOrder = Number.isFinite(b.sortOrder as number) ? Number(b.sortOrder) : 0;
|
|
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
const leftName = String(a.displayName || "");
|
|
const rightName = String(b.displayName || "");
|
|
return leftName.localeCompare(rightName, "zh-Hans-CN");
|
|
});
|
|
}
|
|
|
|
// 获取前端可用的 AI 模型列表(需要登录但不需要管理员权限)
|
|
export async function GET() {
|
|
try {
|
|
const session = await auth();
|
|
|
|
if (!session?.user) {
|
|
return new ChatSDKError("unauthorized:chat").toResponse();
|
|
}
|
|
|
|
const currentUser = await getUserById({ id: session.user.id });
|
|
const allowHiddenModels =
|
|
session.user.type === "admin" || Boolean(currentUser?.isHighVip);
|
|
|
|
// 只获取主模型(不包括子模型),子模型由后端根据 videoParams.mode 自动选择
|
|
const modelsWithProvider = await getEnabledAiModelsWithProvider(false, true);
|
|
const agentModelWithProvider = await getAgentAiModelWithProvider();
|
|
const defaultModel = await getDefaultAiModel();
|
|
const visibleModelsWithProvider = sortModelsForDisplay(
|
|
modelsWithProvider
|
|
.filter(
|
|
({ model }) =>
|
|
!isAiModelAgentAuto(model) &&
|
|
(allowHiddenModels || !model.isHidden)
|
|
)
|
|
.map((item) => item.model)
|
|
);
|
|
const visibleModelIdSet = new Set(visibleModelsWithProvider.map((m) => m.id));
|
|
const visibleModelsWithProviderSorted = modelsWithProvider.filter(({ model }) =>
|
|
visibleModelIdSet.has(model.id)
|
|
).sort((a, b) => {
|
|
const left = visibleModelsWithProvider.findIndex((m) => m.id === a.model.id);
|
|
const right = visibleModelsWithProvider.findIndex((m) => m.id === b.model.id);
|
|
return left - right;
|
|
});
|
|
|
|
// 转换为前端需要的格式
|
|
const models = visibleModelsWithProviderSorted.map(({ model, provider }) => ({
|
|
id: model.id,
|
|
modelId: model.modelId,
|
|
displayName: model.displayName,
|
|
avatarUrl: model.avatarUrl,
|
|
description: model.description,
|
|
supportsVision: model.supportsVision,
|
|
supportsImageGen: model.supportsImageGen,
|
|
isMultimodal: isAiModelMultimodal(model),
|
|
isDefault: model.isDefault,
|
|
providerName: provider.name,
|
|
providerType: provider.type,
|
|
isSubModel: model.isSubModel,
|
|
parentModelId: model.parentModelId,
|
|
capabilities: model.capabilities, // 🆕 返回子模型的能力配置
|
|
videoToolCapability: buildVideoToolCapability({
|
|
providerType: provider.type,
|
|
modelId: model.modelId,
|
|
rawCapabilities: model.capabilities,
|
|
}),
|
|
imageGenConfig: model.imageGenConfig,
|
|
sortOrder: model.sortOrder ?? 0,
|
|
}));
|
|
const preferredNanoBananaModel = models.find(
|
|
(model) =>
|
|
isImageGenerationModelDescriptor({
|
|
providerType: model.providerType,
|
|
modelId: model.modelId,
|
|
capabilities: model.capabilities,
|
|
supportsImageGen: model.supportsImageGen,
|
|
}) &&
|
|
!isVideoModelDescriptor({
|
|
providerType: model.providerType,
|
|
modelId: model.modelId,
|
|
capabilities: model.capabilities,
|
|
supportsImageGen: model.supportsImageGen,
|
|
}) &&
|
|
isNanoBananaProModel(model)
|
|
);
|
|
const preferredImageModel = models.find(
|
|
(model) =>
|
|
isImageGenerationModelDescriptor({
|
|
providerType: model.providerType,
|
|
modelId: model.modelId,
|
|
capabilities: model.capabilities,
|
|
supportsImageGen: model.supportsImageGen,
|
|
}) &&
|
|
!isVideoModelDescriptor({
|
|
providerType: model.providerType,
|
|
modelId: model.modelId,
|
|
capabilities: model.capabilities,
|
|
supportsImageGen: model.supportsImageGen,
|
|
})
|
|
);
|
|
|
|
const resolvedDefaultModelId =
|
|
(preferredNanoBananaModel?.id ??
|
|
preferredImageModel?.id ??
|
|
(defaultModel &&
|
|
!isAiModelAgentAuto(defaultModel) &&
|
|
models.some((model) => model.id === defaultModel.id)
|
|
? defaultModel.id
|
|
: models.find((model) => model.isDefault)?.id)) ??
|
|
models[0]?.id ??
|
|
null;
|
|
|
|
return Response.json({
|
|
models,
|
|
defaultModelId: resolvedDefaultModelId,
|
|
agentModel: agentModelWithProvider
|
|
? {
|
|
id: agentModelWithProvider.model.id,
|
|
modelId: agentModelWithProvider.model.modelId,
|
|
displayName: agentModelWithProvider.model.displayName,
|
|
avatarUrl: agentModelWithProvider.model.avatarUrl,
|
|
providerType: agentModelWithProvider.provider.type,
|
|
supportsVision: agentModelWithProvider.model.supportsVision,
|
|
supportsImageGen: agentModelWithProvider.model.supportsImageGen,
|
|
isMultimodal: isAiModelMultimodal(agentModelWithProvider.model),
|
|
imageGenConfig: agentModelWithProvider.model.imageGenConfig,
|
|
}
|
|
: null,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ChatSDKError) {
|
|
return error.toResponse();
|
|
}
|
|
return new ChatSDKError("offline:chat").toResponse();
|
|
}
|
|
}
|