fix(ai): block costly and text-to-image VIP98K models
This commit is contained in:
parent
9438cf2f36
commit
0a17a2842c
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user