File size: 3,436 Bytes
46b1fd8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
 * 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 = <your pit_... 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 <condition>.
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 });
  },
};