import { NextRequest, NextResponse } from 'next/server' import { getOpenRouterModel, getModelTier } from '@/lib/config/models' import { usageTracker } from '@/lib/services/usage-tracker' import { createServerSupabaseClient } from '@/lib/supabase' const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions' interface GenerateTextRequest { prompt: string model?: string userId?: string stream?: boolean } // Default model for text generation (fast and efficient) const DEFAULT_GENERATE_MODEL = 'gpt-4o-mini' export async function POST(request: NextRequest) { try { const body: GenerateTextRequest = await request.json() const { prompt, model = DEFAULT_GENERATE_MODEL, userId, stream = false } = body // Validate input if (!prompt) { return NextResponse.json( { error: 'Prompt is required' }, { status: 400 } ) } if (!OPENROUTER_API_KEY) { return NextResponse.json( { error: 'OpenRouter API key not configured' }, { status: 500 } ) } // Check usage limits if userId is provided if (userId) { try { const supabase = createServerSupabaseClient() const { data: subscription } = await supabase .from('subscriptions') .select('price_id, status') .eq('user_id', userId) .eq('status', 'active') .order('created_at', { ascending: false }) .limit(1) .single() const { PRICING_TIERS } = await import('@/lib/stripe') let smartPromptsLimit = PRICING_TIERS.FREE.maxSmartPrompts let fastPromptsLimit = PRICING_TIERS.FREE.hasUnlimitedFastPrompts ? 999999 : PRICING_TIERS.FREE.maxFastPrompts if (subscription?.price_id) { const { getPlanByPriceId } = await import('@/lib/stripe') const plan = getPlanByPriceId(subscription.price_id) if (plan) { smartPromptsLimit = plan.maxSmartPrompts fastPromptsLimit = plan.hasUnlimitedFastPrompts ? 999999 : plan.maxFastPrompts } } const limitCheckResult = await usageTracker.checkRequestLimits( userId, model, smartPromptsLimit, fastPromptsLimit ) if (!limitCheckResult.canProceed) { const modelTier = getModelTier(model) const tierDisplay = modelTier === 'smart' ? '🧠 Smart AI' : '⚡ Fast AI' return NextResponse.json({ error: `You've reached your monthly limit of ${limitCheckResult.limit} ${tierDisplay} requests.`, limitExceeded: true }, { status: 429 }) } } catch (error) { console.error('Error checking usage limits:', error) // Continue with request if limit check fails } } // Build generation prompt const systemPrompt = `You are a helpful AI writing assistant. Generate creative, engaging, and informative content based on the user's request. IMPORTANT RULES: 1. Write content that is relevant and useful 2. Be creative but maintain coherence 3. Use appropriate tone and style for the context 4. Keep responses concise but informative 5. Avoid repetitive or generic content 6. Write in a natural, flowing style` const openRouterModel = getOpenRouterModel(model) // If streaming is requested, return a streaming response if (stream) { const encoder = new TextEncoder() const streamingResponse = new ReadableStream({ async start(controller) { try { // Make API call to OpenRouter const response = await fetch(OPENROUTER_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, 'Content-Type': 'application/json', 'HTTP-Referer': process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000', 'X-Title': 'Bookwiz Text Generator' }, body: JSON.stringify({ model: openRouterModel, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens: 1000, temperature: 0.8, stream: true }) }) if (!response.ok) { const errorData = await response.text() console.error('OpenRouter API error:', errorData) let errorMessage = 'Failed to generate content' if (response.status === 429) { errorMessage = 'AI model is rate limited. Please try again in a moment.' } else if (response.status === 401) { errorMessage = 'API authentication failed. Please check configuration.' } else if (response.status >= 500) { errorMessage = 'AI service is temporarily unavailable. Please try again.' } controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: errorMessage })}\n\n`)) controller.close() return } const reader = response.body?.getReader() if (!reader) { controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: 'No response body' })}\n\n`)) controller.close() return } const decoder = new TextDecoder() let buffer = '' let fullContent = '' let totalTokens = 0 let promptTokens = 0 let completionTokens = 0 while (true) { const { done, value } = await reader.read() if (done) break const chunk = decoder.decode(value, { stream: true }) buffer += chunk const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6).trim() if (!data || data === '[DONE]') continue try { const parsed = JSON.parse(data) const delta = parsed.choices?.[0]?.delta // Handle content streaming if (delta?.content) { fullContent += delta.content controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: delta.content })}\n\n`)) } // Capture usage statistics if (parsed.usage) { totalTokens = parsed.usage.total_tokens || 0 promptTokens = parsed.usage.prompt_tokens || 0 completionTokens = parsed.usage.completion_tokens || 0 } } catch (e) { // Skip invalid JSON } } } } // Track usage if userId is provided if (userId && fullContent) { try { const getModelProvider = (modelName: string): string => { if (modelName.includes('openai') || modelName.includes('gpt')) return 'openai' if (modelName.includes('anthropic') || modelName.includes('claude')) return 'anthropic' if (modelName.includes('google') || modelName.includes('gemini')) return 'google' if (modelName.includes('meta') || modelName.includes('llama')) return 'meta' if (modelName.includes('mistral')) return 'mistral' return 'openrouter' } await usageTracker.recordUsage({ user_id: userId, model_name: model, model_provider: getModelProvider(openRouterModel), prompt_tokens: promptTokens || 0, completion_tokens: completionTokens || fullContent.split(' ').length, total_tokens: totalTokens || 0, request_type: 'text_generation', success: true }) } catch (error) { console.error('Error tracking usage:', error) } } // Send completion signal controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`)) controller.close() } catch (error) { console.error('AI generation streaming error:', error) controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: 'Failed to generate content' })}\n\n`)) controller.close() } } }) return new Response(streamingResponse, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }) } // Non-streaming response (existing logic) // Make API call to OpenRouter const response = await fetch(OPENROUTER_URL, { method: 'POST', headers: { 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, 'Content-Type': 'application/json', 'HTTP-Referer': process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000', 'X-Title': 'Bookwiz Text Generator' }, body: JSON.stringify({ model: openRouterModel, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: prompt } ], max_tokens: 1000, temperature: 0.8, }) }) if (!response.ok) { const errorData = await response.text() console.error('OpenRouter API error:', errorData) let errorMessage = 'Failed to generate content' if (response.status === 429) { errorMessage = 'AI model is rate limited. Please try again in a moment.' } else if (response.status === 401) { errorMessage = 'API authentication failed. Please check configuration.' } else if (response.status >= 500) { errorMessage = 'AI service is temporarily unavailable. Please try again.' } return NextResponse.json({ error: errorMessage }, { status: response.status }) } const data = await response.json() const generatedText = data.choices?.[0]?.message?.content?.trim() if (!generatedText) { return NextResponse.json( { error: 'No content generated from AI model' }, { status: 500 } ) } // Track usage if userId is provided if (userId) { try { const getModelProvider = (modelName: string): string => { if (modelName.includes('openai') || modelName.includes('gpt')) return 'openai' if (modelName.includes('anthropic') || modelName.includes('claude')) return 'anthropic' if (modelName.includes('google') || modelName.includes('gemini')) return 'google' if (modelName.includes('meta') || modelName.includes('llama')) return 'meta' if (modelName.includes('mistral')) return 'mistral' return 'openrouter' } await usageTracker.recordUsage({ user_id: userId, model_name: model, model_provider: getModelProvider(openRouterModel), prompt_tokens: data.usage?.prompt_tokens || 0, completion_tokens: data.usage?.completion_tokens || generatedText.split(' ').length, total_tokens: data.usage?.total_tokens || 0, request_type: 'text_generation', success: true }) } catch (error) { console.error('Error tracking usage:', error) } } return NextResponse.json({ generatedText, model: model, usage: data.usage }) } catch (error) { console.error('AI generation error:', error) return NextResponse.json( { error: 'Failed to generate content' }, { status: 500 } ) } }