Browse Source

fix: display Gemini fallback model parameters in node panel

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 3 months ago
parent
commit
e6f6acb2f3
  1. 48
      src/app/api/models/[modelId]/route.ts
  2. 26
      src/components/__tests__/ModelParameters.test.tsx
  3. 8
      src/components/nodes/ModelParameters.tsx
  4. 8
      src/store/execution/nanoBananaExecutor.ts

48
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<string, ExtractedSchema> = {
"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") {

26
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<typeof vi.fn>).mockImplementation(
() => new Promise(() => {})
);
render(
<ModelParameters {...defaultProps} provider="gemini" />
);
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<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
success: true,
parameters: [],
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }],
}),
});
render(
<ModelParameters
@ -778,7 +790,11 @@ describe("ModelParameters", () => {
/>
);
expect(onInputsLoaded).toHaveBeenCalledWith([]);
await waitFor(() => {
expect(onInputsLoaded).toHaveBeenCalledWith([
{ name: "prompt", type: "text", required: true, label: "Prompt" },
]);
});
});
});

8
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;
}

8
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,

Loading…
Cancel
Save