fix: recognize runway video models in canvas chat
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-03-20 23:51:43 +08:00
parent 7e7c7f3a1a
commit dae5581c76
4 changed files with 81 additions and 61 deletions

View File

@ -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";
// 智能自动模式下,如果当前轮输入包含图片,默认按图片生成链路跟踪

View File

@ -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 秒)

View File

@ -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<DynamicModel> | 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({
) : (
<span className="flex h-5 w-5 items-center justify-center">
{(() => {
const providerType = model.providerType || "";
if (
providerType === "volcano-video" ||
providerType === "doubao-video" ||
providerType === "qianwen-video" ||
providerType === "fal-video" ||
providerType === "ruxa-video"
) {
if (isVideoProviderType(model)) {
return <Video className="h-4 w-4" strokeWidth={2.2} />;
}
if (
providerType === "gemini" ||
providerType === "volcano-vision" ||
providerType === "alibaba-image" ||
providerType === "ruxa-image" ||
providerType === "fal-image" ||
providerType === "openai-image" ||
providerType === "midjourney"
) {
if (isImageGenerationModel(model)) {
return <ImageIcon className="h-4 w-4" strokeWidth={2.2} />;
}
if (providerType === "system") {
if ((model.providerType || "") === "system") {
return <Sparkles className="h-4 w-4" strokeWidth={2.2} />;
}
return <Bot className="h-4 w-4" strokeWidth={2.2} />;

View File

@ -18,6 +18,7 @@ const PROVIDER_MODEL_LABEL: Record<string, string> = {
"fal-video": "FAL 视频",
"ruxa-video": "RUXA 视频",
"volcano-video": "火山视频",
runway: "Runway 视频",
};
const PHASE_HINT: Record<VideoGenerationPhase, string> = {
@ -33,6 +34,7 @@ const PROVIDER_GRADIENT_CLASS: Record<string, string> = {
"fal-video": "from-sky-400 via-cyan-400 to-indigo-400",
"ruxa-video": "from-violet-400 via-fuchsia-400 to-pink-400",
"volcano-video": "from-orange-400 via-amber-400 to-yellow-400",
runway: "from-rose-400 via-orange-400 to-amber-400",
};
/**