import { z } from "zod"; import { refineHomeworkText } from "@/lib/ai"; import { getServerConfig, resolveModelName } from "@/lib/config"; import { INERT_OLLAMA_AUTH, resolveModelTarget } from "@/lib/providers-server"; 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({ model: z.string().min(1).max(200).optional(), 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); // Ollama (server-configured) or a cloud provider the browser sent a key for. const resolved = resolveModelTarget(config, request, locale); if ("error" in resolved) { return Response.json({ error: resolved.error.message, kind: resolved.error.kind }, { status: resolved.error.status }); } const target = resolved.target; const provider = target.kind === "provider" ? target.target : undefined; const baseAuth = target.kind === "provider" ? INERT_OLLAMA_AUTH : target.auth; 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 }, ); } // Provider runs carry the model the user picked in Settings → Providers. const providerModel = provider ? parsed.data.model : undefined; if (provider && !providerModel) { return Response.json( { error: translate(locale, "server.providerModelNeeded"), kind: "bad_provider" }, { status: 400 }, ); } const textModel = provider ? providerModel! : config.textModel; const gate = acquireModelGate(request, config, locale); if ("response" in gate) return gate.response; try { const text = await refineHomeworkText({ model: textModel, auth: baseAuth, provider, instruction: parsed.data.instruction, text: parsed.data.text, context: parsed.data.context, }); return Response.json( { text, model: provider ? textModel : resolveModelName(config.textModel, baseAuth.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, baseAuth.baseUrl || provider?.baseUrl || '', locale), kind: error instanceof OllamaError ? error.kind : "unknown", }, { status: 502 }, ); } finally { gate.gate.release(); } }