From 6b2c2c7b42d8a893b170736c06448f99aa059881 Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 10 Aug 2026 14:18:45 +0800 Subject: [PATCH] fix(ai): block costly and text-to-image VIP98K models --- app/api/admin/vip98k-models/route.ts | 33 ++++++++++++++++- lib/ai/vip98k-models.ts | 41 +++++++++++++++++++++ tests/routes/model-classification.test.ts | 45 ++++++++++++++++++++--- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/app/api/admin/vip98k-models/route.ts b/app/api/admin/vip98k-models/route.ts index 145884a3b..c461bdce2 100644 --- a/app/api/admin/vip98k-models/route.ts +++ b/app/api/admin/vip98k-models/route.ts @@ -5,7 +5,11 @@ import { type Vip98kModelPreset, } from "@/lib/ai/vip98k-models"; import { db } from "@/lib/db"; -import { getAiProviderById } from "@/lib/db/queries"; +import { + deleteAiModel, + getAiModelsByProviderId, + getAiProviderById, +} from "@/lib/db/queries"; import { aiModel, type AiModel } from "@/lib/db/schema"; import { requireAdmin } from "@/lib/middleware/admin"; @@ -97,8 +101,33 @@ export async function POST(request: Request) { const addedModels: AiModel[] = []; const updatedModels: AiModel[] = []; + const removedModels: AiModel[] = []; const errors: Array<{ modelId: string; error: string }> = []; + const allowedCatalogModelIds = new Set( + catalogPresets.map((preset) => preset.modelId), + ); + const existingModels = await getAiModelsByProviderId({ providerId }); + const blockedImageModels = existingModels.filter( + (model) => + model.supportsImageGen && !allowedCatalogModelIds.has(model.modelId), + ); + for (const model of blockedImageModels) { + try { + const deleted = await deleteAiModel({ id: model.id }); + removedModels.push(...deleted); + } catch (error) { + console.error( + `[VIP98K Models API] Failed to remove blocked image model ${model.modelId}:`, + error, + ); + errors.push({ + modelId: model.modelId, + error: error instanceof Error ? error.message : "Removal failed", + }); + } + } + for (const preset of presets) { try { const existing = await db.query.aiModel.findFirst({ @@ -161,8 +190,10 @@ export async function POST(request: Request) { success: true, addedCount: addedModels.length, updatedCount: updatedModels.length, + removedCount: removedModels.length, addedModels, updatedModels, + removedModels, errors: errors.length > 0 ? errors : undefined, counts: { requested: presets.length, diff --git a/lib/ai/vip98k-models.ts b/lib/ai/vip98k-models.ts index 9bc8fca6b..240f9b2bf 100644 --- a/lib/ai/vip98k-models.ts +++ b/lib/ai/vip98k-models.ts @@ -1,4 +1,5 @@ export const VIP98K_BASE_URL = "https://api.vip98k.com"; +export const VIP98K_MAX_IMAGE_PRICE_CNY = 0.3; export type Vip98kCatalogModelType = | "image" @@ -355,6 +356,12 @@ function getCapabilities(model: Vip98kCatalogModel) { ); } +function getTaskTypes(model: Vip98kCatalogModel) { + return parseStringArray(model.taskTypes || model.task_types).map((item) => + item.trim().toLowerCase().replace(/_/g, "-"), + ); +} + function getInputModes(model: Vip98kCatalogModel) { return parseStringArray(model.inputModes || model.input_modes).map((item) => item.trim().toLowerCase(), @@ -393,6 +400,39 @@ function getBillingMode(model: Vip98kCatalogModel) { return String(model.billingMode || model.billing_mode || "per_request").trim(); } +export function getVip98kImageModelPolicy(model: Vip98kCatalogModel) { + const presetType = resolvePresetType(model); + const effectivePrice = getEffectivePrice(model); + if (presetType !== "image") { + return { + allowed: true, + effectivePrice, + isTextToImage: false, + reasons: [] as string[], + }; + } + + const modelId = getModelId(model).toLowerCase().replace(/_/g, "-"); + const modes = new Set([...getCapabilities(model), ...getTaskTypes(model)]); + const isTextToImage = + modes.has("text-to-image") || modelId.includes("text-to-image"); + const reasons: string[] = []; + if (isTextToImage) reasons.push("text-to-image"); + if ( + effectivePrice !== undefined && + effectivePrice > VIP98K_MAX_IMAGE_PRICE_CNY + ) { + reasons.push("price-over-limit"); + } + + return { + allowed: reasons.length === 0, + effectivePrice, + isTextToImage, + reasons, + }; +} + function getVip98kMetadata(model: Vip98kCatalogModel, type: string) { const invocation = toRecord(model.invocation); return { @@ -771,6 +811,7 @@ export function buildVip98kModelPresets( .map((model, index) => { const modelId = getModelId(model); if (!modelId) return null; + if (!getVip98kImageModelPolicy(model).allowed) return null; const type = resolvePresetType(model); if (type === "image") return createImagePreset(model, index); if (type === "video") return createVideoPreset(model, index); diff --git a/tests/routes/model-classification.test.ts b/tests/routes/model-classification.test.ts index 010b1351a..53c54603a 100644 --- a/tests/routes/model-classification.test.ts +++ b/tests/routes/model-classification.test.ts @@ -385,35 +385,68 @@ test.describe("AI model media classification", () => { modelId: "black-forest-labs/flux-dev", displayName: "Flux Dev", type: "image", - capabilities: ["text_to_image"], + capabilities: ["image_to_image"], }, { - modelId: "google/nano-banana-2/text-to-image", + modelId: "google/nano-banana-2/edit", displayName: "Nano Banana 2", type: "image", - capabilities: ["text_to_image"], + capabilities: ["image_to_image"], }, { modelId: "future-image-with-date", displayName: "Future image", type: "image", - capabilities: ["text_to_image"], + capabilities: ["image_to_image"], releasedAt: "2027-02-01", }, { modelId: "old-image-with-date", displayName: "Old image", type: "image", - capabilities: ["text_to_image"], + capabilities: ["image_to_image"], releasedAt: "2025-12-15", }, ]); expect(presets.find((item) => item.modelId === "black-forest-labs/flux-dev")?.isHidden).toBe(true); expect( - presets.find((item) => item.modelId === "google/nano-banana-2/text-to-image")?.isHidden, + presets.find((item) => item.modelId === "google/nano-banana-2/edit")?.isHidden, ).toBe(false); expect(presets.find((item) => item.modelId === "future-image-with-date")?.isHidden).toBe(false); expect(presets.find((item) => item.modelId === "old-image-with-date")?.isHidden).toBe(true); }); + + test("excludes VIP98K text-to-image and image models priced over CNY 0.30", () => { + const presets = buildVip98kModelPresets([ + { + modelId: "cheap-text-to-image", + type: "image", + task_types: ["text_to_image"], + pricing: { effective_price: 0.02 }, + }, + { + modelId: "expensive-image-edit", + type: "image", + capabilities: ["image_to_image"], + pricing: { effective_price: 0.31 }, + }, + { + modelId: "affordable-image-edit", + type: "image", + capabilities: ["image_to_image"], + pricing: { effective_price: 0.3 }, + }, + { + modelId: "remove-background", + type: "image_tool", + pricing: { effective_price: 0.9 }, + }, + ]); + + expect(presets.map((item) => item.modelId)).toEqual([ + "affordable-image-edit", + "remove-background", + ]); + }); });