import "server-only"; export const VIP98K_BASE_URL = "https://api.vip98k.com"; export type Vip98kCatalogModelType = | "image" | "video" | "image_tool" | "document_tool" | "model" | "audio" | "world" | string; export interface Vip98kCatalogModel { modelId?: string; id?: string; name?: string; displayName?: string; display_name?: string; description?: string; type?: Vip98kCatalogModelType; modelType?: Vip98kCatalogModelType; model_type?: Vip98kCatalogModelType; category?: Vip98kCatalogModelType; capabilities?: unknown; inputModes?: unknown; input_modes?: unknown; outputModes?: unknown; output_modes?: unknown; supportedResolutions?: unknown; supported_resolutions?: unknown; resolutions?: unknown; durations?: unknown; durationOptions?: unknown; duration_options?: unknown; defaultDuration?: unknown; default_duration?: unknown; supportsAsync?: unknown; supports_async?: unknown; pollIntervalSeconds?: unknown; poll_interval_seconds?: unknown; maxPollSeconds?: unknown; max_poll_seconds?: unknown; resultTTLSeconds?: unknown; result_ttl_seconds?: unknown; billingMode?: string; billing_mode?: string; pricing?: unknown; price?: unknown; status?: string; [key: string]: unknown; } export interface Vip98kModelPreset { modelId: string; displayName: string; description: string; type: Vip98kCatalogModelType; supportsVision: boolean; supportsImageGen: boolean; isHidden: boolean; capabilities: Record; defaultParams: Record; imageGenConfig: Record | null; sortOrder: number; } const DEFAULT_ASPECT_RATIOS = [ "auto", "1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "4:5", "5:4", ] as const; const DEFAULT_VIDEO_RATIOS = ["16:9", "9:16", "1:1", "4:3", "3:4"] as const; const DEFAULT_IMAGE_SIZES = ["1K", "2K", "4K"] as const; const TYPE_SORT_ORDER: Record = { image: 100, video: 200, world: 300, model: 400, image_tool: 800, audio: 900, document_tool: 950, }; function toRecord(value: unknown): Record | null { if (!value || typeof value !== "object" || Array.isArray(value)) return null; return value as Record; } function parseStringArray(value: unknown): string[] { if (!Array.isArray(value)) return []; return Array.from( new Set( value .map((item) => String(item || "").trim()) .filter(Boolean), ), ); } function parseNumberArray(value: unknown): number[] { if (!Array.isArray(value)) return []; return Array.from( new Set( value .map((item) => Number(item)) .filter((item) => Number.isFinite(item) && item > 0), ), ).sort((a, b) => a - b); } function parseNumber(value: unknown): number | undefined { const numeric = Number(value); return Number.isFinite(numeric) ? numeric : undefined; } function normalizeCatalogType(model: Vip98kCatalogModel): Vip98kCatalogModelType { return String( model.type || model.modelType || model.model_type || model.category || "model", ) .trim() .toLowerCase(); } function normalizeImageSize(value: string): string | null { const normalized = value.trim().toUpperCase(); if (normalized === "1K" || normalized === "2K" || normalized === "4K") { return normalized; } const kMatch = normalized.match(/^(\d+(?:\.\d+)?)\s*K$/); if (kMatch?.[1]) return `${Number(kMatch[1]).toString()}K`; return null; } function normalizeVideoResolution(value: string): string | null { const normalized = value.trim().toLowerCase(); if (normalized === "720p" || normalized === "1080p" || normalized === "4k") { return normalized; } if (normalized === "720" || normalized === "720P".toLowerCase()) return "720p"; if (normalized === "1080" || normalized === "1080P".toLowerCase()) return "1080p"; if (normalized === "2160p" || normalized === "2160" || normalized === "4K".toLowerCase()) { return "4k"; } return normalized || null; } function getSupportedResolutions(model: Vip98kCatalogModel) { return parseStringArray( model.supportedResolutions || model.supported_resolutions || model.resolutions, ); } function getSupportedImageSizes(model: Vip98kCatalogModel) { const sizes = getSupportedResolutions(model) .map(normalizeImageSize) .filter((value): value is string => Boolean(value)); return sizes.length ? sizes : [...DEFAULT_IMAGE_SIZES]; } function getSupportedVideoResolutions(model: Vip98kCatalogModel) { const resolutions = getSupportedResolutions(model) .map(normalizeVideoResolution) .filter((value): value is string => Boolean(value)); return resolutions.length ? resolutions : ["720p"]; } function getDurations(model: Vip98kCatalogModel) { return parseNumberArray( model.durations || model.durationOptions || model.duration_options, ); } function getCapabilities(model: Vip98kCatalogModel) { return parseStringArray(model.capabilities).map((item) => item.trim().toLowerCase().replace(/_/g, "-"), ); } function getInputModes(model: Vip98kCatalogModel) { return parseStringArray(model.inputModes || model.input_modes).map((item) => item.trim().toLowerCase(), ); } function getOutputModes(model: Vip98kCatalogModel) { return parseStringArray(model.outputModes || model.output_modes).map((item) => item.trim().toLowerCase(), ); } function getDisplayName(model: Vip98kCatalogModel, modelId: string) { return String( model.displayName || model.display_name || model.name || modelId, ).trim(); } function getModelId(model: Vip98kCatalogModel) { return String(model.modelId || model.id || model.name || "").trim(); } function getEffectivePrice(model: Vip98kCatalogModel) { const pricing = toRecord(model.pricing); return parseNumber( model.price || pricing?.effectivePrice || pricing?.effective_price || pricing?.price || pricing?.requestPrice || pricing?.request_price, ); } function getBillingMode(model: Vip98kCatalogModel) { return String(model.billingMode || model.billing_mode || "per_request").trim(); } function getVip98kMetadata(model: Vip98kCatalogModel, type: string) { return { modelType: type, taskApi: "external-task", supportsAsync: Boolean(model.supportsAsync ?? model.supports_async ?? true), pollIntervalSeconds: parseNumber( model.pollIntervalSeconds || model.poll_interval_seconds, ) || 8, maxPollSeconds: parseNumber(model.maxPollSeconds || model.max_poll_seconds) || 600, resultTTLSeconds: parseNumber( model.resultTTLSeconds || model.result_ttl_seconds, ), billingMode: getBillingMode(model), effectivePrice: getEffectivePrice(model), pricing: model.pricing || null, status: model.status || null, }; } function createImagePreset( model: Vip98kCatalogModel, index: number, ): Vip98kModelPreset { const modelId = getModelId(model); const displayName = getDisplayName(model, modelId); const capabilities = getCapabilities(model); const inputModes = getInputModes(model); const outputModes = getOutputModes(model); const supportedImageSizes = getSupportedImageSizes(model); const supportsReferenceImages = capabilities.includes("image-to-image") || inputModes.includes("url") || inputModes.includes("file"); const supportedModes = [ ...(capabilities.includes("text-to-image") || capabilities.length === 0 ? ["text-to-image"] : []), ...(supportsReferenceImages ? ["image-to-image", "reference-images"] : []), ]; const maxReferenceImages = supportsReferenceImages ? 8 : 0; const defaultImageSize = supportedImageSizes.find((size) => size === "2K") || supportedImageSizes[0] || "1K"; return { modelId, displayName, description: model.description || `VIP98K image model (${modelId})`, type: "image", supportsVision: supportsReferenceImages, supportsImageGen: true, isHidden: false, capabilities: { provider: "vip98k", kind: "image", mediaType: "image", modelType: "image", outputType: "image", supportedModes, inputModes, outputModes, supportedImageSizes, supportedAspectRatios: [...DEFAULT_ASPECT_RATIOS], supportsMultiImageReference: supportsReferenceImages, maxReferenceImages, vip98k: getVip98kMetadata(model, "image"), }, defaultParams: { provider: "vip98k", kind: "image", endpoint: modelId, apiFormat: "openai-images-compatible", taskApi: "openai-images", supportedModes, imageSize: defaultImageSize, aspectRatio: "auto", imageCount: 1, }, imageGenConfig: { maxImages: 1, maxReferenceImages, supportsSize: true, supports1K: supportedImageSizes.includes("1K"), supports2K: supportedImageSizes.includes("2K"), supports4K: supportedImageSizes.includes("4K"), supportedImageSizes, supportedAspectRatios: [...DEFAULT_ASPECT_RATIOS], supportsPromptExtend: false, supportsNegativePrompt: false, supportsWatermark: false, supportsSeed: false, creditCostPerImage: 0, creditCostMultiplierBySize: { "1K": 1, "2K": 1, "4K": 1, }, enableLoadingPlaceholder: true, loadingColor: "yellow", vip98k: getVip98kMetadata(model, "image"), }, sortOrder: TYPE_SORT_ORDER.image + index, }; } function createVideoPreset( model: Vip98kCatalogModel, index: number, ): Vip98kModelPreset { const modelId = getModelId(model); const displayName = getDisplayName(model, modelId); const capabilities = getCapabilities(model); const inputModes = getInputModes(model); const outputModes = getOutputModes(model); const supportedResolutions = getSupportedVideoResolutions(model); const durations = getDurations(model); const defaultDuration = parseNumber(model.defaultDuration || model.default_duration) || durations[durations.length - 1] || 8; const supportsReferenceImages = capabilities.includes("image-to-video") || inputModes.includes("url") || inputModes.includes("file"); const supportedModes = [ ...(capabilities.includes("text-to-video") || capabilities.length === 0 ? ["text-to-video"] : []), ...(supportsReferenceImages ? ["image-to-video", "first-frame"] : []), ]; return { modelId, displayName, description: model.description || `VIP98K video model (${modelId})`, type: "video", supportsVision: supportsReferenceImages, supportsImageGen: false, isHidden: false, capabilities: { provider: "vip98k", kind: "video", mediaType: "video", modelType: "video", outputType: "video", supportedModes, inputModes, outputModes, supportedResolutions, supportedRatios: [...DEFAULT_VIDEO_RATIOS], durationOptions: durations, durationRange: durations.length ? { min: durations[0], max: durations[durations.length - 1] } : { min: defaultDuration, max: defaultDuration }, defaultDuration, supportWatermark: false, supportCameraFixed: false, maxReferenceImages: supportsReferenceImages ? 4 : 0, vip98k: getVip98kMetadata(model, "video"), }, defaultParams: { provider: "vip98k", kind: "video", endpoint: modelId, taskApi: "external-task", supportedModes, duration: defaultDuration, resolution: supportedResolutions[0] || "720p", ratio: "16:9", }, imageGenConfig: { creditCostPerVideo: 0, videoDurationOptions: durations, creditCostPerVideoByDuration: Object.fromEntries( durations.map((duration) => [String(duration), 0]), ), creditCostPerVideoByResolution: Object.fromEntries( supportedResolutions.map((resolution) => [resolution, 0]), ), enableLoadingPlaceholder: true, loadingColor: "blue", vip98k: getVip98kMetadata(model, "video"), }, sortOrder: TYPE_SORT_ORDER.video + index, }; } function createWorldPreset( model: Vip98kCatalogModel, index: number, ): Vip98kModelPreset { const modelId = getModelId(model); const displayName = getDisplayName(model, modelId); return { modelId, displayName, description: model.description || `VIP98K 3D world model (${modelId})`, type: "world", supportsVision: false, supportsImageGen: false, isHidden: false, capabilities: { provider: "vip98k", kind: "world", mediaType: "world", modelType: "world-generation", outputType: "3d", supportedModes: ["world-generation", "text-to-3d"], vip98k: getVip98kMetadata(model, "world"), }, defaultParams: { provider: "vip98k", kind: "world", endpoint: modelId, taskApi: "external-task", }, imageGenConfig: null, sortOrder: TYPE_SORT_ORDER.world + index, }; } function createChatPreset( model: Vip98kCatalogModel, index: number, ): Vip98kModelPreset { const modelId = getModelId(model); const displayName = getDisplayName(model, modelId); return { modelId, displayName, description: model.description || `VIP98K text model (${modelId})`, type: "model", supportsVision: false, supportsImageGen: false, isHidden: false, capabilities: { provider: "vip98k", kind: "chat", mediaType: "text", modelType: "model", outputType: "text", apiFormat: "openai-compatible", vip98k: getVip98kMetadata(model, "model"), }, defaultParams: { provider: "vip98k", kind: "chat", endpoint: modelId, apiFormat: "openai-compatible", }, imageGenConfig: null, sortOrder: TYPE_SORT_ORDER.model + index, }; } function createToolPreset( model: Vip98kCatalogModel, type: string, index: number, ): Vip98kModelPreset { const modelId = getModelId(model); const displayName = getDisplayName(model, modelId); const inputModes = getInputModes(model); const outputModes = getOutputModes(model); const isImageTool = type === "image_tool"; return { modelId, displayName, description: model.description || `VIP98K ${type} model (${modelId})`, type, supportsVision: isImageTool, supportsImageGen: false, isHidden: true, capabilities: { provider: "vip98k", kind: type.replace(/_/g, "-"), mediaType: isImageTool ? "image" : type === "audio" ? "audio" : "document", modelType: type, outputType: outputModes[0] || (isImageTool ? "image" : "text"), inputModes, outputModes, supportedModes: getCapabilities(model), vip98k: getVip98kMetadata(model, type), }, defaultParams: { provider: "vip98k", kind: type.replace(/_/g, "-"), endpoint: modelId, taskApi: type === "audio" ? "openai-compatible" : "external-task", }, imageGenConfig: null, sortOrder: (TYPE_SORT_ORDER[type] || 990) + index, }; } export function buildVip98kModelPresets( models: Vip98kCatalogModel[], ): Vip98kModelPreset[] { return models .map((model, index) => { const modelId = getModelId(model); if (!modelId) return null; const type = normalizeCatalogType(model); if (type === "image") return createImagePreset(model, index); if (type === "video") return createVideoPreset(model, index); if (type === "world") return createWorldPreset(model, index); if (type === "model") return createChatPreset(model, index); return createToolPreset(model, type, index); }) .filter((item): item is Vip98kModelPreset => Boolean(item)) .sort((a, b) => a.sortOrder - b.sortOrder || a.displayName.localeCompare(b.displayName)); } export function normalizeVip98kBaseUrl(baseURL?: string | null) { const normalized = String(baseURL || VIP98K_BASE_URL).trim().replace(/\/+$/, ""); if (!normalized) return VIP98K_BASE_URL; if (/\/v1$/i.test(normalized)) return normalized.replace(/\/v1$/i, ""); if (/\/api\/external\/task$/i.test(normalized)) { return normalized.replace(/\/api\/external\/task$/i, ""); } return normalized; } function extractCatalogModels(payload: unknown): Vip98kCatalogModel[] { const record = toRecord(payload); const data = record?.data; const dataRecord = toRecord(data); const candidates = [ dataRecord?.models, dataRecord?.items, dataRecord?.list, data, record?.models, record?.items, ]; for (const candidate of candidates) { if (Array.isArray(candidate)) return candidate as Vip98kCatalogModel[]; } return []; } async function parseJsonResponse(response: Response) { const text = await response.text(); if (!text.trim()) return null; try { return JSON.parse(text); } catch { return text; } } function extractErrorMessage(payload: unknown, fallback = "VIP98K model catalog request failed") { if (typeof payload === "string" && payload.trim()) return payload.trim(); const record = toRecord(payload); const data = toRecord(record?.data); const error = toRecord(record?.error); const value = data?.message || data?.error || error?.message || record?.message || record?.error || fallback; return typeof value === "string" && value.trim() ? value.trim() : fallback; } export async function fetchVip98kCatalog(params: { baseURL?: string | null; apiKey: string; }): Promise { const baseURL = normalizeVip98kBaseUrl(params.baseURL); const response = await fetch(`${baseURL}/api/external/task/models`, { cache: "no-store", headers: { Authorization: `Bearer ${params.apiKey}`, Accept: "application/json", }, signal: AbortSignal.timeout(60_000), }); const payload = await parseJsonResponse(response); if (!response.ok) { throw new Error(`${extractErrorMessage(payload)} (HTTP ${response.status})`); } const models = extractCatalogModels(payload); if (models.length === 0) { throw new Error("VIP98K model catalog is empty or has an unsupported response shape"); } return models; } export async function fetchVip98kModelPresets(params: { baseURL?: string | null; apiKey: string; }) { const models = await fetchVip98kCatalog(params); return buildVip98kModelPresets(models); }