fix(ai): block costly and text-to-image VIP98K models

This commit is contained in:
134 2026-08-10 14:18:45 +08:00
parent 9438cf2f36
commit 0a17a2842c
3 changed files with 112 additions and 7 deletions

View File

@ -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,

View File

@ -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);

View File

@ -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",
]);
});
});