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 {
getStorageObjectKeyFromPublicUrl,
getVip98kStagedMediaRetryDelay,
shouldStageVip98kReferenceUrl,
} from "@/lib/ai/vip98k-reference-staging";
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) {
let lastError: unknown;
for (let attempt = 1; attempt <= REFERENCE_DOWNLOAD_ATTEMPTS; attempt += 1) {
@ -791,25 +800,55 @@ async function submitTask(params: {
const baseURL = normalizeVip98kBaseUrl(params.baseURL);
const path = String(params.path || "/api/external/task/generate").trim();
if (!path.startsWith("/")) throw new Error("VIP98K invocation path is invalid");
const response = await fetch(`${baseURL}${path}`, {
method: String(params.method || "POST").toUpperCase(),
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) {
throw new Error(`${extractErrorMessage(payload)} (HTTP ${response.status})`);
const stagedReferences = Array.isArray(params.body.file_urls)
? params.body.file_urls.filter(
(value): value is string =>
typeof value === "string" && isStagedReferenceUrl(value),
)
: [];
const maxAttempts = stagedReferences.length > 0 ? 3 : 1;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const response = await fetch(`${baseURL}${path}`, {
method: String(params.method || "POST").toUpperCase(),
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,
taskId: extractTaskId(payload),
resultPath: extractResultPath(payload),
outputs: collectOutputUrls(payload),
};
throw new Error("VIP98K request failed after media readiness retries");
}
export type Vip98kExternalTaskSubmission = {

View File

@ -1,3 +1,5 @@
import { isMediaInputFailure } from "@/lib/ai/runtime/request-failure-classification";
function parseUrl(value: string) {
try {
return new URL(String(value || "").trim());
@ -49,3 +51,21 @@ export function getStorageObjectKeyFromPublicUrl(
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,
isVip98kImageModelVisible,
} from "@/lib/ai/vip98k-models";
import { shouldStageVip98kReferenceUrl } from "@/lib/ai/vip98k-reference-staging";
import { getStorageObjectKeyFromPublicUrl } from "@/lib/ai/vip98k-reference-staging";
import {
getStorageObjectKeyFromPublicUrl,
getVip98kStagedMediaRetryDelay,
shouldStageVip98kReferenceUrl,
} from "@/lib/ai/vip98k-reference-staging";
import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery";
const textToImageInvocation: Vip98kTaskInvocation = {
@ -25,7 +28,7 @@ const textToImageInvocation: Vip98kTaskInvocation = {
};
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(
shouldStageVip98kReferenceUrl({
mediaUrl: "https://default.example/uploads/reference.jpg",
@ -55,6 +58,27 @@ test.describe("VIP98K reference-image routing", () => {
).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", () => {
expect(
isVip98kImageModelVisible({