import { z } from "zod"; import { gradeShortAnswer } from "@/lib/ai"; import { authErrorMessage, authErrorKind, getServerConfig, resolveAuth } from "@/lib/config"; import { friendlyErrorMessage, OllamaError } from "@/lib/ollama"; import { rejectCrossOrigin, rejectOversizedBody, requestLocale } from "@/lib/http-guard"; import { clientKey, rateLimit, rateLimitResponse } from "@/lib/rate-limit"; import { translate } from "@/lib/i18n-dict"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const requestSchema = z.object({ question: z.string().min(1).max(1000), referenceAnswer: z.string().min(1).max(2000), keyPoints: z.array(z.string().max(300)).max(8).default([]), userAnswer: z.string().min(1).max(2000), /** Language of the deck so feedback comes back in the same language. */ language: z.string().max(60).optional(), }); /** Generous but finite: ~16 compressed images plus JSON overhead. */ const MAX_BODY_BYTES = 65536; export async function POST(request: Request) { const locale = requestLocale(request); const crossOrigin = rejectCrossOrigin(request); if (crossOrigin) return crossOrigin; const oversized = rejectOversizedBody(request, MAX_BODY_BYTES); if (oversized) return oversized; const config = getServerConfig(); const limit = rateLimit(`grade:${clientKey(request)}`, config.rateLimits.grade, config.rateLimits.windowMs); if (!limit.allowed) return rateLimitResponse(limit, locale); const auth = resolveAuth(config, request); if ("error" in auth) { return Response.json( { error: authErrorMessage(locale, auth.error), kind: authErrorKind(auth.error) }, { status: 401 }, ); } let payload: unknown; try { payload = await request.json(); } catch { return Response.json({ error: translate(locale, "server.bodyNotJson") }, { status: 400 }); } const parsed = requestSchema.safeParse(payload); if (!parsed.success) { return Response.json({ error: translate(locale, "server.invalidGrade") }, { status: 400 }); } try { const result = await gradeShortAnswer({ model: config.textModel, auth: auth.auth, ...parsed.data, // Grading is quick; don't let a hung request block the feed for minutes. signal: AbortSignal.timeout(Math.min(config.timeoutMs, 120_000)), }); return Response.json(result); } catch (error) { console.error("[api/grade]", error instanceof OllamaError ? `${error.kind}: ${error.message}` : error); return Response.json( { error: friendlyErrorMessage(error, auth.auth.baseUrl, locale), kind: error instanceof OllamaError ? error.kind : "unknown", }, { status: 502 }, ); } }