@@ -2,53 +2,16 @@ export const runtime = 'nodejs';
22
33import { NextRequest , NextResponse } from 'next/server' ;
44import { z } from 'zod' ;
5+ import Groq from 'groq-sdk' ;
56import {
67 createExplainPrompt ,
78 type ExplanationResponse ,
89} from '@/lib/ai/prompts' ;
910import { getClientIp } from '@/lib/security/client-ip' ;
1011
1112// =============================================================================
12- // SERVER-SIDE LOGGING (sanitized - no sensitive data exposed )
13+ // Rate Limiter (in-memory, resets on cold start )
1314// =============================================================================
14- function logEnvironmentDiagnostics ( ) {
15- const apiKey = process . env . GROQ_API_KEY ;
16- console . log ( '[ENV] GROQ_API_KEY configured:' , ! ! apiKey ) ;
17- console . log ( '[ENV] NODE_ENV:' , process . env . NODE_ENV ) ;
18- }
19-
20- function logRequestDiagnostics ( request : NextRequest ) {
21- console . log ( '[REQ] Method:' , request . method ) ;
22- console . log ( '[REQ] URL path:' , new URL ( request . url ) . pathname ) ;
23- }
24-
25- function logBodyParsingResult ( success : boolean , error ?: unknown ) {
26- console . log ( '[BODY] Parse success:' , success ) ;
27- if ( error ) {
28- console . log ( '[BODY] Parse error:' , error instanceof Error ? error . message : 'Unknown error' ) ;
29- }
30- }
31-
32- function logGroqInitialization ( success : boolean , error ?: unknown ) {
33- console . log ( '[GROQ] Init success:' , success ) ;
34- if ( error ) {
35- const err = error as Error & { status ?: number ; code ?: string } ;
36- console . log ( '[GROQ] Init error:' , err . name , err . message ) ;
37- }
38- }
39-
40- function logGroqApiCall ( phase : 'start' | 'success' | 'error' , details ?: unknown ) {
41- if ( phase === 'start' ) {
42- console . log ( '[GROQ] Starting API call' ) ;
43- } else if ( phase === 'success' ) {
44- console . log ( '[GROQ] API call successful' ) ;
45- } else if ( phase === 'error' ) {
46- const err = details as Error & { status ?: number ; code ?: string } ;
47- console . log ( '[GROQ] API error:' , err ?. name , err ?. message ) ;
48- }
49- }
50- // =============================================================================
51-
5215const rateLimiter = new Map < string , { count : number ; resetAt : number } > ( ) ;
5316const MAX_REQUESTS_PER_WINDOW = 10 ;
5417const RATE_LIMIT_WINDOW_MS = 20 * 60 * 1000 ;
@@ -136,22 +99,21 @@ function parseExplanationResponse(content: string): ExplanationResponse {
13699 return parsed as ExplanationResponse ;
137100}
138101
102+ // =============================================================================
103+ // POST /api/ai/explain - Generate term explanation in 3 languages
104+ // =============================================================================
139105export async function POST ( request : NextRequest ) {
140- logEnvironmentDiagnostics ( ) ;
141- logRequestDiagnostics ( request ) ;
142-
106+ // Fail fast if API key is missing
143107 const apiKey = process . env . GROQ_API_KEY ;
144108 if ( ! apiKey ) {
145- console . error ( '[FATAL ] GROQ_API_KEY is not configured. Check environment variables. ' ) ;
109+ console . error ( '[ai/explain ] GROQ_API_KEY not configured' ) ;
146110 return NextResponse . json (
147- {
148- error : 'AI service not configured' ,
149- code : 'SERVICE_UNAVAILABLE' ,
150- } ,
111+ { error : 'AI service not configured' , code : 'SERVICE_UNAVAILABLE' } ,
151112 { status : 503 }
152113 ) ;
153114 }
154115
116+ // Rate limiting
155117 const clientIp = getClientIp ( request ) ?? 'unknown' ;
156118 const rateLimit = checkRateLimit ( clientIp ) ;
157119
@@ -174,21 +136,11 @@ export async function POST(request: NextRequest) {
174136 ) ;
175137 }
176138
177- // Safe JSON body parsing for Netlify
139+ // Parse and validate request body
178140 let body : unknown ;
179141 try {
180- const text = await request . text ( ) ;
181- if ( ! text || text . trim ( ) === '' ) {
182- console . log ( '[BODY] Empty request body received' ) ;
183- return NextResponse . json (
184- { error : 'Request body is empty' , code : 'EMPTY_BODY' } ,
185- { status : 400 }
186- ) ;
187- }
188- body = JSON . parse ( text ) ;
189- logBodyParsingResult ( true ) ;
190- } catch ( parseError ) {
191- logBodyParsingResult ( false , parseError ) ;
142+ body = await request . json ( ) ;
143+ } catch {
192144 return NextResponse . json (
193145 { error : 'Invalid JSON body' , code : 'INVALID_JSON' } ,
194146 { status : 400 }
@@ -209,65 +161,28 @@ export async function POST(request: NextRequest) {
209161
210162 const { term, context } = validationResult . data ;
211163
212- // Dynamic import for Netlify compatibility
213- let Groq : typeof import ( 'groq-sdk' ) . default ;
214- try {
215- const groqModule = await import ( 'groq-sdk' ) ;
216- Groq = groqModule . default ;
217- } catch ( importError ) {
218- console . error ( '[SDK_IMPORT_ERROR] Failed to import groq-sdk:' ,
219- importError instanceof Error ? importError . message : String ( importError )
220- ) ;
221- return NextResponse . json (
222- {
223- error : 'Failed to load AI client' ,
224- code : 'SDK_IMPORT_ERROR' ,
225- } ,
226- { status : 503 }
227- ) ;
228- }
229-
230- let groq : InstanceType < typeof Groq > ;
231- try {
232- groq = new Groq ( { apiKey } ) ;
233- logGroqInitialization ( true ) ;
234- } catch ( initError ) {
235- logGroqInitialization ( false , initError ) ;
236- console . error ( '[SDK_INIT_ERROR] Failed to initialize Groq client:' ,
237- initError instanceof Error ? initError . message : String ( initError )
238- ) ;
239- return NextResponse . json (
240- {
241- error : 'Failed to initialize AI client' ,
242- code : 'SDK_INIT_ERROR' ,
243- } ,
244- { status : 503 }
245- ) ;
246- }
164+ // Initialize Groq client
165+ const groq = new Groq ( { apiKey } ) ;
247166
248167 try {
249168 const prompt = createExplainPrompt ( { term, context } ) ;
250169
251- logGroqApiCall ( 'start' ) ;
252170 const chatCompletion = await groq . chat . completions . create ( {
253- messages : [
254- {
255- role : 'user' ,
256- content : prompt ,
257- } ,
258- ] ,
259- model : 'llama3-70b-8192' ,
171+ messages : [ { role : 'user' , content : prompt } ] ,
172+ model : 'llama-3.3-70b-versatile' ,
260173 temperature : 0.7 ,
261174 max_tokens : 1500 ,
262175 top_p : 1 ,
263176 } ) ;
264- logGroqApiCall ( 'success' , chatCompletion ) ;
265177
266178 const content = chatCompletion . choices [ 0 ] ?. message ?. content ;
267179
268180 if ( ! content ) {
269- console . error ( '[ERROR] No content in Groq response' ) ;
270- throw new Error ( 'No content in response' ) ;
181+ console . error ( '[ai/explain] Empty response from Groq' ) ;
182+ return NextResponse . json (
183+ { error : 'AI returned empty response' , code : 'EMPTY_RESPONSE' } ,
184+ { status : 502 }
185+ ) ;
271186 }
272187
273188 const explanation = parseExplanationResponse ( content ) ;
@@ -281,64 +196,87 @@ export async function POST(request: NextRequest) {
281196 } ,
282197 } ) ;
283198 } catch ( error ) {
284- logGroqApiCall ( 'error' , error ) ;
285- console . error ( '[GROQ_ERROR]' , error instanceof Error ? error . message : 'Unknown error' ) ;
286-
287- if ( error instanceof Error ) {
288- if (
289- error . message . includes ( '401' ) ||
290- error . message . includes ( 'authentication' ) ||
291- error . message . includes ( 'Invalid API Key' )
292- ) {
293- console . error ( '[AUTH_ERROR] API key authentication failed' ) ;
294- return NextResponse . json (
295- {
296- error : 'AI service authentication failed' ,
297- code : 'AUTH_ERROR' ,
298- } ,
299- { status : 503 }
300- ) ;
301- }
302- if (
303- error . message . includes ( '429' ) ||
304- error . message . includes ( 'rate limit' )
305- ) {
306- return NextResponse . json (
307- { error : 'AI service rate limited' , code : 'API_RATE_LIMITED' } ,
308- { status : 503 }
309- ) ;
310- }
311- if ( error . message . includes ( 'model' ) ) {
312- return NextResponse . json (
313- {
314- error : 'AI model not available' ,
315- code : 'MODEL_ERROR' ,
316- } ,
317- { status : 503 }
318- ) ;
319- }
320- }
199+ return handleGroqError ( error ) ;
200+ }
201+ }
202+
203+ // =============================================================================
204+ // GET /api/ai/explain - Health check
205+ // =============================================================================
206+ export async function GET ( ) {
207+ const hasApiKey = ! ! process . env . GROQ_API_KEY ;
321208
209+ if ( ! hasApiKey ) {
322210 return NextResponse . json (
323- {
324- error : 'Failed to generate explanation' ,
325- code : 'AI_ERROR' ,
326- } ,
327- { status : 500 }
211+ { status : 'error' , service : 'ai-explain' , message : 'API key not configured' } ,
212+ { status : 503 }
328213 ) ;
329214 }
330- }
331215
332- export async function GET ( ) {
333216 return NextResponse . json (
334- {
335- status : 'ok' ,
336- service : 'ai-explain' ,
337- env : {
338- hasGroqKey : ! ! process . env . GROQ_API_KEY ,
339- nodeEnv : process . env . NODE_ENV ,
340- } ,
341- } ,
217+ { status : 'ok' , service : 'ai-explain' } ,
342218 { status : 200 }
343219 ) ;
344220}
221+
222+ // =============================================================================
223+ // Error Handling
224+ // =============================================================================
225+ function handleGroqError ( error : unknown ) : NextResponse {
226+ // Handle Groq SDK specific errors
227+ if ( error instanceof Groq . APIError ) {
228+ console . error ( `[ai/explain] Groq API error: ${ error . status } ${ error . message } ` ) ;
229+
230+ if ( error . status === 401 ) {
231+ return NextResponse . json (
232+ { error : 'AI service authentication failed' , code : 'AUTH_ERROR' } ,
233+ { status : 503 }
234+ ) ;
235+ }
236+
237+ if ( error . status === 429 ) {
238+ return NextResponse . json (
239+ { error : 'AI service rate limited' , code : 'API_RATE_LIMITED' } ,
240+ { status : 503 }
241+ ) ;
242+ }
243+
244+ if ( error . status === 404 ) {
245+ return NextResponse . json (
246+ { error : 'AI model not available' , code : 'MODEL_ERROR' } ,
247+ { status : 503 }
248+ ) ;
249+ }
250+
251+ // Other API errors (500, 503, etc.)
252+ return NextResponse . json (
253+ { error : 'AI service temporarily unavailable' , code : 'API_ERROR' } ,
254+ { status : 503 }
255+ ) ;
256+ }
257+
258+ // Handle JSON parse errors from response parsing
259+ if ( error instanceof SyntaxError ) {
260+ console . error ( '[ai/explain] Failed to parse AI response as JSON' ) ;
261+ return NextResponse . json (
262+ { error : 'AI returned invalid format' , code : 'PARSE_ERROR' } ,
263+ { status : 502 }
264+ ) ;
265+ }
266+
267+ // Handle response structure validation errors
268+ if ( error instanceof Error && error . message === 'Invalid response structure' ) {
269+ console . error ( '[ai/explain] AI response missing required fields' ) ;
270+ return NextResponse . json (
271+ { error : 'AI returned incomplete response' , code : 'INVALID_STRUCTURE' } ,
272+ { status : 502 }
273+ ) ;
274+ }
275+
276+ // Unknown errors
277+ console . error ( '[ai/explain] Unexpected error:' , error ) ;
278+ return NextResponse . json (
279+ { error : 'Failed to generate explanation' , code : 'AI_ERROR' } ,
280+ { status : 500 }
281+ ) ;
282+ }
0 commit comments