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.
820 lines
26 KiB
820 lines
26 KiB
import { NextRequest, NextResponse } from "next/server";
|
|
import { GoogleGenAI } from "@google/genai";
|
|
import { GenerateRequest, GenerateResponse, ModelType, SelectedModel, ProviderType } from "@/types";
|
|
import { GenerationInput, GenerationOutput, ProviderModel } from "@/lib/providers/types";
|
|
import { uploadImageForUrl, shouldUseImageUrl, deleteImages } from "@/lib/images";
|
|
|
|
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
|
|
const MODEL_MAP: Record<ModelType, string> = {
|
|
"nano-banana": "gemini-2.5-flash-image", // Updated to correct model name
|
|
"nano-banana-pro": "gemini-3-pro-image-preview",
|
|
};
|
|
|
|
/**
|
|
* Extended request format that supports both legacy and multi-provider requests
|
|
*/
|
|
interface MultiProviderGenerateRequest extends GenerateRequest {
|
|
selectedModel?: SelectedModel;
|
|
parameters?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* 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<NextResponse<GenerateResponse>> {
|
|
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<string, unknown> = {
|
|
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}`);
|
|
}
|
|
|
|
// 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;
|
|
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`);
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
// 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<GenerateResponse>(
|
|
{
|
|
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<GenerateResponse>(
|
|
{
|
|
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<GenerateResponse>(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<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 response`);
|
|
console.error(`[API:${requestId}] All parts:`, JSON.stringify(parts, null, 2));
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "No image in response",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate image using Replicate API
|
|
*/
|
|
async function generateWithReplicate(
|
|
requestId: string,
|
|
apiKey: string,
|
|
input: GenerationInput
|
|
): Promise<GenerationOutput> {
|
|
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`);
|
|
console.log(`[API:${requestId}] - Images count: ${input.images?.length || 0}`);
|
|
|
|
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;
|
|
|
|
if (!version) {
|
|
return {
|
|
success: false,
|
|
error: "Model has no available version",
|
|
};
|
|
}
|
|
|
|
// Build input for the prediction
|
|
const predictionInput: Record<string, unknown> = {
|
|
prompt: input.prompt,
|
|
...input.parameters,
|
|
};
|
|
|
|
// Add image input if provided (for img2img workflows)
|
|
// Note: Different Replicate models use different parameter names
|
|
// Using 'image' as it's most common for img2img models
|
|
if (input.images && input.images.length > 0) {
|
|
predictionInput.image = input.images[0];
|
|
console.log(`[API:${requestId}] Added image input to prediction (${input.images[0].substring(0, 50)}...)`);
|
|
}
|
|
|
|
// 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",
|
|
};
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
|
|
const pollResponse = await fetch(
|
|
`${REPLICATE_API_BASE}/predictions/${currentPrediction.id}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${apiKey}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!pollResponse.ok) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to poll prediction: ${pollResponse.status}`,
|
|
};
|
|
}
|
|
|
|
currentPrediction = await pollResponse.json();
|
|
console.log(`[API:${requestId}] Prediction status: ${currentPrediction.status}`);
|
|
}
|
|
|
|
if (currentPrediction.status === "failed") {
|
|
return {
|
|
success: false,
|
|
error: currentPrediction.error || "Prediction failed",
|
|
};
|
|
}
|
|
|
|
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",
|
|
};
|
|
}
|
|
|
|
// Output can be a single URL string or an array of URLs
|
|
const outputUrls: string[] = Array.isArray(output) ? output : [output];
|
|
|
|
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<GenerationOutput> {
|
|
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}] - Images count: ${input.images?.length || 0}`);
|
|
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<string, unknown> = {
|
|
prompt: input.prompt,
|
|
...input.parameters,
|
|
};
|
|
|
|
// Add image_url if provided (for img2img workflows)
|
|
// fal.ai accepts both URLs and data URIs in this field
|
|
if (input.images && input.images.length > 0) {
|
|
requestBody.image_url = input.images[0];
|
|
console.log(`[API:${requestId}] Added image_url to request (${input.images[0].substring(0, 50)}...)`);
|
|
}
|
|
|
|
// Build headers
|
|
const headers: Record<string, string> = {
|
|
"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<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "Prompt is required",
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// 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<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "Replicate API key not provided. Include X-Replicate-API-Key header.",
|
|
},
|
|
{ status: 401 }
|
|
);
|
|
}
|
|
|
|
// Process images: convert large images to URLs for provider to fetch
|
|
const baseUrl = new URL(request.url).origin;
|
|
const uploadedImageIds: string[] = [];
|
|
const processedImages: string[] = [];
|
|
|
|
if (images && images.length > 0) {
|
|
for (const image of images) {
|
|
if (shouldUseImageUrl(image)) {
|
|
const { url, id } = uploadImageForUrl(image, baseUrl);
|
|
uploadedImageIds.push(id);
|
|
processedImages.push(url);
|
|
console.log(`[API:${requestId}] Converted large image to URL: ${url}`);
|
|
} else {
|
|
processedImages.push(image);
|
|
console.log(`[API:${requestId}] Using base64 directly (small image)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Build generation input
|
|
const genInput: GenerationInput = {
|
|
model: {
|
|
id: selectedModel!.modelId,
|
|
name: selectedModel!.displayName,
|
|
provider: "replicate",
|
|
capabilities: ["text-to-image"],
|
|
description: null,
|
|
},
|
|
prompt,
|
|
images: processedImages,
|
|
parameters,
|
|
};
|
|
|
|
const result = await generateWithReplicate(requestId, replicateApiKey, genInput);
|
|
|
|
if (!result.success) {
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: result.error || "Generation failed",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
// Return first output image
|
|
const outputImage = result.outputs?.[0]?.data;
|
|
if (!outputImage) {
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "No image in generation output",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json<GenerateResponse>({
|
|
success: true,
|
|
image: outputImage,
|
|
});
|
|
} finally {
|
|
// Clean up uploaded images
|
|
if (uploadedImageIds.length > 0) {
|
|
deleteImages(uploadedImageIds);
|
|
console.log(`[API:${requestId}] Cleaned up ${uploadedImageIds.length} uploaded images`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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");
|
|
|
|
// Process images: convert large images to URLs for provider to fetch
|
|
const baseUrl = new URL(request.url).origin;
|
|
const uploadedImageIds: string[] = [];
|
|
const processedImages: string[] = [];
|
|
|
|
if (images && images.length > 0) {
|
|
for (const image of images) {
|
|
if (shouldUseImageUrl(image)) {
|
|
const { url, id } = uploadImageForUrl(image, baseUrl);
|
|
uploadedImageIds.push(id);
|
|
processedImages.push(url);
|
|
console.log(`[API:${requestId}] Converted large image to URL: ${url}`);
|
|
} else {
|
|
processedImages.push(image);
|
|
console.log(`[API:${requestId}] Using base64 directly (small image)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Build generation input
|
|
const genInput: GenerationInput = {
|
|
model: {
|
|
id: selectedModel!.modelId,
|
|
name: selectedModel!.displayName,
|
|
provider: "fal",
|
|
capabilities: ["text-to-image"],
|
|
description: null,
|
|
},
|
|
prompt,
|
|
images: processedImages,
|
|
parameters,
|
|
};
|
|
|
|
const result = await generateWithFal(requestId, falApiKey, genInput);
|
|
|
|
if (!result.success) {
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: result.error || "Generation failed",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
// Return first output image
|
|
const outputImage = result.outputs?.[0]?.data;
|
|
if (!outputImage) {
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "No image in generation output",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json<GenerateResponse>({
|
|
success: true,
|
|
image: outputImage,
|
|
});
|
|
} finally {
|
|
// Clean up uploaded images
|
|
if (uploadedImageIds.length > 0) {
|
|
deleteImages(uploadedImageIds);
|
|
console.log(`[API:${requestId}] Cleaned up ${uploadedImageIds.length} uploaded images`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default: Use Gemini
|
|
const geminiApiKey = process.env.GEMINI_API_KEY;
|
|
|
|
if (!geminiApiKey) {
|
|
console.error(`[API:${requestId}] No Gemini API key configured`);
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
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) {
|
|
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));
|
|
|
|
// Extract detailed error information
|
|
let errorMessage = "Generation failed";
|
|
let errorDetails = "";
|
|
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
errorDetails = error.stack || "";
|
|
console.error(`[API:${requestId}] Error message:`, errorMessage);
|
|
console.error(`[API:${requestId}] Error stack:`, error.stack);
|
|
|
|
// Check for specific error types
|
|
if ("cause" in error && error.cause) {
|
|
console.error(`[API:${requestId}] Error cause:`, error.cause);
|
|
errorDetails += `\nCause: ${JSON.stringify(error.cause)}`;
|
|
}
|
|
}
|
|
|
|
// Try to extract more details from API errors
|
|
if (error && typeof error === "object") {
|
|
const apiError = error as Record<string, unknown>;
|
|
console.error(`[API:${requestId}] Error object keys:`, Object.keys(apiError));
|
|
|
|
if (apiError.status) {
|
|
console.error(`[API:${requestId}] Error status:`, apiError.status);
|
|
errorDetails += `\nStatus: ${apiError.status}`;
|
|
}
|
|
if (apiError.statusText) {
|
|
console.error(`[API:${requestId}] Error statusText:`, apiError.statusText);
|
|
errorDetails += `\nStatusText: ${apiError.statusText}`;
|
|
}
|
|
if (apiError.errorDetails) {
|
|
console.error(`[API:${requestId}] Error errorDetails:`, apiError.errorDetails);
|
|
errorDetails += `\nDetails: ${JSON.stringify(apiError.errorDetails)}`;
|
|
}
|
|
if (apiError.response) {
|
|
try {
|
|
console.error(`[API:${requestId}] Error response:`, apiError.response);
|
|
errorDetails += `\nResponse: ${JSON.stringify(apiError.response)}`;
|
|
} catch {
|
|
errorDetails += `\nResponse: [unable to stringify]`;
|
|
}
|
|
}
|
|
|
|
// Log entire error object for debugging
|
|
try {
|
|
console.error(`[API:${requestId}] Full error object:`, JSON.stringify(apiError, null, 2));
|
|
} catch {
|
|
console.error(`[API:${requestId}] Could not stringify full error object`);
|
|
}
|
|
}
|
|
|
|
console.error(`[API:${requestId}] Compiled error details:`, errorDetails);
|
|
|
|
// Handle rate limiting
|
|
if (errorMessage.includes("429")) {
|
|
console.error(`[API:${requestId}] Rate limit error detected`);
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: "Rate limit reached. Please wait and try again.",
|
|
},
|
|
{ status: 429 }
|
|
);
|
|
}
|
|
|
|
console.error(`[API:${requestId}] Returning 500 error response`);
|
|
return NextResponse.json<GenerateResponse>(
|
|
{
|
|
success: false,
|
|
error: `${errorMessage}${errorDetails ? ` | Details: ${errorDetails.substring(0, 500)}` : ""}`,
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|
|
|