diff --git a/lib/ai/providers/openai-image.ts b/lib/ai/providers/openai-image.ts index ca0a350b5..2e8e546cb 100644 --- a/lib/ai/providers/openai-image.ts +++ b/lib/ai/providers/openai-image.ts @@ -25,6 +25,7 @@ interface UploadedImage { const DEFAULT_IMAGE_SIZE = "2K"; const DEFAULT_TIMEOUT_MS = 300000; +const LONG_RUNNING_COMFLY_TIMEOUT_MS = 590000; const OPENAI_IMAGE_MIN_TOTAL_PIXELS = 3_686_400; const OPENAI_IMAGE_DIMENSION_MULTIPLE = 16; const BLTCY_HOST_REGEX = /(^|\.)bltcy\.ai$/i; @@ -216,6 +217,13 @@ function resolveProviderImageSize( return resolveOpenAISize(aspectRatio, imageSize); } +function resolveRequestTimeoutMs(baseURL: string, imageSize?: "1K" | "2K" | "4K") { + if (isComflyBaseURL(baseURL) && imageSize === "4K") { + return LONG_RUNNING_COMFLY_TIMEOUT_MS; + } + return DEFAULT_TIMEOUT_MS; +} + function normalizeQuality(value?: string) { const normalized = String(value || "").trim().toLowerCase(); if (normalized === "low" || normalized === "medium" || normalized === "high") { @@ -453,9 +461,10 @@ async function requestOpenAIImageGeneration(params: { typeof aspectRatio === "string" && aspectRatio.trim() && aspectRatio !== "auto" ? aspectRatio.trim() : undefined; + const timeoutMs = resolveRequestTimeoutMs(baseURL, imageSizeLabel); const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { if (imageUrls.length > 0) { @@ -588,6 +597,7 @@ export function createOpenAIImage({ apiKey, baseURL }: OpenAIImageConfig) { promptAspectRatio: promptAspectRatio || undefined, imageSize, resolutionMode: isComflyBaseURL(baseURL) ? "longest-edge" : "pixel-area", + timeoutMs: resolveRequestTimeoutMs(baseURL, imageSize), useBltcyImageCompatParams: shouldUseBltcyImageCompatParams(baseURL), imageCount: imageUrls.length, baseURL: normalizeBaseURL(baseURL), diff --git a/lib/ai/runtime/model-health.ts b/lib/ai/runtime/model-health.ts index 4d19aea38..3530c01ab 100644 --- a/lib/ai/runtime/model-health.ts +++ b/lib/ai/runtime/model-health.ts @@ -89,6 +89,22 @@ export function classifyAiModelHealthFailure(error: unknown): { }; } + if ( + normalized.includes("fetch failed") || + normalized.includes("econnreset") || + normalized.includes("econnrefused") || + normalized.includes("socket hang up") || + normalized.includes("networkerror") || + normalized.includes("network error") + ) { + return { + eventType: "unavailable", + severity: "warning", + counted: false, + reason: "transport_failure_no_provider_response", + }; + } + if ( normalized.includes("model not exist") || normalized.includes("model_not_found") || @@ -133,9 +149,9 @@ export function classifyAiModelHealthFailure(error: unknown): { ) { return { eventType: "timeout", - severity: "critical", - counted: true, - reason: "timeout", + severity: "warning", + counted: false, + reason: "timeout_without_provider_response", }; } @@ -150,8 +166,7 @@ export function classifyAiModelHealthFailure(error: unknown): { normalized.includes("no available channel") || normalized.includes("no supported channel") || normalized.includes("service unavailable") || - normalized.includes("fetch failed") || - normalized.includes("econnreset") + normalized.includes("temporarily unavailable") ) { return { eventType: "unavailable", @@ -184,7 +199,32 @@ export function classifyAiModelHealthFailure(error: unknown): { } export function shouldFailoverAiModelError(error: unknown) { - return classifyAiModelHealthFailure(error).counted; + const classification = classifyAiModelHealthFailure(error); + if (!classification.counted) return false; + + const normalized = getErrorMessage(error).toLowerCase(); + if (classification.eventType === "timeout" || classification.eventType === "download_error") { + return false; + } + if (classification.eventType === "invalid_model" || classification.eventType === "quota") { + return true; + } + if (classification.eventType === "unavailable") { + return ( + /(?:^|\D)(?:429|500|502|503|504)(?:\D|$)/.test(normalized) || + normalized.includes("no available channel") || + normalized.includes("no supported channel") || + normalized.includes("service unavailable") || + normalized.includes("temporarily unavailable") || + normalized.includes("model maintenance") || + normalized.includes("maintenance") || + normalized.includes("修复中") || + normalized.includes("系统繁忙") || + normalized.includes("无可用渠道") + ); + } + + return /(?:^|\D)[45]\d\d(?:\D|$)/.test(normalized); } function buildMetadata(value?: Record) {