From 7bb1f06b11842999045a4de8469a8e124a9ec9ec Mon Sep 17 00:00:00 2001 From: 134 Date: Sat, 11 Jul 2026 22:59:56 +0800 Subject: [PATCH] fix: route Pascal through multimodal model --- .../pascal-agent/v1/chat/completions/route.ts | 20 ++++++++------ docs/pascal-editor-integration.md | 5 ++-- docs/pascal-production-deployment.md | 4 +-- lib/db/queries.ts | 27 +++++++++++++++++++ 4 files changed, 44 insertions(+), 12 deletions(-) diff --git a/app/api/internal/pascal-agent/v1/chat/completions/route.ts b/app/api/internal/pascal-agent/v1/chat/completions/route.ts index 89343118c..7a61f5a7c 100644 --- a/app/api/internal/pascal-agent/v1/chat/completions/route.ts +++ b/app/api/internal/pascal-agent/v1/chat/completions/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import { getProviderDefaultBaseUrl } from "@/lib/ai/provider-defaults"; import { resolveModelRuntimeId } from "@/lib/ai/model-runtime"; -import { getAgentAiModelWithProvider } from "@/lib/db/queries"; +import { getMultimodalAiModelWithProvider } from "@/lib/db/queries"; import { createLogger } from "@/lib/observability/logger"; import { isPascalInternalRequest } from "@/lib/pascal/internal-auth"; @@ -36,9 +36,9 @@ export async function GET(request: Request) { return NextResponse.json({ error: "not_found" }, { status: 404 }); } - const configured = await getAgentAiModelWithProvider(); + const configured = await getMultimodalAiModelWithProvider(); if (!configured) { - return NextResponse.json({ configured: false }); + return NextResponse.json({ configured: false, selection: "multimodal" }); } return NextResponse.json({ @@ -47,6 +47,7 @@ export async function GET(request: Request) { model: configured.model.displayName || configured.model.modelId, provider: configured.provider.name, providerType: configured.provider.type, + selection: "multimodal", }); } @@ -66,15 +67,15 @@ export async function POST(request: Request) { return openAiError("Invalid JSON body.", 400); } - const configured = await getAgentAiModelWithProvider(); + const configured = await getMultimodalAiModelWithProvider(); if (!configured) { - return openAiError("No enabled Agent text model is configured.", 503); + return openAiError("No enabled multimodal model is configured.", 503); } const { model, provider } = configured; if (!OPENAI_COMPATIBLE_PROVIDER_TYPES.has(provider.type)) { return openAiError( - `The configured Agent provider type '${provider.type}' is not OpenAI-compatible.`, + `The configured multimodal provider type '${provider.type}' is not OpenAI-compatible.`, 503 ); } @@ -85,7 +86,7 @@ export async function POST(request: Request) { providerType: provider.type, }); if (!baseUrl || !runtimeModelId || !provider.apiKey) { - return openAiError("The configured Agent model is incomplete.", 503); + return openAiError("The configured multimodal model is incomplete.", 503); } const startedAt = Date.now(); @@ -122,6 +123,9 @@ export async function POST(request: Request) { modelId: model.id, providerId: provider.id, }); - return openAiError("The configured Agent model is temporarily unavailable.", 502); + return openAiError( + "The configured multimodal model is temporarily unavailable.", + 502 + ); } } diff --git a/docs/pascal-editor-integration.md b/docs/pascal-editor-integration.md index 578db886f..d4b2ef826 100644 --- a/docs/pascal-editor-integration.md +++ b/docs/pascal-editor-integration.md @@ -110,8 +110,9 @@ include it inside the main HTTPS `server` block. The private Pascal service points its OpenAI-compatible model base URL to `/api/internal/pascal-agent/v1` on this application and authenticates with -`PASCAL_PROXY_SECRET`. That gateway resolves the current enabled Agent model -and provider from system settings on every request. OpenAI-compatible +`PASCAL_PROXY_SECRET`. On every request, that gateway resolves the enabled +model explicitly marked as `multimodalModel` and its provider from system +settings. It does not fall back to the Agent Auto model. OpenAI-compatible `openai`, `custom`, `alibaba`, and `zhipu` providers work without copying their API keys into the Pascal service. diff --git a/docs/pascal-production-deployment.md b/docs/pascal-production-deployment.md index 8641c76e3..80e25f298 100644 --- a/docs/pascal-production-deployment.md +++ b/docs/pascal-production-deployment.md @@ -1,7 +1,7 @@ # Pascal Editor 可复用生产部署 本项目把 Pascal Editor 作为独立 Bun/Next.js 服务运行,并通过主站 Nginx -同域挂载。部署脚本会复用主站登录态、系统设置中的 Agent 文本模型和主站 +同域挂载。部署脚本会复用主站登录态、系统设置中显式标记的“多模态模型”和主站 Git 仓库里的 `pascal-editor-ai-agent` 独立分支。 ## 默认结构 @@ -72,7 +72,7 @@ sudo env \ sudo env PASCAL_SMOKE_CONFIRM=1 pnpm pascal:smoke ``` -真实冒烟会调用当前系统设置中的 Agent 模型,因此默认不会在一键部署时 +真实冒烟会调用当前系统设置中显式标记的“多模态模型”,因此默认不会在一键部署时 执行。需要每次部署自动执行时设置 `RUN_AGENT_SMOKE_TEST=true`。 ## 常用覆盖项 diff --git a/lib/db/queries.ts b/lib/db/queries.ts index a56847d83..87bec9bb4 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -4782,6 +4782,33 @@ export async function getAgentAiModelWithProvider() { return candidates[0]; } +export async function getMultimodalAiModelWithProvider() { + const models = await getEnabledAiModelsWithProvider(false); + const healthPriority = (status?: string | null) => { + if (status === "healthy") return 0; + if (status === "degraded") return 1; + if (status === "auto_disabled") return 2; + return 3; + }; + const candidates = models + .filter( + ({ model }) => + isAiModelMultimodal(model) && model.healthStatus !== "auto_disabled" + ) + .sort((a, b) => { + const byHealth = + healthPriority(a.model.healthStatus) - + healthPriority(b.model.healthStatus); + if (byHealth !== 0) return byHealth; + if (a.model.isDefault !== b.model.isDefault) { + return Number(b.model.isDefault) - Number(a.model.isDefault); + } + return (a.model.sortOrder ?? 0) - (b.model.sortOrder ?? 0); + }); + + return candidates[0]; +} + /** * 🆕 获取指定父模型的所有子模型 * 用于火山引擎视频等需要智能选择模型的场景