import { z } from "zod"; import { refineHomeworkText } from "@/lib/ai"; import { authErrorKind, authErrorMessage, getServerConfig, resolveAuth, resolveModelName } from "@/lib/config"; import { friendlyErrorMessage, OllamaError } from "@/lib/ollama"; import { rejectCrossOrigin, rejectOversizedBody, requestLocale } from "@/lib/http-guard"; import { translate } from "@/lib/i18n-dict"; import { clientKey, rateLimit, rateLimitResponse } from "@/lib/rate-limit"; import { acquireModelGate } from "@/lib/model-guard"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; /** * Homework follow-ups: rewrite one finished piece — shorter, more detail, or * a step explained differently. One short model call, plain JSON back. */ const requestSchema = z.object({ instruction: z.enum(["shorter", "more", "explain"]), text: z.string().min(1).max(20_000), /** The assignment, for context (optional). */ context: z.string().max(8_000).optional(), language: z.string().max(60).optional(), }); export async function POST(request: Request) { const locale = requestLocale(request); const crossOrigin = rejectCrossOrigin(request); if (crossOrigin) return crossOrigin; const oversized = rejectOversizedBody(request, 128 * 1024); if (oversized) return oversized; const config = getServerConfig(); const limit = rateLimit(`refine:${clientKey(request)}`, config.rateLimits.refine, 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.invalidRequest", { detail: parsed.error.issues .slice(0, 3) .map((issue) => `${issue.path.join(".") || "body"} ${issue.message}`) .join("; "), }), }, { status: 400 }, ); } const gate = acquireModelGate(request, config, locale); if ("response" in gate) return gate.response; try { const text = await refineHomeworkText({ model: config.textModel, auth: auth.auth, instruction: parsed.data.instruction, text: parsed.data.text, context: parsed.data.context, }); return Response.json( { text, model: resolveModelName(config.textModel, auth.auth.directCloud) }, { headers: { "Cache-Control": "no-store" } }, ); } catch (error) { // Never log the text itself: it may contain the student's homework. console.error("[api/homework/refine]", 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 }, ); } finally { gate.gate.release(); } }