Repository navigation
Expand file tree
/
Copy pathcloudflare_worker.js
More file actions
104 lines (88 loc) · 2.96 KB
/
Copy pathcloudflare_worker.js
File metadata and controls
104 lines (88 loc) · 2.96 KB
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// Scryptian LLM proxy — Cloudflare Worker
//
// Setup:
// 1. Create a Worker in the Cloudflare dashboard and paste this file.
// 2. Add a secret: OPENROUTER_API_KEY = sk-or-...
// 3. Deploy and call: POST https://<your-worker>.workers.dev/v1/chat/completions
//
// The desktop app talks to this Worker instead of OpenRouter directly, so the
// API key never ships in the app.
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
// The single model used for every request — keeps costs 100% predictable.
const MODEL = "meta-llama/llama-3.3-70b-instruct";
// Fixed-window rate limit (per Worker isolate; approximate for MVP).
const RATE_LIMIT = new Map();
function rateLimit(ip, limit = 30, windowMs = 60_000) {
const now = Date.now();
const entry = RATE_LIMIT.get(ip);
if (!entry || entry.resetAt <= now) {
RATE_LIMIT.set(ip, { count: 1, resetAt: now + windowMs });
return true;
}
entry.count += 1;
return entry.count <= limit;
}
function cors() {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
}
function json(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: { "Content-Type": "application/json", ...cors() },
});
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: cors() });
}
if (request.method !== "POST" || url.pathname !== "/v1/chat/completions") {
return json({ error: "Not found" }, 404);
}
const ip = request.headers.get("CF-Connecting-IP") || "unknown";
if (!rateLimit(ip)) {
return json({ error: "Rate limit exceeded. Try again later." }, 429);
}
let body;
try {
body = await request.json();
} catch {
return json({ error: "Invalid JSON body" }, 400);
}
// Always use the fixed model — ignore whatever the client sends.
body.model = MODEL;
// Cap output to avoid runaway bills.
body.max_tokens = Math.min(body.max_tokens || 1024, 2048);
const upstream = await fetch(OPENROUTER_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${env.OPENROUTER_API_KEY}`,
"HTTP-Referer": "https://scryptian.app",
"X-Title": "Scryptian",
},
body: JSON.stringify(body),
});
// Pass through SSE streaming if requested.
if (body.stream) {
return new Response(upstream.body, {
status: upstream.status,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
...cors(),
},
});
}
const data = await upstream.json();
return new Response(JSON.stringify(data), {
status: upstream.status,
headers: { "Content-Type": "application/json", ...cors() },
});
},
};