export const VIP98K_BASE_URL = "https://api.vip98k.com"; export type Vip98kCatalogModelType = | "image" | "video" | "image_tool" | "document_tool" | "model" | "audio" | "world" | string; export interface Vip98kInvocationContract { kind?: "chat" | "task" | "tool" | string; method?: string; path?: string; content_type?: string; request_example?: Record; result_method?: string; result_path?: 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; available?: boolean; isAvailable?: boolean; enabled?: boolean; isEnabled?: boolean; is_active?: boolean; invocation?: Vip98kInvocationContract; taskTypes?: unknown; task_types?: unknown; releasedAt?: unknown; released_at?: unknown; releaseDate?: unknown; release_date?: unknown; releaseYear?: unknown; release_year?: unknown; publishedAt?: unknown; published_at?: unknown; launchDate?: unknown; launch_date?: unknown; [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_3d: 320, model: 400, image_tool: 800, audio: 900, document_tool: 950, }; const IMAGE_TOOL_MODEL_IDS = new Set([ "background-generator", "clothing-segmentation", "colorize", "compress", "enhance", "face-swap-image", "head-cutout", "id-photo", "image-expansion", "image-upscaler", "photo-cleanup", "remove-background", "remove-object", "remove-watermark", "smart-crop", "youchuan/v8.1/remove-background", ]); const VIDEO_CAPABILITY_KEYS = new Set([ "audio-to-video", "first-frame-to-video", "image-to-video", "reference-to-video", "start-end-frame-to-video", "text-to-video", "video-extend", "video-to-video", ]); const IMAGE_CAPABILITY_KEYS = new Set([ "image-to-image", "reference-to-image", "text-to-image", ]); const IMAGE_TOOL_OPERATION_BY_MODEL_ID: Record = { "background-generator": "background-generation", "clothing-segmentation": "segmentation", colorize: "colorize", compress: "compress", enhance: "enhance", "face-swap-image": "face-swap", "head-cutout": "remove-background", "id-photo": "id-photo", "image-expansion": "extend", "image-upscaler": "enhance", "photo-cleanup": "cleanup", "remove-background": "remove-background", "remove-object": "remove-object", "remove-watermark": "remove-watermark", "smart-crop": "smart-crop", "youchuan/v8.1/remove-background": "remove-background", }; // The VIP98K catalog currently has no release-date field. Unknown image models stay // available to backend routing but remain hidden until their 2026+ release is verified. const VERIFIED_2026_IMAGE_MODEL_PATTERNS = [ /^nano-banana-2(?:$|[-/])/, /^gpt-image-2(?:$|[-/])/, /^alibaba\/wan-2\.(?:6|7)(?:$|[-/])/, /^black-forest-labs\/flux-2(?:$|[-/])/, /^bytedance\/seedream-v5(?:$|[.\-/])/, /^google\/nano-banana-2(?:$|[-/])/, /^hidream-o1-1\.5(?:$|[-/])/, /^ideogram\/v4(?:$|[-/])/, /^krea-2(?:$|[-/])/, /^microsoft\/mai-image-2\.5(?:$|[-/])/, /^nvidia\/cosmos-3(?:$|[-/])/, /^openai\/gpt-image-2(?:$|[-/])/, /^qwen\/qwen-image-2\.0(?:$|[-/])/, /^youchuan\/v8\.1(?:$|[-/])/, ]; 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 getReleaseYear(model: Vip98kCatalogModel): number | null { const candidates = [ model.releaseYear, model.release_year, model.releasedAt, model.released_at, model.releaseDate, model.release_date, model.publishedAt, model.published_at, model.launchDate, model.launch_date, ]; for (const candidate of candidates) { const match = String(candidate || "").match(/(?:19|20)\d{2}/); if (match) return Number(match[0]); } const datedModelId = getModelId(model).match(/(?:^|[^\d])((?:19|20)\d{2})\d{4}(?:[^\d]|$)/); return datedModelId?.[1] ? Number(datedModelId[1]) : null; } export function isVip98kImageModelVisible(model: Vip98kCatalogModel) { const releaseYear = getReleaseYear(model); if (releaseYear !== null) return releaseYear >= 2026; const modelId = getModelId(model).toLowerCase(); return VERIFIED_2026_IMAGE_MODEL_PATTERNS.some((pattern) => pattern.test(modelId)); } function resolvePresetType(model: Vip98kCatalogModel): Vip98kCatalogModelType { const catalogType = normalizeCatalogType(model); const modelId = getModelId(model).toLowerCase(); const capabilities = getCapabilities(model); if ( capabilities.includes("image-to-3d") || capabilities.includes("text-to-3d") ) { return "model_3d"; } if (catalogType === "world") return "world"; if (catalogType === "image_tool" || IMAGE_TOOL_MODEL_IDS.has(modelId)) { return "image_tool"; } if ( catalogType === "video" || capabilities.some( (capability) => VIDEO_CAPABILITY_KEYS.has(capability) || capability.endsWith("-to-video") || capability.startsWith("video-"), ) ) { return "video"; } if ( catalogType === "image" || capabilities.some( (capability) => IMAGE_CAPABILITY_KEYS.has(capability) || capability.endsWith("-to-image"), ) ) { return "image"; } return catalogType; } const UNAVAILABLE_STATUS_TOKENS = [ "disabled", "unavailable", "closed", "offline", "deprecated", "removed", "retired", "suspended", ] as const; function getInvocationRequestKeys(model: Vip98kCatalogModel) { const invocation = toRecord(model.invocation); return Object.keys(toRecord(invocation?.request_example) || {}).map((key) => key.trim().toLowerCase().replace(/_/g, "-"), ); } function hasMediaReferenceInput( model: Vip98kCatalogModel, type: Vip98kCatalogModelType, ) { const capabilities = getCapabilities(model); const inputModes = getInputModes(model).map((mode) => mode.replace(/_/g, "-")); const requestKeys = getInvocationRequestKeys(model); const modelId = getModelId(model).toLowerCase(); const mediaInputModes = new Set([ "image", "images", "url", "file", "video", "audio", "base64", ]); const hasMediaCapability = capabilities.some((capability) => { if (capability.startsWith("text-to-")) return false; if (type === "image") { return capability.endsWith("-to-image") || capability.includes("image-edit"); } return ( capability.endsWith("-to-video") || capability.startsWith("video-") || capability.includes("video-edit") ); }); const hasMediaRequestField = requestKeys.some((key) => /(?:image|video|audio|reference|first-?frame|last-?frame|file|url)/.test(key), ); const hasMediaModelPath = type === "image" ? /(?:image-to-image|reference-to-image|\/edit(?:$|[\/-]))/.test(modelId) : /(?:image-to-video|video-to-video|reference-to-video|first-frame|start-end|video-edit|video-extend)/.test( modelId, ); return ( hasMediaCapability || inputModes.some((mode) => mediaInputModes.has(mode)) || hasMediaRequestField || hasMediaModelPath ); } export function isVip98kCatalogModelAvailable(model: Vip98kCatalogModel) { const status = String(model.status || "").trim().toLowerCase(); if (UNAVAILABLE_STATUS_TOKENS.some((token) => status.includes(token))) { return false; } return !( model.available === false || model.isAvailable === false || model.enabled === false || model.isEnabled === false || model.is_active === false ); } export function isVip98kCatalogModelEligible(model: Vip98kCatalogModel) { if (!getModelId(model) || !isVip98kCatalogModelAvailable(model)) return false; const type = resolvePresetType(model); if (type !== "image" && type !== "video") return true; return hasMediaReferenceInput(model, type); } export function filterVip98kCatalogModels(models: Vip98kCatalogModel[]) { return models.filter(isVip98kCatalogModelEligible); } 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 normalizeMode(value: unknown) { return String(value || "").trim().toLowerCase().replace(/_/g, "-"); } function getStoredSupportedModes(value: unknown) { const record = toRecord(value); return parseStringArray( record?.supportedModes || record?.supported_modes, ).map(normalizeMode); } export function hasVip98kTextToImageMode(input: { modelId?: string | null; capabilities?: unknown; defaultParams?: unknown; }) { const modes = new Set([ ...getStoredSupportedModes(input.capabilities), ...getStoredSupportedModes(input.defaultParams), ]); return ( modes.has("text-to-image") || normalizeMode(input.modelId).includes("text-to-image") ); } export function isVip98kTextToImageCatalogModel( model: Vip98kCatalogModel, ) { if (resolvePresetType(model) !== "image") return false; const capabilities = getCapabilities(model); return capabilities.length === 0 || capabilities.includes("text-to-image"); } 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) { const invocation = toRecord(model.invocation); return { modelType: type, taskApi: String(invocation?.kind || "").toLowerCase() === "tool" ? "external-tool" : "external-task", invocation: invocation ? { kind: String(invocation.kind || ""), method: String(invocation.method || "POST"), path: String(invocation.path || ""), contentType: String(invocation.content_type || "application/json"), requestExample: toRecord(invocation.request_example) || {}, resultMethod: String(invocation.result_method || "GET"), resultPath: String(invocation.result_path || ""), } : null, 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: !isVip98kImageModelVisible(model), 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 createThreeDPreset( model: Vip98kCatalogModel, index: number, ): Vip98kModelPreset { const modelId = getModelId(model); const displayName = getDisplayName(model, modelId); const catalogCapabilities = getCapabilities(model); const supportsImageInput = catalogCapabilities.includes("image-to-3d"); const supportedModes = [ ...(catalogCapabilities.includes("text-to-3d") ? ["text-to-3d"] : []), ...(supportsImageInput ? ["image-to-3d"] : []), ]; return { modelId, displayName, description: model.description || `VIP98K 3D model (${modelId})`, type: "model_3d", supportsVision: supportsImageInput, supportsImageGen: false, // Keep the dedicated Tripo 3D integration as the only frontend 3D route. isHidden: true, capabilities: { provider: "vip98k", kind: "model-3d", mediaType: "model-3d", modelType: "model-3d", outputType: "3d", supportedModes, maxReferenceImages: supportsImageInput ? 1 : 0, vip98k: getVip98kMetadata(model, "model_3d"), }, defaultParams: { provider: "vip98k", kind: "model-3d", endpoint: modelId, taskApi: "external-task", supportedModes, }, imageGenConfig: null, sortOrder: TYPE_SORT_ORDER.model_3d + index, }; } function createChatPreset( model: Vip98kCatalogModel, index: number, ): Vip98kModelPreset { const modelId = getModelId(model); const displayName = getDisplayName(model, modelId); const catalogCapabilities = getCapabilities(model); const inputModes = getInputModes(model); const supportsVision = catalogCapabilities.some((capability) => ["vision", "multimodal", "image-input", "image-understanding"].includes( capability, ), ) || inputModes.some((mode) => ["image", "url", "file"].includes(mode)) || /^(?:gpt-|claude-|qwen)/i.test(modelId); return { modelId, displayName, description: model.description || `VIP98K text model (${modelId})`, type: "model", supportsVision, supportsImageGen: false, isHidden: false, capabilities: { provider: "vip98k", kind: "chat", mediaType: "text", modelType: "model", outputType: "text", apiFormat: "openai-compatible", inputModes, supportedModes: supportsVision ? ["text", "vision"] : ["text"], 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"; const toolOperation = IMAGE_TOOL_OPERATION_BY_MODEL_ID[modelId.toLowerCase()] || type; 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), toolOperation, vip98k: getVip98kMetadata(model, type), }, defaultParams: { provider: "vip98k", kind: type.replace(/_/g, "-"), endpoint: modelId, toolOperation, taskApi: type === "audio" ? "openai-compatible" : "external-task", }, imageGenConfig: null, sortOrder: (TYPE_SORT_ORDER[type] || 990) + index, }; } export function buildVip98kModelPresets( models: Vip98kCatalogModel[], ): Vip98kModelPreset[] { return filterVip98kCatalogModels(models) .map((model, index) => { const modelId = getModelId(model); if (!modelId) return null; const type = resolvePresetType(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_3d") return createThreeDPreset(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 []; } function extractPublicCatalogModels(payload: unknown): Vip98kCatalogModel[] { const record = toRecord(payload); const data = toRecord(record?.data); const sections = Array.isArray(data?.sections) ? data.sections : []; return sections.flatMap((section) => { const sectionRecord = toRecord(section); const models = Array.isArray(sectionRecord?.models) ? sectionRecord.models : []; return models.filter((model): model is Vip98kCatalogModel => Boolean(toRecord(model))); }); } 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 [authorizedResponse, publicResponse] = await Promise.all([ fetch(`${baseURL}/api/external/task/models`, { cache: "no-store", headers: { Authorization: `Bearer ${params.apiKey}`, Accept: "application/json", }, signal: AbortSignal.timeout(60_000), }), fetch(`${baseURL}/api/v1/public/models`, { cache: "no-store", headers: { Accept: "application/json" }, signal: AbortSignal.timeout(60_000), }), ]); const [authorizedPayload, publicPayload] = await Promise.all([ parseJsonResponse(authorizedResponse), parseJsonResponse(publicResponse), ]); if (!authorizedResponse.ok) { throw new Error( `${extractErrorMessage(authorizedPayload)} (HTTP ${authorizedResponse.status})`, ); } if (!publicResponse.ok) { throw new Error( `VIP98K public invocation catalog failed (HTTP ${publicResponse.status})`, ); } const authorizedModels = extractCatalogModels(authorizedPayload); const publicModels = extractPublicCatalogModels(publicPayload); if (authorizedModels.length === 0) { throw new Error("VIP98K model catalog is empty or has an unsupported response shape"); } if (publicModels.length === 0) { throw new Error("VIP98K public invocation catalog is empty"); } const publicById = new Map( publicModels.map((model) => [getModelId(model).toLowerCase(), model]), ); return authorizedModels.map((model) => { const publicModel = publicById.get(getModelId(model).toLowerCase()); if (!publicModel) return model; return { ...publicModel, ...model, invocation: publicModel.invocation, taskTypes: model.taskTypes || model.task_types || publicModel.taskTypes || publicModel.task_types, capabilities: model.capabilities || publicModel.capabilities, }; }); } export async function fetchVip98kModelPresets(params: { baseURL?: string | null; apiKey: string; }) { const models = await fetchVip98kCatalog(params); return buildVip98kModelPresets(models); }