fix: add failover chain for openai-compatible image generation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-25 11:32:02 +08:00
parent d89e8590a9
commit 3e594d723f

View File

@ -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<string, number> = {
"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();