From 3e594d723fbac5b28d11ea50323790ccf533b3d7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 25 Feb 2026 11:32:02 +0800 Subject: [PATCH] fix: add failover chain for openai-compatible image generation --- app/(chat)/api/chat/route.ts | 196 ++++++++++++++++++++++++++++++++--- 1 file changed, 183 insertions(+), 13 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index b0beb115c..1f8b78335 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -4219,6 +4219,39 @@ export async function POST(request: Request) { // ๐Ÿ†• ๅค„็† OpenAI ๅ…ผๅฎน็š„็”Ÿๅ›พๆจกๅž‹่พ“ๅ‡บ๏ผˆ้˜ฒๆญข่ฟ”ๅ›ž data:image;base64 ๅŽŸๆ–‡๏ผ‰ if (isOpenAICompatImageModel) { console.log("[Chat API] Using generateText for OpenAI-compatible image model"); + const configuredFallbacks = [ + currentModelConfig?.fallbackModelId1, + currentModelConfig?.fallbackModelId2, + ].filter(Boolean) as string[]; + const allModels = await getEnabledAiModelsWithProvider(); + const sameProviderFallbacks = currentModelConfig + ? allModels + .filter( + (m) => + m.provider.id === currentModelConfig.providerId && + m.model.supportsImageGen && + m.model.isEnabled && + !m.model.isSubModel && + m.model.modelId !== actualModelId + ) + .map((m) => m.model.modelId) + : []; + const fallbackModelIds = Array.from( + new Set([...configuredFallbacks, ...sameProviderFallbacks]) + ).filter((modelId) => modelId !== actualModelId); + console.log("[Chat API] OpenAI-compatible fallback model IDs:", fallbackModelIds); + + const lastUserMessage = uiMessagesForModel.filter((m) => m.role === "user").pop(); + let messagesForCrossProvider = lastUserMessage ? [lastUserMessage] : uiMessagesForModel; + messagesForCrossProvider = applyOptimizedPromptToLatestUserMessage( + messagesForCrossProvider, + smartAutoOptimizedPrompt, + hiddenPromptPrefix, + ); + messagesForCrossProvider = injectHistoryImagesToLastUserMessage( + messagesForCrossProvider, + smartAutoHistoryImages + ); // ็”Ÿๆˆๅผ€ๅง‹ๆ ‡่ฎฐ dataStream.write({ @@ -4231,22 +4264,159 @@ export async function POST(request: Request) { }); try { - const imageRequestTimeoutMs = 180_000; - const generated = await generateText({ - model: languageModel, - system: systemPrompt({ selectedChatModel: finalModelId, requestHints }), - messages: convertedMessages, - abortSignal: AbortSignal.timeout(imageRequestTimeoutMs), - }); + const tryGenerateWithOpenAICompatModel = async (modelToUse: any) => { + const imageRequestTimeoutMs = 180_000; + const generated = await generateText({ + model: modelToUse, + system: systemPrompt({ selectedChatModel: finalModelId, requestHints }), + messages: convertedMessages, + abortSignal: AbortSignal.timeout(imageRequestTimeoutMs), + }); - const rawText = generated.text?.trim() || "ๅ›พ็‰‡็”Ÿๆˆๅคฑ่ดฅ๏ผŒ่ฏท็จๅŽ้‡่ฏ•"; - const normalizedText = await normalizeAssistantImageMarkdown( - rawText, - effectiveImageParams?.imageSize || "2K" - ); + const rawText = generated.text?.trim() || "ๅ›พ็‰‡็”Ÿๆˆๅคฑ่ดฅ๏ผŒ่ฏท็จๅŽ้‡่ฏ•"; + return normalizeAssistantImageMarkdown( + rawText, + effectiveImageParams?.imageSize || "2K" + ); + }; + + let normalizedText = ""; + let usedModelId = actualModelId; + + try { + normalizedText = await tryGenerateWithOpenAICompatModel(languageModel); + } catch (primaryError) { + console.error("[Chat API] OpenAI-compatible primary model failed:", primaryError); + + let fallbackSuccess = false; + for (const fallbackModelId of fallbackModelIds) { + try { + console.log( + `[Chat API] ๐Ÿ”„ OpenAI-compatible fallback model: ${fallbackModelId}` + ); + const fallbackLanguageModel = await getLanguageModelById( + finalModelId, + fallbackModelId + ); + if (!fallbackLanguageModel) { + console.log( + `[Chat API] Skip unavailable OpenAI-compatible fallback model: ${fallbackModelId}` + ); + continue; + } + normalizedText = await tryGenerateWithOpenAICompatModel(fallbackLanguageModel); + usedModelId = fallbackModelId; + fallbackSuccess = true; + console.log( + `[Chat API] โœ… OpenAI-compatible fallback model ${fallbackModelId} succeeded!` + ); + break; + } catch (fallbackError) { + console.error( + `[Chat API] โŒ OpenAI-compatible fallback model ${fallbackModelId} failed:`, + fallbackError + ); + } + } + + if (!fallbackSuccess) { + const providerPriority: Record = { + "volcano-vision": 0, + "alibaba-image": 1, + "ruxa-image": 2, + "fal-image": 3, + gemini: 4, + "huanke-gemini": 5, + }; + const crossProviderCandidates = getImageCrossProviderCandidates(allModels, { + currentModelId: finalModelId, + currentProviderId: currentModelConfig?.providerId, + providerPriority, + }); + + for (const candidate of crossProviderCandidates) { + try { + console.log( + `[Chat API] ๐Ÿ” OpenAI-compatible cross-provider failover: ${candidate.model.displayName} (${candidate.provider.type})` + ); + + const candidateLanguageModel = await getLanguageModelById(candidate.model.id); + if ( + !candidateLanguageModel || + typeof (candidateLanguageModel as any).doGenerate !== "function" + ) { + console.log( + `[Chat API] Skip cross-provider candidate without doGenerate: ${candidate.model.id}` + ); + continue; + } + + const convertProvider = + candidate.provider.type === "ruxa-image" + ? "ruxa-image" + : candidate.provider.type === "fal-image" + ? "fal-image" + : candidate.provider.type === "gemini" + ? "gemini" + : candidate.provider.type === "huanke-gemini" + ? "huanke-gemini" + : candidate.provider.type === "volcano-vision" + ? "volcano" + : "alibaba-image"; + + const candidateMessages = await convertMessagesWithImages( + messagesForCrossProvider, + convertProvider + ); + + const candidateResult = await (candidateLanguageModel as any).doGenerate({ + messages: candidateMessages, + imageParams: + effectiveImageParams || { + aspectRatio: defaultImageQualityParams.aspectRatio, + imageSize: defaultImageQualityParams.imageSize, + imageCount: defaultImageQualityParams.imageCount, + }, + modelDisplayName: candidate.model.displayName, + overrideModelId: candidate.model.modelId, + }); + + if (candidateResult?.finishReason === "error") { + throw new Error(candidateResult.text || "ๅ›พ็‰‡็”Ÿๆˆๅคฑ่ดฅ"); + } + + normalizedText = candidateResult.text || "ๅ›พ็‰‡็”Ÿๆˆๅคฑ่ดฅ"; + usedModelId = candidate.model.modelId; + fallbackSuccess = true; + console.log( + `[Chat API] โœ… OpenAI-compatible cross-provider failover succeeded: ${candidate.model.displayName} (${candidate.provider.type})` + ); + break; + } catch (crossError) { + const crossErrorMessage = normalizeFailoverErrorMessage(crossError); + if (shouldCooldownImageFallbackModel(crossErrorMessage)) { + setImageFallbackModelCooldown(candidate.model.id, crossErrorMessage); + } + console.error( + `[Chat API] โŒ OpenAI-compatible cross-provider failover failed: ${candidate.model.displayName} (${candidate.provider.type})`, + crossError + ); + } + } + } + + if (!fallbackSuccess) { + throw primaryError; + } + } + + const usedModelDisplayName = + resolveModelDisplayNameByModelId(allModels, usedModelId) || + currentModelConfig?.displayName || + actualModelId; const normalizedTextWithModel = prependImageModelBadge( normalizedText, - currentModelConfig?.displayName || actualModelId, + usedModelDisplayName, ); const messageId = generateUUID();