From 821b8658a8f49368ebe205fdf8457bba506254c4 Mon Sep 17 00:00:00 2001 From: 134 Date: Thu, 10 Sep 2026 10:08:13 +0800 Subject: [PATCH] feat: add verified VIP98K multi-image models --- lib/ai/providers/vip98k.ts | 4 + lib/ai/vip98k-models.ts | 5 ++ lib/ai/vip98k-reference-staging.ts | 5 +- tests/routes/vip98k-reference-routing.test.ts | 83 +++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/lib/ai/providers/vip98k.ts b/lib/ai/providers/vip98k.ts index a9b25a3a4..87ae6f5bd 100644 --- a/lib/ai/providers/vip98k.ts +++ b/lib/ai/providers/vip98k.ts @@ -1427,6 +1427,7 @@ function buildTaskBody(params: Vip98kTaskParams, invocation: Vip98kInvocation | videoParams.resolution || imageParams.size || imageParams.imageSize || "", ).trim(); const negativePrompt = String(videoParams.negativePrompt || imageParams.negativePrompt || "").trim(); + const quality = String(imageParams.quality || "").trim().toLowerCase(); const outputConstraintFallbacks: string[] = []; let hasNativeAspectRatio = false; let hasNativeResolution = false; @@ -1481,6 +1482,9 @@ function buildTaskBody(params: Vip98kTaskParams, invocation: Vip98kInvocation | if ("negativePrompt" in body || !invocation) body.negativePrompt = negativePrompt; if ("negative_prompt" in body || !invocation) body.negative_prompt = negativePrompt; } + if (quality && invocationSupportsBodyField(invocation, "quality")) { + body.quality = quality; + } const imageToolParamAliases = [ ["scaleFactor", "scale_factor"], ["xScale", "x_scale"], diff --git a/lib/ai/vip98k-models.ts b/lib/ai/vip98k-models.ts index b267c158e..c1374ebbb 100644 --- a/lib/ai/vip98k-models.ts +++ b/lib/ai/vip98k-models.ts @@ -185,6 +185,8 @@ const VERIFIED_2026_IMAGE_MODEL_PATTERNS = [ /^nano-banana-fast$/, /^nano-banana-pro$/, /^gpt-image-2(?:$|[-/])/, + /^gpt-image-2\.5$/, + /^gpt-image-2\.5-flare$/, /^alibaba\/wan-2\.(?:6|7)(?:$|[-/])/, /^black-forest-labs\/flux-2(?:$|[-/])/, /^bytedance\/seedream-v5(?:$|[.\-/])/, @@ -689,6 +691,7 @@ function createImagePreset( "ratio", "aspect-ratio", ].some((key) => invocationKeys.has(key)) || supportsNativeSize; + const supportsQuality = invocationKeys.has("quality"); const supportedModes = [ ...(capabilities.includes("text-to-image") || capabilities.length === 0 ? ["text-to-image"] @@ -738,6 +741,7 @@ function createImagePreset( taskApi: "external-task", supportedModes, ...(supportsNativeSize ? { imageSize: defaultImageSize } : {}), + ...(supportsQuality ? { quality: "auto" } : {}), aspectRatio: "auto", imageCount: 1, }, @@ -745,6 +749,7 @@ function createImagePreset( maxImages: 1, maxReferenceImages, supportsSize: supportsNativeSize, + supportsQuality, supports1K: supportsNativeSize && supportedImageSizes.includes("1K"), supports2K: supportsNativeSize && supportedImageSizes.includes("2K"), supports4K: supportsNativeSize && supportedImageSizes.includes("4K"), diff --git a/lib/ai/vip98k-reference-staging.ts b/lib/ai/vip98k-reference-staging.ts index 80f30a8c6..d768e53af 100644 --- a/lib/ai/vip98k-reference-staging.ts +++ b/lib/ai/vip98k-reference-staging.ts @@ -99,14 +99,15 @@ export function getVip98kSubmissionRetryDelay(params: { maxAttempts: number; retryAfterMs?: number | null; }) { + const recoverableGatewayFailure = params.status === 502 || params.status === 504; const recoverableSubmission = /pending\s+safe\s+reconciliation/i.test(params.message) || /request\s+outcome\s+(?:is\s+)?unknown/i.test(params.message) || /final\s+outcome\s+could\s+not\s+be\s+confirmed/i.test(params.message); if ( - params.status !== 503 || params.attempt >= params.maxAttempts || - !recoverableSubmission + (!recoverableGatewayFailure && + (params.status !== 503 || !recoverableSubmission)) ) { return null; } diff --git a/tests/routes/vip98k-reference-routing.test.ts b/tests/routes/vip98k-reference-routing.test.ts index fbde48251..2a92a2b5d 100644 --- a/tests/routes/vip98k-reference-routing.test.ts +++ b/tests/routes/vip98k-reference-routing.test.ts @@ -193,6 +193,23 @@ test.describe("VIP98K reference-image routing", () => { maxAttempts: 3, }), ).toBeNull(); + expect( + getVip98kSubmissionRetryDelay({ + status: 502, + message: "Bad Gateway", + attempt: 1, + maxAttempts: 3, + }), + ).toBe(15_000); + expect( + getVip98kSubmissionRetryDelay({ + status: 504, + message: "Gateway Timeout", + attempt: 2, + maxAttempts: 3, + retryAfterMs: 6_000, + }), + ).toBe(6_000); }); test("shows the newly published Nano Banana Pro image editor", () => { @@ -523,6 +540,72 @@ test.describe("VIP98K reference-image routing", () => { }); }); + test("publishes only the verified GPT Image 2.5 multi-image variants", () => { + const models = [ + "gpt-image-2.5", + "gpt-image-2.5-flare", + "gpt-image-2.5-sunburst", + ].map((modelId) => ({ + id: modelId, + type: "image", + capabilities: ["image_to_image", "openai.images.generations"], + invocation: { + kind: "tool", + method: "POST", + path: "/v1/images/generations", + content_type: "application/json", + request_example: { + image: ["https://api.vip98k.com/logo.png"], + model: modelId, + n: 1, + prompt: "Edit", + quality: "auto", + stream: false, + }, + }, + })); + + expect(models.map((model) => isVip98kImageModelVisible(model))).toEqual([ + true, + true, + false, + ]); + + const presets = buildVip98kModelPresets(models); + expect( + presets.map((preset) => ({ + modelId: preset.modelId, + isHidden: preset.isHidden, + supportsQuality: preset.imageGenConfig?.supportsQuality, + quality: preset.defaultParams.quality, + supportedAspectRatios: + preset.imageGenConfig?.supportedAspectRatios, + })), + ).toEqual([ + { + modelId: "gpt-image-2.5", + isHidden: false, + supportsQuality: true, + quality: "auto", + supportedAspectRatios: ["auto"], + }, + { + modelId: "gpt-image-2.5-flare", + isHidden: false, + supportsQuality: true, + quality: "auto", + supportedAspectRatios: ["auto"], + }, + { + modelId: "gpt-image-2.5-sunburst", + isHidden: true, + supportsQuality: true, + quality: "auto", + supportedAspectRatios: ["auto"], + }, + ]); + }); + test("does not inherit multi-image support from broad authorized metadata", () => { const presets = buildVip98kModelPresets([ {