You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

191 lines
6.0 KiB

/**
* 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<ModelType, string> = {
"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<NextResponse<GenerateResponse>> {
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<string, unknown> = {
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<string, unknown>).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<string, Record<string, never>> = {};
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<GenerateResponse>(
{
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<GenerateResponse>(
{
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<GenerateResponse>(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<GenerateResponse>(
{
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<GenerateResponse>(
{
success: false,
error: "No image in response",
},
{ status: 500 }
);
}