fix(ai): block costly and text-to-image VIP98K models
This commit is contained in:
parent
0a1026c237
commit
6b2c2c7b42
@ -5,7 +5,11 @@ import {
|
|||||||
type Vip98kModelPreset,
|
type Vip98kModelPreset,
|
||||||
} from "@/lib/ai/vip98k-models";
|
} from "@/lib/ai/vip98k-models";
|
||||||
import { db } from "@/lib/db";
|
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 { aiModel, type AiModel } from "@/lib/db/schema";
|
||||||
import { requireAdmin } from "@/lib/middleware/admin";
|
import { requireAdmin } from "@/lib/middleware/admin";
|
||||||
|
|
||||||
@ -97,8 +101,33 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const addedModels: AiModel[] = [];
|
const addedModels: AiModel[] = [];
|
||||||
const updatedModels: AiModel[] = [];
|
const updatedModels: AiModel[] = [];
|
||||||
|
const removedModels: AiModel[] = [];
|
||||||
const errors: Array<{ modelId: string; error: string }> = [];
|
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) {
|
for (const preset of presets) {
|
||||||
try {
|
try {
|
||||||
const existing = await db.query.aiModel.findFirst({
|
const existing = await db.query.aiModel.findFirst({
|
||||||
@ -161,8 +190,10 @@ export async function POST(request: Request) {
|
|||||||
success: true,
|
success: true,
|
||||||
addedCount: addedModels.length,
|
addedCount: addedModels.length,
|
||||||
updatedCount: updatedModels.length,
|
updatedCount: updatedModels.length,
|
||||||
|
removedCount: removedModels.length,
|
||||||
addedModels,
|
addedModels,
|
||||||
updatedModels,
|
updatedModels,
|
||||||
|
removedModels,
|
||||||
errors: errors.length > 0 ? errors : undefined,
|
errors: errors.length > 0 ? errors : undefined,
|
||||||
counts: {
|
counts: {
|
||||||
requested: presets.length,
|
requested: presets.length,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export const VIP98K_BASE_URL = "https://api.vip98k.com";
|
export const VIP98K_BASE_URL = "https://api.vip98k.com";
|
||||||
|
export const VIP98K_MAX_IMAGE_PRICE_CNY = 0.3;
|
||||||
|
|
||||||
export type Vip98kCatalogModelType =
|
export type Vip98kCatalogModelType =
|
||||||
| "image"
|
| "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) {
|
function getInputModes(model: Vip98kCatalogModel) {
|
||||||
return parseStringArray(model.inputModes || model.input_modes).map((item) =>
|
return parseStringArray(model.inputModes || model.input_modes).map((item) =>
|
||||||
item.trim().toLowerCase(),
|
item.trim().toLowerCase(),
|
||||||
@ -393,6 +400,39 @@ function getBillingMode(model: Vip98kCatalogModel) {
|
|||||||
return String(model.billingMode || model.billing_mode || "per_request").trim();
|
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) {
|
function getVip98kMetadata(model: Vip98kCatalogModel, type: string) {
|
||||||
const invocation = toRecord(model.invocation);
|
const invocation = toRecord(model.invocation);
|
||||||
return {
|
return {
|
||||||
@ -771,6 +811,7 @@ export function buildVip98kModelPresets(
|
|||||||
.map((model, index) => {
|
.map((model, index) => {
|
||||||
const modelId = getModelId(model);
|
const modelId = getModelId(model);
|
||||||
if (!modelId) return null;
|
if (!modelId) return null;
|
||||||
|
if (!getVip98kImageModelPolicy(model).allowed) return null;
|
||||||
const type = resolvePresetType(model);
|
const type = resolvePresetType(model);
|
||||||
if (type === "image") return createImagePreset(model, index);
|
if (type === "image") return createImagePreset(model, index);
|
||||||
if (type === "video") return createVideoPreset(model, index);
|
if (type === "video") return createVideoPreset(model, index);
|
||||||
|
|||||||
@ -385,35 +385,68 @@ test.describe("AI model media classification", () => {
|
|||||||
modelId: "black-forest-labs/flux-dev",
|
modelId: "black-forest-labs/flux-dev",
|
||||||
displayName: "Flux Dev",
|
displayName: "Flux Dev",
|
||||||
type: "image",
|
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",
|
displayName: "Nano Banana 2",
|
||||||
type: "image",
|
type: "image",
|
||||||
capabilities: ["text_to_image"],
|
capabilities: ["image_to_image"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
modelId: "future-image-with-date",
|
modelId: "future-image-with-date",
|
||||||
displayName: "Future image",
|
displayName: "Future image",
|
||||||
type: "image",
|
type: "image",
|
||||||
capabilities: ["text_to_image"],
|
capabilities: ["image_to_image"],
|
||||||
releasedAt: "2027-02-01",
|
releasedAt: "2027-02-01",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
modelId: "old-image-with-date",
|
modelId: "old-image-with-date",
|
||||||
displayName: "Old image",
|
displayName: "Old image",
|
||||||
type: "image",
|
type: "image",
|
||||||
capabilities: ["text_to_image"],
|
capabilities: ["image_to_image"],
|
||||||
releasedAt: "2025-12-15",
|
releasedAt: "2025-12-15",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(presets.find((item) => item.modelId === "black-forest-labs/flux-dev")?.isHidden).toBe(true);
|
expect(presets.find((item) => item.modelId === "black-forest-labs/flux-dev")?.isHidden).toBe(true);
|
||||||
expect(
|
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);
|
).toBe(false);
|
||||||
expect(presets.find((item) => item.modelId === "future-image-with-date")?.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);
|
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",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user