fix: retry vip98k while staged media propagates
This commit is contained in:
parent
01b3f6eec1
commit
c69ddb1bb3
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user