diff --git a/src/app/api/llm/route.ts b/src/app/api/llm/route.ts index 764c23a1..18e13738 100644 --- a/src/app/api/llm/route.ts +++ b/src/app/api/llm/route.ts @@ -334,6 +334,7 @@ async function generateWithPopiserver( aiModelId?: number, images?: string[], videos?: string[], + parameters?: Record, requestId?: string ): Promise { const modelId = model || "kimi-k2.6"; @@ -346,6 +347,7 @@ async function generateWithPopiserver( "Content-Type": "application/json", }, body: JSON.stringify({ + ...(parameters || {}), messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content }, @@ -500,6 +502,7 @@ export async function POST(request: NextRequest) { provider, model, aiModelId, + parameters, temperature = 0.7, maxTokens = 1024 } = body; @@ -527,7 +530,7 @@ export async function POST(request: NextRequest) { let text: string; if (provider === "popiserver") { - text = await generateWithPopiserver(request, login!.token, prompt, model, aiModelId, images, videos, requestId); + text = await generateWithPopiserver(request, login!.token, prompt, model, aiModelId, images, videos, parameters, requestId); } else if (provider === "newapiwg") { const newApiWGKey = await resolveUserGatewayApiKey(request); text = await generateWithNewApiWG(prompt, model, temperature, maxTokens, images, videos, requestId, newApiWGKey, newApiWGBaseUrl, loginToken); diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index b5289d95..8af1ff20 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -217,18 +217,22 @@ function buildGatewayDimensionSelects( }); } -function applyGatewayDimensionParameterDefaults( +function getSchemaParameterFallbackValue(parameter: ModelParameter): unknown { + return parameter.default ?? parameter.options?.[0]?.value ?? parameter.enum?.[0]; +} + +function applySchemaParameterDefaults( schema: ModelParameter[] | undefined, parameters: Record | undefined ): { parameters: Record; changed: boolean } { const next = { ...(parameters ?? {}) }; let changed = false; - const nameSet = new Set(GATEWAY_DIMENSION_PARAMETER_NAMES); for (const parameter of schema ?? []) { - if (!nameSet.has(parameter.name) || !parameter.options || parameter.options.length === 0) continue; if (next[parameter.name] !== undefined && next[parameter.name] !== null && next[parameter.name] !== "") continue; - next[parameter.name] = parameter.options[0]?.value; + const value = getSchemaParameterFallbackValue(parameter); + if (value === undefined || value === null || value === "") continue; + next[parameter.name] = value; changed = true; } @@ -1569,11 +1573,11 @@ export function GenerationComposer() { const activeParameterSchema = generationNodeData?.parameterSchema ?? []; useEffect(() => { if (context.mode !== "node-edit" || !context.node || activeParameterSchema.length === 0) return; - const normalized = applyGatewayDimensionParameterDefaults(activeParameterSchema, draftRef.current.parameters); + const normalized = applySchemaParameterDefaults(activeParameterSchema, draftRef.current.parameters); if (!normalized.changed) return; setDraft((current) => { - const currentNormalized = applyGatewayDimensionParameterDefaults(activeParameterSchema, current.parameters); + const currentNormalized = applySchemaParameterDefaults(activeParameterSchema, current.parameters); if (!currentNormalized.changed) return current; const next = { ...current, parameters: currentNormalized.parameters }; draftRef.current = next; diff --git a/src/store/execution/generateVideoExecutor.ts b/src/store/execution/generateVideoExecutor.ts index 4d07ed11..ebf03081 100644 --- a/src/store/execution/generateVideoExecutor.ts +++ b/src/store/execution/generateVideoExecutor.ts @@ -32,6 +32,7 @@ import { uploadTemporaryDynamicInputsForGeneration, uploadTemporaryImagesForGeneration, } from "@/utils/temporaryImageUpload"; +import { mergeConfiguredParameters } from "./generationParameters"; export interface GenerateVideoOptions { /** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ @@ -44,14 +45,22 @@ function buildVideoParameters( parametersOverride?: Record ): Record | undefined { const parameters = { ...(parametersOverride ?? nodeData.parameters ?? {}) }; - if (modelToUse.provider === "popiserver") { - return Object.keys(parameters).length > 0 ? parameters : undefined; - } - const durationSeconds = normalizeVideoDurationSeconds(nodeData.durationSeconds); const resolution = nodeData.resolution ?? DEFAULT_VIDEO_RESOLUTION; const soundParameterName = getVideoSoundParameterName(modelToUse); + if (modelToUse.provider === "popiserver") { + const merged = mergeConfiguredParameters(parameters, { + ...(nodeData.aspectRatio ? { aspectRatio: nodeData.aspectRatio, aspect_ratio: nodeData.aspectRatio } : {}), + resolution, + duration: durationSeconds, + durationSeconds, + ...(nodeData.fps !== undefined ? { fps: nodeData.fps } : {}), + ...(soundParameterName ? { [soundParameterName]: nodeData.soundEnabled ?? getDefaultVideoSoundEnabled(modelToUse) } : {}), + }); + return Object.keys(merged).length > 0 ? merged : undefined; + } + if (nodeData.aspectRatio) parameters.aspectRatio = nodeData.aspectRatio; for (const name of VIDEO_RESOLUTION_PARAMETER_NAMES) delete parameters[name]; parameters.resolution = resolution; diff --git a/src/store/execution/generationParameters.ts b/src/store/execution/generationParameters.ts new file mode 100644 index 00000000..67a5a4b0 --- /dev/null +++ b/src/store/execution/generationParameters.ts @@ -0,0 +1,52 @@ +const IMAGE_COUNT_PARAMETER_NAMES = [ + "batchSize", + "imageCount", + "numImages", + "num_images", + "numOutputs", + "num_outputs", + "count", +]; + +export function positiveInteger(value: unknown): number | null { + const numeric = typeof value === "number" + ? value + : typeof value === "string" && value.trim().length > 0 + ? Number(value) + : NaN; + return Number.isInteger(numeric) && numeric > 0 ? numeric : null; +} + +export function mergeConfiguredParameters( + base: Record | undefined, + configured: Record +): Record { + const parameters = { ...(base ?? {}) }; + for (const [key, value] of Object.entries(configured)) { + if (value !== undefined && value !== null) { + parameters[key] = parameters[key] ?? value; + } + } + return parameters; +} + +export function resolveImageGenerationCount( + parameters: Record | undefined, + nodeImageCount: number | undefined +): number { + for (const name of IMAGE_COUNT_PARAMETER_NAMES) { + const value = positiveInteger(parameters?.[name]); + if (value !== null) return value; + } + return positiveInteger(nodeImageCount) ?? 1; +} + +export function mergeImageGenerationParameters( + base: Record | undefined, + configured: Record, + imageCount: number +): Record { + const parameters = mergeConfiguredParameters(base, configured); + parameters.batchSize = positiveInteger(parameters.batchSize) ?? imageCount; + return parameters; +} diff --git a/src/store/execution/llmGenerateExecutor.ts b/src/store/execution/llmGenerateExecutor.ts index 0cd42049..4cc8cb64 100644 --- a/src/store/execution/llmGenerateExecutor.ts +++ b/src/store/execution/llmGenerateExecutor.ts @@ -16,6 +16,7 @@ import { buildLlmHeaders } from "@/store/utils/buildApiHeaders"; import { isPopiProviderMode } from "@/lib/providerMode"; import { runWithFallback } from "./runWithFallback"; import type { NodeExecutionContext } from "./types"; +import { mergeConfiguredParameters } from "./generationParameters"; export interface LlmGenerateOptions { /** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ @@ -105,6 +106,11 @@ export async function executeLlmGenerate( const temperature = (parametersOverride?.temperature as number | undefined) ?? nodeData.temperature; const maxTokens = (parametersOverride?.maxTokens as number | undefined) ?? nodeData.maxTokens; + const effectiveParameters = mergeConfiguredParameters(parametersOverride, { + temperature, + maxTokens, + max_tokens: maxTokens, + }); try { const response = await fetch("/api/llm", { @@ -119,6 +125,7 @@ export async function executeLlmGenerate( ...(aiModelId !== undefined && { aiModelId }), temperature, maxTokens, + parameters: effectiveParameters, }), ...(signal ? { signal } : {}), }); diff --git a/src/store/execution/nanoBananaExecutor.ts b/src/store/execution/nanoBananaExecutor.ts index 5c01ed64..fa3cba3f 100644 --- a/src/store/execution/nanoBananaExecutor.ts +++ b/src/store/execution/nanoBananaExecutor.ts @@ -34,6 +34,11 @@ import { uploadTemporaryDynamicInputsForGeneration, uploadTemporaryImagesForGeneration, } from "@/utils/temporaryImageUpload"; +import { + mergeConfiguredParameters, + mergeImageGenerationParameters, + resolveImageGenerationCount, +} from "./generationParameters"; export interface NanoBananaOptions { /** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ @@ -161,6 +166,25 @@ export async function executeNanoBanana( ...preparedDynamicInputs.cleanupIds, ]; + const requestedImageCount = resolveImageGenerationCount( + parametersOverride ?? nodeData.parameters, + nodeData.imageCount + ); + const configuredParameters = { + aspectRatio: (parametersOverride?.aspectRatio as string | undefined) ?? nodeData.aspectRatio, + aspect_ratio: (parametersOverride?.aspectRatio as string | undefined) ?? nodeData.aspectRatio, + resolution: (parametersOverride?.resolution as string | undefined) ?? nodeData.resolution, + imageSize: (parametersOverride?.resolution as string | undefined) ?? nodeData.resolution, + useGoogleSearch: (parametersOverride?.useGoogleSearch as boolean | undefined) ?? nodeData.useGoogleSearch, + useImageSearch: (parametersOverride?.useImageSearch as boolean | undefined) ?? nodeData.useImageSearch, + }; + const effectiveParameters = provider === "popiserver" + ? mergeImageGenerationParameters( + parametersOverride ?? nodeData.parameters, + configuredParameters, + requestedImageCount + ) + : mergeConfiguredParameters(parametersOverride ?? nodeData.parameters, configuredParameters); const requestPayload = { images: preparedImages.images, prompt: finalPrompt, @@ -170,7 +194,7 @@ export async function executeNanoBanana( useGoogleSearch: (parametersOverride?.useGoogleSearch as boolean) ?? nodeData.useGoogleSearch, useImageSearch: (parametersOverride?.useImageSearch as boolean) ?? nodeData.useImageSearch, selectedModel: modelToUse, - parameters: parametersOverride ?? nodeData.parameters, + parameters: effectiveParameters, dynamicInputs: preparedDynamicInputs.dynamicInputs, }; @@ -182,9 +206,6 @@ export async function executeNanoBanana( throw new Error(errorMsg); } - const requestedImageCount = nodeData.imageCount === 2 || nodeData.imageCount === 4 - ? nodeData.imageCount - : 1; const usePopiserverBatch = provider === "popiserver" && requestedImageCount > 1; const requestCount = usePopiserverBatch ? 1 : requestedImageCount; const usedModelId = modelToUse.modelId || nodeData.model; @@ -198,19 +219,10 @@ export async function executeNanoBanana( let partialError: string | null = null; for (let requestIndex = 0; requestIndex < requestCount; requestIndex++) { - const payloadForRequest = usePopiserverBatch - ? { - ...requestPayload, - parameters: { - ...(requestPayload.parameters || {}), - batchSize: requestedImageCount, - }, - } - : requestPayload; const response = await fetch("/api/generate", { method: "POST", headers, - body: JSON.stringify(payloadForRequest), + body: JSON.stringify(requestPayload), ...(signal ? { signal } : {}), }); diff --git a/src/types/api.ts b/src/types/api.ts index 0e76d679..bbfc57a1 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -51,6 +51,7 @@ export interface LLMGenerateRequest { aiModelId?: number; temperature?: number; maxTokens?: number; + parameters?: Record; } export interface LLMGenerateResponse {