fix: retry vip98k while staged media propagates

This commit is contained in:
134 2026-09-07 12:31:57 +08:00 committed by root
parent 01b3f6eec1
commit c69ddb1bb3
3 changed files with 104 additions and 21 deletions

View File

@ -13,6 +13,7 @@ import { VIP98K_BASE_URL, normalizeVip98kBaseUrl } from "@/lib/ai/vip98k-models"
import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery"; import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery";
import { import {
getStorageObjectKeyFromPublicUrl, getStorageObjectKeyFromPublicUrl,
getVip98kStagedMediaRetryDelay,
shouldStageVip98kReferenceUrl, shouldStageVip98kReferenceUrl,
} from "@/lib/ai/vip98k-reference-staging"; } from "@/lib/ai/vip98k-reference-staging";
import { createLogger } from "@/lib/observability/logger"; import { createLogger } from "@/lib/observability/logger";
@ -499,6 +500,14 @@ function getUrlHost(value: string) {
} }
} }
function isStagedReferenceUrl(value: string) {
try {
return new URL(value).pathname.startsWith(`/${REFERENCE_STAGING_KEY_PREFIX}`);
} catch {
return false;
}
}
async function downloadReferenceImage(mediaUrl: string) { async function downloadReferenceImage(mediaUrl: string) {
let lastError: unknown; let lastError: unknown;
for (let attempt = 1; attempt <= REFERENCE_DOWNLOAD_ATTEMPTS; attempt += 1) { for (let attempt = 1; attempt <= REFERENCE_DOWNLOAD_ATTEMPTS; attempt += 1) {
@ -791,25 +800,55 @@ async function submitTask(params: {
const baseURL = normalizeVip98kBaseUrl(params.baseURL); const baseURL = normalizeVip98kBaseUrl(params.baseURL);
const path = String(params.path || "/api/external/task/generate").trim(); const path = String(params.path || "/api/external/task/generate").trim();
if (!path.startsWith("/")) throw new Error("VIP98K invocation path is invalid"); if (!path.startsWith("/")) throw new Error("VIP98K invocation path is invalid");
const response = await fetch(`${baseURL}${path}`, { const stagedReferences = Array.isArray(params.body.file_urls)
method: String(params.method || "POST").toUpperCase(), ? params.body.file_urls.filter(
headers: { (value): value is string =>
Authorization: `Bearer ${params.apiKey}`, typeof value === "string" && isStagedReferenceUrl(value),
"Content-Type": params.contentType || "application/json", )
}, : [];
body: JSON.stringify(params.body), const maxAttempts = stagedReferences.length > 0 ? 3 : 1;
signal: AbortSignal.timeout(120_000),
}); for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const payload = await parseJsonResponse(response); const response = await fetch(`${baseURL}${path}`, {
if (!response.ok) { method: String(params.method || "POST").toUpperCase(),
throw new Error(`${extractErrorMessage(payload)} (HTTP ${response.status})`); headers: {
Authorization: `Bearer ${params.apiKey}`,
"Content-Type": params.contentType || "application/json",
},
body: JSON.stringify(params.body),
signal: AbortSignal.timeout(120_000),
});
const payload = await parseJsonResponse(response);
if (response.ok) {
return {
payload,
taskId: extractTaskId(payload),
resultPath: extractResultPath(payload),
outputs: collectOutputUrls(payload),
};
}
const errorMessage = extractErrorMessage(payload);
const retryDelayMs = getVip98kStagedMediaRetryDelay({
status: response.status,
message: errorMessage,
attempt,
maxAttempts,
stagedReferenceCount: stagedReferences.length,
});
if (retryDelayMs !== null) {
logger.warn("staged reference not ready for provider; retrying submission", {
attempt,
retryDelayMs,
stagedReferenceCount: stagedReferences.length,
});
await delay(retryDelayMs);
continue;
}
throw new Error(`${errorMessage} (HTTP ${response.status})`);
} }
return {
payload, throw new Error("VIP98K request failed after media readiness retries");
taskId: extractTaskId(payload),
resultPath: extractResultPath(payload),
outputs: collectOutputUrls(payload),
};
} }
export type Vip98kExternalTaskSubmission = { export type Vip98kExternalTaskSubmission = {

View File

@ -1,3 +1,5 @@
import { isMediaInputFailure } from "@/lib/ai/runtime/request-failure-classification";
function parseUrl(value: string) { function parseUrl(value: string) {
try { try {
return new URL(String(value || "").trim()); return new URL(String(value || "").trim());
@ -49,3 +51,21 @@ export function getStorageObjectKeyFromPublicUrl(
return encodedKey; return encodedKey;
} }
} }
export function getVip98kStagedMediaRetryDelay(params: {
status: number;
message: string;
attempt: number;
maxAttempts: number;
stagedReferenceCount: number;
}) {
if (
params.status !== 400 ||
params.stagedReferenceCount < 1 ||
params.attempt >= params.maxAttempts ||
!isMediaInputFailure(params.message)
) {
return null;
}
return params.attempt * 20_000;
}

View File

@ -9,8 +9,11 @@ import {
buildVip98kModelPresets, buildVip98kModelPresets,
isVip98kImageModelVisible, isVip98kImageModelVisible,
} from "@/lib/ai/vip98k-models"; } from "@/lib/ai/vip98k-models";
import { shouldStageVip98kReferenceUrl } from "@/lib/ai/vip98k-reference-staging"; import {
import { getStorageObjectKeyFromPublicUrl } from "@/lib/ai/vip98k-reference-staging"; getStorageObjectKeyFromPublicUrl,
getVip98kStagedMediaRetryDelay,
shouldStageVip98kReferenceUrl,
} from "@/lib/ai/vip98k-reference-staging";
import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery"; import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery";
const textToImageInvocation: Vip98kTaskInvocation = { const textToImageInvocation: Vip98kTaskInvocation = {
@ -25,7 +28,7 @@ const textToImageInvocation: Vip98kTaskInvocation = {
}; };
test.describe("VIP98K reference-image routing", () => { test.describe("VIP98K reference-image routing", () => {
test("stages references from the configured default storage into backup storage", () => { test("identifies default-storage references that need provider staging", () => {
expect( expect(
shouldStageVip98kReferenceUrl({ shouldStageVip98kReferenceUrl({
mediaUrl: "https://default.example/uploads/reference.jpg", mediaUrl: "https://default.example/uploads/reference.jpg",
@ -55,6 +58,27 @@ test.describe("VIP98K reference-image routing", () => {
).toBe("folder name/reference.jpg"); ).toBe("folder name/reference.jpg");
}); });
test("retries transient media rejection while a staged URL propagates", () => {
expect(
getVip98kStagedMediaRetryDelay({
status: 400,
message: "The media request is invalid.",
attempt: 1,
maxAttempts: 3,
stagedReferenceCount: 2,
}),
).toBe(20_000);
expect(
getVip98kStagedMediaRetryDelay({
status: 400,
message: "The media request is invalid.",
attempt: 3,
maxAttempts: 3,
stagedReferenceCount: 2,
}),
).toBeNull();
});
test("shows the newly published Nano Banana Pro image editor", () => { test("shows the newly published Nano Banana Pro image editor", () => {
expect( expect(
isVip98kImageModelVisible({ isVip98kImageModelVisible({