fix(canvas): preserve uncertain image submissions without paid failover
This commit is contained in:
parent
c45a6a77a0
commit
96d67955cb
@ -1,6 +1,7 @@
|
||||
import { mapVip98kVideoParameters } from "../vip98k-video-parameters";
|
||||
import {
|
||||
ProviderTaskFailedError,
|
||||
ProviderTaskUncertainError,
|
||||
type ProviderTaskLifecycle,
|
||||
} from "@/lib/video-canvas/video-task-lifecycle";
|
||||
import "server-only";
|
||||
@ -1113,7 +1114,7 @@ async function submitTask(params: {
|
||||
submissionTimeoutMs: params.submissionTimeoutMs,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
throw new ProviderTaskUncertainError(`供应商提交结果未确认,可能仍在处理,请勿重复生成:${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
const retryDelayMs = attempt * params.submissionRetryBaseDelayMs;
|
||||
logger.warn("provider submission transport failed; retrying idempotently", {
|
||||
@ -1192,6 +1193,10 @@ async function submitTask(params: {
|
||||
await delay(reconciliationRetryDelayMs);
|
||||
continue;
|
||||
}
|
||||
if ((response.status === 409 && /idempotency\s+key.*(?:still\s+in\s+progress|processing)/i.test(errorMessage))
|
||||
|| [502, 504].includes(response.status)
|
||||
|| /pending\s+safe\s+reconciliation|request\s+outcome\s+(?:is\s+)?unknown|final\s+outcome\s+could\s+not\s+be\s+confirmed/i.test(errorMessage))
|
||||
throw new ProviderTaskUncertainError(`供应商提交结果未确认,可能仍在处理,请勿重复生成:${errorMessage} (HTTP ${response.status})`);
|
||||
throw new Error(`${errorMessage} (HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
|
||||
@ -100,7 +100,8 @@ export function getVip98kSubmissionRetryDelay(params: {
|
||||
maxAttempts: number;
|
||||
retryAfterMs?: number | null;
|
||||
}) {
|
||||
const recoverableGatewayFailure = params.status === 502 || params.status === 504;
|
||||
const inProgress = params.status === 409 && /idempotency\s+key.*(?:still\s+in\s+progress|processing)/i.test(params.message);
|
||||
const recoverableGatewayFailure = params.status === 502 || params.status === 504 || inProgress;
|
||||
const recoverableSubmission =
|
||||
/pending\s+safe\s+reconciliation/i.test(params.message) ||
|
||||
/request\s+outcome\s+(?:is\s+)?unknown/i.test(params.message) ||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import "server-only";
|
||||
import { ProviderTaskUncertainError } from "@/lib/video-canvas/video-task-lifecycle";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
createCanvasImageTask,
|
||||
@ -120,8 +121,9 @@ export async function executeCanvasImageResponse(body: GenerateImageRequest, use
|
||||
} catch (error) {
|
||||
console.error("[Canvas Generate Image] Error:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: getUserFacingImageGenerationErrorMessage(error) },
|
||||
{ status: 500 },
|
||||
{ success: false, error: getUserFacingImageGenerationErrorMessage(error),
|
||||
...(error instanceof ProviderTaskUncertainError ? { code: "PROVIDER_TASK_UNCERTAIN" } : {}) },
|
||||
{ status: error instanceof ProviderTaskUncertainError ? 503 : 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import "server-only";
|
||||
import { ProviderTaskUncertainError } from "@/lib/video-canvas/video-task-lifecycle";
|
||||
|
||||
import sharp from "sharp";
|
||||
import { getEnabledAiModelsWithProvider } from "@/lib/db/queries";
|
||||
@ -5623,6 +5624,9 @@ export async function generateCanvasImage(
|
||||
await slot.release();
|
||||
}
|
||||
|
||||
// An accepted or uncertain request must not start another paid model/variant.
|
||||
if (caughtError instanceof ProviderTaskUncertainError) throw caughtError;
|
||||
|
||||
const retryVariants = Array.isArray(request.retryVariants)
|
||||
? request.retryVariants
|
||||
: [];
|
||||
|
||||
@ -159,6 +159,7 @@ async function execute(task: CanvasGenerationTask, userId: string) {
|
||||
)
|
||||
: await executeWorkflowAnalysisResponse(params);
|
||||
const body = await response.json();
|
||||
if (body.code === "PROVIDER_TASK_UNCERTAIN") throw new ProviderTaskUncertainError(body.error);
|
||||
if (!response.ok || !body.success) throw Error(body.error || "任务执行失败");
|
||||
if (
|
||||
request.kind === "image" &&
|
||||
|
||||
@ -232,6 +232,12 @@ test.describe("VIP98K reference-image routing", () => {
|
||||
});
|
||||
|
||||
test("retries provider reconciliation without treating it as a media failure", () => {
|
||||
expect(getVip98kSubmissionRetryDelay({ status: 409, message: "A request with this idempotency key is still in progress.",
|
||||
attempt: 1, maxAttempts: 3 })).toBe(15_000);
|
||||
expect(getVip98kSubmissionRetryDelay({ status: 409, message: "Conflicting request parameters",
|
||||
attempt: 1, maxAttempts: 3 })).toBeNull();
|
||||
expect(getVip98kSubmissionRetryDelay({ status: 409, message: "A request with this idempotency key is still in progress.",
|
||||
attempt: 3, maxAttempts: 3 })).toBeNull();
|
||||
expect(
|
||||
getVip98kSubmissionRetryDelay({
|
||||
status: 503,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user