Spaces:
Running
Running
Devdan Schretlen
Claude Opus 5 (1M context)
Expand the calendar corpus to every faculty, and rewrite the UI copy
fcdba1d Download web/limits.py from DevSchretl/UBCHelper: direct link, hf CLI and curl.
- Browser
- Download file 9.56 kB
-
https://huggingface.co/spaces/DevSchretl/UBCHelper/resolve/main/web/limits.py
- Command line
-
hf download hf://spaces/DevSchretl/UBCHelper/web/limits.py
-
curl -L -o limits.py https://huggingface.co/spaces/DevSchretl/UBCHelper/resolve/main/web/limits.py
9.56 kB
| """Spend and abuse controls for the public demo. | |
| The demo answers unauthenticated questions using three paid APIs (OpenAI embeddings, | |
| Cohere rerank, Anthropic generation), so the guiding rule is that the worst case has to be | |
| *degradation*, never a surprise bill. Two independent limits enforce that: | |
| per-IP a sliding window (hour + day) so one visitor cannot monopolise the demo | |
| global a hard daily ceiling on paid API calls; once hit, live querying is refused | |
| and the UI falls back to the precomputed example runs. The site stays up. | |
| The global ceiling counts **API calls, not dollars**. A call count is exact and knowable | |
| up front (a simple-route run is 2 LLM calls + 1 embed + 1 rerank; a complex run is 3 LLM | |
| calls and up to 3 of each retrieval call), whereas a dollar figure drifts every time a | |
| provider changes its price list. Money is shown to the operator as an estimate derived | |
| from PRICES below, but it is never what the limiter enforces. | |
| IP addresses are stored only as salted SHA-256. The limiter needs to recognise a repeat | |
| visitor, which a hash does exactly as well as an address — so the demo keeps no PII. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import os | |
| import sqlite3 | |
| import time | |
| from contextlib import closing | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| # -------------------------------------------------------------------------------------- | |
| # Knobs (all overridable in the environment; the defaults suit a free-tier portfolio demo) | |
| # -------------------------------------------------------------------------------------- | |
| DB_PATH = Path(os.getenv("DEMO_DB_PATH", "/tmp/ubchelper-demo.sqlite3")) | |
| IP_SALT = os.getenv("DEMO_IP_SALT", "change-me-in-production") | |
| DAILY_LLM_CALLS = int(os.getenv("DEMO_DAILY_LLM_CALLS", "400")) | |
| IP_PER_HOUR = int(os.getenv("DEMO_IP_PER_HOUR", "6")) | |
| IP_PER_DAY = int(os.getenv("DEMO_IP_PER_DAY", "20")) | |
| MAX_QUESTION_CHARS = int(os.getenv("DEMO_MAX_QUESTION_CHARS", "300")) | |
| MIN_QUESTION_CHARS = 3 | |
| # USD per call, for the operator-facing estimate only — never used for enforcement. | |
| # Refresh these from the providers' current pricing; being stale costs nothing but accuracy. | |
| PRICES = { | |
| "llm": float(os.getenv("DEMO_PRICE_LLM", "0.0015")), # one Haiku call, ~2.5k tokens | |
| "embed": float(os.getenv("DEMO_PRICE_EMBED", "0.000001")), # one short query embedding | |
| "rerank": float(os.getenv("DEMO_PRICE_RERANK", "0.002")), # one Cohere rerank search | |
| } | |
| HOUR = 3600 | |
| DAY = 86400 | |
| # Which trace events correspond to a paid API call, and which counter each feeds. Every paid | |
| # call announces itself in the stream, so tallying these is exact rather than estimated — and | |
| # it stays correct on its own if the pipeline's shape changes. Shared by every delivery mode | |
| # so the three of them cannot drift apart on what counts as spend. | |
| PAID_EVENT_KINDS = { | |
| "llm_call": "llm", # one generation (route, decompose, or answer) | |
| "retrieval_start": "embed", # one query embedding | |
| "shortlist": "rerank", # one Cohere rerank (hybrid_rerank mode only) | |
| } | |
| def tally(counts: dict, event: dict) -> None: | |
| """Fold one trace event into a {llm, embed, rerank} tally, in place.""" | |
| bucket = PAID_EVENT_KINDS.get(event.get("kind")) | |
| if bucket: | |
| counts[bucket] = counts.get(bucket, 0) + 1 | |
| class Decision: | |
| """The outcome of a limit check. `retry_after` is seconds, 0 when not applicable.""" | |
| allowed: bool | |
| reason: str = "" | |
| retry_after: int = 0 | |
| # True when the *global* budget is what blocked this, so the UI can switch to | |
| # cached-examples-only mode rather than telling the visitor to slow down. | |
| budget_exhausted: bool = False | |
| def hash_ip(ip: str) -> str: | |
| return hashlib.sha256(f"{IP_SALT}:{ip}".encode()).hexdigest() | |
| def client_ip(request) -> str: | |
| """The visitor's address, accounting for the reverse proxy in front of the app. | |
| Hugging Face Spaces (like any hosted platform) terminates TLS upstream, so | |
| `request.client.host` is the proxy for every visitor. Reading the first hop of | |
| X-Forwarded-For is what separates per-visitor limits from one global limit applied to | |
| everybody — getting this wrong silently rate-limits the whole internet as one IP. | |
| """ | |
| forwarded = request.headers.get("x-forwarded-for", "") | |
| if forwarded: | |
| return forwarded.split(",")[0].strip() | |
| return request.client.host if request.client else "unknown" | |
| def _connect() -> sqlite3.Connection: | |
| DB_PATH.parent.mkdir(parents=True, exist_ok=True) | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.execute("PRAGMA journal_mode=WAL") | |
| return conn | |
| def init() -> None: | |
| """Create the tables. Safe to call on every startup.""" | |
| with closing(_connect()) as conn, conn: | |
| conn.execute( | |
| "CREATE TABLE IF NOT EXISTS usage (" | |
| " day TEXT PRIMARY KEY, runs INT DEFAULT 0, llm INT DEFAULT 0," | |
| " embed INT DEFAULT 0, rerank INT DEFAULT 0)" | |
| ) | |
| conn.execute("CREATE TABLE IF NOT EXISTS hits (ip_hash TEXT, ts REAL)") | |
| conn.execute("CREATE INDEX IF NOT EXISTS hits_ts ON hits(ts)") | |
| def _today() -> str: | |
| return time.strftime("%Y-%m-%d", time.gmtime()) | |
| def validate_question(raw: str) -> tuple[str, str]: | |
| """Clean and bounds-check a question. Returns (question, error) — error empty if OK. | |
| Runs before any paid call, so a junk request costs nothing. | |
| """ | |
| question = "".join(ch for ch in (raw or "") if ch.isprintable()).strip() | |
| if len(question) < MIN_QUESTION_CHARS: | |
| return "", "Please enter a question." | |
| if len(question) > MAX_QUESTION_CHARS: | |
| return "", f"Questions are limited to {MAX_QUESTION_CHARS} characters." | |
| return question, "" | |
| def check(ip_hash: str) -> Decision: | |
| """Decide whether this visitor may run a live query right now.""" | |
| now = time.time() | |
| with closing(_connect()) as conn, conn: | |
| # Age out anything older than the widest window; keeps the table small forever. | |
| conn.execute("DELETE FROM hits WHERE ts < ?", (now - DAY,)) | |
| spent = conn.execute( | |
| "SELECT llm FROM usage WHERE day = ?", (_today(),) | |
| ).fetchone() | |
| if spent and spent[0] >= DAILY_LLM_CALLS: | |
| # Kept neutral about what else is on offer: the FastAPI and static builds can | |
| # fall back to precomputed runs, the Gradio build has none, and this module is | |
| # shared by all three. | |
| return Decision( | |
| allowed=False, | |
| reason="This demo is out of model calls for today. The limit resets at " | |
| "00:00 UTC.", | |
| budget_exhausted=True, | |
| ) | |
| hour_count = conn.execute( | |
| "SELECT COUNT(*) FROM hits WHERE ip_hash = ? AND ts > ?", (ip_hash, now - HOUR) | |
| ).fetchone()[0] | |
| if hour_count >= IP_PER_HOUR: | |
| oldest = conn.execute( | |
| "SELECT MIN(ts) FROM hits WHERE ip_hash = ? AND ts > ?", | |
| (ip_hash, now - HOUR), | |
| ).fetchone()[0] | |
| wait = max(1, int(oldest + HOUR - now)) | |
| return Decision( | |
| allowed=False, | |
| reason=f"You have used your {IP_PER_HOUR} questions for this hour. " | |
| f"Try again in about {max(1, round(wait / 60))} minutes.", | |
| retry_after=wait, | |
| ) | |
| day_count = conn.execute( | |
| "SELECT COUNT(*) FROM hits WHERE ip_hash = ? AND ts > ?", (ip_hash, now - DAY) | |
| ).fetchone()[0] | |
| if day_count >= IP_PER_DAY: | |
| return Decision( | |
| allowed=False, | |
| reason=f"You have used your {IP_PER_DAY} questions for today.", | |
| retry_after=DAY, | |
| ) | |
| return Decision(allowed=True) | |
| def reserve(ip_hash: str) -> None: | |
| """Record that this visitor started a run (counts against their window immediately). | |
| Charged on start rather than on success so that a run which errors out — or a visitor | |
| who reloads mid-stream — still consumes their slot. The paid calls were made either way. | |
| """ | |
| with closing(_connect()) as conn, conn: | |
| conn.execute("INSERT INTO hits (ip_hash, ts) VALUES (?, ?)", (ip_hash, time.time())) | |
| def record(llm: int = 0, embed: int = 0, rerank: int = 0) -> None: | |
| """Add one finished run's actual API call counts to today's global total.""" | |
| with closing(_connect()) as conn, conn: | |
| conn.execute( | |
| "INSERT INTO usage (day, runs, llm, embed, rerank) VALUES (?, 1, ?, ?, ?) " | |
| "ON CONFLICT(day) DO UPDATE SET runs = runs + 1, llm = llm + ?, " | |
| "embed = embed + ?, rerank = rerank + ?", | |
| (_today(), llm, embed, rerank, llm, embed, rerank), | |
| ) | |
| def snapshot() -> dict: | |
| """Today's usage, for /api/health and the UI's budget badge.""" | |
| with closing(_connect()) as conn: | |
| row = conn.execute( | |
| "SELECT runs, llm, embed, rerank FROM usage WHERE day = ?", (_today(),) | |
| ).fetchone() | |
| runs, llm, embed, rerank = row if row else (0, 0, 0, 0) | |
| return { | |
| "day": _today(), | |
| "runs": runs, | |
| "llm_calls": llm, | |
| "llm_calls_limit": DAILY_LLM_CALLS, | |
| "llm_calls_remaining": max(0, DAILY_LLM_CALLS - llm), | |
| "budget_exhausted": llm >= DAILY_LLM_CALLS, | |
| "estimated_cost_usd": round( | |
| llm * PRICES["llm"] + embed * PRICES["embed"] + rerank * PRICES["rerank"], 4 | |
| ), | |
| "per_ip_hour": IP_PER_HOUR, | |
| "per_ip_day": IP_PER_DAY, | |
| } | |