diff --git a/components/chat.tsx b/components/chat.tsx index 8133fd6e9..965a05510 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -28,6 +28,10 @@ import type { Attachment, ChatMessage } from "@/lib/types"; import type { AppUsage } from "@/lib/usage"; import { requestLongTaskResult } from "@/lib/client/long-task"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; +import { + isImageGenerationModelDescriptor, + isVideoModelDescriptor, +} from "@/lib/ai/model-classification"; import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils"; import { Artifact } from "@/components/artifact"; import { ProtocolDebugPanel } from "@/components/protocol-debug-panel"; @@ -55,6 +59,11 @@ function normalizeImageParamsForProvider( return normalized; } + if (providerType === "runway") { + normalized.imageCount = 1; + return normalized; + } + if (providerType === "alibaba-image") { normalized.imageCount = Math.min(Math.max(currentCount, 1), 6); return normalized; @@ -487,6 +496,7 @@ export function Chat({ providerType: string; supportsVision?: boolean; supportsImageGen?: boolean; + capabilities?: any; videoToolCapability?: VideoToolCapabilityContract | null; imageGenConfig?: { maxImages?: number; @@ -503,13 +513,18 @@ export function Chat({ }>("/api/models", fetcher); const currentModel = modelsData?.models?.find(m => m.id === currentModelId); - const isVideoModel = - currentModel?.providerType === "volcano-video" || - currentModel?.providerType === "doubao-video" || - currentModel?.providerType === "qianwen-video" || - currentModel?.providerType === "fal-video" || - currentModel?.providerType === "ruxa-video"; - const isImageModel = currentModel?.supportsImageGen === true; + const isVideoModel = isVideoModelDescriptor({ + providerType: currentModel?.providerType, + modelId: currentModel?.modelId, + capabilities: currentModel?.capabilities, + supportsImageGen: currentModel?.supportsImageGen, + }); + const isImageModel = isImageGenerationModelDescriptor({ + providerType: currentModel?.providerType, + modelId: currentModel?.modelId, + capabilities: currentModel?.capabilities, + supportsImageGen: currentModel?.supportsImageGen, + }); // 🔍 调试日志 console.log('[Chat] Render - currentModelId:', currentModelId); @@ -1074,16 +1089,27 @@ export function Chat({ // 🆕 gemini-image 和 huanke-gemini 都视为 gemini 类型 if (model?.providerType === "gemini" || model?.providerType === "gemini-image" || model?.providerType === "huanke-gemini") modelType = "gemini"; else if (model?.providerType === "volcano-vision") modelType = "volcano-vision"; - else if (model?.providerType === "volcano-video" || model?.providerType === "doubao-video") modelType = "volcano-video"; - else if (model?.providerType === "qianwen-video") modelType = "qianwen-video"; - else if (model?.providerType === "fal-video") modelType = "fal-video"; - else if (model?.providerType === "ruxa-video") modelType = "ruxa-video"; else if ( - model?.providerType === "alibaba-image" || - model?.providerType === "ruxa-image" || - model?.providerType === "fal-image" || - model?.providerType === "openai-image" || - model?.providerType === "midjourney" + isVideoModelDescriptor({ + providerType: model?.providerType, + modelId: model?.modelId, + capabilities: model?.capabilities, + supportsImageGen: model?.supportsImageGen, + }) + ) modelType = model?.providerType === "qianwen-video" + ? "qianwen-video" + : model?.providerType === "fal-video" + ? "fal-video" + : model?.providerType === "ruxa-video" + ? "ruxa-video" + : "volcano-video"; + else if ( + isImageGenerationModelDescriptor({ + providerType: model?.providerType, + modelId: model?.modelId, + capabilities: model?.capabilities, + supportsImageGen: model?.supportsImageGen, + }) ) modelType = "alibaba-image"; // 智能自动模式下,如果当前轮输入包含图片,默认按图片生成链路跟踪 diff --git a/components/messages.tsx b/components/messages.tsx index 8efa5f9e0..181225aec 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -3,6 +3,10 @@ import equal from "fast-deep-equal"; import { ArrowDownIcon } from "lucide-react"; import { memo, useEffect, useState, useRef } from "react"; import useSWR from "swr"; +import { + isImageGenerationModelDescriptor, + isVideoModelDescriptor, +} from "@/lib/ai/model-classification"; import { useMessages } from "@/hooks/use-messages"; import type { PendingGeneration } from "@/hooks/use-pending-generation"; import type { Vote } from "@/lib/db/schema"; @@ -121,11 +125,11 @@ function PureMessages({ const isAlibabaImageModel = modelsData?.models?.some( (m: DynamicModel) => m.id === selectedModelId && - (m.providerType === "alibaba-image" || - m.providerType === "ruxa-image" || - m.providerType === "fal-image" || - m.providerType === "openai-image" || - m.providerType === "midjourney") + isImageGenerationModelDescriptor({ + providerType: m.providerType, + modelId: m.modelId, + supportsImageGen: m.supportsImageGen, + }) ) ?? false; // 🆕 获取当前模型的显示名称 @@ -140,14 +144,11 @@ function PureMessages({ const isVideoModel = modelsData?.models?.some( (m: DynamicModel) => m.id === selectedModelId && - (m.providerType === "volcano-video" || - m.providerType === "qianwen-video" || - m.providerType === "fal-video" || - m.providerType === "ruxa-video" || - m.providerType === "doubao-video" || - m.modelId?.toLowerCase().includes("video") || - m.modelId?.toLowerCase().includes("sora") || - m.displayName?.includes("视频")) + isVideoModelDescriptor({ + providerType: m.providerType, + modelId: m.modelId, + supportsImageGen: m.supportsImageGen, + }) ) ?? false; // 控制等待动画延迟消失(Gemini 图片模型延迟 4 秒,其他模型 6 秒) diff --git a/components/multimodal-input.tsx b/components/multimodal-input.tsx index 39480f872..967612f58 100644 --- a/components/multimodal-input.tsx +++ b/components/multimodal-input.tsx @@ -3,6 +3,10 @@ import type { UseChatHelpers } from "@ai-sdk/react"; import { Bot, ImageIcon, Sparkles, Video } from "lucide-react"; import { useRouter } from "next/navigation"; +import { + isImageGenerationModelDescriptor, + isVideoModelDescriptor, +} from "@/lib/ai/model-classification"; // 动态模型类型定义 type DynamicModel = { @@ -48,20 +52,22 @@ type PointsSummaryResponse = { const fetcher = (url: string) => fetch(url).then((res) => res.json()); -const VIDEO_PROVIDER_TYPES = new Set([ - "volcano-video", - "doubao-video", - "qianwen-video", - "fal-video", - "ruxa-video", -]); - -function isVideoProviderType(providerType?: string | null) { - return VIDEO_PROVIDER_TYPES.has(String(providerType || "")); +function isVideoProviderType(model?: Partial | null) { + return isVideoModelDescriptor({ + providerType: model?.providerType, + modelId: model?.modelId, + capabilities: model?.capabilities, + supportsImageGen: model?.supportsImageGen, + }); } function isImageGenerationModel(model: DynamicModel) { - return model.supportsImageGen === true && !isVideoProviderType(model.providerType); + return isImageGenerationModelDescriptor({ + providerType: model.providerType, + modelId: model.modelId, + capabilities: model.capabilities, + supportsImageGen: model.supportsImageGen, + }); } function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) { @@ -263,10 +269,10 @@ function PureMultimodalInput({ (model) => isImageGenerationModel(model) ); const videoMain = models.find( - (model) => isVideoProviderType(model.providerType) && !model.isSubModel + (model) => isVideoProviderType(model) && !model.isSubModel ); const videoFallback = models.find((model) => - isVideoProviderType(model.providerType) + isVideoProviderType(model) ); return { image: nanoBananaImage || imageMain || imageFallback || null, @@ -275,7 +281,7 @@ function PureMultimodalInput({ }, [modelCatalog?.models]); const activeQuickMode = useMemo<"image" | "video" | null>(() => { - if (isVideoModel || isVideoProviderType(currentSelectedModel?.providerType)) { + if (isVideoModel || isVideoProviderType(currentSelectedModel)) { return "video"; } if (isImageModel || (currentSelectedModel && isImageGenerationModel(currentSelectedModel))) { @@ -1779,28 +1785,13 @@ function PureModelSelectorCompact({ ) : ( {(() => { - const providerType = model.providerType || ""; - if ( - providerType === "volcano-video" || - providerType === "doubao-video" || - providerType === "qianwen-video" || - providerType === "fal-video" || - providerType === "ruxa-video" - ) { + if (isVideoProviderType(model)) { return