Browse Source
Move MODEL_MAP and generateWithGemini() into providers/gemini.ts. Remove unused imports (GoogleGenAI, uploadImageForUrl, shouldUseImageUrl, deleteImages, ProviderModel) from route.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>handoff-20260429-1057
2 changed files with 190 additions and 178 deletions
@ -0,0 +1,188 @@ |
|||
/** |
|||
* 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", |
|||
}; |
|||
|
|||
/** |
|||
* 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 |
|||
): 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 only for Nano Banana Pro
|
|||
if (model === "nano-banana-pro" && resolution) { |
|||
if (!config.imageConfig) { |
|||
config.imageConfig = {}; |
|||
} |
|||
(config.imageConfig as Record<string, unknown>).imageSize = resolution; |
|||
} |
|||
|
|||
// Add tools array for Google Search (only Nano Banana Pro)
|
|||
const tools = []; |
|||
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`); |
|||
|
|||
// Create response with explicit headers to handle large payloads
|
|||
const resp = NextResponse.json<GenerateResponse>(responsePayload); |
|||
resp.headers.set('Content-Type', 'application/json'); |
|||
resp.headers.set('Content-Length', responseSize.toString()); |
|||
|
|||
return resp; |
|||
} |
|||
} |
|||
|
|||
// 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 } |
|||
); |
|||
} |
|||
Loading…
Reference in new issue