/** * Gemini Provider for Generate API Route * * Handles image generation using Google's Gemini API models. */ import { NextResponse } from "next/server"; import { GoogleGenAI } from "@google/genai"; import { GenerateResponse, ModelType } from "@/types"; /** * Map model types to Gemini model IDs */ export const MODEL_MAP: Record = { "nano-banana": "gemini-2.5-flash-image", "nano-banana-pro": "gemini-3-pro-image-preview", "nano-banana-2": "gemini-3.1-flash-image-preview", }; /** * Generate image using Gemini API (legacy/default path) */ export async function generateWithGemini( requestId: string, apiKey: string, prompt: string, images: string[], model: ModelType, aspectRatio?: string, resolution?: string, useGoogleSearch?: boolean, useImageSearch?: boolean ): Promise> { console.log(`[API:${requestId}] Gemini generation - Model: ${model}, Images: ${images?.length || 0}, Prompt: ${prompt?.length || 0} chars`); // Extract base64 data and MIME types from data URLs const imageData = (images || []).map((image, idx) => { if (image.includes("base64,")) { const [header, data] = image.split("base64,"); // Extract MIME type from header (e.g., "data:image/png;" -> "image/png") const mimeMatch = header.match(/data:([^;]+)/); const mimeType = mimeMatch ? mimeMatch[1] : "image/png"; console.log(`[API:${requestId}] Image ${idx + 1}: ${mimeType}, ${(data.length / 1024).toFixed(1)}KB`); return { data, mimeType }; } console.log(`[API:${requestId}] Image ${idx + 1}: raw, ${(image.length / 1024).toFixed(1)}KB`); return { data: image, mimeType: "image/png" }; }); // Initialize Gemini client const ai = new GoogleGenAI({ apiKey }); // Build request parts array with prompt and all images const requestParts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = [ { text: prompt }, ...imageData.map(({ data, mimeType }) => ({ inlineData: { mimeType, data, }, })), ]; // Build config object based on model capabilities const config: Record = { responseModalities: ["IMAGE", "TEXT"], }; // Add imageConfig for both models (both support aspect ratio) if (aspectRatio) { config.imageConfig = { aspectRatio, }; } // Add resolution for Nano Banana Pro and Nano Banana 2 if ((model === "nano-banana-pro" || model === "nano-banana-2") && resolution) { if (!config.imageConfig) { config.imageConfig = {}; } (config.imageConfig as Record).imageSize = resolution; } // Add tools array for Google Search (Nano Banana Pro and Nano Banana 2) const tools = []; if (model === "nano-banana-2" && (useGoogleSearch || useImageSearch)) { // Nano Banana 2 uses searchTypes to enable web and/or image search independently const searchTypes: Record> = {}; if (useGoogleSearch) searchTypes.webSearch = {}; if (useImageSearch) searchTypes.imageSearch = {}; tools.push({ googleSearch: { searchTypes } }); } else if (model === "nano-banana-pro" && useGoogleSearch) { tools.push({ googleSearch: {} }); } console.log(`[API:${requestId}] Config: ${JSON.stringify(config)}`); // Make request to Gemini const geminiStartTime = Date.now(); const response = await ai.models.generateContent({ model: MODEL_MAP[model], contents: [ { role: "user", parts: requestParts, }, ], config, ...(tools.length > 0 && { tools }), }); const geminiDuration = Date.now() - geminiStartTime; console.log(`[API:${requestId}] Gemini API completed in ${geminiDuration}ms`); // Extract image from response const candidates = response.candidates; if (!candidates || candidates.length === 0) { console.error(`[API:${requestId}] No candidates in Gemini response`); return NextResponse.json( { success: false, error: "No response from AI model", }, { status: 500 } ); } const parts = candidates[0].content?.parts; console.log(`[API:${requestId}] Response parts: ${parts?.length || 0}`); if (!parts) { console.error(`[API:${requestId}] No parts in Gemini candidate content`); return NextResponse.json( { success: false, error: "No content in response", }, { status: 500 } ); } // Find image part in response for (const part of parts) { if (part.inlineData && part.inlineData.data) { const mimeType = part.inlineData.mimeType || "image/png"; const imgData = part.inlineData.data; const imageSizeKB = (imgData.length / 1024).toFixed(1); console.log(`[API:${requestId}] Output image: ${mimeType}, ${imageSizeKB}KB`); const dataUrl = `data:${mimeType};base64,${imgData}`; const responsePayload = { success: true, image: dataUrl }; const responseSize = JSON.stringify(responsePayload).length; const responseSizeMB = (responseSize / (1024 * 1024)).toFixed(2); if (responseSize > 4.5 * 1024 * 1024) { console.warn(`[API:${requestId}] Response size (${responseSizeMB}MB) approaching Next.js 5MB limit`); } console.log(`[API:${requestId}] SUCCESS - Returning ${responseSizeMB}MB payload`); return NextResponse.json(responsePayload); } } // If no image found, check for text error for (const part of parts) { if (part.text) { console.error(`[API:${requestId}] Gemini returned text instead of image: ${part.text.substring(0, 100)}`); return NextResponse.json( { success: false, error: `Model returned text instead of image: ${part.text.substring(0, 200)}`, }, { status: 500 } ); } } console.error(`[API:${requestId}] No image or text found in Gemini response`); return NextResponse.json( { success: false, error: "No image in response", }, { status: 500 } ); }