import "server-only"; import type { DiscountableImageGenConfigLike } from "@/lib/ai/image-model-pricing"; import { getImageCreditCostFromConfig, getImageModelCreditDiscountLabel, } from "@/lib/ai/image-model-pricing"; import { getAiModelFrontendAvailability, type ModelFrontendAvailabilityStatus, } from "@/lib/ai/model-frontend-availability"; import { getEnabledAiModelsWithProvider, getFrontendDisplayAiModelsWithProvider, } from "@/lib/db/queries"; export type StyleProModelEntry = Awaited< ReturnType >[number]; export type StyleProDisplayModelEntry = Awaited< ReturnType >[number]; export type PublicStyleProModelOption = { id: string; modelId: string; displayName: string; avatarUrl: string | null; providerName: string; providerType: string; isDefault: boolean; unitCost: number; creditDiscountLabel: string | null; availabilityStatus: ModelFrontendAvailabilityStatus; availabilityLabel: string | null; selectable: boolean; disabledReason: string | null; }; const VIDEO_PROVIDER_TYPES = new Set([ "grsai-video", "qianwen-video", "doubao-video", "ruxa-video", "volcano-video", "fal-video", "runway", ]); const VIDEO_MODEL_KEYWORDS = [ "/generate/video/", "video", "veo", "sora", "kling", "hailuo", "seedance", "runway", "gen-3", "gen-4", "gen 3", "gen 4", "wan-2.", "wan 2.", "t2v", "i2v", ]; const EXPLICIT_IMAGE_MODEL_KEYWORDS = [ "/generate/image/", "gpt-image", "sora_image", "qwen-image", "seedream", "nano-banana", "banana", "flux", "z-image", "image-edit", "gen4_image", "gen-4 image", ]; function hasVideoGenerationConfig(value: unknown) { if (!value || typeof value !== "object") return false; const config = value as Record; return ( "creditCostPerVideo" in config || "creditCostPerVideoByDuration" in config || "creditCostPerVideoByResolution" in config || "videoDurationOptions" in config || "videoResolutionOptions" in config ); } function hasVideoCapabilities(value: unknown) { if (!value || typeof value !== "object") return false; const serialized = JSON.stringify(value).toLowerCase(); return ( serialized.includes("text-to-video") || serialized.includes("image-to-video") ); } function isExplicitImageGenerationModel( entry: StyleProModelEntry | StyleProDisplayModelEntry ) { const searchable = [ entry.model.modelId, entry.model.displayName, entry.model.description, ] .filter(Boolean) .join(" ") .toLowerCase(); if ( searchable.includes("image-to-video") || searchable.includes("text-to-video") || searchable.includes("i2v") || searchable.includes("t2v") ) { return false; } return EXPLICIT_IMAGE_MODEL_KEYWORDS.some((keyword) => searchable.includes(keyword) ); } function isVideoLikeModel( entry: StyleProModelEntry | StyleProDisplayModelEntry ) { if (VIDEO_PROVIDER_TYPES.has(entry.provider.type)) return true; if ( hasVideoGenerationConfig(entry.model.imageGenConfig) || hasVideoCapabilities(entry.model.capabilities) || hasVideoCapabilities(entry.model.defaultParams) ) { return true; } const searchable = [ entry.provider.type, entry.provider.name, entry.model.modelId, entry.model.displayName, entry.model.description, ] .filter(Boolean) .join(" ") .toLowerCase(); return VIDEO_MODEL_KEYWORDS.some((keyword) => searchable.includes(keyword)); } export function isStyleProImageModel( entry: StyleProModelEntry | StyleProDisplayModelEntry ) { if (!entry.model.supportsImageGen) return false; if (isExplicitImageGenerationModel(entry)) return true; return !isVideoLikeModel(entry); } export async function getStyleProImageModelEntries(options?: { includeHidden?: boolean; }) { const models = await getEnabledAiModelsWithProvider( false, options?.includeHidden === true ); return models.filter(isStyleProImageModel); } export async function getStyleProImageModelDisplayEntries(options?: { includeHidden?: boolean; }) { const models = await getFrontendDisplayAiModelsWithProvider( false, options?.includeHidden === true ); return models.filter(isStyleProImageModel); } export function findStyleProImageModelByRef( entries: Array, modelRef?: string | null ) { const normalized = String(modelRef || "").trim(); if (!normalized) return null; return ( entries.find( ({ model }) => model.id === normalized || model.modelId === normalized ) || null ); } export function sortStyleProImageModelEntries< T extends StyleProModelEntry | StyleProDisplayModelEntry, >(entries: T[]) { return [...entries].sort((left, right) => { const leftOrder = Number.isFinite(Number(left.model.sortOrder)) ? Number(left.model.sortOrder) : 0; const rightOrder = Number.isFinite(Number(right.model.sortOrder)) ? Number(right.model.sortOrder) : 0; if (leftOrder !== rightOrder) return leftOrder - rightOrder; const leftName = String(left.model.displayName || left.model.modelId || ""); const rightName = String(right.model.displayName || right.model.modelId || ""); return leftName.localeCompare(rightName, "zh-Hans-CN"); }); } export function toPublicStyleProModelOption( entry: StyleProModelEntry | StyleProDisplayModelEntry ): PublicStyleProModelOption { const imageGenConfig = (entry.model.imageGenConfig || null) as DiscountableImageGenConfigLike | null; const availability = getAiModelFrontendAvailability(entry.model); return { id: entry.model.id, modelId: entry.model.modelId, displayName: entry.model.displayName, avatarUrl: entry.model.avatarUrl || null, providerName: entry.provider.name, providerType: entry.provider.type, isDefault: entry.model.isDefault, unitCost: getImageCreditCostFromConfig(imageGenConfig), creditDiscountLabel: getImageModelCreditDiscountLabel(imageGenConfig), ...availability, }; }