-
-
Notifications
You must be signed in to change notification settings - Fork 4
Feat/translate about #228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat/translate about #228
Changes from all commits
155e5ae
ab1f2c3
a3f28b4
5dc7a57
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,19 +1,69 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const runtime = 'nodejs'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const maxDuration = 25; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from 'next/server'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Groq from 'groq-sdk'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from 'zod'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createExplainPrompt, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ExplanationResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@/lib/ai/prompts'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getClientIp } from '@/lib/security/client-ip'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================================= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // SERVER-SIDE LOGGING (sanitized - no sensitive data exposed) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================================= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function logEnvironmentDiagnostics() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = process.env.GROQ_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[ENV] GROQ_API_KEY configured:', !!apiKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[ENV] GROQ_API_KEY length:', apiKey ? apiKey.length : 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[ENV] NODE_ENV:', process.env.NODE_ENV); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[ENV] NETLIFY:', process.env.NETLIFY ?? 'false'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[ENV] CONTEXT:', process.env.CONTEXT ?? 'unknown'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function logRequestDiagnostics(request: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[REQ] Method:', request.method); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[REQ] URL path:', new URL(request.url).pathname); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function logBodyParsingResult(success: boolean, error?: unknown) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[BODY] Parse success:', success); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[BODY] Parse error:', error instanceof Error ? error.message : 'Unknown error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function logGroqInitialization(success: boolean, error?: unknown) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[GROQ] Init success:', success); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const err = error as Error & { status?: number; code?: string }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[GROQ] Init error:', err.name, err.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function logGroqApiCall(phase: 'start' | 'success' | 'error', details?: unknown) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (phase === 'start') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[GROQ] Starting API call'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (phase === 'success') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[GROQ] API call successful'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (phase === 'error') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const err = details as Error & { status?: number; code?: string }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[GROQ] API error:', err?.name, err?.message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================================= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================================= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // RATE LIMITER (In-memory - limited effectiveness in serverless) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Note: This Map only persists within a single warm function instance. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // For production, consider Upstash Redis or Netlify Blobs for true rate limiting. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Current behavior: works during warm instance, resets on cold start. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ============================================================================= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rateLimiter = new Map<string, { count: number; resetAt: number }>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_REQUESTS_PER_WINDOW = 10; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const RATE_LIMIT_WINDOW_MS = 20 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const RATE_LIMIT_WINDOW_MS = 20 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let lastCleanup = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function cleanupRateLimiter() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -40,27 +90,33 @@ const requestSchema = z.object({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number } { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Bypass rate limiting for unknown IPs (serverless safety) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ip === 'unknown') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+93
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rate limit bypass for unknown IPs may be exploitable. Bypassing rate limiting when Suggested improvement function checkRateLimit(ip: string): { allowed: boolean; remaining: number; resetIn: number; skipped: boolean } {
// Bypass rate limiting for unknown IPs (serverless safety)
if (ip === 'unknown') {
+ console.warn('[RATE_LIMIT] Skipped - unknown IP');
return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW, resetIn: RATE_LIMIT_WINDOW_MS, skipped: true };
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleanupRateLimiter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const entry = rateLimiter.get(ip); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!entry || now > entry.resetAt) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rateLimiter.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { allowed: true, remaining: MAX_REQUESTS_PER_WINDOW - 1, resetIn: RATE_LIMIT_WINDOW_MS, skipped: false }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.count >= MAX_REQUESTS_PER_WINDOW) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resetIn = entry.resetAt - now; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { allowed: false, remaining: 0, resetIn }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { allowed: false, remaining: 0, resetIn, skipped: false }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| entry.count++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allowed: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remaining: MAX_REQUESTS_PER_WINDOW - entry.count, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resetIn: entry.resetAt - now, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| skipped: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -91,14 +147,17 @@ function parseExplanationResponse(content: string): ExplanationResponse { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(request: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logEnvironmentDiagnostics(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logRequestDiagnostics(request); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = process.env.GROQ_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('GROQ_API_KEY is not configured'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Available env vars starting with GROQ:', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.keys(process.env).filter(k => k.startsWith('GROQ')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[FATAL] GROQ_API_KEY is not configured. Check environment variables.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: 'AI service not configured', code: 'SERVICE_UNAVAILABLE' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'AI service not configured', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: 'SERVICE_UNAVAILABLE', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 503 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -125,10 +184,21 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Safe JSON body parsing for Netlify | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let body: unknown; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body = await request.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const text = await request.text(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!text || text.trim() === '') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('[BODY] Empty request body received'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: 'Request body is empty', code: 'EMPTY_BODY' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body = JSON.parse(text); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logBodyParsingResult(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (parseError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logBodyParsingResult(false, parseError); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: 'Invalid JSON body', code: 'INVALID_JSON' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 400 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -149,11 +219,46 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { term, context } = validationResult.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const groq = new Groq({ apiKey }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Dynamic import for Netlify compatibility | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let Groq: typeof import('groq-sdk').default; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const groqModule = await import('groq-sdk'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Groq = groqModule.default; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (importError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[SDK_IMPORT_ERROR] Failed to import groq-sdk:', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| importError instanceof Error ? importError.message : String(importError) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'Failed to load AI client', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: 'SDK_IMPORT_ERROR', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 503 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let groq: InstanceType<typeof Groq>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groq = new Groq({ apiKey }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logGroqInitialization(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (initError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logGroqInitialization(false, initError); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[SDK_INIT_ERROR] Failed to initialize Groq client:', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| initError instanceof Error ? initError.message : String(initError) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'Failed to initialize AI client', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: 'SDK_INIT_ERROR', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 503 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const prompt = createExplainPrompt({ term, context }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logGroqApiCall('start'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const chatCompletion = await groq.chat.completions.create({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -166,10 +271,12 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max_tokens: 1500, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| top_p: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logGroqApiCall('success', chatCompletion); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const content = chatCompletion.choices[0]?.message?.content; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!content) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[ERROR] No content in Groq response'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('No content in response'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -184,26 +291,21 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Groq API error:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errorMessage = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error instanceof Error ? error.message : 'Unknown error'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errorName = error instanceof Error ? error.name : 'UnknownError'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Error details:', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: errorName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: errorMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stack: error instanceof Error ? error.stack : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logGroqApiCall('error', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[GROQ_ERROR]', error instanceof Error ? error.message : 'Unknown error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error instanceof Error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error.message.includes('401') || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error.message.includes('authentication') || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error.message.includes('Invalid API Key') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('[AUTH_ERROR] API key authentication failed'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { error: 'AI service authentication failed', code: 'AUTH_ERROR' }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'AI service authentication failed', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: 'AUTH_ERROR', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 503 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -221,7 +323,6 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'AI model not available', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: 'MODEL_ERROR', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: errorMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 503 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -232,10 +333,27 @@ export async function POST(request: NextRequest) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: 'Failed to generate explanation', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: 'AI_ERROR', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| details: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.NODE_ENV === 'development' ? errorMessage : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function GET() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = process.env.GROQ_API_KEY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: apiKey ? 'ok' : 'misconfigured', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| service: 'ai-explain', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: new Date().toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasGroqKey: !!apiKey, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groqKeyLength: apiKey ? apiKey.length : 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| nodeEnv: process.env.NODE_ENV, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isNetlify: !!process.env.NETLIFY, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| context: process.env.CONTEXT ?? 'unknown', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: apiKey ? 200 : 503 } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+342
to
+359
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Health endpoint exposes potentially sensitive environment details. The GET endpoint publicly exposes Suggested: Reduce public exposure export async function GET() {
const apiKey = process.env.GROQ_API_KEY;
return NextResponse.json(
{
status: apiKey ? 'ok' : 'misconfigured',
service: 'ai-explain',
timestamp: new Date().toISOString(),
- env: {
- hasGroqKey: !!apiKey,
- groqKeyLength: apiKey ? apiKey.length : 0,
- nodeEnv: process.env.NODE_ENV,
- isNetlify: !!process.env.NETLIFY,
- context: process.env.CONTEXT ?? 'unknown',
- },
},
{ status: apiKey ? 200 : 503 }
);
}If detailed diagnostics are needed, consider protecting this endpoint with authentication or moving it to a separate internal route. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing locale parameter in
generateMetadata.Other pages in this codebase (e.g.,
q&a/page.tsx,blog/page.tsx,dashboard/page.tsx) explicitly extract the locale fromparamsand pass it togetTranslations. Without this, the metadata may not be generated in the correct locale.Proposed fix to match existing patterns
🤖 Prompt for AI Agents