/** * Eleusis NL→Python rule translator — Cloudflare Worker proxy. * * Keeps the Prime Inference API key server-side and exposes exactly ONE * capability: translating a short natural-language card rule into a Python * predicate, with a pinned model, pinned prompt, and pinned token caps — * so the endpoint is useless as a general LLM proxy. * * Deploy: * 1. Cloudflare dashboard → Workers & Pages → Create Worker → paste this. * 2. Settings → Variables → add SECRET PRIME_API_KEY = * 3. Deploy; give the workers.dev URL back so the Space can be wired to it. * Optional hardening: add a dashboard Rate Limiting rule (e.g. 20 req/min/IP). */ const ALLOWED_ORIGINS = [ "https://nph4rd-eleusis.static.hf.space", "http://localhost:7861", ]; const MODEL = "mistralai/mistral-small-3.2-24b-instruct"; const SYSTEM = `You translate natural-language card-game rules into a single Python predicate. Available: card.rank (int, A=1,J=11,Q=12,K=13), card.suit ('hearts','diamonds','clubs','spades'), card.color ('red','black'), card.is_face (rank 11-13), mainline (list of already-accepted cards, oldest to newest; may be empty). Relational rules compare card to mainline[-1] and must be guarded: not mainline or . Reply with ONLY the predicate expression, no backticks, no explanation.`; function cors(origin) { const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0]; return { "Access-Control-Allow-Origin": allowed, "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "content-type", "Content-Type": "application/json", }; } export default { async fetch(request, env) { const origin = request.headers.get("Origin") || ""; const headers = cors(origin); if (request.method === "OPTIONS") return new Response(null, { headers }); if (request.method !== "POST") return new Response(JSON.stringify({ error: "POST only" }), { status: 405, headers }); let text; try { ({ text } = await request.json()); } catch { return new Response(JSON.stringify({ error: "bad json" }), { status: 400, headers }); } if (typeof text !== "string" || !text.trim() || text.length > 300) return new Response(JSON.stringify({ error: "text must be 1-300 chars" }), { status: 400, headers }); const upstream = await fetch("https://api.pinference.ai/api/v1/chat/completions", { method: "POST", headers: { Authorization: `Bearer ${env.PRIME_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: MODEL, max_tokens: 120, temperature: 0, messages: [ { role: "system", content: SYSTEM }, { role: "user", content: text.trim() }, ], }), }); if (!upstream.ok) { return new Response( JSON.stringify({ error: `upstream ${upstream.status}` }), { status: 502, headers } ); } const data = await upstream.json(); let out = (data.choices?.[0]?.message?.content || "").trim(); // strip code fences and lambda wrappers the model sometimes adds out = out.replace(/^```(python)?|```$/g, "").trim(); const lam = out.match(/^\(?\s*lambda\s+[^:]*:\s*([\s\S]*?)\)?\s*$/); if (lam) out = lam[1].trim(); return new Response(JSON.stringify({ predicate: out }), { headers }); }, };