From 87b56a4d3f1ccc2695494fd65934ef9c8d219335 Mon Sep 17 00:00:00 2001 From: jiajia Date: Mon, 18 May 2026 15:34:29 +0800 Subject: [PATCH] Keep new Popi users on tested gateway defaults New users now land on Popi gateway defaults that match the tested production-style path: APIYI Nano Banana 2 for images, Doubao Seedance for video, and Doubao Seed 2.0 Lite for LLM chat/tool use. Settings and FTUX screens render these effective defaults instead of empty placeholders, while existing local node defaults keep taking precedence. The same pass keeps provider-overload errors user-facing by formatting the model name that the node currently displays, so fallback and switched-model failures are easier to understand across media nodes. Constraint: Popi mode should hide internal NewApiWG naming and default to models available through the Popi gateway. Rejected: Persist default values into localStorage for every new user | would make future system default changes harder and could overwrite explicit user preferences. Confidence: high Scope-risk: moderate Directive: Do not change these defaults without checking the Popi gateway model ids and www.popi.art login/model-routing assumptions. Tested: npm run test:run -- src/lib/__tests__/llmModels.test.ts src/store/utils/__tests__/nodeDefaults.test.ts src/store/execution/__tests__/generateVideoExecutor.test.ts src/components/__tests__/ProjectSetupModal.test.tsx src/components/__tests__/GenerationComposer.test.tsx src/app/api/chat/__tests__/route.test.ts src/components/__tests__/LLMGenerateNode.test.tsx src/utils/__tests__/userFacingErrors.test.ts Tested: PROVIDER_MODE=popi NEXT_PUBLIC_PROVIDER_MODE=popi npm run build Tested: git diff --check Not-tested: npm run lint currently fails because next lint is no longer a valid project command in this Next.js setup --- src/app/api/chat/__tests__/route.test.ts | 4 +- src/components/ProjectSetupModal.tsx | 206 +++++++++--------- .../__tests__/GenerationComposer.test.tsx | 42 +++- .../__tests__/LLMGenerateNode.test.tsx | 16 ++ .../__tests__/ProjectSetupModal.test.tsx | 32 +++ .../composer/GenerationComposer.tsx | 5 +- src/components/nodes/Generate3DNode.tsx | 16 +- src/components/nodes/GenerateAudioNode.tsx | 4 +- src/components/nodes/GenerateImageNode.tsx | 20 +- src/components/nodes/GenerateVideoNode.tsx | 20 +- src/components/nodes/LLMGenerateNode.tsx | 19 +- .../onboarding/FTUXModelDefaultsStep.tsx | 142 ++++++------ src/i18n/index.tsx | 12 +- src/lib/__tests__/llmModels.test.ts | 12 +- src/lib/chat/tools.ts | 2 +- src/lib/llmModels.ts | 4 +- .../__tests__/generateVideoExecutor.test.ts | 12 +- .../utils/__tests__/nodeDefaults.test.ts | 8 +- src/store/utils/defaultImageModel.ts | 6 +- src/utils/__tests__/userFacingErrors.test.ts | 31 +++ src/utils/userFacingErrors.ts | 23 ++ 21 files changed, 381 insertions(+), 255 deletions(-) create mode 100644 src/utils/__tests__/userFacingErrors.test.ts create mode 100644 src/utils/userFacingErrors.ts diff --git a/src/app/api/chat/__tests__/route.test.ts b/src/app/api/chat/__tests__/route.test.ts index c8496d5d..4c9e7841 100644 --- a/src/app/api/chat/__tests__/route.test.ts +++ b/src/app/api/chat/__tests__/route.test.ts @@ -58,7 +58,7 @@ describe("/api/chat route", () => { }); }); - it("uses NewApiWG GPT-4.1 Mini for chat requests", async () => { + it("uses NewApiWG Doubao Seed 2.0 Lite for chat requests", async () => { const request = new Request("http://localhost/api/chat", { method: "POST", headers: { @@ -80,7 +80,7 @@ describe("/api/chat route", () => { apiKey: "browser-newapi-key", baseURL: "https://llmapitest.popi.art/v1", }); - expect(mockNewApiWGProvider).toHaveBeenCalledWith("gpt-4.1-mini"); + expect(mockNewApiWGProvider).toHaveBeenCalledWith("doubao-seed-2-0-lite-260428"); expect(mockStreamText).toHaveBeenCalledWith( expect.objectContaining({ model: "newapiwg-model", diff --git a/src/components/ProjectSetupModal.tsx b/src/components/ProjectSetupModal.tsx index 44a68a5d..6b87865c 100644 --- a/src/components/ProjectSetupModal.tsx +++ b/src/components/ProjectSetupModal.tsx @@ -10,8 +10,12 @@ import { ProviderModel } from "@/lib/providers/types"; import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; import { useInlineParameters } from "@/hooks/useInlineParameters"; import { useI18n } from "@/i18n"; -import { LLM_MODELS, LLM_PROVIDERS } from "@/lib/llmModels"; +import { DEFAULT_NEWAPIWG_LLM_MODEL_ID, LLM_MODELS, LLM_PROVIDERS } from "@/lib/llmModels"; import { getProviderDisplayName, isPopiProviderMode } from "@/lib/providerMode"; +import { + createNewApiWGDefaultImageModel, + createNewApiWGDefaultVideoModel, +} from "@/store/utils/defaultImageModel"; import { isBrowserDirectoryPickerSupported, isBrowserFileSystemPath, @@ -74,6 +78,13 @@ const getProviderIcon = (provider: ProviderType) => { }; const DEFAULT_LLM_PROVIDER: LLMProvider = "newapiwg"; +const DEFAULT_LLM_TEMPERATURE = 0.7; +const DEFAULT_LLM_MAX_TOKENS = 8192; + +function getDefaultModelForLLMProvider(provider: LLMProvider): LLMModelType { + if (provider === DEFAULT_LLM_PROVIDER) return DEFAULT_NEWAPIWG_LLM_MODEL_ID; + return LLM_MODELS[provider][0].value; +} interface ProjectSetupModalProps { isOpen: boolean; @@ -414,6 +425,14 @@ export function ProjectSetupModal({ })); }; + const effectiveImageModel = localNodeDefaults.generateImage?.selectedModel ?? createNewApiWGDefaultImageModel(); + const effectiveVideoModel = localNodeDefaults.generateVideo?.selectedModel ?? createNewApiWGDefaultVideoModel(); + const effectiveLLMProvider = localNodeDefaults.llm?.provider ?? DEFAULT_LLM_PROVIDER; + const effectiveLLMModels = LLM_MODELS[effectiveLLMProvider] || LLM_MODELS.newapiwg; + const effectiveLLMModel = localNodeDefaults.llm?.model ?? getDefaultModelForLLMProvider(effectiveLLMProvider); + const effectiveLLMTemperature = localNodeDefaults.llm?.temperature ?? DEFAULT_LLM_TEMPERATURE; + const effectiveLLMMaxTokens = localNodeDefaults.llm?.maxTokens ?? DEFAULT_LLM_MAX_TOKENS; + if (!isOpen) return null; return ( @@ -965,44 +984,29 @@ export function ProjectSetupModal({
{t("ftux.defaultImageModel")}
- {localNodeDefaults.generateImage?.selectedModel ? ( - <> -
- {getProviderIcon(localNodeDefaults.generateImage.selectedModel.provider)} - - {localNodeDefaults.generateImage.selectedModel.displayName} - -
- - - - ) : ( - <> - {t("project.systemImageDefault")} - - - )} +
+ {getProviderIcon(effectiveImageModel.provider)} + + {effectiveImageModel.displayName} + +
+ +
@@ -1012,44 +1016,29 @@ export function ProjectSetupModal({
{t("ftux.defaultVideoModel")}
- {localNodeDefaults.generateVideo?.selectedModel ? ( - <> -
- {getProviderIcon(localNodeDefaults.generateVideo.selectedModel.provider)} - - {localNodeDefaults.generateVideo.selectedModel.displayName} - -
- - - - ) : ( - <> - {t("project.noneSetFirstUse")} - - - )} +
+ {getProviderIcon(effectiveVideoModel.provider)} + + {effectiveVideoModel.displayName} + +
+ +
@@ -1059,39 +1048,32 @@ export function ProjectSetupModal({
{t("project.defaultLLM")} - {localNodeDefaults.llm && ( - - )} +
- {!localNodeDefaults.llm ? ( -

{t("project.systemLLMDefault")}

- ) : null} - {/* Provider dropdown */}
{ setLocalNodeDefaults(prev => ({ ...prev, - llm: { ...prev.llm, model: e.target.value as LLMModelType } + llm: { ...prev.llm, provider: effectiveLLMProvider, model: e.target.value as LLMModelType } })); }} className="flex-1 px-2 py-1 text-xs bg-neutral-800 border border-neutral-600 rounded text-neutral-100 focus:outline-none focus:border-neutral-500" > - {LLM_MODELS[localNodeDefaults.llm?.provider || DEFAULT_LLM_PROVIDER].map((m) => ( + {effectiveLLMModels.map((m) => ( ))} @@ -1127,18 +1109,23 @@ export function ProjectSetupModal({ {/* Temperature slider */}
{ setLocalNodeDefaults(prev => ({ ...prev, - llm: { ...prev.llm, temperature: parseFloat(e.target.value) } + llm: { + ...prev.llm, + provider: effectiveLLMProvider, + model: effectiveLLMModel, + temperature: parseFloat(e.target.value), + } })); }} className="flex-1 h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400" @@ -1148,18 +1135,23 @@ export function ProjectSetupModal({ {/* Max Tokens slider */}
{ setLocalNodeDefaults(prev => ({ ...prev, - llm: { ...prev.llm, maxTokens: parseInt(e.target.value, 10) } + llm: { + ...prev.llm, + provider: effectiveLLMProvider, + model: effectiveLLMModel, + maxTokens: parseInt(e.target.value, 10), + } })); }} className="flex-1 h-1 bg-neutral-700 rounded-lg appearance-none cursor-pointer accent-neutral-400" diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx index 3fcb9463..bf7cb1b8 100644 --- a/src/components/__tests__/GenerationComposer.test.tsx +++ b/src/components/__tests__/GenerationComposer.test.tsx @@ -31,11 +31,24 @@ vi.mock("@/components/modals/ModelSearchDialog", () => ({ onModelSelected: (model: { id: string; name: string; - provider: "fal" | "kie"; - capabilities: Array<"text-to-video" | "image-to-video" | "text-to-audio">; + provider: "newapiwg" | "fal" | "kie"; + capabilities: Array<"text-to-image" | "image-to-image" | "text-to-video" | "image-to-video" | "text-to-audio">; }) => void; }) => (
+
)} @@ -452,11 +456,11 @@ export function Generate3DNode({ id, data, selected }: NodeProps ) : nodeData.status === "error" ? ( - {nodeData.error || "Failed"} + {displayError || "Failed"} ) : ( - Run to generate + {t("node.runToGenerate")} )}
diff --git a/src/components/nodes/GenerateAudioNode.tsx b/src/components/nodes/GenerateAudioNode.tsx index 6a2cef88..8063a364 100644 --- a/src/components/nodes/GenerateAudioNode.tsx +++ b/src/components/nodes/GenerateAudioNode.tsx @@ -20,12 +20,14 @@ import { useShowHandleLabels } from "@/hooks/useShowHandleLabels"; import { HandleLabel } from "./HandleLabel"; import { isPopiProviderMode } from "@/lib/providerMode"; import { useI18n } from "@/i18n"; +import { formatUserFacingError } from "@/utils/userFacingErrors"; type GenerateAudioNodeType = Node; export function GenerateAudioNode({ id, data, selected }: NodeProps) { const { t } = useI18n(); const nodeData = data; + const displayError = formatUserFacingError(nodeData.error, t, nodeData.selectedModel?.displayName); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const generationsPath = useWorkflowStore((state) => state.generationsPath); const popiProviderMode = isPopiProviderMode(); @@ -465,7 +467,7 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps - {nodeData.error} + {displayError}
)} diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index f5ac150e..7eb853c3 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -28,6 +28,7 @@ import { joinBrowserFileSystemPath, loadBrowserGenerationFile, } from "@/utils/browserFileSystem"; +import { formatUserFacingError } from "@/utils/userFacingErrors"; /** Reorder items so they read column-first in a row-based CSS grid. * e.g. [1,2,3,4,5,6,7,8] with 2 cols → [1,5,2,6,3,7,4,8] */ @@ -462,23 +463,24 @@ export function GenerateImageNode({ id, data, selected }: NodeProps { - if (nodeData.outputImage && nodeData.lastUsedModel?.displayName) { - return nodeData.lastUsedModel.displayName; - } if (nodeData.selectedModel?.displayName && nodeData.selectedModel.modelId) { return nodeData.selectedModel.displayName; } + if (nodeData.outputImage && nodeData.lastUsedModel?.displayName) { + return nodeData.lastUsedModel.displayName; + } // Fallback for legacy data or no model selected if (nodeData.model) { return GEMINI_IMAGE_MODELS.find(m => m.value === nodeData.model)?.label || nodeData.model; } return t("node.selectModel"); }, [nodeData.lastUsedModel?.displayName, nodeData.model, nodeData.outputImage, nodeData.selectedModel?.displayName, nodeData.selectedModel?.modelId, t]); + const displayError = formatUserFacingError(nodeData.error, t, displayTitle); // Provider badge as title prefix const titlePrefix = useMemo(() => ( - - ), [currentProvider, nodeData.lastUsedModel?.provider, nodeData.outputImage]); + + ), [currentProvider, nodeData.lastUsedModel?.provider, nodeData.outputImage, nodeData.selectedModel?.provider]); const selectedModelId = nodeData.selectedModel?.modelId || nodeData.model; const usesFirstClassImageControls = @@ -527,11 +529,11 @@ export function GenerateImageNode({ id, data, selected }: NodeProps { - if (nodeData.status === "error" && prevStatusRef.current !== "error" && nodeData.error) { - useToast.getState().show(t("node.generationFailed"), "error", true, nodeData.error); + if (nodeData.status === "error" && prevStatusRef.current !== "error" && displayError) { + useToast.getState().show(t("node.generationFailed"), "error", true, displayError); } prevStatusRef.current = nodeData.status; - }, [nodeData.status, nodeData.error, t]); + }, [nodeData.status, displayError, t]); // Auto-resize node when output image changes const prevOutputImageRef = useRef(null); @@ -934,7 +936,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps ) : nodeData.status === "error" ? ( - {nodeData.error || "Failed"} + {displayError || "Failed"} ) : ( diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index d0c88a6d..276dff72 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -23,6 +23,7 @@ import { useShowHandleLabels } from "@/hooks/useShowHandleLabels"; import { HandleLabel } from "./HandleLabel"; import { useI18n } from "@/i18n"; import { getProviderDisplayName, isPopiProviderMode } from "@/lib/providerMode"; +import { formatUserFacingError } from "@/utils/userFacingErrors"; // Video generation capabilities const VIDEO_CAPABILITIES: ModelCapability[] = ["text-to-video", "image-to-video", "video-to-video", "audio-to-video"]; @@ -348,19 +349,20 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps { - if (nodeData.outputVideo && nodeData.lastUsedModel?.displayName) { - return nodeData.lastUsedModel.displayName; - } if (nodeData.selectedModel?.displayName && nodeData.selectedModel.modelId) { return nodeData.selectedModel.displayName; } + if (nodeData.outputVideo && nodeData.lastUsedModel?.displayName) { + return nodeData.lastUsedModel.displayName; + } return t("node.selectModel"); }, [nodeData.lastUsedModel?.displayName, nodeData.outputVideo, nodeData.selectedModel?.displayName, nodeData.selectedModel?.modelId, t]); + const displayError = formatUserFacingError(nodeData.error, t, displayTitle); // Provider badge as title prefix const titlePrefix = useMemo(() => ( - - ), [currentProvider, nodeData.lastUsedModel?.provider, nodeData.outputVideo]); + + ), [currentProvider, nodeData.lastUsedModel?.provider, nodeData.outputVideo, nodeData.selectedModel?.provider]); const storageBadge = useMemo(() => { switch (nodeData.outputVideoStorageStatus) { @@ -384,11 +386,11 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps { - if (nodeData.status === "error" && prevStatusRef.current !== "error" && nodeData.error) { - useToast.getState().show(t("node.videoGenerationFailed"), "error", true, nodeData.error); + if (nodeData.status === "error" && prevStatusRef.current !== "error" && displayError) { + useToast.getState().show(t("node.videoGenerationFailed"), "error", true, displayError); } prevStatusRef.current = nodeData.status; - }, [nodeData.status, nodeData.error, t]); + }, [nodeData.status, displayError, t]); const localizeInputLabel = useCallback((label: string) => { const normalized = label.toLowerCase(); @@ -912,7 +914,7 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps ) : nodeData.status === "error" ? ( - {nodeData.error || "Failed"} + {displayError || "Failed"} ) : ( diff --git a/src/components/nodes/LLMGenerateNode.tsx b/src/components/nodes/LLMGenerateNode.tsx index 9273b332..00be6428 100644 --- a/src/components/nodes/LLMGenerateNode.tsx +++ b/src/components/nodes/LLMGenerateNode.tsx @@ -11,11 +11,19 @@ import { SettingsTabBar } from "./SettingsTabBar"; import { useShowHandleLabels } from "@/hooks/useShowHandleLabels"; import { HandleLabel } from "./HandleLabel"; import { LLM_MODELS, LLM_PROVIDERS } from "@/lib/llmModels"; +import { useI18n } from "@/i18n"; +import { formatUserFacingError } from "@/utils/userFacingErrors"; type LLMGenerateNodeType = Node; export function LLMGenerateNode({ id, data, selected }: NodeProps) { + const { t } = useI18n(); const nodeData = data; + const provider = nodeData.provider || "newapiwg"; + const availableModels = LLM_MODELS[provider] || LLM_MODELS.newapiwg; + const selectedModelValue = nodeData.model || availableModels[0].value; + const displayModelName = availableModels.find((model) => model.value === selectedModelValue)?.label || selectedModelValue; + const displayError = formatUserFacingError(nodeData.error, t, displayModelName); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const regenerateNode = useWorkflowStore((state) => state.regenerateNode); @@ -99,9 +107,6 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps )} @@ -314,9 +319,9 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps - Generation failed - {nodeData.error && ( - {nodeData.error} + {t("node.generationFailed")} + {displayError && ( + {displayError} )}
) : nodeData.outputText ? ( diff --git a/src/components/onboarding/FTUXModelDefaultsStep.tsx b/src/components/onboarding/FTUXModelDefaultsStep.tsx index 2dc1bc69..96a3adff 100644 --- a/src/components/onboarding/FTUXModelDefaultsStep.tsx +++ b/src/components/onboarding/FTUXModelDefaultsStep.tsx @@ -7,6 +7,10 @@ import { ModelSearchDialog } from "@/components/modals/ModelSearchDialog"; import { ProviderModel } from "@/lib/providers/types"; import { loadNodeDefaults, saveNodeDefaults } from "@/store/utils/localStorage"; import { useI18n } from "@/i18n"; +import { + createNewApiWGDefaultImageModel, + createNewApiWGDefaultVideoModel, +} from "@/store/utils/defaultImageModel"; // Provider icons const GeminiIcon = () => ( @@ -37,8 +41,17 @@ const WaveSpeedIcon = () => ( ); +const NewApiWGIcon = () => ( + + + + +); + const getProviderIcon = (provider: string) => { switch (provider) { + case "newapiwg": + return ; case "gemini": return ; case "replicate": @@ -64,6 +77,9 @@ export function FTUXModelDefaultsStep({}: FTUXStepProps) { setLocalDefaults(currentDefaults); }, []); + const effectiveImageModel = localDefaults.generateImage?.selectedModel ?? createNewApiWGDefaultImageModel(); + const effectiveVideoModel = localDefaults.generateVideo?.selectedModel ?? createNewApiWGDefaultVideoModel(); + return (

@@ -79,45 +95,30 @@ export function FTUXModelDefaultsStep({}: FTUXStepProps) {
{t("ftux.defaultImageModel")}
- {localDefaults.generateImage?.selectedModel ? ( - <> -
- {getProviderIcon(localDefaults.generateImage.selectedModel.provider)} - - {localDefaults.generateImage.selectedModel.displayName} - -
- - - - ) : ( - <> - {t("ftux.noneSet")} - - - )} +
+ {getProviderIcon(effectiveImageModel.provider)} + + {effectiveImageModel.displayName} + +
+ +

@@ -127,45 +128,30 @@ export function FTUXModelDefaultsStep({}: FTUXStepProps) {
{t("ftux.defaultVideoModel")}
- {localDefaults.generateVideo?.selectedModel ? ( - <> -
- {getProviderIcon(localDefaults.generateVideo.selectedModel.provider)} - - {localDefaults.generateVideo.selectedModel.displayName} - -
- - - - ) : ( - <> - {t("ftux.noneSet")} - - - )} +
+ {getProviderIcon(effectiveVideoModel.provider)} + + {effectiveVideoModel.displayName} + +
+ +
diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index a6c71b6b..188f24f2 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -47,6 +47,7 @@ const en = { "common.previous": "Previous", "common.nextItem": "Next", "common.processing": "Processing...", + "error.providerOverloaded": "{model}: provider is temporarily overloaded. Please retry in a moment or switch models.", "node.optionalTitle": "This input is optional - empty inputs will be skipped", "node.markOptionalTitle": "Mark as optional - empty inputs will skip this branch", "header.openWelcome": "Open welcome screen", @@ -538,7 +539,7 @@ const en = { "project.systemImageDefault": "System default (Popi Models)", "project.noneSetFirstUse": "None set (select on first use)", "project.defaultLLM": "Default LLM Settings", - "project.systemLLMDefault": "Using system defaults (Popi Models GPT-4.1 Mini)", + "project.systemLLMDefault": "Using system defaults (Popi Models Doubao Seed 2.0 Lite)", "project.provider": "Provider", "project.model": "Model", "project.temp": "Temp: {value}", @@ -659,6 +660,7 @@ const zhCN: Dictionary = { "common.previous": "上一个", "common.nextItem": "下一个", "common.processing": "处理中...", + "error.providerOverloaded": "{model}:服务商暂时过载。请稍后重试,或切换到其他模型。", "node.optionalTitle": "此输入为可选,空输入会被跳过", "node.markOptionalTitle": "标记为可选,空输入会跳过这个分支", "header.openWelcome": "打开欢迎页", @@ -1148,7 +1150,7 @@ const zhCN: Dictionary = { "project.systemImageDefault": "系统默认(Popi Models)", "project.noneSetFirstUse": "未设置(首次使用时选择)", "project.defaultLLM": "默认 LLM 设置", - "project.systemLLMDefault": "使用系统默认值(Popi Models GPT-4.1 Mini)", + "project.systemLLMDefault": "使用系统默认值(Popi Models Doubao Seed 2.0 Lite)", "project.provider": "服务商", "project.model": "模型", "project.temp": "温度:{value}", @@ -1283,6 +1285,7 @@ const zhTW: Dictionary = { "common.previous": "上一個", "common.nextItem": "下一個", "common.processing": "處理中...", + "error.providerOverloaded": "{model}:服務商暫時過載。請稍後重試,或切換到其他模型。", "node.optionalTitle": "此輸入為可選,空輸入會被略過", "node.markOptionalTitle": "標記為可選,空輸入會略過這個分支", "header.openWelcome": "開啟歡迎頁", @@ -1670,7 +1673,7 @@ const zhTW: Dictionary = { "project.newapiwgDesc": "Popi 統一模型閘道", "project.baseUrl": "Base URL", "project.defaultLLM": "預設 LLM 設定", - "project.systemLLMDefault": "使用系統預設值(Popi Models GPT-4.1 Mini)", + "project.systemLLMDefault": "使用系統預設值(Popi Models Doubao Seed 2.0 Lite)", "project.executionSettings": "執行設定", "project.maxParallel": "最大平行呼叫:{count}", "project.canvasHelp": "設定畫布導覽和互動方式。", @@ -1746,6 +1749,7 @@ const ja: Dictionary = { "common.previous": "前へ", "common.nextItem": "次へ", "common.processing": "処理中...", + "error.providerOverloaded": "{model}: プロバイダーが一時的に混雑しています。少し待って再試行するか、別のモデルに切り替えてください。", "node.optionalTitle": "この入力は任意です。空の入力はスキップされます", "node.markOptionalTitle": "任意に設定します。空の入力はこの分岐をスキップします", "header.openWelcome": "ウェルカム画面を開く", @@ -2181,7 +2185,7 @@ const ja: Dictionary = { "project.systemImageDefault": "システム既定(Popi Models)", "project.noneSetFirstUse": "未設定(初回使用時に選択)", "project.defaultLLM": "既定の LLM 設定", - "project.systemLLMDefault": "システム既定値を使用(Popi Models GPT-4.1 Mini)", + "project.systemLLMDefault": "システム既定値を使用(Popi Models Doubao Seed 2.0 Lite)", "project.provider": "プロバイダー", "project.model": "モデル", "project.temp": "温度: {value}", diff --git a/src/lib/__tests__/llmModels.test.ts b/src/lib/__tests__/llmModels.test.ts index 7c578bac..6893ba8c 100644 --- a/src/lib/__tests__/llmModels.test.ts +++ b/src/lib/__tests__/llmModels.test.ts @@ -2,15 +2,11 @@ import { describe, expect, it } from "vitest"; import { DEFAULT_NEWAPIWG_LLM_MODEL_ID, LLM_MODELS } from "@/lib/llmModels"; describe("LLM model options", () => { - it("defaults NewApiWG text generation to GPT-4.1 Mini", () => { - expect(DEFAULT_NEWAPIWG_LLM_MODEL_ID).toBe("gpt-4.1-mini"); + it("defaults NewApiWG text generation to Doubao Seed 2.0 Lite", () => { + expect(DEFAULT_NEWAPIWG_LLM_MODEL_ID).toBe("doubao-seed-2-0-lite-260428"); }); it("includes Doubao Seed 2.0 for NewApiWG selectors", () => { - expect(LLM_MODELS.newapiwg).toContainEqual({ - value: "gpt-4.1-mini", - label: "GPT-4.1 Mini", - }); expect(LLM_MODELS.newapiwg).toContainEqual({ value: "doubao-seed-2-0-lite-260428", label: "Doubao Seed 2.0 Lite", @@ -19,5 +15,9 @@ describe("LLM model options", () => { value: "doubao-seed-2-0-pro-260215", label: "Doubao Seed 2.0 Pro", }); + expect(LLM_MODELS.newapiwg).toContainEqual({ + value: "gpt-4.1-mini", + label: "GPT-4.1 Mini", + }); }); }); diff --git a/src/lib/chat/tools.ts b/src/lib/chat/tools.ts index d2142285..e068a6bc 100644 --- a/src/lib/chat/tools.ts +++ b/src/lib/chat/tools.ts @@ -42,7 +42,7 @@ AI video generation. Takes image + text inputs, outputs video. Use Popi Models f ### LLM Text Generation AI text generation for expanding prompts or analyzing images. - **Provider dropdown**: Popi Models -- **Model dropdown**: GPT-4.1 Mini (Popi Models default), Doubao Seed 2.0 Lite/Pro (Popi Models options) +- **Model dropdown**: Doubao Seed 2.0 Lite (Popi Models default), Doubao Seed 2.0 Pro and GPT-4.1 Mini (Popi Models options) - **Parameters** (collapsible): Temperature slider (0-2), Max Tokens slider (256-16384) - Takes **text** input (required), optional **image** input diff --git a/src/lib/llmModels.ts b/src/lib/llmModels.ts index bbbe0efa..7f04f8ae 100644 --- a/src/lib/llmModels.ts +++ b/src/lib/llmModels.ts @@ -6,7 +6,7 @@ export interface LLMModelOption { label: string; } -export const DEFAULT_NEWAPIWG_LLM_MODEL_ID: LLMModelType = "gpt-4.1-mini"; +export const DEFAULT_NEWAPIWG_LLM_MODEL_ID: LLMModelType = "doubao-seed-2-0-lite-260428"; const ALL_LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [ { value: "newapiwg", label: getProviderDisplayName("newapiwg") }, @@ -22,9 +22,9 @@ export const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = export const LLM_MODELS: Record = { newapiwg: [ - { value: "gpt-4.1-mini", label: "GPT-4.1 Mini" }, { value: "doubao-seed-2-0-lite-260428", label: "Doubao Seed 2.0 Lite" }, { value: "doubao-seed-2-0-pro-260215", label: "Doubao Seed 2.0 Pro" }, + { value: "gpt-4.1-mini", label: "GPT-4.1 Mini" }, ], google: [ { value: "gemini-3-flash-preview", label: "Gemini 3 Flash" }, diff --git a/src/store/execution/__tests__/generateVideoExecutor.test.ts b/src/store/execution/__tests__/generateVideoExecutor.test.ts index 79d6479c..6567b646 100644 --- a/src/store/execution/__tests__/generateVideoExecutor.test.ts +++ b/src/store/execution/__tests__/generateVideoExecutor.test.ts @@ -214,8 +214,8 @@ describe("executeGenerateVideo", () => { }, fallbackModel: { provider: "newapiwg", - modelId: "viduq2-pro-fast", - displayName: "Vidu Q2 Pro Fast", + modelId: "doubao-seedance-2-0-260128", + displayName: "doubao-seedance-2-0-260128", }, fallbackParameters: { duration: 5 }, }); @@ -239,13 +239,13 @@ describe("executeGenerateVideo", () => { const body = JSON.parse(mockFetch.mock.calls[0][1].body); expect(body.selectedModel).toMatchObject({ provider: "newapiwg", - modelId: "viduq2-pro-fast", + modelId: "doubao-seedance-2-0-260128", }); expect(body.parameters).toEqual({ duration: 5 }); expect(ctx.updateNodeData).toHaveBeenCalledWith("vid-1", expect.objectContaining({ selectedModel: expect.objectContaining({ provider: "newapiwg", - modelId: "viduq2-pro-fast", + modelId: "doubao-seedance-2-0-260128", }), parameters: { duration: 5 }, fallbackModel: undefined, @@ -280,7 +280,7 @@ describe("executeGenerateVideo", () => { const body = JSON.parse(mockFetch.mock.calls[0][1].body); expect(body.selectedModel).toMatchObject({ provider: "newapiwg", - modelId: "viduq2-pro-fast", + modelId: "doubao-seedance-2-0-260128", }); }); @@ -315,7 +315,7 @@ describe("executeGenerateVideo", () => { const body = JSON.parse(request[1].body); expect(body.selectedModel).toMatchObject({ provider: "newapiwg", - modelId: "viduq2-pro-fast", + modelId: "doubao-seedance-2-0-260128", }); expect(request[1].headers["X-NewApiWG-Key"]).toBeUndefined(); }); diff --git a/src/store/utils/__tests__/nodeDefaults.test.ts b/src/store/utils/__tests__/nodeDefaults.test.ts index 1f4be47f..31abfe2d 100644 --- a/src/store/utils/__tests__/nodeDefaults.test.ts +++ b/src/store/utils/__tests__/nodeDefaults.test.ts @@ -278,8 +278,8 @@ describe("nodeDefaults utilities", () => { expect((data as any).selectedModel).toEqual({ provider: "newapiwg", - modelId: "viduq2-pro-fast", - displayName: "Vidu Q2 Pro Fast", + modelId: "doubao-seedance-2-0-260128", + displayName: "doubao-seedance-2-0-260128", capabilities: ["text-to-video", "image-to-video"], }); }); @@ -310,7 +310,7 @@ describe("nodeDefaults utilities", () => { const data = createDefaultNodeData("llmGenerate"); expect((data as any).provider).toBe("newapiwg"); - expect((data as any).model).toBe("gpt-4.1-mini"); + expect((data as any).model).toBe("doubao-seed-2-0-lite-260428"); expect((data as any).temperature).toBe(0.7); expect((data as any).maxTokens).toBe(8192); }); @@ -332,7 +332,7 @@ describe("nodeDefaults utilities", () => { expect((data as any).temperature).toBe(0.9); // Should fall back to NewApiWG defaults for others expect((data as any).provider).toBe("newapiwg"); - expect((data as any).model).toBe("gpt-4.1-mini"); + expect((data as any).model).toBe("doubao-seed-2-0-lite-260428"); expect((data as any).maxTokens).toBe(8192); }); }); diff --git a/src/store/utils/defaultImageModel.ts b/src/store/utils/defaultImageModel.ts index bc8183e4..daacd8ef 100644 --- a/src/store/utils/defaultImageModel.ts +++ b/src/store/utils/defaultImageModel.ts @@ -2,14 +2,14 @@ import type { ModelType, ProviderEnvStatus, ProviderSettings, ProviderType, Sele import { MODEL_DISPLAY_NAMES } from "@/types"; const NEWAPIWG_DEFAULT_IMAGE_MODEL_ID = "apiyi_nano_banana_2"; -const NEWAPIWG_DEFAULT_VIDEO_MODEL_ID = "viduq2-pro-fast"; +const NEWAPIWG_DEFAULT_VIDEO_MODEL_ID = "doubao-seedance-2-0-260128"; const DEFAULT_AUDIO_MODEL_ID = "elevenlabs/turbo-v2.5"; export function createNewApiWGDefaultImageModel(): SelectedModel { return { provider: "newapiwg", modelId: NEWAPIWG_DEFAULT_IMAGE_MODEL_ID, - displayName: "APIYI Nano Banana 2", + displayName: NEWAPIWG_DEFAULT_IMAGE_MODEL_ID, capabilities: ["text-to-image", "image-to-image"], }; } @@ -18,7 +18,7 @@ export function createNewApiWGDefaultVideoModel(): SelectedModel { return { provider: "newapiwg", modelId: NEWAPIWG_DEFAULT_VIDEO_MODEL_ID, - displayName: "Vidu Q2 Pro Fast", + displayName: NEWAPIWG_DEFAULT_VIDEO_MODEL_ID, capabilities: ["text-to-video", "image-to-video"], }; } diff --git a/src/utils/__tests__/userFacingErrors.test.ts b/src/utils/__tests__/userFacingErrors.test.ts new file mode 100644 index 00000000..4cdf84a5 --- /dev/null +++ b/src/utils/__tests__/userFacingErrors.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import type { TranslationKey } from "@/i18n"; +import { formatUserFacingError } from "@/utils/userFacingErrors"; + +const t = (key: TranslationKey, values?: Record) => { + if (key === "error.providerOverloaded") { + return `${values?.model}: localized overload`; + } + return key; +}; + +describe("formatUserFacingError", () => { + it("localizes normalized provider overload errors", () => { + expect(formatUserFacingError( + "apiyi_nano_banana_pro: provider is temporarily overloaded. Please retry in a moment or switch models.", + t + )).toBe("apiyi_nano_banana_pro: localized overload"); + }); + + it("uses the currently displayed model name when provided", () => { + expect(formatUserFacingError( + "apiyi_nano_banana_pro: provider is temporarily overloaded. Please retry in a moment or switch models.", + t, + "PopiArt Nano Pro" + )).toBe("PopiArt Nano Pro: localized overload"); + }); + + it("preserves unknown upstream errors", () => { + expect(formatUserFacingError("unexpected upstream failure", t)).toBe("unexpected upstream failure"); + }); +}); diff --git a/src/utils/userFacingErrors.ts b/src/utils/userFacingErrors.ts new file mode 100644 index 00000000..55e71b98 --- /dev/null +++ b/src/utils/userFacingErrors.ts @@ -0,0 +1,23 @@ +import type { TranslationKey } from "@/i18n"; + +type Translate = (key: TranslationKey, values?: Record) => string; + +const PROVIDER_OVERLOADED_RE = + /^(.*?): provider is temporarily overloaded\. Please retry in a moment or switch models\.$/i; + +export function formatUserFacingError( + message: string | null | undefined, + t: Translate, + modelDisplayName?: string | null +): string | null { + if (!message) return null; + + const overloadedMatch = message.match(PROVIDER_OVERLOADED_RE); + if (overloadedMatch) { + return t("error.providerOverloaded", { + model: modelDisplayName?.trim() || (overloadedMatch[1] ?? "Model").trim(), + }); + } + + return message; +}