From 50206ae694fc184279543e39125c6e66272d8ff2 Mon Sep 17 00:00:00 2001 From: weige <772752726@qq.com> Date: Mon, 25 May 2026 17:14:22 +0800 Subject: [PATCH 1/2] fix --- src/app/api/_popiserverModels.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/api/_popiserverModels.ts b/src/app/api/_popiserverModels.ts index bf4d7437..ed74ad65 100644 --- a/src/app/api/_popiserverModels.ts +++ b/src/app/api/_popiserverModels.ts @@ -98,7 +98,6 @@ const SUBTYPE_TO_CAPABILITIES: Record = { function getPopiserverBaseUrl(request: NextRequest): string { const baseUrl = - process.env.POPISERVER_BASE_URL || process.env.POPIART_API_BASE_URL || process.env.NEXT_PUBLIC_APP_URL || request.nextUrl.origin; From 640eb0d9c9b257c9043149f0229cc9eacfb6450a Mon Sep 17 00:00:00 2001 From: weige <772752726@qq.com> Date: Mon, 25 May 2026 18:38:01 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=88=B0popiserver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/_popiserverChat.ts | 20 + src/app/api/_popiserverTasks.ts | 345 ++++++++++++++++++ src/app/api/chat/route.ts | 27 +- src/app/api/generate/poll/route.ts | 15 +- src/app/api/generate/route.ts | 23 ++ src/app/api/llm/route.ts | 12 +- src/app/api/popi/asset/create/route.ts | 176 +++++++++ src/app/api/popi/media/upload/route.ts | 137 +++++++ src/app/api/quickstart/propose/route.ts | 5 +- src/app/api/quickstart/route.ts | 5 +- src/components/ProjectSetupModal.tsx | 2 +- .../composer/GenerationComposer.tsx | 5 +- src/components/nodes/ControlPanel.tsx | 2 +- src/components/nodes/LLMGenerateNode.tsx | 2 +- src/lib/llmModels.ts | 6 +- src/lib/quickstart/modelClient.ts | 94 ++++- src/store/execution/llmGenerateExecutor.ts | 6 +- src/store/execution/simpleNodeExecutors.ts | 144 +++++++- src/store/utils/buildApiHeaders.ts | 4 + src/store/utils/nodeDefaults.ts | 2 +- src/types/providers.ts | 2 +- 21 files changed, 1009 insertions(+), 25 deletions(-) create mode 100644 src/app/api/_popiserverChat.ts create mode 100644 src/app/api/_popiserverTasks.ts create mode 100644 src/app/api/popi/asset/create/route.ts create mode 100644 src/app/api/popi/media/upload/route.ts diff --git a/src/app/api/_popiserverChat.ts b/src/app/api/_popiserverChat.ts new file mode 100644 index 00000000..4d5276b1 --- /dev/null +++ b/src/app/api/_popiserverChat.ts @@ -0,0 +1,20 @@ +import type { NextRequest } from "next/server"; + +type PopiserverChatInput = { + prompt: string; + model?: string; + temperature?: number; + maxTokens?: number; + images?: string[]; + videos?: string[]; +}; + +export async function callPopiserverChatMock( + _request: NextRequest | Request, + input: PopiserverChatInput +): Promise { + const prefix = "[mock popiserver chat]"; + const model = input.model ? ` model=${input.model}` : ""; + return `${prefix}${model}\n${input.prompt}`; +} + diff --git a/src/app/api/_popiserverTasks.ts b/src/app/api/_popiserverTasks.ts new file mode 100644 index 00000000..93075ccd --- /dev/null +++ b/src/app/api/_popiserverTasks.ts @@ -0,0 +1,345 @@ +import type { NextRequest } from "next/server"; +import type { GenerateResponse } from "@/types"; +import type { ModelCapability } from "@/lib/providers/types"; +import { ApiError } from "./_errors"; +import type { SelectedModel } from "@/types"; + +const POPI_TASK_CREATE_PATH = "/api_client/anime/task/create"; +const POPI_TASK_DETAIL_PATH = "/api_client/anime/task/detail"; +const POPI_MEDIA_UPLOAD_BASE64_PATH = "/api_client/media/uploadFileBase64"; + +type PopiserverResponse = { + status?: string; + message?: string; + data?: T; +}; + +type PopiAsset = { + images?: string[]; + video?: string; + voice?: string; + cover?: string; +}; + +type PopiMedia = { + path?: string; + url?: string; + originPath?: string; + originUrl?: string; +}; + +type PopiTask = { + id: number; + type?: number; + subType?: number; + status: number; + errorMsg?: string; + userErrorTipMsg?: string; + resultList?: PopiAsset[]; +}; + +type PopiCreateTaskPayload = { + type: number; + subType: number; + aiModelId: number; + chatPrompt: string; + images?: string[]; + voice?: string; + videos?: string[]; + ratio?: string; + aspectRatio?: string; + resolution?: string; + duration?: number; + batchSize?: number; +}; + +type PopiserverGenerationInput = { + prompt?: string; + selectedModel?: SelectedModel; + images?: string[]; + parameters?: Record; + dynamicInputs?: Record; + mediaType?: "image" | "video" | "3d" | "audio"; + aspectRatio?: string; + resolution?: string; +}; + +function getPopiBaseUrl(request: NextRequest): string { + const baseUrl = + process.env.POPIART_API_BASE_URL || + process.env.NEXT_PUBLIC_APP_URL || + request.nextUrl.origin; + return baseUrl.replace(/\/+$/, ""); +} + +function buildForwardHeaders(request: NextRequest, token: string): HeadersInit { + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + token, + }; + const cookie = request.headers.get("cookie"); + if (cookie) headers.Cookie = cookie; + return headers; +} + +async function fetchPopiJson( + request: NextRequest, + path: string, + token: string, + init?: RequestInit, + searchParams?: URLSearchParams +): Promise { + const url = new URL(getPopiBaseUrl(request) + path); + searchParams?.forEach((value, key) => { + if (value !== "") url.searchParams.set(key, value); + }); + + const response = await fetch(url, { + ...init, + headers: { + ...buildForwardHeaders(request, token), + ...(init?.headers || {}), + }, + cache: "no-store", + }); + + const body = (await response.json().catch(() => null)) as PopiserverResponse | null; + if (!response.ok) { + throw ApiError.provider(body?.message || "Popiserver task API request failed.", response.status); + } + if (!body || body.status !== "0000") { + throw ApiError.provider(body?.message || "Popiserver task API returned an error."); + } + return body.data as T; +} + +function firstString(value: unknown): string | undefined { + if (typeof value === "string" && value) return value; + if (Array.isArray(value)) { + return value.find((item) => typeof item === "string" && item) as string | undefined; + } + return undefined; +} + +function collectStrings(...values: unknown[]): string[] { + const result: string[] = []; + for (const value of values) { + if (typeof value === "string" && value) { + result.push(value); + } else if (Array.isArray(value)) { + for (const item of value) { + if (typeof item === "string" && item) result.push(item); + } + } + } + return result; +} + +function parseDataUrl(value: string): { mimeType: string; base64: string } | undefined { + const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/); + if (!match) return undefined; + return { + mimeType: match[1] || "application/octet-stream", + base64: match[2], + }; +} + +function extensionForMimeType(mimeType: string): string { + const normalized = mimeType.toLowerCase(); + if (normalized === "image/jpeg") return "jpg"; + if (normalized === "image/png") return "png"; + if (normalized === "image/webp") return "webp"; + if (normalized === "image/gif") return "gif"; + if (normalized === "video/mp4") return "mp4"; + if (normalized === "video/webm") return "webm"; + if (normalized === "audio/mpeg") return "mp3"; + if (normalized === "audio/mp3") return "mp3"; + if (normalized === "audio/wav") return "wav"; + if (normalized === "audio/x-wav") return "wav"; + if (normalized === "model/gltf-binary") return "glb"; + return normalized.split("/")[1]?.replace(/[^a-z0-9]/g, "") || "bin"; +} + +function mediaUrlFromUpload(media: PopiMedia): string { + const value = media.path || media.originPath || media.url || media.originUrl; + if (!value) { + throw ApiError.provider("Popiserver media upload response is missing media URL."); + } + return value; +} + +async function uploadPopiserverMediaIfNeeded( + request: NextRequest, + token: string, + value: string, + index: number +): Promise { + const dataUrl = parseDataUrl(value); + if (!dataUrl) return value; + + const extension = extensionForMimeType(dataUrl.mimeType); + const media = await fetchPopiJson(request, POPI_MEDIA_UPLOAD_BASE64_PATH, token, { + method: "POST", + body: JSON.stringify({ + data: dataUrl.base64, + fileName: `canvas-input-${Date.now()}-${index}.${extension}`, + }), + }); + return mediaUrlFromUpload(media); +} + +async function uploadPopiserverMediaList( + request: NextRequest, + token: string, + values: string[] +): Promise { + const uploaded = await Promise.all( + values.map((value, index) => uploadPopiserverMediaIfNeeded(request, token, value, index)) + ); + return uploaded.filter(Boolean); +} + +function inferMediaType(input: PopiserverGenerationInput): "image" | "video" | "audio" | "3d" { + if (input.mediaType) return input.mediaType; + const capabilities = (input.selectedModel?.capabilities || []) as ModelCapability[]; + if (capabilities.some((capability) => capability.includes("video"))) return "video"; + if (capabilities.some((capability) => capability.includes("audio"))) return "audio"; + if (capabilities.some((capability) => capability.includes("3d"))) return "3d"; + return "image"; +} + +function inferTaskType(mediaType: string): number { + if (mediaType === "video") return 2; + if (mediaType === "audio") return 3; + if (mediaType === "3d") return 4; + return 1; +} + +function inferTaskSubType(input: PopiserverGenerationInput, mediaType: string, images: string[], voice?: string): number { + const explicitSubType = Number(input.parameters?.subType); + if (Number.isFinite(explicitSubType) && explicitSubType > 0) return explicitSubType; + + if (mediaType === "video") { + if (input.dynamicInputs?.first_frame || input.dynamicInputs?.last_frame) return 204; + return images.length > 0 ? 202 : 201; + } + if (mediaType === "audio") { + return voice ? 302 : 301; + } + if (mediaType === "3d") { + return images.length > 0 ? 402 : 401; + } + return images.length > 0 ? 103 : 102; +} + +async function buildPopiTaskPayload( + request: NextRequest, + token: string, + input: PopiserverGenerationInput +): Promise { + const mediaType = inferMediaType(input); + const parameters = input.parameters || {}; + const dynamicInputs = input.dynamicInputs || {}; + const imageInputs = collectStrings( + input.images, + dynamicInputs.image, + dynamicInputs.images, + dynamicInputs.image_url, + dynamicInputs.image_urls, + dynamicInputs.first_frame, + dynamicInputs.last_frame, + dynamicInputs.start_image, + dynamicInputs.end_image + ); + const videoInputs = collectStrings(dynamicInputs.video, dynamicInputs.videos, dynamicInputs.video_url, dynamicInputs.video_urls); + const voiceInput = firstString(dynamicInputs.audio) || firstString(dynamicInputs.audio_url); + const images = await uploadPopiserverMediaList(request, token, imageInputs); + const videos = await uploadPopiserverMediaList(request, token, videoInputs); + const voice = voiceInput ? await uploadPopiserverMediaIfNeeded(request, token, voiceInput, 0) : undefined; + const prompt = input.prompt || firstString(dynamicInputs.prompt) || ""; + const modelId = Number(input.selectedModel?.modelId); + + if (!Number.isFinite(modelId) || modelId <= 0) { + throw ApiError.badRequest("Popiserver generation requires selectedModel.modelId to be a popiserver AI model ID."); + } + + return { + type: inferTaskType(mediaType), + subType: inferTaskSubType(input, mediaType, images, voice), + aiModelId: modelId, + chatPrompt: prompt, + images, + videos, + voice, + ratio: String(parameters.aspectRatio || parameters.ratio || input.aspectRatio || ""), + aspectRatio: String(parameters.aspectRatio || parameters.ratio || input.aspectRatio || ""), + resolution: String(parameters.resolution || input.resolution || ""), + duration: Number(parameters.duration) || undefined, + batchSize: Number(parameters.batchSize) || 1, + }; +} + +export async function createPopiserverTask( + request: NextRequest, + token: string, + input: PopiserverGenerationInput +): Promise { + const payload = await buildPopiTaskPayload(request, token, input); + return fetchPopiJson(request, POPI_TASK_CREATE_PATH, token, { + method: "POST", + body: JSON.stringify(payload), + }); +} + +export async function fetchPopiserverTaskDetail( + request: NextRequest, + token: string, + taskId: string +): Promise { + return fetchPopiJson( + request, + POPI_TASK_DETAIL_PATH, + token, + { method: "GET" }, + new URLSearchParams({ id: taskId }) + ); +} + +function firstTaskOutput(task: PopiTask): string | undefined { + const asset = task.resultList?.[0]; + if (!asset) return undefined; + return asset.images?.[0] || asset.video || asset.voice || asset.cover; +} + +export function popiserverTaskToGenerateResponse(task: PopiTask): GenerateResponse { + if (task.status === 0 || task.status === 1) { + return { + success: true, + polling: true, + taskId: String(task.id), + pollProvider: "popiserver", + pollModelId: "", + pollModelName: "", + pollMediaType: task.type === 2 ? "video" : task.type === 3 ? "audio" : task.type === 4 ? "3d" : "image", + }; + } + + if (task.status === -1 || task.status === -2) { + return { + success: false, + error: task.userErrorTipMsg || task.errorMsg || "Popiserver task failed", + }; + } + + const output = firstTaskOutput(task); + if (!output) { + return { success: false, error: "No output in popiserver task result" }; + } + + if (task.type === 2) return { success: true, videoUrl: output, contentType: "video" }; + if (task.type === 3) return { success: true, audioUrl: output, contentType: "audio" }; + if (task.type === 4) return { success: true, model3dUrl: output, contentType: "3d" }; + return { success: true, image: output, contentType: "image" }; +} diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index bb817811..a9c38932 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -10,6 +10,8 @@ import { DEFAULT_NEWAPIWG_LLM_MODEL_ID } from '@/lib/llmModels'; import { buildUserForwardHeaders, requireLogin } from '@/app/api/_auth'; import { ApiError, apiErrorResponse } from '@/app/api/_errors'; import { resolveUserGatewayApiKey } from '@/app/api/_gatewayApiKey'; +import { callPopiserverChatMock } from '@/app/api/_popiserverChat'; +import { isPopiProviderMode } from '@/lib/providerMode'; export const maxDuration = 60; // 1 minute timeout @@ -22,11 +24,6 @@ export async function POST(request: Request) { selectedNodeIds?: string[]; }; - const apiKey = await resolveUserGatewayApiKey(request); - const baseUrl = normalizeNewApiWGBaseUrl( - request.headers.get('X-NewApiWG-Base-URL') || process.env.NEWAPIWG_BASE_URL - ); - // Extract subgraph if nodes are selected, otherwise use full workflow const subgraph = extractSubgraph( workflowState?.nodes || [], @@ -43,6 +40,26 @@ export async function POST(request: Request) { // Build context-aware system prompt with optional rest summary const systemPrompt = buildEditSystemPrompt(context, subgraph.restSummary); + if (isPopiProviderMode()) { + const lastMessage = messages[messages.length - 1] as { parts?: Array<{ type?: string; text?: string }>; content?: string } | undefined; + const userPrompt = + lastMessage?.parts?.map((part) => part.text).filter(Boolean).join("\n") || + lastMessage?.content || + ""; + const text = await callPopiserverChatMock(request, { + prompt: `${systemPrompt}\n\n${userPrompt}`, + model: DEFAULT_NEWAPIWG_LLM_MODEL_ID, + }); + return new Response(text, { + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }); + } + + const apiKey = await resolveUserGatewayApiKey(request); + const baseUrl = normalizeNewApiWGBaseUrl( + request.headers.get('X-NewApiWG-Base-URL') || process.env.NEWAPIWG_BASE_URL + ); + // Extract node IDs for tool validation const nodeIds = (workflowState?.nodes || []).map(n => n.id); diff --git a/src/app/api/generate/poll/route.ts b/src/app/api/generate/poll/route.ts index 124a6bfb..609343b4 100644 --- a/src/app/api/generate/poll/route.ts +++ b/src/app/api/generate/poll/route.ts @@ -11,6 +11,10 @@ import { checkKieTaskOnce, fetchKieMediaResult, isVeoModel } from "../providers/ import { buildMediaResponse } from "../route"; import { requireConfiguredValue, requireLogin } from "../../_auth"; import { ApiError, apiErrorResponse } from "../../_errors"; +import { + fetchPopiserverTaskDetail, + popiserverTaskToGenerateResponse, +} from "../../_popiserverTasks"; export const maxDuration = 120; // 2 min — enough for media fetch, not for polling export const dynamic = 'force-dynamic'; @@ -27,7 +31,7 @@ export async function POST(request: NextRequest) { const requestId = Math.random().toString(36).substring(7); try { - requireLogin(request); + const login = requireLogin(request); const body: PollRequest = await request.json(); const { taskId, provider, modelId, modelName, mediaType } = body; @@ -35,6 +39,15 @@ export async function POST(request: NextRequest) { throw ApiError.badRequest("taskId and provider are required"); } + if (provider === "popiserver") { + const task = await fetchPopiserverTaskDetail(request, login.token, taskId); + const result = popiserverTaskToGenerateResponse(task); + return NextResponse.json( + { requestId, ...result }, + result.success ? undefined : { status: 500 } + ); + } + if (provider !== 'kie') { throw ApiError.badRequest(`Unsupported poll provider: ${provider}`); } diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index d3d3dddf..39510809 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -22,6 +22,10 @@ import { generateWithNewApiWG } from "./providers/newapiwg"; import { requireConfiguredValue, requireLogin } from "../_auth"; import { ApiError, apiErrorResponse } from "../_errors"; import { resolveUserGatewayApiKey } from "../_gatewayApiKey"; +import { + createPopiserverTask, + popiserverTaskToGenerateResponse, +} from "../_popiserverTasks"; // Re-export for backward compatibility (test file imports from route) export const clearFalInputMappingCache = _clearFalInputMappingCache; @@ -214,6 +218,25 @@ export async function POST(request: NextRequest) { console.log(`[API:${requestId}] Provider: ${provider}, Model: ${selectedModel?.modelId || model}`); // Route to appropriate provider + if (provider === "popiserver") { + const selected = requireSelectedModel(selectedModel, "Popiserver"); + const task = await createPopiserverTask(request, login.token, { + prompt, + images, + selectedModel: selected, + parameters: mergeImageGenerationParameters(parameters, aspectRatio, resolution), + dynamicInputs, + mediaType, + aspectRatio, + resolution, + }); + const result = popiserverTaskToGenerateResponse(task); + return NextResponse.json( + { requestId, ...result }, + result.success ? undefined : { status: 500 } + ); + } + if (provider === "newapiwg") { const selected = requireSelectedModel(selectedModel, "NewApiWG"); diff --git a/src/app/api/llm/route.ts b/src/app/api/llm/route.ts index ef70f6fb..ed6c255d 100644 --- a/src/app/api/llm/route.ts +++ b/src/app/api/llm/route.ts @@ -7,6 +7,7 @@ import { buildGatewayHeaders, requireLogin } from "@/app/api/_auth"; import { ApiError, apiErrorResponse } from "@/app/api/_errors"; import { resolveUserGatewayApiKey } from "@/app/api/_gatewayApiKey"; import { logGatewayRequest } from "@/app/api/_gatewayLogging"; +import { callPopiserverChatMock } from "@/app/api/_popiserverChat"; export const maxDuration = 60; // 1 minute timeout @@ -442,7 +443,16 @@ export async function POST(request: NextRequest) { let text: string; - if (provider === "newapiwg") { + if (provider === "popiserver") { + text = await callPopiserverChatMock(request, { + prompt, + model, + temperature, + maxTokens, + images, + videos, + }); + } else if (provider === "newapiwg") { const newApiWGKey = await resolveUserGatewayApiKey(request); text = await generateWithNewApiWG(prompt, model, temperature, maxTokens, images, videos, requestId, newApiWGKey, newApiWGBaseUrl, login.token); } else if (provider === "google") { diff --git a/src/app/api/popi/asset/create/route.ts b/src/app/api/popi/asset/create/route.ts new file mode 100644 index 00000000..726af711 --- /dev/null +++ b/src/app/api/popi/asset/create/route.ts @@ -0,0 +1,176 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireLogin } from "@/app/api/_auth"; +import { ApiError } from "@/app/api/_errors"; + +const POPI_MEDIA_UPLOAD_BASE64_PATH = "/api_client/media/uploadFileBase64"; +const POPI_ASSET_CREATE_PATH = "/api_client/anime/asset/create"; + +type PopiserverResponse = { + status?: string; + message?: string; + data?: T; +}; + +type PopiMedia = { + path?: string; + url?: string; + originPath?: string; + originUrl?: string; +}; + +type PopiAsset = { + id?: number; + type?: number; + images?: string[]; + video?: string; +}; + +function getPopiBaseUrl(request: NextRequest): string { + const baseUrl = + process.env.POPIART_API_BASE_URL || + process.env.NEXT_PUBLIC_APP_URL || + request.nextUrl.origin; + return baseUrl.replace(/\/+$/, ""); +} + +function parseDataUrl(value: string): { mimeType: string; base64: string } | undefined { + const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/); + if (!match) return undefined; + return { + mimeType: match[1] || "application/octet-stream", + base64: match[2], + }; +} + +function extensionForMimeType(mimeType: string): string { + const normalized = mimeType.toLowerCase(); + if (normalized === "image/jpeg") return "jpg"; + if (normalized === "image/png") return "png"; + if (normalized === "image/webp") return "webp"; + if (normalized === "image/gif") return "gif"; + if (normalized === "video/mp4") return "mp4"; + if (normalized === "video/webm") return "webm"; + return normalized.split("/")[1]?.replace(/[^a-z0-9]/g, "") || "bin"; +} + +function mediaResourceUrl(media: PopiMedia): string | null { + return media.url || media.originUrl || media.path || media.originPath || null; +} + +function isPopiserverMediaUrl(value: string): boolean { + if (value.startsWith("/media/") || value.startsWith("/media_private/")) return true; + try { + const url = new URL(value); + return url.pathname.startsWith("/media/") || url.pathname.startsWith("/media_private/"); + } catch { + return false; + } +} + +async function postPopiJson( + request: NextRequest, + token: string, + path: string, + body: Record +): Promise { + const upstream = await fetch(getPopiBaseUrl(request) + path, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + token, + }, + body: JSON.stringify(body), + cache: "no-store", + }); + + const payload = (await upstream.json().catch(() => null)) as PopiserverResponse | null; + if (!upstream.ok) { + throw ApiError.provider(payload?.message || "Popiserver API request failed.", upstream.status); + } + if (!payload || payload.status !== "0000") { + throw ApiError.provider(payload?.message || "Popiserver API returned an error.", 502); + } + return payload.data as T; +} + +async function ensurePopiserverMedia( + request: NextRequest, + token: string, + value: string, + kind: "image" | "video", + fileName?: string +): Promise { + if (isPopiserverMediaUrl(value)) return value; + + const dataUrl = parseDataUrl(value); + if (!dataUrl) { + throw ApiError.badRequest("Only popiserver media URLs or base64 data URLs can be saved as assets."); + } + + const extension = extensionForMimeType(dataUrl.mimeType); + const media = await postPopiJson(request, token, POPI_MEDIA_UPLOAD_BASE64_PATH, { + data: dataUrl.base64, + fileName: fileName || `canvas-${kind}-${Date.now()}.${extension}`, + }); + const url = mediaResourceUrl(media); + if (!url) { + throw ApiError.provider("Popiserver media upload response is missing resource URL.", 502); + } + return url; +} + +export async function POST(request: NextRequest) { + try { + const login = requireLogin(request); + const body = (await request.json().catch(() => null)) as { + type?: unknown; + media?: unknown; + title?: unknown; + description?: unknown; + fileName?: unknown; + } | null; + + const type = body?.type === "video" ? "video" : body?.type === "image" ? "image" : null; + const media = typeof body?.media === "string" ? body.media : ""; + if (!type || !media) { + return NextResponse.json( + { success: false, error: "type=image|video and media are required." }, + { status: 400 } + ); + } + + const resourceUrl = await ensurePopiserverMedia( + request, + login.token, + media, + type, + typeof body?.fileName === "string" ? body.fileName : undefined + ); + + const title = typeof body?.title === "string" ? body.title : ""; + const description = typeof body?.description === "string" ? body.description : ""; + const asset = await postPopiJson(request, login.token, POPI_ASSET_CREATE_PATH, { + type: type === "video" ? 2 : 1, + title, + description, + images: type === "image" ? [resourceUrl] : undefined, + video: type === "video" ? resourceUrl : undefined, + }); + + return NextResponse.json({ + success: true, + url: resourceUrl, + asset, + }); + } catch (error) { + if (error instanceof ApiError) { + return NextResponse.json({ success: false, error: error.message }, { status: error.status }); + } + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : "Asset create failed." }, + { status: 500 } + ); + } +} diff --git a/src/app/api/popi/media/upload/route.ts b/src/app/api/popi/media/upload/route.ts new file mode 100644 index 00000000..6ed268c3 --- /dev/null +++ b/src/app/api/popi/media/upload/route.ts @@ -0,0 +1,137 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireLogin } from "@/app/api/_auth"; +import { ApiError } from "@/app/api/_errors"; + +const POPI_MEDIA_UPLOAD_BASE64_PATH = "/api_client/media/uploadFileBase64"; + +type PopiserverResponse = { + status?: string; + message?: string; + data?: T; +}; + +type PopiMedia = { + id?: number; + path?: string; + url?: string; + originPath?: string; + originUrl?: string; + thumbPath?: string; + thumbUrl?: string; +}; + +function getPopiBaseUrl(request: NextRequest): string { + const baseUrl = + process.env.POPIART_API_BASE_URL || + process.env.NEXT_PUBLIC_APP_URL || + request.nextUrl.origin; + return baseUrl.replace(/\/+$/, ""); +} + +function parseDataUrl(value: string): { mimeType: string; base64: string } | undefined { + const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$/); + if (!match) return undefined; + return { + mimeType: match[1] || "application/octet-stream", + base64: match[2], + }; +} + +function extensionForMimeType(mimeType: string): string { + const normalized = mimeType.toLowerCase(); + if (normalized === "image/jpeg") return "jpg"; + if (normalized === "image/png") return "png"; + if (normalized === "image/webp") return "webp"; + if (normalized === "image/gif") return "gif"; + if (normalized === "video/mp4") return "mp4"; + if (normalized === "video/webm") return "webm"; + if (normalized === "audio/mpeg" || normalized === "audio/mp3") return "mp3"; + if (normalized === "audio/wav" || normalized === "audio/x-wav") return "wav"; + if (normalized === "model/gltf-binary") return "glb"; + return normalized.split("/")[1]?.replace(/[^a-z0-9]/g, "") || "bin"; +} + +function mediaResourceUrl(media: PopiMedia): string | null { + return media.url || media.originUrl || media.path || media.originPath || null; +} + +export async function POST(request: NextRequest) { + try { + const login = requireLogin(request); + const body = (await request.json().catch(() => null)) as { + data?: unknown; + fileName?: unknown; + contentType?: unknown; + } | null; + + const value = typeof body?.data === "string" ? body.data : ""; + const dataUrl = parseDataUrl(value); + if (!dataUrl) { + return NextResponse.json( + { success: false, error: "Only base64 data URLs can be uploaded." }, + { status: 400 } + ); + } + + const extension = extensionForMimeType( + typeof body?.contentType === "string" ? body.contentType : dataUrl.mimeType + ); + const rawFileName = + typeof body?.fileName === "string" && body.fileName.trim() + ? body.fileName.trim() + : `canvas-media-${Date.now()}.${extension}`; + const fileName = rawFileName.includes(".") ? rawFileName : `${rawFileName}.${extension}`; + + const upstream = await fetch(getPopiBaseUrl(request) + POPI_MEDIA_UPLOAD_BASE64_PATH, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${login.token}`, + token: login.token, + }, + body: JSON.stringify({ + data: dataUrl.base64, + fileName, + }), + cache: "no-store", + }); + + const payload = (await upstream.json().catch(() => null)) as PopiserverResponse | null; + if (!upstream.ok) { + return NextResponse.json( + { success: false, error: payload?.message || "Popiserver media upload failed." }, + { status: upstream.status } + ); + } + if (!payload || payload.status !== "0000" || !payload.data) { + return NextResponse.json( + { success: false, error: payload?.message || "Popiserver media upload returned an error." }, + { status: 502 } + ); + } + + const resourceUrl = mediaResourceUrl(payload.data); + if (!resourceUrl) { + return NextResponse.json( + { success: false, error: "Popiserver media upload response is missing resource URL." }, + { status: 502 } + ); + } + + return NextResponse.json({ + success: true, + url: resourceUrl, + path: payload.data.path, + media: payload.data, + }); + } catch (error) { + if (error instanceof ApiError) { + return NextResponse.json({ success: false, error: error.message }, { status: error.status }); + } + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : "Upload failed." }, + { status: 500 } + ); + } +} diff --git a/src/app/api/quickstart/propose/route.ts b/src/app/api/quickstart/propose/route.ts index 22e0a9e8..f5c6a6fa 100644 --- a/src/app/api/quickstart/propose/route.ts +++ b/src/app/api/quickstart/propose/route.ts @@ -9,6 +9,7 @@ import { resolveUserGatewayApiKey } from "@/app/api/_gatewayApiKey"; import { ApiError } from "@/app/api/_errors"; import { parseJSONFromResponse } from "@/lib/quickstart/validation"; import type { WorkflowProposal, WorkflowComplexity, NodeType } from "@/types"; +import { isPopiProviderMode } from "@/lib/providerMode"; export const maxDuration = 60; // 1 minute timeout @@ -196,7 +197,9 @@ export async function POST(request: NextRequest) { ); } - const newApiWGKey = await resolveUserGatewayApiKey(request); + const newApiWGKey = isPopiProviderMode() + ? null + : await resolveUserGatewayApiKey(request); const modelConfig = resolveQuickstartModelConfig(request.headers, { newApiWGApiKey: newApiWGKey, }); diff --git a/src/app/api/quickstart/route.ts b/src/app/api/quickstart/route.ts index 5e88799c..8bf81b38 100644 --- a/src/app/api/quickstart/route.ts +++ b/src/app/api/quickstart/route.ts @@ -9,6 +9,7 @@ import { } from "@/lib/quickstart/modelClient"; import { resolveUserGatewayApiKey } from "@/app/api/_gatewayApiKey"; import { ApiError } from "@/app/api/_errors"; +import { isPopiProviderMode } from "@/lib/providerMode"; import { validateWorkflowJSON, repairWorkflowJSON, @@ -131,7 +132,9 @@ export async function POST(request: NextRequest) { ); } - const newApiWGKey = await resolveUserGatewayApiKey(request); + const newApiWGKey = isPopiProviderMode() + ? null + : await resolveUserGatewayApiKey(request); const modelConfig = resolveQuickstartModelConfig(request.headers, { newApiWGApiKey: newApiWGKey, }); diff --git a/src/components/ProjectSetupModal.tsx b/src/components/ProjectSetupModal.tsx index 556e3bac..86efe2af 100644 --- a/src/components/ProjectSetupModal.tsx +++ b/src/components/ProjectSetupModal.tsx @@ -81,7 +81,7 @@ const getProviderIcon = (provider: ProviderType) => { } }; -const DEFAULT_LLM_PROVIDER: LLMProvider = "newapiwg"; +const DEFAULT_LLM_PROVIDER: LLMProvider = isPopiProviderMode() ? "popiserver" : "newapiwg"; const DEFAULT_LLM_TEMPERATURE = 0.7; const DEFAULT_LLM_MAX_TOKENS = 8192; diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 234b2be2..ef7bdc58 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -107,9 +107,10 @@ const DEFAULT_VIDEO_MODEL = isPopiProviderMode() : createNewApiWGDefaultVideoModel(); const DEFAULT_AUDIO_MODEL = createDefaultAudioModel(); const DEFAULT_PROMPT_TEXT_MODEL: SelectedModel = { - provider: "newapiwg", + provider: isPopiProviderMode() ? "popiserver" : "newapiwg", modelId: DEFAULT_NEWAPIWG_LLM_MODEL_ID, - displayName: LLM_MODELS.newapiwg.find((model) => model.value === DEFAULT_NEWAPIWG_LLM_MODEL_ID)?.label || DEFAULT_NEWAPIWG_LLM_MODEL_ID, + displayName: (isPopiProviderMode() ? LLM_MODELS.popiserver : LLM_MODELS.newapiwg) + .find((model) => model.value === DEFAULT_NEWAPIWG_LLM_MODEL_ID)?.label || DEFAULT_NEWAPIWG_LLM_MODEL_ID, capabilities: ["text-to-text"], }; diff --git a/src/components/nodes/ControlPanel.tsx b/src/components/nodes/ControlPanel.tsx index f5c8b313..1ec8ff10 100644 --- a/src/components/nodes/ControlPanel.tsx +++ b/src/components/nodes/ControlPanel.tsx @@ -881,7 +881,7 @@ function LLMControls({ node }: { node: Node }) { [node.id, updateNodeData] ); - const provider = popiProviderMode ? "newapiwg" : nodeData.provider || "newapiwg"; + const provider = popiProviderMode ? "popiserver" : nodeData.provider || "newapiwg"; const availableModels = LLM_MODELS[provider] || LLM_MODELS.newapiwg; return ( diff --git a/src/components/nodes/LLMGenerateNode.tsx b/src/components/nodes/LLMGenerateNode.tsx index 55be72a4..3cba1205 100644 --- a/src/components/nodes/LLMGenerateNode.tsx +++ b/src/components/nodes/LLMGenerateNode.tsx @@ -21,7 +21,7 @@ export function LLMGenerateNode({ id, data, selected }: NodeProps model.value === selectedModelValue)?.label || selectedModelValue; diff --git a/src/lib/llmModels.ts b/src/lib/llmModels.ts index d0907156..939c8d92 100644 --- a/src/lib/llmModels.ts +++ b/src/lib/llmModels.ts @@ -9,6 +9,7 @@ export interface LLMModelOption { export const DEFAULT_NEWAPIWG_LLM_MODEL_ID: LLMModelType = "doubao-seed-2-0-lite-260428"; const ALL_LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [ + { value: "popiserver", label: getProviderDisplayName("popiserver") }, { value: "newapiwg", label: getProviderDisplayName("newapiwg") }, { value: "google", label: "Google" }, { value: "openai", label: "OpenAI" }, @@ -17,10 +18,13 @@ const ALL_LLM_PROVIDERS: { value: LLMProvider; label: string }[] = [ export const LLM_PROVIDERS: { value: LLMProvider; label: string }[] = isPopiProviderMode() - ? ALL_LLM_PROVIDERS.filter((provider) => provider.value === "newapiwg") + ? ALL_LLM_PROVIDERS.filter((provider) => provider.value === "popiserver") : ALL_LLM_PROVIDERS; export const LLM_MODELS: Record = { + popiserver: [ + { value: DEFAULT_NEWAPIWG_LLM_MODEL_ID, label: "Popiserver Chat" }, + ], newapiwg: [ { 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" }, diff --git a/src/lib/quickstart/modelClient.ts b/src/lib/quickstart/modelClient.ts index 74c1d9c6..dadfbc37 100644 --- a/src/lib/quickstart/modelClient.ts +++ b/src/lib/quickstart/modelClient.ts @@ -1,8 +1,13 @@ import { GoogleGenAI } from "@google/genai"; import { DEFAULT_NEWAPIWG_LLM_MODEL_ID } from "@/lib/llmModels"; import { DEFAULT_NEWAPIWG_BASE_URL } from "@/store/utils/localStorage"; +import { isPopiProviderMode } from "@/lib/providerMode"; export type QuickstartModelConfig = + | { + provider: "popiserver"; + model: string; + } | { provider: "newapiwg"; apiKey: string; @@ -33,6 +38,13 @@ export function resolveQuickstartModelConfig( options: ResolveQuickstartModelConfigOptions = {} ): QuickstartModelConfig | null { const env = options.env ?? process.env; + if (isPopiProviderMode()) { + return { + provider: "popiserver", + model: DEFAULT_NEWAPIWG_LLM_MODEL_ID, + }; + } + const newApiWGKey = options.newApiWGApiKey; if (newApiWGKey) { return { @@ -56,13 +68,19 @@ export function resolveQuickstartModelConfig( } export function getQuickstartMissingConfigError(): string { - return "Gateway API key not found for current user."; + return isPopiProviderMode() + ? "Popiserver chat API is not available." + : "Gateway API key not found for current user."; } export async function generateQuickstartText( prompt: string, options: GenerateQuickstartTextOptions ): Promise { + if (options.config.provider === "popiserver") { + return generateWithPopiserverMock(prompt); + } + if (options.config.provider === "newapiwg") { return generateWithNewApiWG(prompt, { ...options, @@ -76,6 +94,80 @@ export async function generateQuickstartText( }); } +async function generateWithPopiserverMock(prompt: string): Promise { + if (prompt.includes("estimatedComplexity") || prompt.includes("connections")) { + return JSON.stringify({ + name: "Mock Popiserver Workflow", + description: "Temporary mock response from popiserver chat.", + nodes: [ + { + id: "prompt-1", + type: "prompt", + purpose: "Input prompt", + suggestedTitle: "Prompt", + }, + { + id: "generate-1", + type: "nanoBanana", + purpose: "Generate an image", + suggestedTitle: "Generate Image", + }, + ], + connections: [ + { + from: "prompt-1", + to: "generate-1", + type: "text", + description: "Use prompt text for image generation", + }, + ], + estimatedComplexity: "simple", + warnings: ["Mock response until popiserver chat API is implemented."], + }); + } + + return JSON.stringify({ + version: 1, + id: `wf_${Date.now()}_mock_popiserver`, + name: "Mock Popiserver Workflow", + nodes: [ + { + id: "prompt-1", + type: "prompt", + position: { x: 100, y: 120 }, + data: { + text: "Describe the image to generate", + outputText: "Describe the image to generate", + }, + }, + { + id: "generate-1", + type: "nanoBanana", + position: { x: 480, y: 120 }, + data: { + inputImages: [], + inputPrompt: null, + outputImage: null, + aspectRatio: "1:1", + resolution: "1K", + status: "idle", + error: null, + }, + }, + ], + edges: [ + { + id: "edge-prompt-1-generate-1-text-text", + source: "prompt-1", + target: "generate-1", + sourceHandle: "text", + targetHandle: "text", + }, + ], + viewport: { x: 0, y: 0, zoom: 1 }, + }); +} + function normalizeBaseUrl(baseUrl?: string | null): string { return (baseUrl || DEFAULT_NEWAPIWG_BASE_URL).replace(/\/+$/, ""); } diff --git a/src/store/execution/llmGenerateExecutor.ts b/src/store/execution/llmGenerateExecutor.ts index 8c565889..8542fef6 100644 --- a/src/store/execution/llmGenerateExecutor.ts +++ b/src/store/execution/llmGenerateExecutor.ts @@ -15,6 +15,7 @@ import type { import { buildLlmHeaders } from "@/store/utils/buildApiHeaders"; import { runWithFallback } from "./runWithFallback"; import type { NodeExecutionContext } from "./types"; +import { isPopiProviderMode } from "@/lib/providerMode"; export interface LlmGenerateOptions { /** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ @@ -28,6 +29,7 @@ export interface LlmGenerateOptions { */ function providerTypeToLlmProvider(p: ProviderType): LLMProvider { if (p === "gemini") return "google"; + if (p === "popiserver") return "popiserver"; if (p === "newapiwg") return "newapiwg"; if (p === "openai") return "openai"; if (p === "anthropic") return "anthropic"; @@ -167,7 +169,9 @@ export async function executeLlmGenerate( // Synthesize a SelectedModel for the primary from the LLM's legacy provider/model fields. // "google" → "gemini" so SelectedModel carries the canonical ProviderType. const primaryProviderType: ProviderType = - nodeData.provider === "google" ? "gemini" : (nodeData.provider as ProviderType); + isPopiProviderMode() + ? "popiserver" + : nodeData.provider === "google" ? "gemini" : (nodeData.provider as ProviderType); const primaryModel: SelectedModel = { provider: primaryProviderType, diff --git a/src/store/execution/simpleNodeExecutors.ts b/src/store/execution/simpleNodeExecutors.ts index f499ad39..7e9f0bbb 100644 --- a/src/store/execution/simpleNodeExecutors.ts +++ b/src/store/execution/simpleNodeExecutors.ts @@ -21,6 +21,123 @@ import type { NodeExecutionContext } from "./types"; import { resolveTextSourcesThroughRouters } from "@/store/utils/connectedInputs"; import { parseTextToArray } from "@/utils/arrayParser"; import { parseVarTags } from "@/utils/parseVarTags"; +import { isPopiProviderMode } from "@/lib/providerMode"; + +const popiserverOutputAssetCache = new Map>(); +const USER_SESSION_STORAGE_KEY = "node-banana-user-session"; + +function isPopiserverMediaUrl(value: string): boolean { + if (value.startsWith("/media/") || value.startsWith("/media_private/")) return true; + try { + const url = new URL(value); + return url.pathname.startsWith("/media/") || url.pathname.startsWith("/media_private/"); + } catch { + return false; + } +} + +function isLocalMediaSource(value: string): boolean { + return value.startsWith("data:") || value.startsWith("blob:"); +} + +function extensionForMimeType(mimeType: string, kind: "image" | "video"): string { + const normalized = mimeType.toLowerCase(); + if (normalized === "image/jpeg") return "jpg"; + if (normalized === "image/png") return "png"; + if (normalized === "image/webp") return "webp"; + if (normalized === "image/gif") return "gif"; + if (normalized === "video/mp4") return "mp4"; + if (normalized === "video/webm") return "webm"; + return kind === "video" ? "mp4" : "png"; +} + +async function blobToDataUrl(value: string): Promise { + const response = await fetch(value); + if (!response.ok) { + throw new Error(`Failed to read local blob output: HTTP ${response.status}`); + } + const blob = await response.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(new Error("FileReader error while reading local output")); + reader.onabort = () => reject(new Error("FileReader aborted while reading local output")); + reader.readAsDataURL(blob); + }); +} + +function mimeTypeFromDataUrl(value: string): string | null { + const match = value.match(/^data:([^;,]+)?(?:;[^,]*)?;base64,/); + return match?.[1] || null; +} + +function getStoredLoginToken(): string | null { + if (typeof window === "undefined") return null; + try { + const stored = window.localStorage.getItem(USER_SESSION_STORAGE_KEY); + if (!stored) return null; + const parsed = JSON.parse(stored) as { state?: { token?: unknown } }; + const token = parsed.state?.token; + return typeof token === "string" && token.trim() ? token.trim() : null; + } catch { + return null; + } +} + +async function createPopiserverAssetForOutput( + kind: "image" | "video", + source: string | null | undefined, + title?: string +): Promise { + if (!source || !isPopiProviderMode() || isPopiserverMediaUrl(source) || !isLocalMediaSource(source)) { + return source; + } + + const token = getStoredLoginToken(); + if (!token) return source; + + const cacheKey = `${kind}:${source}`; + const cached = popiserverOutputAssetCache.get(cacheKey); + if (cached) return cached; + + const uploadPromise = (async () => { + try { + const dataUrl = source.startsWith("blob:") ? await blobToDataUrl(source) : source; + const mimeType = mimeTypeFromDataUrl(dataUrl) || (kind === "video" ? "video/mp4" : "image/png"); + const extension = extensionForMimeType(mimeType, kind); + const response = await fetch("/api/popi/asset/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + token, + }, + body: JSON.stringify({ + type: kind, + media: dataUrl, + title: title || "", + fileName: `canvas-output-${Date.now()}.${extension}`, + }), + }); + + const payload = (await response.json().catch(() => null)) as { + success?: boolean; + url?: unknown; + error?: string; + } | null; + + if (!response.ok || !payload?.success || typeof payload.url !== "string") { + throw new Error(payload?.error || `Popiserver asset create failed: HTTP ${response.status}`); + } + return payload.url; + } catch (error) { + console.error("Failed to create popiserver asset for output:", error); + return source; + } + })(); + popiserverOutputAssetCache.set(cacheKey, uploadPromise); + return uploadPromise; +} /** * Annotation node: receives upstream image as source, passes through if no annotations. @@ -221,9 +338,16 @@ export async function executeOutput(ctx: NodeExecutionContext): Promise { model3d ? "3d" : undefined; + const uploadedImage = rawImage && !imageLooksLikeVideo + ? await createPopiserverAssetForOutput("image", rawImage, outputNodeData.outputFilename) + : rawImage; + const uploadedVideo = videoContent + ? await createPopiserverAssetForOutput("video", videoContent, outputNodeData.outputFilename) + : videoContent; + updateNodeData(node.id, { - image: imageContent, - video: videoContent, + image: uploadedImage ?? imageContent, + video: uploadedVideo, audio: audioContent, model3d: model3d ?? null, contentType, @@ -251,15 +375,23 @@ export async function executeOutputGallery(ctx: NodeExecutionContext): Promise = {}; const existingImages = new Set(galleryImages); - const newImages = images.filter((img) => !existingImages.has(img)); + const newImages = await Promise.all( + images + .filter((img) => !existingImages.has(img)) + .map((img) => createPopiserverAssetForOutput("image", img)) + ); if (newImages.length > 0) { - updates.images = [...newImages, ...galleryImages]; + updates.images = [...newImages.filter((img): img is string => !!img), ...galleryImages]; } const existingVideos = new Set(galleryVideos); - const newVideos = videos.filter((v) => !existingVideos.has(v)); + const newVideos = await Promise.all( + videos + .filter((v) => !existingVideos.has(v)) + .map((video) => createPopiserverAssetForOutput("video", video)) + ); if (newVideos.length > 0) { - updates.videos = [...newVideos, ...galleryVideos]; + updates.videos = [...newVideos.filter((video): video is string => !!video), ...galleryVideos]; } if (Object.keys(updates).length > 0) { diff --git a/src/store/utils/buildApiHeaders.ts b/src/store/utils/buildApiHeaders.ts index e8f62929..427cc3ae 100644 --- a/src/store/utils/buildApiHeaders.ts +++ b/src/store/utils/buildApiHeaders.ts @@ -59,6 +59,10 @@ export function buildLlmHeaders( "Content-Type": "application/json", }; + if (llmProvider === "popiserver") { + return headers; + } + if (llmProvider === "newapiwg") { const newApiConfig = providerSettings.providers.newapiwg; if (newApiConfig?.baseUrl) { diff --git a/src/store/utils/nodeDefaults.ts b/src/store/utils/nodeDefaults.ts index bcd2af65..39453554 100644 --- a/src/store/utils/nodeDefaults.ts +++ b/src/store/utils/nodeDefaults.ts @@ -242,7 +242,7 @@ export const createDefaultNodeData = (type: NodeType): WorkflowNodeData => { inputImages: [], inputVideos: [], outputText: null, - provider: llmDefaults?.provider ?? "newapiwg", + provider: llmDefaults?.provider ?? (isPopiProviderMode() ? "popiserver" : "newapiwg"), model: llmDefaults?.model ?? DEFAULT_NEWAPIWG_LLM_MODEL_ID, temperature: llmDefaults?.temperature ?? 0.7, maxTokens: llmDefaults?.maxTokens ?? 8192, diff --git a/src/types/providers.ts b/src/types/providers.ts index f7bf2575..3260dfe1 100644 --- a/src/types/providers.ts +++ b/src/types/providers.ts @@ -40,7 +40,7 @@ export interface ProviderSettings { export type ProviderEnvStatus = Record; // LLM Provider Options -export type LLMProvider = "newapiwg" | "google" | "openai" | "anthropic"; +export type LLMProvider = "popiserver" | "newapiwg" | "google" | "openai" | "anthropic"; // LLM Model Options export type LLMModelType =