Browse Source

feat(03-02): update generate API route for multi-provider dispatch

Refactor generate API route to support multiple providers:
- Accept both legacy format and new format with selectedModel
- Route requests to Replicate, fal.ai, or Gemini based on provider
- Extract Gemini logic into generateWithGemini() helper function
- Add generateWithReplicate() and generateWithFal() server-side helpers
- Get provider API keys from request headers (X-Replicate-API-Key, X-Fal-API-Key)
- Preserve backward compatibility for existing Gemini-based workflows

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
ac11c6e964
  1. 790
      src/app/api/generate/route.ts

790
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<ModelType, string> = {
"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<string, unknown>;
}
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<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}`);
}
if (!apiKey) {
console.error(`[API:${requestId}] ❌ No API key configured`);
return NextResponse.json<GenerateResponse>(
{
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<string, unknown>).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<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: "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<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`);
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<string, unknown> = {
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<GenerateResponse>(
{
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<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}] - 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,
};
// 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: "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<GenerateResponse>(
{
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<GenerateResponse>(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<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,
});
}
// 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<GenerateResponse>(
{
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<GenerateResponse>(
{
success: false,
error: "No image in generation output",
},
{ status: 500 }
);
}
return NextResponse.json<GenerateResponse>({
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<GenerateResponse>(
{
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<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) {
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<string, unknown>;
console.error(`[API:${requestId}] Error object keys:`, Object.keys(apiError));

Loading…
Cancel
Save