From 7141704f65aecc3afe9840689586bd7032d92877 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 20 Feb 2026 21:58:57 +0800 Subject: [PATCH] fix: align canvas auto-title model routing with agent multimodal --- app/api/canvas/auto-title/route.ts | 105 ++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/app/api/canvas/auto-title/route.ts b/app/api/canvas/auto-title/route.ts index 62bb37508..fe898c106 100644 --- a/app/api/canvas/auto-title/route.ts +++ b/app/api/canvas/auto-title/route.ts @@ -1,8 +1,64 @@ import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/(auth)/auth"; import { generateText } from "ai"; -import { getEnabledAiModelsWithProvider } from "@/lib/db/queries"; +import { + getAgentAiModelWithProvider, + getEnabledAiModelsWithProvider, +} from "@/lib/db/queries"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; +import { isAiModelMultimodal } from "@/lib/db/ai-model-flags"; + +const BLOCKED_PROVIDER_TYPES = new Set([ + "fal-video", + "qianwen-video", + "volcano-video", + "doubao-video", + "alibaba-image", + "ruxa-image", + "fal-image", + "volcano-vision", +]); + +type VisionModelItem = Awaited>[number]; + +function isVisionCandidate(item: VisionModelItem) { + const providerType = String(item.provider?.type || ""); + if (BLOCKED_PROVIDER_TYPES.has(providerType)) return false; + + if (item.model.supportsVision) return true; + if (isAiModelMultimodal(item.model)) return true; + + const modelText = `${item.model.modelId || ""} ${item.model.displayName || ""}`.toLowerCase(); + if (/vl|vision|多模态|glm-4v|gpt-4o|doubao-seed-2-0|seed-2-0/.test(modelText)) { + return true; + } + + return false; +} + +function scoreVisionCandidate(item: VisionModelItem) { + let score = 0; + const providerType = String(item.provider?.type || ""); + if (isAiModelMultimodal(item.model)) score += 220; + if (item.model.supportsVision) score += 160; + if (providerType === "volcano-multimodal") score += 80; + if (providerType === "zhipu") score += 50; + if (providerType === "alibaba") score += 40; + if (providerType === "custom") score += 30; + if (item.model.isDefault) score += 20; + score -= Math.max(0, item.model.sortOrder ?? 0); + return score; +} + +function pickAutoTitleModel(allModels: VisionModelItem[], agentModel: VisionModelItem | undefined) { + if (agentModel && isVisionCandidate(agentModel)) { + return agentModel; + } + + const candidates = allModels.filter((item) => isVisionCandidate(item)); + if (candidates.length === 0) return null; + return [...candidates].sort((a, b) => scoreVisionCandidate(b) - scoreVisionCandidate(a))[0]; +} export async function POST(request: NextRequest) { console.log("[Auto-title] API called"); @@ -21,42 +77,25 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Image URL is required" }, { status: 400 }); } - const blockedProviderTypes = new Set([ - "fal-video", - "qianwen-video", - "volcano-video", - "doubao-video", - ]); - - // 从数据库获取支持视觉的模型(优先使用 supportsVision) + // 从数据库获取视觉模型,优先使用 AGENT 多模态模型 const modelsData = await getEnabledAiModelsWithProvider(); + const agentModel = await getAgentAiModelWithProvider(); + console.log("[Auto-title] Available models count:", modelsData.length); console.log("[Auto-title] All model IDs:", modelsData.map(m => m.model.modelId)); - - let visionModelData = modelsData.find( - (m) => m.model.supportsVision && !blockedProviderTypes.has(String(m.provider?.type || "")), - ); - if (!visionModelData) { - // 兼容旧配置:根据模型名称兜底匹配 - visionModelData = modelsData.find(m => { - if (blockedProviderTypes.has(String(m.provider?.type || ""))) { - return false; - } - const id = m.model.modelId?.toLowerCase() || ''; - const name = m.model.displayName?.toLowerCase() || ''; - return id.includes('qwen-vl') || - id.includes('qwen3-vl') || - id.includes('qwen2.5-vl') || - id.includes('vision') || - name.includes('vl') || - name.includes('视觉'); - }); - } - - console.log("[Auto-title] Found vision model:", visionModelData?.model.modelId, visionModelData?.model.displayName); + + const visionModelData = pickAutoTitleModel(modelsData, agentModel); + + console.log("[Auto-title] Selected vision model:", { + modelId: visionModelData?.model.modelId, + displayName: visionModelData?.model.displayName, + providerType: visionModelData?.provider?.type, + usedAgentModel: Boolean(agentModel && visionModelData && agentModel.model.id === visionModelData.model.id), + isMultimodal: Boolean(visionModelData && isAiModelMultimodal(visionModelData.model)), + }); if (!visionModelData) { - console.log("[Auto-title] No vision model found, using default title"); + console.log("[Auto-title] No compatible vision model found, using default title"); return NextResponse.json({ title: "未命名画布" }); } @@ -82,7 +121,7 @@ export async function POST(request: NextRequest) { }, ], temperature: 0.2, - maxTokens: 50, + maxOutputTokens: 50, }); let title = (text || "").trim();