diff --git a/src/app/api/generate/route.ts b/src/app/api/generate/route.ts index d28323f5..a4c88464 100644 --- a/src/app/api/generate/route.ts +++ b/src/app/api/generate/route.ts @@ -1,8 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { GoogleGenAI } from "@google/genai"; -import { GenerateRequest, GenerateResponse, ModelType } from "@/types"; +import { GenerateRequest, GenerateResponse, ModelType, SelectedModel, ProviderType } from "@/types"; +import { GenerationInput, GenerationOutput, ProviderModel } from "@/lib/providers/types"; -export const maxDuration = 300; // 5 minute timeout for Gemini API calls +export const maxDuration = 300; // 5 minute timeout for API calls export const dynamic = 'force-dynamic'; // Ensure this route is always dynamic // Map model types to Gemini model IDs @@ -11,233 +12,662 @@ const MODEL_MAP: Record = { "nano-banana-pro": "gemini-3-pro-image-preview", }; -export async function POST(request: NextRequest) { - const requestId = Math.random().toString(36).substring(7); - console.log(`\n[API:${requestId}] ========== NEW GENERATE REQUEST ==========`); - console.log(`[API:${requestId}] Timestamp: ${new Date().toISOString()}`); +/** + * Extended request format that supports both legacy and multi-provider requests + */ +interface MultiProviderGenerateRequest extends GenerateRequest { + selectedModel?: SelectedModel; + parameters?: Record; +} - try { - const apiKey = process.env.GEMINI_API_KEY; +/** + * Generate image using Gemini API (legacy/default path) + */ +async function generateWithGemini( + requestId: string, + apiKey: string, + prompt: string, + images: string[], + model: ModelType, + aspectRatio?: string, + resolution?: string, + useGoogleSearch?: boolean +): Promise> { + console.log(`[API:${requestId}] Request parameters:`); + console.log(`[API:${requestId}] - Model: ${model} -> ${MODEL_MAP[model]}`); + console.log(`[API:${requestId}] - Images count: ${images?.length || 0}`); + console.log(`[API:${requestId}] - Prompt length: ${prompt?.length || 0} chars`); + console.log(`[API:${requestId}] - Aspect Ratio: ${aspectRatio || 'default'}`); + console.log(`[API:${requestId}] - Resolution: ${resolution || 'default'}`); + console.log(`[API:${requestId}] - Google Search: ${useGoogleSearch || false}`); + + console.log(`[API:${requestId}] Extracting image data...`); + // 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(2)}KB base64`); + return { data, mimeType }; + } + console.log(`[API:${requestId}] Image ${idx + 1}: No base64 header, assuming PNG, ${(image.length / 1024).toFixed(2)}KB`); + return { data: image, mimeType: "image/png" }; + }); + + // Initialize Gemini client + console.log(`[API:${requestId}] Initializing Gemini client...`); + const ai = new GoogleGenAI({ apiKey }); + + // Build request parts array with prompt and all images + console.log(`[API:${requestId}] Building request parts...`); + const requestParts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = [ + { text: prompt }, + ...imageData.map(({ data, mimeType }) => ({ + inlineData: { + mimeType, + data, + }, + })), + ]; + console.log(`[API:${requestId}] Request parts count: ${requestParts.length} (1 text + ${imageData.length} images)`); + + // Build config object based on model capabilities + console.log(`[API:${requestId}] Building generation config...`); + const config: Record = { + responseModalities: ["IMAGE", "TEXT"], + }; + + // Add imageConfig for both models (both support aspect ratio) + if (aspectRatio) { + config.imageConfig = { + aspectRatio, + }; + console.log(`[API:${requestId}] Added aspect ratio: ${aspectRatio}`); + } - if (!apiKey) { - console.error(`[API:${requestId}] ❌ No API key configured`); - return NextResponse.json( - { - success: false, - error: "API key not configured. Add GEMINI_API_KEY to .env.local", - }, - { status: 500 } - ); + // Add resolution only for Nano Banana Pro + if (model === "nano-banana-pro" && resolution) { + if (!config.imageConfig) { + config.imageConfig = {}; } + (config.imageConfig as Record).imageSize = resolution; + console.log(`[API:${requestId}] Added resolution: ${resolution}`); + } - console.log(`[API:${requestId}] Parsing request body...`); - const body: GenerateRequest = await request.json(); - const { images, prompt, model = "nano-banana-pro", aspectRatio, resolution, useGoogleSearch } = body; + // 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}] Added Google Search tool`); + } - console.log(`[API:${requestId}] Request parameters:`); - console.log(`[API:${requestId}] - Model: ${model} -> ${MODEL_MAP[model]}`); - console.log(`[API:${requestId}] - Images count: ${images?.length || 0}`); - console.log(`[API:${requestId}] - Prompt length: ${prompt?.length || 0} chars`); - console.log(`[API:${requestId}] - Aspect Ratio: ${aspectRatio || 'default'}`); - console.log(`[API:${requestId}] - Resolution: ${resolution || 'default'}`); - console.log(`[API:${requestId}] - Google Search: ${useGoogleSearch || false}`); + console.log(`[API:${requestId}] Final config:`, JSON.stringify(config, null, 2)); + if (tools.length > 0) { + console.log(`[API:${requestId}] Tools:`, JSON.stringify(tools, null, 2)); + } - if (!prompt) { - console.error(`[API:${requestId}] ❌ Validation failed: missing prompt`); + // Make request to Gemini + console.log(`[API:${requestId}] Calling Gemini API...`); + 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 call completed in ${geminiDuration}ms`); + + // Extract image from response + console.log(`[API:${requestId}] Processing response...`); + const candidates = response.candidates; + console.log(`[API:${requestId}] Candidates count: ${candidates?.length || 0}`); + + if (!candidates || candidates.length === 0) { + console.error(`[API:${requestId}] No candidates in response`); + console.error(`[API:${requestId}] Full response:`, JSON.stringify(response, null, 2)); + return NextResponse.json( + { + success: false, + error: "No response from AI model", + }, + { status: 500 } + ); + } + + const parts = candidates[0].content?.parts; + console.log(`[API:${requestId}] Parts count in first candidate: ${parts?.length || 0}`); + + if (!parts) { + console.error(`[API:${requestId}] No parts in candidate content`); + console.error(`[API:${requestId}] Candidate:`, JSON.stringify(candidates[0], null, 2)); + return NextResponse.json( + { + success: false, + error: "No content in response", + }, + { status: 500 } + ); + } + + // Log all parts + parts.forEach((part, idx) => { + const partKeys = Object.keys(part); + console.log(`[API:${requestId}] Part ${idx + 1}: ${partKeys.join(', ')}`); + }); + + // 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(2); + console.log(`[API:${requestId}] Found image in response: ${mimeType}, ${imageSizeKB}KB base64`); + + const dataUrl = `data:${mimeType};base64,${imgData}`; + const dataUrlSizeKB = (dataUrl.length / 1024).toFixed(2); + console.log(`[API:${requestId}] Data URL size: ${dataUrlSizeKB}KB`); + + const responsePayload = { success: true, image: dataUrl }; + const responseSize = JSON.stringify(responsePayload).length; + const responseSizeMB = (responseSize / (1024 * 1024)).toFixed(2); + console.log(`[API:${requestId}] Total response payload size: ${responseSizeMB}MB`); + + if (responseSize > 4.5 * 1024 * 1024) { + console.warn(`[API:${requestId}] Response size (${responseSizeMB}MB) is approaching Next.js 5MB limit!`); + } + + console.log(`[API:${requestId}] SUCCESS - Returning image`); + + // Create response with explicit headers to handle large payloads + const resp = NextResponse.json(responsePayload); + resp.headers.set('Content-Type', 'application/json'); + resp.headers.set('Content-Length', responseSize.toString()); + + console.log(`[API:${requestId}] Response headers set, returning...`); + return resp; + } + } + + // If no image found, check for text error + console.warn(`[API:${requestId}] No image found in parts, checking for text...`); + for (const part of parts) { + if (part.text) { + console.error(`[API:${requestId}] Model returned text instead of image`); + console.error(`[API:${requestId}] Text preview: "${part.text.substring(0, 200)}"`); return NextResponse.json( { success: false, - error: "Prompt is required", + error: `Model returned text instead of image: ${part.text.substring(0, 200)}`, }, - { status: 400 } + { status: 500 } ); } + } - console.log(`[API:${requestId}] Extracting image data...`); - // 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(2)}KB base64`); - return { data, mimeType }; - } - console.log(`[API:${requestId}] Image ${idx + 1}: No base64 header, assuming PNG, ${(image.length / 1024).toFixed(2)}KB`); - return { data: image, mimeType: "image/png" }; - }); - - // Initialize Gemini client - console.log(`[API:${requestId}] Initializing Gemini client...`); - const ai = new GoogleGenAI({ apiKey }); - - // Build request parts array with prompt and all images - console.log(`[API:${requestId}] Building request parts...`); - const requestParts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = [ - { text: prompt }, - ...imageData.map(({ data, mimeType }) => ({ - inlineData: { - mimeType, - data, - }, - })), - ]; - console.log(`[API:${requestId}] Request parts count: ${requestParts.length} (1 text + ${imageData.length} images)`); - - // Build config object based on model capabilities - console.log(`[API:${requestId}] Building generation config...`); - const config: any = { - responseModalities: ["IMAGE", "TEXT"], + console.error(`[API:${requestId}] No image or text found in response`); + console.error(`[API:${requestId}] All parts:`, JSON.stringify(parts, null, 2)); + return NextResponse.json( + { + success: false, + error: "No image in response", + }, + { status: 500 } + ); +} + +/** + * Generate image using Replicate API + */ +async function generateWithReplicate( + requestId: string, + apiKey: string, + input: GenerationInput +): Promise { + console.log(`[API:${requestId}] Generating with Replicate...`); + console.log(`[API:${requestId}] - Model: ${input.model.id}`); + console.log(`[API:${requestId}] - Prompt length: ${input.prompt.length} chars`); + + const REPLICATE_API_BASE = "https://api.replicate.com/v1"; + + // Get the latest version of the model + const modelId = input.model.id; + const [owner, name] = modelId.split("/"); + + // First, get the model to find the latest version + const modelResponse = await fetch( + `${REPLICATE_API_BASE}/models/${owner}/${name}`, + { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + } + ); + + if (!modelResponse.ok) { + return { + success: false, + error: `Failed to get model info: ${modelResponse.status}`, }; + } + + const modelData = await modelResponse.json(); + const version = modelData.latest_version?.id; - // Add imageConfig for both models (both support aspect ratio) - if (aspectRatio) { - config.imageConfig = { - aspectRatio, + if (!version) { + return { + success: false, + error: "Model has no available version", + }; + } + + // Build input for the prediction + const predictionInput: Record = { + prompt: input.prompt, + ...input.parameters, + }; + + // Create a prediction + console.log(`[API:${requestId}] Creating Replicate prediction...`); + const createResponse = await fetch(`${REPLICATE_API_BASE}/predictions`, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + version, + input: predictionInput, + }), + }); + + if (!createResponse.ok) { + const errorText = await createResponse.text(); + return { + success: false, + error: `Failed to create prediction: ${createResponse.status} - ${errorText}`, + }; + } + + const prediction = await createResponse.json(); + + // Poll for completion + const maxWaitTime = 5 * 60 * 1000; // 5 minutes + const pollInterval = 1000; // 1 second + const startTime = Date.now(); + + let currentPrediction = prediction; + + while ( + currentPrediction.status !== "succeeded" && + currentPrediction.status !== "failed" && + currentPrediction.status !== "canceled" + ) { + if (Date.now() - startTime > maxWaitTime) { + return { + success: false, + error: "Prediction timed out after 5 minutes", }; - console.log(`[API:${requestId}] Added aspect ratio: ${aspectRatio}`); } - // Add resolution only for Nano Banana Pro - if (model === "nano-banana-pro" && resolution) { - if (!config.imageConfig) { - config.imageConfig = {}; + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + + const pollResponse = await fetch( + `${REPLICATE_API_BASE}/predictions/${currentPrediction.id}`, + { + headers: { + Authorization: `Bearer ${apiKey}`, + }, } - config.imageConfig.imageSize = resolution; - console.log(`[API:${requestId}] Added resolution: ${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}] Added Google Search tool`); + if (!pollResponse.ok) { + return { + success: false, + error: `Failed to poll prediction: ${pollResponse.status}`, + }; } - console.log(`[API:${requestId}] Final config:`, JSON.stringify(config, null, 2)); - if (tools.length > 0) { - console.log(`[API:${requestId}] Tools:`, JSON.stringify(tools, null, 2)); - } + currentPrediction = await pollResponse.json(); + console.log(`[API:${requestId}] Prediction status: ${currentPrediction.status}`); + } - // Make request to Gemini - console.log(`[API:${requestId}] Calling Gemini API...`); - const geminiStartTime = Date.now(); + if (currentPrediction.status === "failed") { + return { + success: false, + error: currentPrediction.error || "Prediction failed", + }; + } - 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 call completed in ${geminiDuration}ms`); - - // Extract image from response - console.log(`[API:${requestId}] Processing response...`); - const candidates = response.candidates; - console.log(`[API:${requestId}] Candidates count: ${candidates?.length || 0}`); - - if (!candidates || candidates.length === 0) { - console.error(`[API:${requestId}] ❌ No candidates in response`); - console.error(`[API:${requestId}] Full response:`, JSON.stringify(response, null, 2)); - return NextResponse.json( - { - success: false, - error: "No response from AI model", - }, - { status: 500 } - ); - } + if (currentPrediction.status === "canceled") { + return { + success: false, + error: "Prediction was canceled", + }; + } + + // Extract output image(s) + const output = currentPrediction.output; + if (!output) { + return { + success: false, + error: "No output from prediction", + }; + } - const parts = candidates[0].content?.parts; - console.log(`[API:${requestId}] Parts count in first candidate: ${parts?.length || 0}`); + // Output can be a single URL string or an array of URLs + const outputUrls: string[] = Array.isArray(output) ? output : [output]; - if (!parts) { - console.error(`[API:${requestId}] ❌ No parts in candidate content`); - console.error(`[API:${requestId}] Candidate:`, JSON.stringify(candidates[0], null, 2)); + if (outputUrls.length === 0) { + return { + success: false, + error: "No output images from prediction", + }; + } + + // Fetch the first output image and convert to base64 + const imageUrl = outputUrls[0]; + console.log(`[API:${requestId}] Fetching output image from: ${imageUrl}`); + const imageResponse = await fetch(imageUrl); + + if (!imageResponse.ok) { + return { + success: false, + error: `Failed to fetch output image: ${imageResponse.status}`, + }; + } + + const imageArrayBuffer = await imageResponse.arrayBuffer(); + const imageBase64 = Buffer.from(imageArrayBuffer).toString("base64"); + + // Determine MIME type from response + const contentType = imageResponse.headers.get("content-type") || "image/png"; + + console.log(`[API:${requestId}] Replicate generation successful`); + return { + success: true, + outputs: [ + { + type: "image", + data: `data:${contentType};base64,${imageBase64}`, + url: imageUrl, + }, + ], + }; +} + +/** + * Generate image using fal.ai API + */ +async function generateWithFal( + requestId: string, + apiKey: string | null, + input: GenerationInput +): Promise { + console.log(`[API:${requestId}] Generating with fal.ai...`); + console.log(`[API:${requestId}] - Model: ${input.model.id}`); + console.log(`[API:${requestId}] - Prompt length: ${input.prompt.length} chars`); + console.log(`[API:${requestId}] - API key: ${apiKey ? "provided" : "not provided (using rate-limited access)"}`); + + const modelId = input.model.id; + + // Build request body + const requestBody: Record = { + prompt: input.prompt, + ...input.parameters, + }; + + // Build headers + const headers: Record = { + "Content-Type": "application/json", + }; + if (apiKey) { + headers["Authorization"] = `Key ${apiKey}`; + } + + // POST to fal.run/{modelId} + console.log(`[API:${requestId}] Calling fal.ai API...`); + const response = await fetch(`https://fal.run/${modelId}`, { + method: "POST", + headers, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + return { + success: false, + error: `fal.ai API error: ${response.status} - ${errorText}`, + }; + } + + const result = await response.json(); + + // fal.ai response typically has "images" array with url field + // or "image" object with url field depending on the model + let imageUrl: string | null = null; + + if (result.images && Array.isArray(result.images) && result.images.length > 0) { + imageUrl = result.images[0].url; + } else if (result.image && result.image.url) { + imageUrl = result.image.url; + } else if (result.output && typeof result.output === "string") { + // Some models return URL directly in output + imageUrl = result.output; + } + + if (!imageUrl) { + return { + success: false, + error: "No image URL in response", + }; + } + + // Fetch the image and convert to base64 + console.log(`[API:${requestId}] Fetching output image from: ${imageUrl}`); + const imageResponse = await fetch(imageUrl); + + if (!imageResponse.ok) { + return { + success: false, + error: `Failed to fetch output image: ${imageResponse.status}`, + }; + } + + const imageArrayBuffer = await imageResponse.arrayBuffer(); + const imageBase64 = Buffer.from(imageArrayBuffer).toString("base64"); + + // Determine MIME type from response + const contentType = imageResponse.headers.get("content-type") || "image/png"; + + console.log(`[API:${requestId}] fal.ai generation successful`); + return { + success: true, + outputs: [ + { + type: "image", + data: `data:${contentType};base64,${imageBase64}`, + url: imageUrl, + }, + ], + }; +} + +export async function POST(request: NextRequest) { + const requestId = Math.random().toString(36).substring(7); + console.log(`\n[API:${requestId}] ========== NEW GENERATE REQUEST ==========`); + console.log(`[API:${requestId}] Timestamp: ${new Date().toISOString()}`); + + try { + console.log(`[API:${requestId}] Parsing request body...`); + const body: MultiProviderGenerateRequest = await request.json(); + const { + images, + prompt, + model = "nano-banana-pro", + aspectRatio, + resolution, + useGoogleSearch, + selectedModel, + parameters, + } = body; + + if (!prompt) { + console.error(`[API:${requestId}] Validation failed: missing prompt`); return NextResponse.json( { success: false, - error: "No content in response", + error: "Prompt is required", }, - { status: 500 } + { status: 400 } ); } - // Log all parts - parts.forEach((part, idx) => { - const partKeys = Object.keys(part); - console.log(`[API:${requestId}] Part ${idx + 1}: ${partKeys.join(', ')}`); - }); - - // Find image part in response - for (const part of parts) { - if (part.inlineData && part.inlineData.data) { - const mimeType = part.inlineData.mimeType || "image/png"; - const imageData = part.inlineData.data; - const imageSizeKB = (imageData.length / 1024).toFixed(2); - console.log(`[API:${requestId}] ✓ Found image in response: ${mimeType}, ${imageSizeKB}KB base64`); - - const dataUrl = `data:${mimeType};base64,${imageData}`; - const dataUrlSizeKB = (dataUrl.length / 1024).toFixed(2); - console.log(`[API:${requestId}] Data URL size: ${dataUrlSizeKB}KB`); - - const responsePayload = { success: true, image: dataUrl }; - const responseSize = JSON.stringify(responsePayload).length; - const responseSizeMB = (responseSize / (1024 * 1024)).toFixed(2); - console.log(`[API:${requestId}] Total response payload size: ${responseSizeMB}MB`); - - if (responseSize > 4.5 * 1024 * 1024) { - console.warn(`[API:${requestId}] ⚠️ Response size (${responseSizeMB}MB) is approaching Next.js 5MB limit!`); - } + // Determine which provider to use + const provider: ProviderType = selectedModel?.provider || "gemini"; + console.log(`[API:${requestId}] Provider: ${provider}`); + + // Route to appropriate provider + if (provider === "replicate") { + // Get Replicate API key from request headers + const replicateApiKey = request.headers.get("X-Replicate-API-Key"); + if (!replicateApiKey) { + return NextResponse.json( + { + success: false, + error: "Replicate API key not provided. Include X-Replicate-API-Key header.", + }, + { status: 401 } + ); + } - console.log(`[API:${requestId}] ✓✓✓ SUCCESS - Returning image ✓✓✓`); + // Build generation input + const genInput: GenerationInput = { + model: { + id: selectedModel!.modelId, + name: selectedModel!.displayName, + provider: "replicate", + capabilities: ["text-to-image"], + description: null, + }, + prompt, + images, + parameters, + }; - // Create response with explicit headers to handle large payloads - const response = NextResponse.json(responsePayload); - response.headers.set('Content-Type', 'application/json'); - response.headers.set('Content-Length', responseSize.toString()); + const result = await generateWithReplicate(requestId, replicateApiKey, genInput); - console.log(`[API:${requestId}] Response headers set, returning...`); - return response; + if (!result.success) { + return NextResponse.json( + { + success: false, + error: result.error || "Generation failed", + }, + { status: 500 } + ); } + + // Return first output image + const outputImage = result.outputs?.[0]?.data; + if (!outputImage) { + return NextResponse.json( + { + success: false, + error: "No image in generation output", + }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + image: outputImage, + }); } - // If no image found, check for text error - console.warn(`[API:${requestId}] ⚠ No image found in parts, checking for text...`); - for (const part of parts) { - if (part.text) { - console.error(`[API:${requestId}] ❌ Model returned text instead of image`); - console.error(`[API:${requestId}] Text preview: "${part.text.substring(0, 200)}"`); + if (provider === "fal") { + // Get fal.ai API key from request headers (optional - fal.ai works without key but rate limited) + const falApiKey = request.headers.get("X-Fal-API-Key"); + + // Build generation input + const genInput: GenerationInput = { + model: { + id: selectedModel!.modelId, + name: selectedModel!.displayName, + provider: "fal", + capabilities: ["text-to-image"], + description: null, + }, + prompt, + images, + parameters, + }; + + const result = await generateWithFal(requestId, falApiKey, genInput); + + if (!result.success) { return NextResponse.json( { success: false, - error: `Model returned text instead of image: ${part.text.substring(0, 200)}`, + error: result.error || "Generation failed", }, { status: 500 } ); } + + // Return first output image + const outputImage = result.outputs?.[0]?.data; + if (!outputImage) { + return NextResponse.json( + { + success: false, + error: "No image in generation output", + }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + image: outputImage, + }); } - console.error(`[API:${requestId}] ❌ No image or text found in response`); - console.error(`[API:${requestId}] All parts:`, JSON.stringify(parts, null, 2)); - return NextResponse.json( - { - success: false, - error: "No image in response", - }, - { status: 500 } + // Default: Use Gemini + const geminiApiKey = process.env.GEMINI_API_KEY; + + if (!geminiApiKey) { + console.error(`[API:${requestId}] No Gemini API key configured`); + return NextResponse.json( + { + success: false, + error: "API key not configured. Add GEMINI_API_KEY to .env.local", + }, + { status: 500 } + ); + } + + return await generateWithGemini( + requestId, + geminiApiKey, + prompt, + images || [], + model, + aspectRatio, + resolution, + useGoogleSearch ); } catch (error) { - const requestId = 'unknown'; // Fallback if we don't have it in scope - console.error(`[API:${requestId}] ❌❌❌ EXCEPTION CAUGHT IN API ROUTE ❌❌❌`); + console.error(`[API:${requestId}] EXCEPTION CAUGHT IN API ROUTE`); console.error(`[API:${requestId}] Error type:`, error?.constructor?.name); console.error(`[API:${requestId}] Error toString:`, String(error)); @@ -258,7 +688,7 @@ export async function POST(request: NextRequest) { } } - // Try to extract more details from Google API errors + // Try to extract more details from API errors if (error && typeof error === "object") { const apiError = error as Record; console.error(`[API:${requestId}] Error object keys:`, Object.keys(apiError));