Your Name d72ca0b344
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: polish style pro model visibility and billing
2026-05-20 14:26:24 +08:00

266 lines
10 KiB
TypeScript

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 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();
}
}