From e6f6acb2f3bb5016f2176cbc0bd786ceb5574cb5 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Mon, 13 Apr 2026 07:32:58 +1200 Subject: [PATCH] fix: display Gemini fallback model parameters in node panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemini image models (nano-banana, nano-banana-pro, nano-banana-2) were not showing parameters when set as fallback models. Three root causes: 1. Schema API returned empty for Gemini image models — added getGeminiImageSchema() with aspect ratio, resolution, and search grounding parameters per model 2. ModelParameters had an early-return guard skipping all non-Veo Gemini models — removed to make the component provider-agnostic 3. nanoBananaExecutor ignored parametersOverride for Gemini-specific fields (aspectRatio, resolution, useGoogleSearch, useImageSearch) — now reads from override when available Co-Authored-By: Claude Sonnet 4.5 --- src/app/api/models/[modelId]/route.ts | 48 ++++++++++++++++++- .../__tests__/ModelParameters.test.tsx | 26 ++++++++-- src/components/nodes/ModelParameters.tsx | 8 ++-- src/store/execution/nanoBananaExecutor.ts | 8 ++-- 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/app/api/models/[modelId]/route.ts b/src/app/api/models/[modelId]/route.ts index ec28e6b8..95f08f1e 100644 --- a/src/app/api/models/[modelId]/route.ts +++ b/src/app/api/models/[modelId]/route.ts @@ -1000,6 +1000,48 @@ function getGeminiVideoSchema(modelId: string): ExtractedSchema | null { return schemas[modelId] ?? null; } +/** + * Get schema for Gemini image models (native image generation via Gemini API) + * Returns null if the model is not a Gemini image model. + */ +function getGeminiImageSchema(modelId: string): ExtractedSchema | null { + const baseAspectRatios = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"]; + const extendedAspectRatios = ["1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"]; + + const commonInputs: ModelInput[] = [ + { name: "prompt", type: "text", required: true, label: "Prompt" }, + { name: "image", type: "image", required: false, label: "Image", isArray: true }, + ]; + + const schemas: Record = { + "nano-banana": { + parameters: [ + { name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: baseAspectRatios, default: "1:1" }, + ], + inputs: commonInputs, + }, + "nano-banana-pro": { + parameters: [ + { name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: baseAspectRatios, default: "1:1" }, + { name: "resolution", type: "string", description: "Output resolution", enum: ["1K", "2K", "4K"], default: "2K" }, + { name: "useGoogleSearch", type: "boolean", description: "Enable Google Search grounding", default: false }, + ], + inputs: commonInputs, + }, + "nano-banana-2": { + parameters: [ + { name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: extendedAspectRatios, default: "1:1" }, + { name: "resolution", type: "string", description: "Output resolution", enum: ["512", "1K", "2K", "4K"], default: "2K" }, + { name: "useGoogleSearch", type: "boolean", description: "Enable Google Search grounding", default: false }, + { name: "useImageSearch", type: "boolean", description: "Enable Image Search grounding", default: false }, + ], + inputs: commonInputs, + }, + }; + + return schemas[modelId] ?? null; +} + /** * Get static schema for WaveSpeed models (fallback when dynamic schema not available) */ @@ -1260,12 +1302,14 @@ export async function GET( let result: ExtractedSchema; if (provider === "gemini") { - // Gemini video models use hardcoded schemas + // Gemini models use hardcoded schemas (video and image) const geminiVideoSchema = getGeminiVideoSchema(decodedModelId); + const geminiImageSchema = getGeminiImageSchema(decodedModelId); if (geminiVideoSchema) { result = geminiVideoSchema; + } else if (geminiImageSchema) { + result = geminiImageSchema; } else { - // Gemini image models don't use schema endpoint (params are built-in) result = { parameters: [], inputs: [] }; } } else if (provider === "replicate") { diff --git a/src/components/__tests__/ModelParameters.test.tsx b/src/components/__tests__/ModelParameters.test.tsx index ca9cab87..7f2676ea 100644 --- a/src/components/__tests__/ModelParameters.test.tsx +++ b/src/components/__tests__/ModelParameters.test.tsx @@ -66,11 +66,14 @@ describe("ModelParameters", () => { }); describe("Initial Rendering", () => { - it("should not render for Gemini provider", () => { - const { container } = render( + it("should fetch schema for Gemini provider", () => { + (global.fetch as ReturnType).mockImplementation( + () => new Promise(() => {}) + ); + render( ); - expect(container.firstChild).toBeNull(); + expect(screen.getByText("Loading parameters...")).toBeInTheDocument(); }); it("should not render when modelId is empty", () => { @@ -767,8 +770,17 @@ describe("ModelParameters", () => { }); }); - it("should call onInputsLoaded with empty array for Gemini", () => { + it("should fetch schema and call onInputsLoaded for Gemini", async () => { const onInputsLoaded = vi.fn(); + (global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + success: true, + parameters: [], + inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], + }), + }); render( { /> ); - expect(onInputsLoaded).toHaveBeenCalledWith([]); + await waitFor(() => { + expect(onInputsLoaded).toHaveBeenCalledWith([ + { name: "prompt", type: "text", required: true, label: "Prompt" }, + ]); + }); }); }); diff --git a/src/components/nodes/ModelParameters.tsx b/src/components/nodes/ModelParameters.tsx index 646998ed..ec451d5a 100644 --- a/src/components/nodes/ModelParameters.tsx +++ b/src/components/nodes/ModelParameters.tsx @@ -82,11 +82,9 @@ function ModelParametersInner({ // Use stable selector for API keys to prevent unnecessary re-fetches const { replicateApiKey, falApiKey, kieApiKey, wavespeedApiKey } = useProviderApiKeys(); - const isVeoModel = modelId?.startsWith("veo-"); - // Fetch schema when modelId changes useEffect(() => { - if (!modelId || (provider === "gemini" && !isVeoModel)) { + if (!modelId) { setSchema([]); onInputsLoaded?.([]); return; @@ -219,8 +217,8 @@ function ModelParametersInner({ : sortedSchema; }, [sortedSchema, useGrid, colCount]); - // Don't render anything for Gemini (except Veo) or if no model selected - if ((provider === "gemini" && !isVeoModel) || !modelId) { + // Don't render if no model selected + if (!modelId) { return null; } diff --git a/src/store/execution/nanoBananaExecutor.ts b/src/store/execution/nanoBananaExecutor.ts index ef857576..30ccbbdc 100644 --- a/src/store/execution/nanoBananaExecutor.ts +++ b/src/store/execution/nanoBananaExecutor.ts @@ -104,11 +104,11 @@ export async function executeNanoBanana( const requestPayload = { images, prompt: finalPrompt, - aspectRatio: nodeData.aspectRatio, - resolution: nodeData.resolution, + aspectRatio: (parametersOverride?.aspectRatio as string) ?? nodeData.aspectRatio, + resolution: (parametersOverride?.resolution as string) ?? nodeData.resolution, model: nodeData.model, - useGoogleSearch: nodeData.useGoogleSearch, - useImageSearch: nodeData.useImageSearch, + useGoogleSearch: (parametersOverride?.useGoogleSearch as boolean) ?? nodeData.useGoogleSearch, + useImageSearch: (parametersOverride?.useImageSearch as boolean) ?? nodeData.useImageSearch, selectedModel: modelToUse, parameters: parametersOverride ?? nodeData.parameters, dynamicInputs: sanitizedDynamicInputs,