fix: keep verified vip98k image models available
This commit is contained in:
parent
608bd25679
commit
434d0a63cd
@ -181,6 +181,7 @@ const IMAGE_TOOL_OPERATION_BY_MODEL_ID: Record<string, string> = {
|
||||
// available to backend routing but remain hidden until their 2026+ release is verified.
|
||||
const VERIFIED_2026_IMAGE_MODEL_PATTERNS = [
|
||||
/^nano-banana-2(?:$|[-/])/,
|
||||
/^nano-banana-pro$/,
|
||||
/^gpt-image-2(?:$|[-/])/,
|
||||
/^alibaba\/wan-2\.(?:6|7)(?:$|[-/])/,
|
||||
/^black-forest-labs\/flux-2(?:$|[-/])/,
|
||||
|
||||
@ -60,6 +60,27 @@ async function main() {
|
||||
}
|
||||
const vip98kProvider = vip98kProviders[0];
|
||||
if (!vip98kProvider) throw new Error("VIP98K provider is missing");
|
||||
const existingVip98kModels = await sql<
|
||||
Array<{
|
||||
id: string;
|
||||
modelId: string;
|
||||
healthStatus: string | null;
|
||||
}>
|
||||
>`
|
||||
select id, "modelId", "healthStatus"
|
||||
from "AiModel"
|
||||
where "providerId" = ${vip98kProvider.id}::uuid
|
||||
`;
|
||||
const existingVip98kByModelId = new Map(
|
||||
existingVip98kModels.map((model) => [model.modelId, model]),
|
||||
);
|
||||
const autoDisabledQualifiedModelIds = qualifiedVip98kPresets
|
||||
.filter(
|
||||
(preset) =>
|
||||
existingVip98kByModelId.get(preset.modelId)?.healthStatus ===
|
||||
"auto_disabled",
|
||||
)
|
||||
.map((preset) => preset.modelId);
|
||||
|
||||
const disallowedImageModels = await sql<
|
||||
Array<{ id: string; providerType: string; modelId: string }>
|
||||
@ -102,6 +123,7 @@ async function main() {
|
||||
mode: apply ? "apply" : "dry-run",
|
||||
allowedImageProviderTypes: ALLOWED_IMAGE_PROVIDER_TYPES,
|
||||
qualifiedVip98kModels: qualifiedVip98kPresets.map((preset) => preset.modelId),
|
||||
autoDisabledVip98kModelsPreserved: autoDisabledQualifiedModelIds,
|
||||
disallowedImageModelsToDisable: disallowedImageModels.length,
|
||||
staleVip98kImageModelsToDisable: staleVip98kImageModels.length,
|
||||
imageOnlyProvidersToDisable: imageOnlyProviders.map((provider) => ({
|
||||
@ -128,26 +150,26 @@ async function main() {
|
||||
`;
|
||||
|
||||
for (const preset of qualifiedVip98kPresets) {
|
||||
const preserveAutoDisabled = autoDisabledQualifiedModelIds.includes(
|
||||
preset.modelId,
|
||||
);
|
||||
const capabilities = tx.json(toJsonValue(preset.capabilities));
|
||||
const defaultParams = tx.json(toJsonValue(preset.defaultParams));
|
||||
const imageGenConfig = preset.imageGenConfig
|
||||
? tx.json(toJsonValue(preset.imageGenConfig))
|
||||
: null;
|
||||
const existing = await tx<Array<{ id: string }>>`
|
||||
select id from "AiModel"
|
||||
where "providerId" = ${vip98kProvider.id}::uuid and "modelId" = ${preset.modelId}
|
||||
order by "createdAt" limit 1
|
||||
`;
|
||||
if (existing[0]) {
|
||||
const existing = existingVip98kByModelId.get(preset.modelId);
|
||||
if (existing) {
|
||||
await tx`
|
||||
update "AiModel" set
|
||||
"displayName" = ${preset.displayName}, description = ${preset.description},
|
||||
"supportsVision" = ${preset.supportsVision}, "supportsImageGen" = true,
|
||||
"isEnabled" = true, "isHidden" = false, "sortOrder" = ${preset.sortOrder},
|
||||
"isEnabled" = ${!preserveAutoDisabled}, "isHidden" = false,
|
||||
"sortOrder" = ${preset.sortOrder},
|
||||
capabilities = ${capabilities}, "defaultParams" = ${defaultParams},
|
||||
"imageGenConfig" = ${imageGenConfig}, "fallbackModelId1" = null,
|
||||
"fallbackModelId2" = null, "updatedAt" = now()
|
||||
where id = ${existing[0].id}::uuid
|
||||
where id = ${existing.id}::uuid
|
||||
`;
|
||||
} else {
|
||||
await tx`
|
||||
@ -205,7 +227,9 @@ async function main() {
|
||||
if (invalidModels.length > 0) {
|
||||
throw new Error("Disallowed image models remain enabled after apply");
|
||||
}
|
||||
if (activeVip98kModels.length !== qualifiedVip98kPresets.length) {
|
||||
const expectedActiveVip98kCount =
|
||||
qualifiedVip98kPresets.length - autoDisabledQualifiedModelIds.length;
|
||||
if (activeVip98kModels.length !== expectedActiveVip98kCount) {
|
||||
throw new Error("VIP98K active image model count does not match qualified catalog");
|
||||
}
|
||||
console.log(
|
||||
|
||||
@ -11,10 +11,35 @@ const BASE_URL = String(process.env.VIP98K_BASE_URL || "https://api.vip98k.com")
|
||||
.trim()
|
||||
.replace(/\/+$/, "")
|
||||
.replace(/\/v1$/i, "");
|
||||
const REFERENCE_URLS = [
|
||||
const DEFAULT_REFERENCE_URLS = [
|
||||
"https://dummyimage.com/512x512/2563eb/ffffff.png&text=BLUE+A",
|
||||
"https://dummyimage.com/512x512/dc2626/ffffff.png&text=RED+B",
|
||||
];
|
||||
const REFERENCE_URLS = (() => {
|
||||
const configured = String(process.env.VIP98K_IMAGE_TEST_REFERENCES || "").trim();
|
||||
if (!configured) return DEFAULT_REFERENCE_URLS;
|
||||
const urls = configured
|
||||
.split(/\r?\n/)
|
||||
.map((url) => url.trim())
|
||||
.filter((url) => /^https?:\/\//i.test(url));
|
||||
if (urls.length < 2) {
|
||||
throw new Error("VIP98K_IMAGE_TEST_REFERENCES must contain at least two URLs");
|
||||
}
|
||||
return urls;
|
||||
})();
|
||||
const USES_CUSTOM_REFERENCES = Boolean(
|
||||
String(process.env.VIP98K_IMAGE_TEST_REFERENCES || "").trim(),
|
||||
);
|
||||
const MODEL_FILTER = new Set(
|
||||
String(process.env.VIP98K_IMAGE_TEST_MODELS || "")
|
||||
.split(",")
|
||||
.map((modelId) => modelId.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const TEST_PROMPT =
|
||||
String(process.env.VIP98K_IMAGE_TEST_PROMPT || "").trim() ||
|
||||
"Use BOTH references. Place the blue A card on the left and the red B card on the right. " +
|
||||
"Preserve both labels and colors in a landscape 16:9 composition at 1024x576.";
|
||||
const TERMINAL_STATUSES = new Set(["succeeded", "success", "completed", "done"]);
|
||||
const FAILURE_STATUSES = new Set(["failed", "error", "timeout", "canceled", "cancelled"]);
|
||||
|
||||
@ -162,12 +187,20 @@ async function pollTask(apiKey: string, taskId: string, resultPath: string) {
|
||||
throw new Error("VIP98K task did not finish within 15 minutes");
|
||||
}
|
||||
|
||||
async function analyzeImage(url: string) {
|
||||
async function analyzeImage(url: string, modelId: string) {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(120_000) });
|
||||
if (!response.ok) throw new Error(`Generated image download failed (HTTP ${response.status})`);
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
const image = sharp(buffer).removeAlpha();
|
||||
const metadata = await image.metadata();
|
||||
const outputPath = resolve(
|
||||
".local",
|
||||
`vip98k-${modelId.replace(/[^a-z0-9._-]+/gi, "-")}-latest.${
|
||||
metadata.format === "jpeg" ? "jpg" : metadata.format || "png"
|
||||
}`,
|
||||
);
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await writeFile(outputPath, buffer);
|
||||
const { data, info } = await image.resize(256, 256, { fit: "fill" }).raw().toBuffer({
|
||||
resolveWithObject: true,
|
||||
});
|
||||
@ -188,6 +221,7 @@ async function analyzeImage(url: string) {
|
||||
blueShare: Number((bluePixels / pixels).toFixed(4)),
|
||||
redShare: Number((redPixels / pixels).toFixed(4)),
|
||||
bothReferencesDetected: bluePixels / pixels > 0.03 && redPixels / pixels > 0.03,
|
||||
outputPath,
|
||||
sha256: createHash("sha256").update(buffer).digest("hex"),
|
||||
};
|
||||
}
|
||||
@ -216,9 +250,7 @@ async function testModel(apiKey: string, model: JsonRecord) {
|
||||
const body = { ...asRecord(invocation.request_example) };
|
||||
body.model = modelId;
|
||||
body.file_urls = REFERENCE_URLS;
|
||||
body.prompt =
|
||||
"Use BOTH references. Place the blue A card on the left and the red B card on the right. " +
|
||||
"Preserve both labels and colors in a landscape 16:9 composition at 1024x576.";
|
||||
body.prompt = TEST_PROMPT;
|
||||
const submitted = await readJson(
|
||||
await fetch(`${BASE_URL}${invocation.path}`, {
|
||||
method: String(invocation.method || "POST").toUpperCase(),
|
||||
@ -238,11 +270,13 @@ async function testModel(apiKey: string, model: JsonRecord) {
|
||||
);
|
||||
if (!resultPath) throw new Error(`${modelId} publishes no result path`);
|
||||
const completed = await pollTask(apiKey, taskId, resultPath);
|
||||
const image = await analyzeImage(completed.outputUrl);
|
||||
const image = await analyzeImage(completed.outputUrl, modelId);
|
||||
const passed = USES_CUSTOM_REFERENCES || image.bothReferencesDetected;
|
||||
return {
|
||||
modelId,
|
||||
status: image.bothReferencesDetected ? "passed" : "failed",
|
||||
eligibleForMultiImageEditing: image.bothReferencesDetected,
|
||||
status: passed ? "passed" : "failed",
|
||||
eligibleForMultiImageEditing: passed,
|
||||
referenceVerification: USES_CUSTOM_REFERENCES ? "manual" : "color-marker",
|
||||
publishedRequestKeys: keys.sort(),
|
||||
requested: { referenceImageCount: 2, aspectRatio: "16:9", size: "1024x576" },
|
||||
nativeParameterSupport: { aspectRatio: nativeAspectRatio, size: nativeSize },
|
||||
@ -254,7 +288,14 @@ async function testModel(apiKey: string, model: JsonRecord) {
|
||||
async function main() {
|
||||
const apiKey = String(process.env.VIP98K_API_KEY || "").trim();
|
||||
if (!apiKey) throw new Error("VIP98K_API_KEY is required");
|
||||
const models = await fetchCatalog(apiKey);
|
||||
const authorizedModels = await fetchCatalog(apiKey);
|
||||
const models =
|
||||
MODEL_FILTER.size > 0
|
||||
? authorizedModels.filter((model: JsonRecord) =>
|
||||
MODEL_FILTER.has(String(model.id || "")),
|
||||
)
|
||||
: authorizedModels;
|
||||
if (models.length === 0) throw new Error("No authorized VIP98K image models matched");
|
||||
const results: unknown[] = [];
|
||||
for (const model of models) {
|
||||
try {
|
||||
@ -271,7 +312,8 @@ async function main() {
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
baseUrl: BASE_URL,
|
||||
authorizedImageModelCount: models.length,
|
||||
authorizedImageModelCount: authorizedModels.length,
|
||||
testedImageModelCount: models.length,
|
||||
results,
|
||||
};
|
||||
const outputPath = resolve(
|
||||
|
||||
@ -5,7 +5,10 @@ import {
|
||||
selectVip98kTaskInvocation,
|
||||
type Vip98kTaskInvocation,
|
||||
} from "@/lib/ai/vip98k-task-invocation";
|
||||
import { buildVip98kModelPresets } from "@/lib/ai/vip98k-models";
|
||||
import {
|
||||
buildVip98kModelPresets,
|
||||
isVip98kImageModelVisible,
|
||||
} from "@/lib/ai/vip98k-models";
|
||||
import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery";
|
||||
|
||||
const textToImageInvocation: Vip98kTaskInvocation = {
|
||||
@ -20,6 +23,15 @@ const textToImageInvocation: Vip98kTaskInvocation = {
|
||||
};
|
||||
|
||||
test.describe("VIP98K reference-image routing", () => {
|
||||
test("shows the newly published Nano Banana Pro image editor", () => {
|
||||
expect(
|
||||
isVip98kImageModelVisible({
|
||||
modelId: "nano-banana-pro",
|
||||
type: "image",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("replaces a text-to-image alias invocation with the resolved reference tool", () => {
|
||||
const invocation = resolveVip98kTaskInvocation(
|
||||
"google/nano-banana-2/reference-to-image",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user