import { NextRequest, NextResponse } from 'next/server' import { DEFAULT_MODEL, getModelTier, getOpenRouterModel } 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' export async function POST(req: NextRequest) { try { const { messages: initialUserMessages, model = DEFAULT_MODEL.name, userId, chatId } = await req.json() if (!initialUserMessages || initialUserMessages.length === 0) { return NextResponse.json({ error: 'No messages provided' }, { status: 400 }) } if (!userId) { return NextResponse.json({ error: 'User ID required' }, { status: 400 }) } // Check usage limits 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) } if (req.signal.aborted) { return NextResponse.json({ error: 'Request cancelled' }, { status: 499 }) } const supabase = createServerSupabaseClient() const lastUserMessage = initialUserMessages[initialUserMessages.length - 1] // Create or update chat if needed let currentChatId = chatId if (!currentChatId) { const { data: newChat, error: chatError } = await supabase .from('chats') .insert({ user_id: userId, chat_type: 'standalone', title: 'New Chat', model: model, book_id: null }) .select() .single() if (chatError) { console.error('Error creating chat:', chatError) return NextResponse.json({ error: 'Failed to create chat' }, { status: 500 }) } currentChatId = newChat.id } // Save user message await supabase .from('messages') .insert({ chat_id: currentChatId, type: 'user', content: lastUserMessage.content, sequence_number: initialUserMessages.length }) // Create streaming response const stream = new ReadableStream({ async start(controller) { let fullResponse = '' let promptTokens = 0 let completionTokens = 0 let totalTokens = 0 try { if (!OPENROUTER_API_KEY) { throw new Error('OpenRouter API key not configured') } // Prepare messages for OpenRouter const conversationMessages = initialUserMessages.map((msg: any) => ({ role: msg.type === 'user' ? 'user' : 'assistant', content: msg.content })) // Add system message for standalone chat conversationMessages.unshift({ role: 'system', content: `You are a helpful AI assistant. You can help with various tasks including writing, brainstorming, answering questions, creative projects, and general conversation. Be conversational, helpful, and engaging.` }) const openRouterModel = getOpenRouterModel(model) // 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' }, body: JSON.stringify({ model: openRouterModel, messages: conversationMessages, stream: true, temperature: 0.7, max_tokens: 2000 }), signal: req.signal }) if (!response.ok) { const errorData = await response.text() console.error('OpenRouter API error:', errorData) let errorMessage = 'Failed to get response from AI model' 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.' } throw new Error(errorMessage) } const reader = response.body?.getReader() if (!reader) { throw new Error('No response body') } const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break // Check if request was aborted if (req.signal.aborted) { break } const chunk = decoder.decode(value, { stream: true }) const lines = chunk.split('\n') for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6).trim() if (data === '[DONE]') continue try { const parsed = JSON.parse(data) // Handle different response formats from OpenRouter if (parsed.choices && parsed.choices[0]) { const choice = parsed.choices[0] if (choice.delta?.content) { const content = choice.delta.content fullResponse += content // Send content chunk to client controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: 'content', content: content })}\n\n`)) } // Extract usage information if available if (parsed.usage) { promptTokens = parsed.usage.prompt_tokens || 0 completionTokens = parsed.usage.completion_tokens || 0 totalTokens = parsed.usage.total_tokens || 0 } } } catch (e) { // Skip invalid JSON console.log('Skipping invalid JSON:', data) } } } } // Save AI response to database await supabase .from('messages') .insert({ chat_id: currentChatId, type: 'ai', content: fullResponse, model: model, sequence_number: initialUserMessages.length + 1 }) // Update chat title if new if (!chatId && fullResponse) { const chatTitle = fullResponse.length > 50 ? fullResponse.substring(0, 47) + '...' : fullResponse || 'New Chat' await supabase .from('chats') .update({ title: chatTitle }) .eq('id', currentChatId) } // Track usage 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: promptTokens, completion_tokens: completionTokens || fullResponse.split(' ').length, total_tokens: totalTokens || (promptTokens + (completionTokens || fullResponse.split(' ').length)), request_type: 'chat', success: true }) } catch (error) { console.error('Error tracking usage:', error) } } // Send completion signal controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: 'done', chatId: currentChatId })}\n\n`)) } catch (error: any) { console.error('Error in streaming response:', error) // Track failed usage if (userId) { try { await usageTracker.recordUsage({ user_id: userId, model_name: model, model_provider: 'openrouter', success: false, error_message: error.message, request_type: 'chat' }) } catch (trackingError) { console.error('Error tracking failed usage:', trackingError) } } controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: 'error', error: error instanceof Error ? error.message : 'Unknown error' })}\n\n`)) } finally { controller.close() } } }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }) } catch (error) { console.error('Error in standalone chat API:', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } // GET endpoint to fetch standalone chats export async function GET(req: NextRequest) { try { const { searchParams } = new URL(req.url) const userId = searchParams.get('userId') if (!userId) { return NextResponse.json({ error: 'User ID required' }, { status: 400 }) } const supabase = createServerSupabaseClient() const { data: chats, error } = await supabase .from('chats') .select('*') .eq('user_id', userId) .eq('chat_type', 'standalone') .order('updated_at', { ascending: false }) if (error) { console.error('Error fetching standalone chats:', error) return NextResponse.json({ error: 'Failed to fetch chats' }, { status: 500 }) } return NextResponse.json({ chats }) } catch (error) { console.error('Error in standalone chat GET:', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } // DELETE endpoint export async function DELETE(req: NextRequest) { try { const { searchParams } = new URL(req.url) const chatId = searchParams.get('chatId') const userId = searchParams.get('userId') if (!chatId || !userId) { return NextResponse.json({ error: 'Chat ID and User ID required' }, { status: 400 }) } const supabase = createServerSupabaseClient() const { error } = await supabase .from('chats') .delete() .eq('id', chatId) .eq('user_id', userId) .eq('chat_type', 'standalone') if (error) { console.error('Error deleting standalone chat:', error) return NextResponse.json({ error: 'Failed to delete chat' }, { status: 500 }) } return NextResponse.json({ success: true }) } catch (error) { console.error('Error in standalone chat DELETE:', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } }