From 608bd256798eebf1e7ae2a5017a835237ee2bbd4 Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 7 Sep 2026 00:32:20 +0800 Subject: [PATCH] fix: trust vip98k published image contracts --- lib/ai/vip98k-models.ts | 27 +++++++++++++++++-- tests/routes/vip98k-reference-routing.test.ts | 24 +++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/lib/ai/vip98k-models.ts b/lib/ai/vip98k-models.ts index 920735866..cccaeb74e 100644 --- a/lib/ai/vip98k-models.ts +++ b/lib/ai/vip98k-models.ts @@ -381,6 +381,13 @@ function hasMediaReferenceInput( modelId, ); + // The V2 invocation contract is the executable source of truth. Authorized + // catalog metadata can advertise broad multimodal capability even when the + // published request body only accepts a text prompt. + if (type === "image" && getInvocationCandidates(model).length > 0) { + return hasMediaRequestField; + } + return ( hasMediaCapability || inputModes.some((mode) => mediaInputModes.has(mode)) || @@ -389,6 +396,17 @@ function hasMediaReferenceInput( ); } +function hasMultiImageReferenceInput(model: Vip98kCatalogModel) { + const multiImageKeys = new Set([ + "file-urls", + "images", + "image-urls", + "reference-images", + "video-clips", + ]); + return getInvocationRequestKeys(model).some((key) => multiImageKeys.has(key)); +} + export function isVip98kCatalogModelAvailable(model: Vip98kCatalogModel) { const status = String(model.status || "").trim().toLowerCase(); if (UNAVAILABLE_STATUS_TOKENS.some((token) => status.includes(token))) { @@ -598,6 +616,7 @@ function createImagePreset( const supportedImageSizes = getSupportedImageSizes(model); const invocationKeys = new Set(getInvocationRequestKeys(model)); const supportsReferenceImages = hasMediaReferenceInput(model, "image"); + const supportsMultiImageReference = hasMultiImageReferenceInput(model); const supportsNativeSize = [ "size", "image-size", @@ -615,7 +634,11 @@ function createImagePreset( : []), ...(supportsReferenceImages ? ["image-to-image", "reference-images"] : []), ]; - const maxReferenceImages = supportsReferenceImages ? 8 : 0; + const maxReferenceImages = supportsMultiImageReference + ? 8 + : supportsReferenceImages + ? 1 + : 0; const defaultImageSize = supportedImageSizes.find((size) => size === "2K") || supportedImageSizes[0] || @@ -642,7 +665,7 @@ function createImagePreset( supportedAspectRatios: supportsNativeAspectRatio ? [...DEFAULT_ASPECT_RATIOS] : ["auto"], - supportsMultiImageReference: supportsReferenceImages, + supportsMultiImageReference, maxReferenceImages, vip98k: getVip98kMetadata(model, "image"), }, diff --git a/tests/routes/vip98k-reference-routing.test.ts b/tests/routes/vip98k-reference-routing.test.ts index ac2f0ef92..bb42e16e7 100644 --- a/tests/routes/vip98k-reference-routing.test.ts +++ b/tests/routes/vip98k-reference-routing.test.ts @@ -258,4 +258,28 @@ test.describe("VIP98K reference-image routing", () => { expect(preset).toBeUndefined(); }); + + test("does not inherit multi-image support from broad authorized metadata", () => { + const presets = buildVip98kModelPresets([ + { + id: "nano-banana-2-lite", + type: "image", + capabilities: ["text_to_image", "image_to_image"], + inputModes: ["text", "image"], + invocation: { + kind: "tool", + method: "POST", + path: "/v1/images/generations", + content_type: "application/json", + request_example: { + model: "nano-banana-2-lite", + n: 1, + prompt: "Generate", + }, + }, + }, + ]); + + expect(presets).toEqual([]); + }); });