333 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

import { auth } from "@/app/(auth)/auth";
import {
getAgentAiModelWithProvider,
getDefaultAiModel,
getEnabledAiModelsWithProvider,
getFrontendDisplayAiModelsWithProvider,
getSiteSettings,
getSystemSetting,
getUserById,
} from "@/lib/db/queries";
import { getAiModelFrontendAvailability } from "@/lib/ai/model-frontend-availability";
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";
import {
AGENT_WORKFLOW_UI_CONFIG_KEY,
getAgentWorkflowBusinessModeSettings,
normalizeAgentWorkflowUiConfig,
} from "@/lib/agents/workflow-ui-config";
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");
});
}
function isWorkflowImageGenerationModel(model: {
providerType?: string | null;
modelId?: string | null;
capabilities?: unknown;
supportsImageGen?: boolean | null;
}) {
return (
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,
})
);
}
// 获取前端可用的 AI 模型列表(需要登录但不需要管理员权限)
export async function GET(request: Request) {
try {
const isLocalCanvasE2E = process.env.CANVAS_E2E_LOCAL === "true";
if (isLocalCanvasE2E) {
const models = [
{
id: "local-e2e-image-model",
modelId: "nano-banana-pro-local",
displayName: "Nano Banana Pro",
avatarUrl: null,
description: "本地视频画布测试图片模型",
supportsVision: true,
supportsImageGen: true,
isMultimodal: true,
isDefault: true,
providerName: "Local E2E",
providerType: "local",
isSubModel: false,
parentModelId: null,
capabilities: { image: true },
videoToolCapability: null,
imageGenConfig: {
creditCostPerImage: 1,
creditCostPerVideo: 20,
videoDurationOptions: [5, 10],
},
sortOrder: 0,
selectable: true,
},
{
id: "local-e2e-video-model",
modelId: "veo-3.1-fast",
displayName: "Veo 3.1 Fast(本地布局测试)",
avatarUrl: null,
description: "本地视频画布测试视频模型",
supportsVision: true,
supportsImageGen: false,
isMultimodal: true,
isDefault: false,
providerName: "Local E2E",
providerType: "vip98k",
isSubModel: false,
parentModelId: null,
capabilities: { kind: "video", supportedModes: ["text-to-video", "first-frame", "first-last-frame", "reference-images"], supportedRatios:["16:9"], supportedResolutions:["720p","1080p"], durationOptions:[8], maxReferenceImages:3 },
videoToolCapability: {
supported: true,
durations: [8],
resolutions: ["720p", "1080p"],
},
imageGenConfig: {
creditCostPerVideo: 20,
videoDurationOptions: [8],
},
sortOrder: 1,
selectable: true,
},
];
return Response.json({
models,
defaultModelId: models[0].id,
agentWorkflowConfiguredDefaultModelId: null,
agentWorkflowDefaultModelId: null,
agentWorkflowConfiguredModels: [],
agentModel: null,
});
}
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);
const includeUnavailable =
new URL(request.url).searchParams.get("includeUnavailable") === "1";
// 只获取主模型(不包括子模型),子模型由后端根据 videoParams.mode 自动选择
const modelsWithProvider = includeUnavailable
? await getFrontendDisplayAiModelsWithProvider(false, true)
: await getEnabledAiModelsWithProvider(false, true);
const agentModelWithProvider = await getAgentAiModelWithProvider();
const defaultModel = await getDefaultAiModel();
const [siteSettings, agentWorkflowUiConfigSetting] = await Promise.all([
getSiteSettings(),
getSystemSetting({ key: AGENT_WORKFLOW_UI_CONFIG_KEY }).catch(() => null),
]);
type ModelWithProvider = (typeof modelsWithProvider)[number];
const serializeModel = ({ model, provider }: ModelWithProvider) => ({
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 || null) as
| { videoDurationOptions?: number[] | null }
| null,
}),
imageGenConfig: model.imageGenConfig,
sortOrder: model.sortOrder ?? 0,
...getAiModelFrontendAvailability(model),
});
const workflowImageModelById = new Map<string, ModelWithProvider>();
modelsWithProvider.forEach((entry) => {
const { model, provider } = entry;
if (
!isAiModelAgentAuto(model) &&
isWorkflowImageGenerationModel({
providerType: provider.type,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
})
) {
workflowImageModelById.set(model.id, entry);
}
});
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(serializeModel);
const selectableModels = models.filter((model) => model.selectable !== false);
const preferredNanoBananaModel = models.find(
(model) =>
model.selectable !== false &&
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) =>
model.selectable !== false &&
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) &&
selectableModels.some((model) => model.id === defaultModel.id)
? defaultModel.id
: selectableModels.find((model) => model.isDefault)?.id)) ??
selectableModels[0]?.id ??
null;
const configuredAgentWorkflowModelId = String(
siteSettings?.agentWorkflowDefaultModelId || ""
).trim();
const hasConfiguredAgentWorkflowModel = Boolean(
configuredAgentWorkflowModelId &&
configuredAgentWorkflowModelId !== "auto" &&
configuredAgentWorkflowModelId !== "__auto"
);
const agentWorkflowDefaultModel = hasConfiguredAgentWorkflowModel
? workflowImageModelById.get(configuredAgentWorkflowModelId) || null
: null;
const workflowUiConfig = normalizeAgentWorkflowUiConfig(
agentWorkflowUiConfigSetting?.value
);
const workflowBusinessModeSettings = getAgentWorkflowBusinessModeSettings(
workflowUiConfig
);
const configuredWorkflowModelIds = new Set<string>();
if (agentWorkflowDefaultModel) {
configuredWorkflowModelIds.add(agentWorkflowDefaultModel.model.id);
}
workflowBusinessModeSettings.modes.forEach((mode) => {
const modelId = String(mode.defaultModelId || "").trim();
if (modelId && workflowImageModelById.has(modelId)) {
configuredWorkflowModelIds.add(modelId);
}
});
const agentWorkflowConfiguredModels = Array.from(configuredWorkflowModelIds)
.map((id) => workflowImageModelById.get(id))
.filter((entry): entry is ModelWithProvider => Boolean(entry))
.map(serializeModel);
return Response.json({
models,
defaultModelId: resolvedDefaultModelId,
agentWorkflowConfiguredDefaultModelId: hasConfiguredAgentWorkflowModel
? configuredAgentWorkflowModelId
: null,
agentWorkflowDefaultModelId: agentWorkflowDefaultModel?.model.id || null,
agentWorkflowConfiguredModels,
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();
}
}