feat: add image gen failover and loading placeholder
Enable model fallback for image generation and show the generation placeholder in the canvas chat.
This commit is contained in:
parent
bb4f51d198
commit
b4c18bd554
@ -771,10 +771,23 @@ export async function POST(request: Request) {
|
||||
|
||||
// 🆕 获取备用模型配置
|
||||
const currentModelConfig = await getAiModelById({ id: finalModelId });
|
||||
const fallbackModelIds = [
|
||||
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]));
|
||||
console.log("[Chat API] Fallback model IDs:", fallbackModelIds);
|
||||
|
||||
const geminiStream = createUIMessageStream({
|
||||
@ -1073,6 +1086,24 @@ export async function POST(request: Request) {
|
||||
// 🆕 获取模型显示名称,用于错误提示
|
||||
const alibabaModelConfig = await getAiModelById({ id: finalModelId });
|
||||
const modelDisplayName = alibabaModelConfig?.displayName || actualModelId;
|
||||
const configuredFallbacks = [
|
||||
alibabaModelConfig?.fallbackModelId1,
|
||||
alibabaModelConfig?.fallbackModelId2,
|
||||
].filter(Boolean) as string[];
|
||||
const allModels = await getEnabledAiModelsWithProvider();
|
||||
const sameProviderFallbacks = alibabaModelConfig
|
||||
? allModels
|
||||
.filter(m =>
|
||||
m.provider.id === alibabaModelConfig.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]));
|
||||
console.log("[Chat API] Alibaba fallback model IDs:", fallbackModelIds);
|
||||
|
||||
const alibabaStream = createUIMessageStream({
|
||||
execute: async ({ writer }) => {
|
||||
@ -1163,18 +1194,54 @@ export async function POST(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// 转换消息(阿里生图支持 URL,会自动上传 base64)
|
||||
const modelMessages = await convertMessagesWithImages(messagesForAlibaba, "alibaba-image");
|
||||
const tryGenerateImage = async (modelIdToUse: string, isRetry: boolean) => {
|
||||
// 转换消息(阿里生图支持 URL,会自动上传 base64)
|
||||
const modelMessages = await convertMessagesWithImages(messagesForAlibaba, "alibaba-image");
|
||||
|
||||
// 调用 doGenerate 获取图片,传递模型显示名称
|
||||
const result = await (languageModel as any).doGenerate({
|
||||
messages: modelMessages,
|
||||
imageParams: requestBody.imageParams || { aspectRatio: "auto", imageSize: "2K", imageCount: 1 },
|
||||
modelDisplayName, // 🆕 传递模型显示名称
|
||||
overrideModelId: isRetry ? modelIdToUse : undefined,
|
||||
});
|
||||
|
||||
if (result?.finishReason === "error") {
|
||||
throw new Error(result.text || "图片生成失败");
|
||||
}
|
||||
|
||||
return result.text || "图片生成失败";
|
||||
};
|
||||
|
||||
// 调用 doGenerate 获取图片,传递模型显示名称
|
||||
const result = await (languageModel as any).doGenerate({
|
||||
messages: modelMessages,
|
||||
imageParams: requestBody.imageParams || { aspectRatio: "auto", imageSize: "2K", imageCount: 1 },
|
||||
modelDisplayName, // 🆕 传递模型显示名称
|
||||
});
|
||||
let imageMarkdown = "";
|
||||
let usedModelId = actualModelId;
|
||||
|
||||
try {
|
||||
imageMarkdown = await tryGenerateImage(actualModelId, false);
|
||||
} catch (primaryError) {
|
||||
console.error("[Chat API] Alibaba primary model failed:", primaryError);
|
||||
|
||||
let fallbackSuccess = false;
|
||||
for (const fallbackModelId of fallbackModelIds) {
|
||||
try {
|
||||
console.log(`[Chat API] 🔄 Alibaba fallback model: ${fallbackModelId}`);
|
||||
imageMarkdown = await tryGenerateImage(fallbackModelId, true);
|
||||
usedModelId = fallbackModelId;
|
||||
fallbackSuccess = true;
|
||||
console.log(`[Chat API] ✅ Alibaba fallback model ${fallbackModelId} succeeded!`);
|
||||
break;
|
||||
} catch (fallbackError) {
|
||||
console.error(`[Chat API] ❌ Alibaba fallback model ${fallbackModelId} failed:`, fallbackError);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fallbackSuccess) {
|
||||
throw primaryError;
|
||||
}
|
||||
}
|
||||
|
||||
const imageMarkdown = result.text || "图片生成失败";
|
||||
console.log("[Chat API] Alibaba generated, length:", imageMarkdown.length);
|
||||
console.log("[Chat API] Alibaba used model:", usedModelId);
|
||||
|
||||
// 生成消息 ID(图片消息)
|
||||
const messageId = generateUUID();
|
||||
@ -1220,7 +1287,7 @@ export async function POST(request: Request) {
|
||||
id,
|
||||
messageId,
|
||||
userPrompt,
|
||||
actualModelId
|
||||
usedModelId
|
||||
);
|
||||
} catch (saveError) {
|
||||
console.error("[Chat API] ❌ Failed to save Alibaba message:", saveError);
|
||||
@ -1276,6 +1343,25 @@ export async function POST(request: Request) {
|
||||
// 🆕 检测是否有标注图片,如果有则在消息中注入系统提示
|
||||
const hasAnnotation = hasAnnotatedImage(uiMessages);
|
||||
let messagesForVolcano = uiMessagesForModel;
|
||||
const volcanoModelConfig = await getAiModelById({ id: finalModelId });
|
||||
const configuredFallbacks = [
|
||||
volcanoModelConfig?.fallbackModelId1,
|
||||
volcanoModelConfig?.fallbackModelId2,
|
||||
].filter(Boolean) as string[];
|
||||
const allModels = await getEnabledAiModelsWithProvider();
|
||||
const sameProviderFallbacks = volcanoModelConfig
|
||||
? allModels
|
||||
.filter(m =>
|
||||
m.provider.id === volcanoModelConfig.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]));
|
||||
console.log("[Chat API] Volcano fallback model IDs:", fallbackModelIds);
|
||||
|
||||
if (hasAnnotation) {
|
||||
console.log("[Chat API] 🎨 Volcano Vision: 检测到标注图片,将注入系统提示");
|
||||
@ -1344,12 +1430,40 @@ export async function POST(request: Request) {
|
||||
// 转换消息,火山引擎支持 URL 和 base64
|
||||
const modelMessages = await convertMessagesWithImages(messagesForVolcano, "volcano");
|
||||
|
||||
// 调用 doGenerate 获取图片
|
||||
const result = await (languageModel as any).doGenerate({
|
||||
messages: modelMessages,
|
||||
});
|
||||
|
||||
const imageMarkdown = result.text || "图片生成失败";
|
||||
const tryGenerateImage = async (modelIdToUse: string, isRetry: boolean) => {
|
||||
const result = await (languageModel as any).doGenerate({
|
||||
messages: modelMessages,
|
||||
overrideModelId: isRetry ? modelIdToUse : undefined,
|
||||
});
|
||||
if (result?.finishReason === "error") {
|
||||
throw new Error(result.text || "图片生成失败");
|
||||
}
|
||||
return result.text || "图片生成失败";
|
||||
};
|
||||
|
||||
let imageMarkdown = "";
|
||||
let usedModelId = actualModelId;
|
||||
try {
|
||||
imageMarkdown = await tryGenerateImage(actualModelId, false);
|
||||
} catch (primaryError) {
|
||||
console.error("[Chat API] Volcano primary model failed:", primaryError);
|
||||
let fallbackSuccess = false;
|
||||
for (const fallbackModelId of fallbackModelIds) {
|
||||
try {
|
||||
console.log(`[Chat API] 🔄 Volcano fallback model: ${fallbackModelId}`);
|
||||
imageMarkdown = await tryGenerateImage(fallbackModelId, true);
|
||||
usedModelId = fallbackModelId;
|
||||
fallbackSuccess = true;
|
||||
console.log(`[Chat API] ✅ Volcano fallback model ${fallbackModelId} succeeded!`);
|
||||
break;
|
||||
} catch (fallbackError) {
|
||||
console.error(`[Chat API] ❌ Volcano fallback model ${fallbackModelId} failed:`, fallbackError);
|
||||
}
|
||||
}
|
||||
if (!fallbackSuccess) {
|
||||
throw primaryError;
|
||||
}
|
||||
}
|
||||
console.log("[Chat API] Volcano Vision generated, length:", imageMarkdown.length);
|
||||
console.log("[Chat API] Volcano Vision image markdown:", imageMarkdown.substring(0, 200));
|
||||
|
||||
@ -1398,7 +1512,7 @@ export async function POST(request: Request) {
|
||||
id,
|
||||
messageId,
|
||||
userPrompt,
|
||||
actualModelId
|
||||
usedModelId
|
||||
);
|
||||
} catch (saveError) {
|
||||
console.error("[Chat API] ❌ Failed to save Volcano Vision message to database:", saveError);
|
||||
|
||||
@ -24,6 +24,7 @@ import { cn } from "@/lib/utils";
|
||||
import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils";
|
||||
import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||
import { ImageGenerationLoading } from "@/components/image-generation-loading";
|
||||
|
||||
// 标签悬停预览信息
|
||||
interface TagPreviewInfo {
|
||||
@ -1064,6 +1065,14 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
};
|
||||
|
||||
const isLoading = status === 'streaming' || status === 'submitted';
|
||||
const hasGeneratingMarker = messages.some(msg =>
|
||||
msg.role === "assistant" && msg.parts?.some(
|
||||
(part: any) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->")
|
||||
)
|
||||
);
|
||||
const currentModel = allModels.find(m => m.id === selectedModelId);
|
||||
const shouldShowImageLoading = (isLoading || hasGeneratingMarker) && !!currentModel;
|
||||
const loadingImageCount = imageParams.imageCount || 1;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-background/50 backdrop-blur-sm relative">
|
||||
@ -1331,6 +1340,15 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{shouldShowImageLoading && (
|
||||
<ImageGenerationLoading
|
||||
imageCount={loadingImageCount}
|
||||
maxImages={6}
|
||||
providerType={currentModel?.providerType}
|
||||
modelName={currentModel?.displayName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex gap-3">
|
||||
|
||||
@ -288,9 +288,11 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
|
||||
messages: Array<{ role: string; content: string | Array<{ type: string; text?: string; image?: string }> }>;
|
||||
imageParams?: AlibabaImageParams & { aspectRatio?: string; imageSize?: string; imageCount?: number };
|
||||
modelDisplayName?: string; // 🆕 模型显示名称,用于错误提示
|
||||
overrideModelId?: string; // 🆕 备用模型 ID
|
||||
}) => {
|
||||
console.log("[Alibaba Image] doGenerate called");
|
||||
console.log("[Alibaba Image] Model:", modelId);
|
||||
const activeModelId = options.overrideModelId || modelId;
|
||||
console.log("[Alibaba Image] Model:", activeModelId);
|
||||
console.log("[Alibaba Image] Image params:", options.imageParams);
|
||||
console.log("[Alibaba Image] Messages count:", options.messages.length);
|
||||
console.log("[Alibaba Image] Messages:", JSON.stringify(options.messages, null, 2));
|
||||
@ -481,7 +483,7 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
|
||||
const response = await sendAlibabaImageRequest(
|
||||
baseURL,
|
||||
apiKey,
|
||||
modelId,
|
||||
activeModelId,
|
||||
prompt,
|
||||
imageUrls,
|
||||
params
|
||||
|
||||
@ -530,7 +530,8 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
|
||||
doGenerate: async (options: any) => {
|
||||
console.log("[Volcano Vision] doGenerate called!");
|
||||
console.log("[Volcano Vision] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
|
||||
console.log("[Volcano Vision] Using modelId:", modelId);
|
||||
const activeModelId = options.overrideModelId || modelId;
|
||||
console.log("[Volcano Vision] Using modelId:", activeModelId);
|
||||
console.log("[Volcano Vision] Image params from options:", options.imageParams);
|
||||
|
||||
// 🆕 优先使用用户选择的图片数量,其次是 maxImages 参数,默认 1 张
|
||||
@ -540,7 +541,7 @@ export function createVolcanoVision({ apiKey, baseURL }: VolcanoVisionConfig) {
|
||||
|
||||
try {
|
||||
// 发送请求
|
||||
const response = await sendVolcanoRequest(baseURL, apiKey, options.messages, modelId, maxImages);
|
||||
const response = await sendVolcanoRequest(baseURL, apiKey, options.messages, activeModelId, maxImages);
|
||||
|
||||
// 提取图片并上传到 S3
|
||||
const imageUrls = await extractAndUploadImages(response);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user