import { expect, test } from "@playwright/test"; import { createAtlasCloudImagePreset, inferAtlasCloudMediaKind, resolveAtlasCloudDisplayName, shouldInsertMissingAtlasCloudPreset, } from "@/lib/ai/atlascloud-models"; import { createAtlasCloudRequestBody, createAtlasCloudVideoRequestBody, } from "@/lib/ai/atlascloud-request"; import { is3DModelDescriptor, isImageGenerationModelDescriptor, isVideoModelDescriptor, isAlibabaImageTextToImageModelId, modelSupportsTextToImageDescriptor, } from "@/lib/ai/model-classification"; import { buildVip98kModelPresets, isVip98kCatalogModelEligible, isVip98kImageModelVisible, } from "@/lib/ai/vip98k-models"; const atlasHunyuan3D = { model: "tencent/hunyuan3d-pro/text-to-3d", type: "Image", displayName: "Hunyuan 3D Pro Text-to-3D", categories: ["TEXT-TO-3D"], tags: ["TEXT-TO-3D", "NEW"], }; test.describe("AI model media classification", () => { test("classifies Qwen Image 3.0 as Alibaba text-to-image and image editing", () => { expect(isAlibabaImageTextToImageModelId("qwen-image-3.0")).toBe(true); expect(isAlibabaImageTextToImageModelId("qwen-image-edit-plus")).toBe(false); expect( modelSupportsTextToImageDescriptor({ providerType: "alibaba-image", modelId: "qwen-image-3.0", supportsImageGen: true, }) ).toBe(true); expect( modelSupportsTextToImageDescriptor({ providerType: "alibaba-image", modelId: "qwen-image-edit-plus", supportsImageGen: true, }) ).toBe(false); }); test("preserves administrator-defined AtlasCloud display names", () => { expect( resolveAtlasCloudDisplayName( "视觉 Flux Kontext Pro", "Flux Kontext Pro", "black-forest-labs/flux-kontext-pro" ) ).toBe("视觉 Flux Kontext Pro"); expect( resolveAtlasCloudDisplayName( "", "Flux Kontext Pro", "black-forest-labs/flux-kontext-pro" ) ).toBe("Flux Kontext Pro"); }); test("adds a missing Atlas media kind without restoring deleted models in an existing kind", () => { const existingKinds = new Set(["image", "3d"]); expect(shouldInsertMissingAtlasCloudPreset(existingKinds, "video")).toBe(true); expect(shouldInsertMissingAtlasCloudPreset(existingKinds, "image")).toBe(false); expect(shouldInsertMissingAtlasCloudPreset(existingKinds, "3d")).toBe(false); }); test("keeps multi-reference BananaImage models classified as image models", () => { const descriptor = { providerType: "bananaimage", modelId: "gemini-3-pro-image-preview", capabilities: { supportedModes: [ "text-to-image", "image-to-image", "reference-images", ], }, supportsImageGen: true, }; expect(isVideoModelDescriptor(descriptor)).toBe(false); expect(isImageGenerationModelDescriptor(descriptor)).toBe(true); }); test("still recognizes explicit video generation modes", () => { expect( isVideoModelDescriptor({ providerType: "custom", modelId: "custom-video-model", capabilities: { supportedModes: ["text-to-video"] }, }) ).toBe(true); }); test("keeps Atlas 3D entries out of image and video categories", () => { expect(inferAtlasCloudMediaKind(atlasHunyuan3D)).toBe("3d"); const descriptor = { providerType: "atlascloud", modelId: atlasHunyuan3D.model, capabilities: { kind: "3d", mediaType: "3d", supportedModes: ["text-to-3d"], }, supportsImageGen: false, }; expect(is3DModelDescriptor(descriptor)).toBe(true); expect(isImageGenerationModelDescriptor(descriptor)).toBe(false); expect(isVideoModelDescriptor(descriptor)).toBe(false); }); test("writes 3D Atlas presets with model-level capabilities", () => { const preset = createAtlasCloudImagePreset( atlasHunyuan3D, { components: { schemas: { Input: { type: "object", properties: { model: { type: "string" }, prompt: { type: "string" }, }, required: ["model", "prompt"], }, }, }, }, 0 ); expect(preset.supportsImageGen).toBe(false); expect(preset.capabilities).toMatchObject({ kind: "3d", mediaType: "3d", supportedModes: ["text-to-3d"], }); }); test("still distinguishes Atlas image and video descriptors", () => { const imageDescriptor = { providerType: "atlascloud", modelId: "black-forest-labs/flux-2-pro/text-to-image", capabilities: { kind: "text-to-image", mediaType: "image", supportedModes: ["text-to-image"], }, supportsImageGen: true, }; const videoDescriptor = { providerType: "atlascloud", modelId: "veed/fabric-1.0/image-to-video", capabilities: { kind: "video", mediaType: "video", supportedModes: ["image-to-video"], }, supportsImageGen: false, }; expect(isImageGenerationModelDescriptor(imageDescriptor)).toBe(true); expect(isVideoModelDescriptor(imageDescriptor)).toBe(false); expect(isVideoModelDescriptor(videoDescriptor)).toBe(true); expect(isImageGenerationModelDescriptor(videoDescriptor)).toBe(false); }); test("maps Atlas reference-video image models to a valid frontend request", () => { const preset = createAtlasCloudImagePreset( { model: "google/nano-banana-2/reference-to-image", type: "Image", displayName: "Nano Banana 2 Reference to Image", categories: ["IMAGE-TO-IMAGE"], }, { paths: { "/api/v1/model/generateImage": { post: {} }, "/api/v1/model/prediction/{request_id}": { get: {} }, }, components: { schemas: { Input: { type: "object", properties: { model: { type: "string" }, prompt: { type: "string" }, resolution: { type: "string", enum: ["1k", "2k", "4k"] }, video_clips: { type: "array", minItems: 1, maxItems: 1, items: { type: "object", properties: { url: { type: "string" }, start: { type: "number", default: 0 }, ends: { type: "number", default: 0 }, fps: { type: "number", default: 1 }, }, }, }, }, required: ["model", "prompt", "video_clips"], }, }, }, } ); expect(preset.capabilities).toMatchObject({ mediaType: "image", supportsVideoReference: true, requiresVideoReference: true, maxVideoReferences: 1, }); const { body } = createAtlasCloudRequestBody({ modelId: preset.modelId, prompt: "参考视频生成商品图", referenceImages: [], referenceVideos: ["https://example.com/reference.mp4"], imageParams: { imageSize: "2K", aspectRatio: "1:1" }, modelConfig: preset.imageGenConfig, }); expect(body.video_clips).toEqual([ { url: "https://example.com/reference.mp4", start: 0, ends: 0, fps: 1, }, ]); }); test("maps Atlas video schema capabilities and frontend parameters", () => { const preset = createAtlasCloudImagePreset( { model: "bytedance/seedance-2.0-mini/image-to-video", type: "Video", displayName: "Seedance 2.0 Mini Image to Video", categories: ["IMAGE-TO-VIDEO"], }, { paths: { "/api/v1/model/generateVideo": { post: {} }, "/api/v1/model/prediction/{request_id}": { get: {} }, }, components: { schemas: { Input: { type: "object", properties: { model: { type: "string" }, prompt: { type: "string" }, image: { type: "string", "x-format": "image" }, last_image: { type: "string", "x-format": "image" }, resolution: { type: "string", enum: ["480p", "720p", "1080p"], default: "720p", }, ratio: { type: "string", enum: ["16:9", "9:16", "adaptive"], default: "adaptive", }, duration: { type: "integer", enum: [5, 10], default: 5, }, seed: { type: "integer" }, watermark: { type: "boolean", default: false }, }, required: ["model", "prompt", "image"], }, }, }, } ); expect(preset.supportsImageGen).toBe(false); expect(preset.capabilities).toMatchObject({ kind: "video", mediaType: "video", supportedModes: ["first-frame", "first-last-frame"], supportedResolutions: ["480p", "720p", "1080p"], supportedRatios: ["16:9", "9:16", "adaptive"], durationOptions: [5, 10], }); const { body } = createAtlasCloudVideoRequestBody({ modelId: preset.modelId, options: { model: preset.modelId, prompt: "让商品缓慢旋转", mode: "first-last-frame", firstFrameImage: "https://example.com/first.png", lastFrameImage: "https://example.com/last.png", resolution: "1080p", ratio: "9:16", duration: 10, watermark: true, }, referenceImages: [ "https://example.com/first.png", "https://example.com/last.png", ], modelConfig: preset.imageGenConfig, }); expect(body).toMatchObject({ model: preset.modelId, prompt: "让商品缓慢旋转", image: "https://example.com/first.png", last_image: "https://example.com/last.png", resolution: "1080p", ratio: "9:16", duration: 10, watermark: true, }); }); test("normalizes VIP98K media and tool types before they reach the frontend", () => { const presets = buildVip98kModelPresets([ { modelId: "mislabelled-video", displayName: "Mislabelled video", type: "image", capabilities: ["text_to_video", "image_to_video"], inputModes: ["text", "image"], }, { modelId: "enhance", displayName: "Enhance", type: "image", capabilities: ["image_processing"], }, { modelId: "image-expansion", displayName: "Extend", type: "image_tool", capabilities: ["outpainting"], }, { modelId: "remove-background", displayName: "Remove background", type: "image_tool", capabilities: ["image_processing"], }, { modelId: "multimodal-model", displayName: "Multimodal model", type: "model", capabilities: ["multimodal"], inputModes: ["text", "image"], }, { modelId: "tencent/hunyuan3d-pro/text-to-3d", displayName: "Hunyuan 3D", type: "image", capabilities: ["text-to-3d"], }, ]); expect(presets.find((item) => item.modelId === "mislabelled-video")).toMatchObject({ type: "video", supportsImageGen: false, }); expect(presets.find((item) => item.modelId === "enhance")).toMatchObject({ type: "image_tool", isHidden: true, capabilities: { toolOperation: "enhance" }, }); expect(presets.find((item) => item.modelId === "image-expansion")).toMatchObject({ type: "image_tool", capabilities: { toolOperation: "extend" }, }); expect(presets.find((item) => item.modelId === "remove-background")).toMatchObject({ type: "image_tool", capabilities: { toolOperation: "remove-background" }, }); expect(presets.find((item) => item.modelId === "multimodal-model")).toMatchObject({ type: "model", supportsVision: true, supportsImageGen: false, }); expect( presets.find((item) => item.modelId === "tencent/hunyuan3d-pro/text-to-3d"), ).toMatchObject({ type: "model_3d", supportsImageGen: false, isHidden: true, capabilities: { kind: "model-3d" }, }); }); test("only shows VIP98K image models verified as released in 2026 or later", () => { const models = [ { modelId: "black-forest-labs/flux-dev", displayName: "Flux Dev", type: "image", capabilities: ["image_to_image"], }, { modelId: "google/nano-banana-2/edit", displayName: "Nano Banana 2", type: "image", capabilities: ["image_to_image"], }, { modelId: "future-image-with-date", displayName: "Future image", type: "image", capabilities: ["image_to_image"], releasedAt: "2027-02-01", }, { modelId: "old-image-with-date", displayName: "Old image", type: "image", capabilities: ["image_to_image"], releasedAt: "2025-12-15", }, ]; expect(isVip98kImageModelVisible(models[0])).toBe(false); expect(isVip98kImageModelVisible(models[1])).toBe(true); expect(isVip98kImageModelVisible(models[2])).toBe(true); expect(isVip98kImageModelVisible(models[3])).toBe(false); }); test("excludes pure text image/video models but keeps reference and 3D models", () => { expect( isVip98kCatalogModelEligible({ modelId: "openai/gpt-image-2/text-to-image", type: "image", capabilities: ["text-to-image"], }), ).toBe(false); expect( isVip98kCatalogModelEligible({ modelId: "google/veo/text-to-video", type: "video", capabilities: ["text-to-video"], }), ).toBe(false); expect( isVip98kCatalogModelEligible({ modelId: "google/nano-banana-2/edit", type: "image", capabilities: ["text-to-image", "image-to-image"], }), ).toBe(true); expect( isVip98kCatalogModelEligible({ modelId: "bytedance/seedance/image-to-video", type: "video", capabilities: ["text-to-video", "image-to-video"], }), ).toBe(true); expect( isVip98kCatalogModelEligible({ modelId: "tencent/hunyuan3d-pro/text-to-3d", type: "image", capabilities: ["text-to-3d"], }), ).toBe(true); }); test("excludes catalog models explicitly marked unavailable", () => { expect( isVip98kCatalogModelEligible({ modelId: "google/nano-banana-2/edit", type: "image", capabilities: ["image-to-image"], status: "closed", }), ).toBe(false); }); test("excludes VIP98K text-to-image models without applying a price limit", () => { const presets = buildVip98kModelPresets([ { modelId: "pure-text-to-image", type: "image", capabilities: ["text_to_image"], pricing: { effective_price: 0.02 }, }, { modelId: "mixed-image-model", type: "image", capabilities: ["text_to_image", "image_to_image"], pricing: { effective_price: 0.2 }, }, { modelId: "expensive-image-edit", type: "image", capabilities: ["image_to_image"], pricing: { effective_price: 2.5 }, }, { modelId: "remove-background", type: "image_tool", capabilities: ["image_processing"], pricing: { effective_price: 1.2 }, }, ]); expect(presets.map((item) => item.modelId)).toEqual([ "mixed-image-model", "expensive-image-edit", "remove-background", ]); }); });