fix: retry vip98k reconciliation responses

This commit is contained in:
134 2026-09-07 13:14:13 +08:00
parent 44828ca5a5
commit 11e44f9971
3 changed files with 84 additions and 7 deletions

View File

@ -15,6 +15,7 @@ import {
addVip98kReferenceRetryToken,
getStorageObjectKeyFromPublicUrl,
getVip98kStagedMediaRetryDelay,
getVip98kSubmissionRetryDelay,
shouldStageVip98kReferenceUrl,
} from "@/lib/ai/vip98k-reference-staging";
import { createLogger } from "@/lib/observability/logger";
@ -67,6 +68,7 @@ const REFERENCE_DOWNLOAD_TIMEOUT_MS = 120_000;
const MAX_REFERENCE_DOWNLOAD_BYTES = 40 * 1024 * 1024;
const REFERENCE_STAGING_CACHE_MS = 60_000;
const REFERENCE_STAGING_KEY_PREFIX = "uploads/vip98k-";
const MAX_SUBMISSION_ATTEMPTS = 3;
type ReferenceStagingConfig = {
sourcePublicUrl: string;
@ -807,11 +809,12 @@ async function submitTask(params: {
typeof value === "string" && isStagedReferenceUrl(value),
)
: [];
const maxAttempts = stagedReferences.length > 0 ? 3 : 1;
const maxAttempts = MAX_SUBMISSION_ATTEMPTS;
let refreshStagedReferences = false;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const requestBody =
attempt === 1 || !Array.isArray(params.body.file_urls)
!refreshStagedReferences || !Array.isArray(params.body.file_urls)
? params.body
: {
...params.body,
@ -844,20 +847,41 @@ async function submitTask(params: {
}
const errorMessage = extractErrorMessage(payload);
const retryDelayMs = getVip98kStagedMediaRetryDelay({
const stagedMediaRetryDelayMs = getVip98kStagedMediaRetryDelay({
status: response.status,
message: errorMessage,
attempt,
maxAttempts,
stagedReferenceCount: stagedReferences.length,
});
if (retryDelayMs !== null) {
if (stagedMediaRetryDelayMs !== null) {
logger.warn("staged reference not ready for provider; retrying submission", {
attempt,
retryDelayMs,
retryDelayMs: stagedMediaRetryDelayMs,
stagedReferenceCount: stagedReferences.length,
});
await delay(retryDelayMs);
refreshStagedReferences = true;
await delay(stagedMediaRetryDelayMs);
continue;
}
const retryAfterSeconds = Number(response.headers.get("retry-after"));
const reconciliationRetryDelayMs = getVip98kSubmissionRetryDelay({
status: response.status,
message: errorMessage,
attempt,
maxAttempts,
retryAfterMs: Number.isFinite(retryAfterSeconds)
? retryAfterSeconds * 1000
: null,
});
if (reconciliationRetryDelayMs !== null) {
logger.warn("provider is reconciling task submission; retrying", {
attempt,
retryDelayMs: reconciliationRetryDelayMs,
});
refreshStagedReferences = false;
await delay(reconciliationRetryDelayMs);
continue;
}
throw new Error(`${errorMessage} (HTTP ${response.status})`);
@ -1312,7 +1336,9 @@ export function createVip98k({ apiKey, baseURL }: Vip98kConfig) {
const activeModelId = String(options.overrideModelId || modelId || "").trim();
if (!activeModelId) throw new Error("VIP98K model ID is not configured");
const activeKind = inferKind(activeModelId, runtimeConfig);
const { prompt, imageUrls, videoUrls } = extractPromptAndMedia(options.messages);
const { prompt, imageUrls, videoUrls } = extractPromptAndMedia(
options.messages,
);
const [publicImageUrls, publicVideoUrls] = await Promise.all([
Promise.all(
imageUrls.map((url) => ensurePublicMediaUrl(url, "image")),

View File

@ -70,6 +70,28 @@ export function getVip98kStagedMediaRetryDelay(params: {
return params.attempt * 20_000;
}
export function getVip98kSubmissionRetryDelay(params: {
status: number;
message: string;
attempt: number;
maxAttempts: number;
retryAfterMs?: number | null;
}) {
if (
params.status !== 503 ||
params.attempt >= params.maxAttempts ||
!/pending\s+safe\s+reconciliation/i.test(params.message)
) {
return null;
}
const providerDelay = Number(params.retryAfterMs);
if (Number.isFinite(providerDelay) && providerDelay > 0) {
return Math.min(Math.max(providerDelay, 1_000), 60_000);
}
return Math.min(params.attempt * 15_000, 30_000);
}
export function addVip98kReferenceRetryToken(mediaUrl: string, token: string) {
const parsed = parseUrl(mediaUrl);
if (!parsed) return mediaUrl;

View File

@ -13,6 +13,7 @@ import {
addVip98kReferenceRetryToken,
getStorageObjectKeyFromPublicUrl,
getVip98kStagedMediaRetryDelay,
getVip98kSubmissionRetryDelay,
shouldStageVip98kReferenceUrl,
} from "@/lib/ai/vip98k-reference-staging";
import { shouldResolveVip98kResultDelivery } from "@/lib/ai/vip98k-result-delivery";
@ -88,6 +89,34 @@ test.describe("VIP98K reference-image routing", () => {
);
});
test("retries provider reconciliation without treating it as a media failure", () => {
expect(
getVip98kSubmissionRetryDelay({
status: 503,
message: "The media request is pending safe reconciliation.",
attempt: 1,
maxAttempts: 3,
}),
).toBe(15_000);
expect(
getVip98kSubmissionRetryDelay({
status: 503,
message: "pending safe reconciliation",
attempt: 2,
maxAttempts: 3,
retryAfterMs: 8_000,
}),
).toBe(8_000);
expect(
getVip98kSubmissionRetryDelay({
status: 503,
message: "pending safe reconciliation",
attempt: 3,
maxAttempts: 3,
}),
).toBeNull();
});
test("shows the newly published Nano Banana Pro image editor", () => {
expect(
isVip98kImageModelVisible({