bookwiz.io / app / api / images / generate / route.ts
route.ts
Raw
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { usageTracker } from '@/lib/services/usage-tracker'

const OPENAI_API_KEY = process.env.OPENAI_API_KEY
const OPENAI_URL = 'https://api.openai.com/v1/images/generations'

// Create server-side Supabase client with user session
function createServerSupabaseClient(request: Request) {
  const authHeader = request.headers.get('authorization')
  
  return createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      auth: {
        autoRefreshToken: false,
        persistSession: false
      },
      global: {
        headers: authHeader ? {
          Authorization: authHeader
        } : {}
      }
    }
  )
}

export async function POST(req: NextRequest) {
  let user: any = null
  try {
    const supabase = createServerSupabaseClient(req)
    
    // Get current user
    const { data: { user: currentUser }, error: authError } = await supabase.auth.getUser()
    user = currentUser
    if (authError || !user) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    const { 
      prompt, 
      size = '1024x1024', 
      quality = 'auto', 
      bookId 
    } = await req.json()

    if (!prompt || typeof prompt !== 'string') {
      return NextResponse.json({ error: 'Prompt is required' }, { status: 400 })
    }

    if (!OPENAI_API_KEY) {
      return NextResponse.json({ error: 'OpenAI API key not configured' }, { status: 500 })
    }

    // Validate parameters for GPT Image 1
    const validSizes = ['1024x1024', '1536x1024', '1024x1536']
    const validQualities = ['low', 'medium', 'high', 'auto']

    if (!validSizes.includes(size)) {
      return NextResponse.json({ error: 'Invalid size parameter' }, { status: 400 })
    }

    if (!validQualities.includes(quality)) {
      return NextResponse.json({ error: 'Invalid quality parameter' }, { status: 400 })
    }

    // If bookId is provided, verify user has access to the book
    if (bookId) {
      const { data: book } = await supabase
        .from('books')
        .select('id')
        .eq('id', bookId)
        .eq('user_id', user.id)
        .single()

      if (!book) {
        return NextResponse.json({ error: 'Book not found or access denied' }, { status: 404 })
      }
    }

    // Call GPT-4 Image API
    const openaiResponse = await fetch(OPENAI_URL, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${OPENAI_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: 'gpt-image-1',
        prompt,
        n: 1,
        size,
        quality
      })
    })

    if (!openaiResponse.ok) {
      const errorData = await openaiResponse.json().catch(() => ({}))
      
      // Track failed usage
      try {
        await usageTracker.recordUsage({
          user_id: user.id,
          book_id: bookId || null,
          model_name: 'gpt-image-1',
          model_provider: 'openai',
          request_type: 'image_generation',
          success: false,
          error_message: errorData.error?.message || 'OpenAI API error'
        })
      } catch (error) {
        console.error('Failed to track failed image generation usage:', error)
      }
      
      if (openaiResponse.status === 400 && errorData.error?.code === 'content_policy_violation') {
        return NextResponse.json({ 
          error: 'Image generation failed due to content policy. Please modify your prompt and try again.' 
        }, { status: 400 })
      }
      
      return NextResponse.json({ 
        error: 'Image generation failed. Please try again.' 
      }, { status: 500 })
    }

    const openaiData = await openaiResponse.json()
    const generatedImage = openaiData.data[0]

    // Handle different response formats - GPT Image 1 returns base64 by default
    let imageUrl = generatedImage.url
    if (!imageUrl && generatedImage.b64_json) {
      // Convert base64 to data URL for display
      imageUrl = `data:image/png;base64,${generatedImage.b64_json}`
    }
    
    if (!imageUrl) {
      return NextResponse.json({ 
        error: 'No image data returned from GPT Image 1' 
      }, { status: 500 })
    }

    // Calculate cost (approximate - GPT Image 1 pricing as of 2024)
    const costUsd = quality === 'high' ? 0.080 : 0.040 // High: $0.080, Others: $0.040

    // Store the generated image in our database
    const { data: savedImage, error: saveError } = await supabase
      .from('generated_images')
      .insert({
        user_id: user.id,
        book_id: bookId || null,
        prompt,
        revised_prompt: generatedImage.revised_prompt || '',
        image_url: imageUrl,
        model: 'gpt-image-1',
        size,
        quality,
        style: null, // GPT Image 1 doesn't use style parameter
        cost_usd: costUsd,
        metadata: {
          openai_response: openaiData
        }
      })
      .select()
      .single()

    // Track usage for image generation as 1 smart prompt
    try {
      await usageTracker.recordUsage({
        user_id: user.id,
        book_id: bookId || null,
        model_name: 'gpt-image-1',
        model_provider: 'openai',
        request_type: 'image_generation',
        success: true,
        cost_usd: costUsd
      })
    } catch (error) {
      console.error('Failed to track image generation usage:', error)
      // Don't fail the request if usage tracking fails
    }

    if (saveError) {
      // Still return the image even if we couldn't save it to our database
      return NextResponse.json({
        id: null,
        prompt,
        revised_prompt: generatedImage.revised_prompt || '',
        image_url: imageUrl,
        size,
        quality,
        style: null,
        created_at: new Date().toISOString()
      })
    }

    return NextResponse.json({
      id: savedImage.id,
      prompt: savedImage.prompt,
      revised_prompt: savedImage.revised_prompt,
      image_url: savedImage.image_url,
      size: savedImage.size,
      quality: savedImage.quality,
      style: savedImage.style,
      created_at: savedImage.created_at
    })

  } catch (error) {
    // Track failed usage for unexpected errors
    if (user?.id) {
      try {
        await usageTracker.recordUsage({
          user_id: user.id,
          model_name: 'gpt-image-1',
          model_provider: 'openai',
          request_type: 'image_generation',
          success: false,
          error_message: error instanceof Error ? error.message : 'Unknown error'
        })
      } catch (trackingError) {
        console.error('Failed to track failed image generation usage:', trackingError)
      }
    }
    
    return NextResponse.json({ 
      error: 'An unexpected error occurred during image generation' 
    }, { status: 500 })
  }
}