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