inference: quota counters in KV, not D1 (the Workers token has KV, not D1)
ci / test (push) Successful in 43s
sync / sync (push) Successful in 6s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
This commit is contained in:
teahubxyzandClaude Fable 5.1 committed 2026-10-20 19:08:01 +00:00
1 parent 5f61220fdc
commit 8a2543f2cf
2 files changed
+20 -21

No files matched your search

+11 -13
View File
@@ -13,13 +13,11 @@
// (day, app, kind): CHAT_DAILY/RENDER_DAILY per person, CHAT_TOTAL/RENDER_TOTAL per app. A render is an audio queue
// or an image generate; retrieve polls, quotes and completes are free. Over a limit answers 429 with a plain reason.
//
// Bindings: DB (D1). Secret: VENICE_API_KEY. Vars: REV, APPS, ORIGINS, CHAT_MODELS, AUDIO_MODELS, IMAGE_MODELS,
// Bindings: QUOTA (KV). Secret: VENICE_API_KEY. Vars: REV, APPS, ORIGINS, CHAT_MODELS, AUDIO_MODELS, IMAGE_MODELS,
// AUDIO_SECONDS, MAX_TOKENS, CHAT_DAILY, CHAT_TOTAL, RENDER_DAILY, RENDER_TOTAL.
const VENICE = "https://api.venice.ai/api/v1";
const SCHEMA = ["CREATE TABLE IF NOT EXISTS quota (k TEXT PRIMARY KEY, n INTEGER NOT NULL)"];
const MAX_BODY = 32768;
let ready = null;
const json = (body, status = 200, headers = {}) =>
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8", ...headers } });
@@ -39,22 +37,23 @@ function cors(req, env) {
"access-control-allow-headers": "content-type, x-app", "access-control-max-age": "600", "vary": "origin" };
}
async function setup(env) {
ready ??= env.DB.batch(SCHEMA.map((s) => env.DB.prepare(s))).catch((e) => { ready = null; throw e; });
return ready;
}
async function who(req, app) {
const ip = req.headers.get("cf-connecting-ip") || "?";
const raw = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${app}:${ip}`));
return [...new Uint8Array(raw).slice(0, 8)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
// One counter per (day, app, kind, person) and one per (day, app, kind): over either limit means no.
// One counter per (day, app, kind, person) and one per (day, app, kind) in KV: over either limit means no. KV is
// eventually consistent, so two calls in the same instant can both count; that only ever over-counts, never under.
async function spend(env, app, kind, person, perPerson, total) {
const day = new Date().toISOString().slice(0, 10);
const bump = (k) => env.DB.prepare("INSERT INTO quota (k, n) VALUES (?, 1) ON CONFLICT(k) DO UPDATE SET n = n + 1 RETURNING n").bind(k);
const [mine, all] = await env.DB.batch([bump(`${day}:${app}:${kind}:${person}`), bump(`${day}:${app}:${kind}:*`)]);
if (mine.results[0].n > perPerson) return "you're over today's limit for this app; add your own Venice key or come back tomorrow";
if (all.results[0].n > total) return "this app is over its daily budget; add your own Venice key or come back tomorrow";
const bump = async (k) => {
const n = Number((await env.QUOTA.get(k)) || 0) + 1;
await env.QUOTA.put(k, String(n), { expirationTtl: 2 * 86400 });
return n;
};
const [mine, all] = await Promise.all([bump(`${day}:${app}:${kind}:${person}`), bump(`${day}:${app}:${kind}:*`)]);
if (mine > perPerson) return "you're over today's limit for this app; add your own Venice key or come back tomorrow";
if (all > total) return "this app is over its daily budget; add your own Venice key or come back tomorrow";
return "";
}
@@ -79,7 +78,6 @@ async function api(req, env, url, h) {
if (text.length > MAX_BODY) return json({ error: "too big" }, 413, h);
let body;
try { body = JSON.parse(text); if (!body || typeof body !== "object" || Array.isArray(body)) throw 0; } catch { return json({ error: "send a JSON object" }, 400, h); }
await setup(env);
const person = await who(req, app);
const model = String(body.model || "");
+9 -8
View File
@@ -46,11 +46,12 @@ def hostname() -> str:
return f"inference.{zone}"
def database() -> str:
for d in cf("GET", f"{account()}/d1/database?name={NAME}") or []:
if d.get("name") == NAME:
return d["uuid"]
return cf("POST", f"{account()}/d1/database", {"name": NAME})["uuid"]
def namespace() -> str:
"""The worker's KV namespace for quota counters (created once). Returns its id."""
for n in cf("GET", f"{account()}/storage/kv/namespaces?per_page=100") or []:
if n.get("title") == NAME:
return n["id"]
return cf("POST", f"{account()}/storage/kv/namespaces", {"title": NAME})["id"]
def deploy() -> dict:
@@ -61,15 +62,15 @@ def deploy() -> dict:
h = hashlib.sha256(WORKER.read_bytes())
h.update(json.dumps({**vars_, "host": hostname(), "compat": COMPAT}, sort_keys=True).encode())
vars_["REV"] = rev = h.hexdigest()[:12]
db = database()
kv = namespace()
meta = {"main_module": "inference.js", "compatibility_date": COMPAT,
"bindings": [{"type": "d1", "name": "DB", "id": db}, {"type": "secret_text", "name": "VENICE_API_KEY", "text": key}]
"bindings": [{"type": "kv_namespace", "name": "QUOTA", "namespace_id": kv}, {"type": "secret_text", "name": "VENICE_API_KEY", "text": key}]
+ [{"type": "plain_text", "name": k, "text": str(v)} for k, v in vars_.items()]}
data, ctype = multipart([("metadata", "application/json", json.dumps(meta).encode()),
("inference.js", "application/javascript+module", WORKER.read_bytes())])
cf("PUT", f"{account()}/workers/scripts/{NAME}", data=data, ctype=ctype)
url = attach(NAME, hostname())
return {"url": url, "worker": NAME, "db": db, "rev": rev}
return {"url": url, "worker": NAME, "kv": kv, "rev": rev}
def check() -> dict: