tiny-llm / src /tinyllm /web_common.py
CoolDip00's picture
tiny-llm 9e83e17: D-031: takeover sub-plan and progress notes
b446ce4 verified
Raw History Blame Contribute Delete
5.39 kB
"""HTTP helpers shared by the model server and the replica router (D-031).
No torch here, on purpose: the router runs without it (tests/test_router.py checks that).
"""
from __future__ import annotations
import hashlib
import os
import re
import secrets
import uuid
from pathlib import Path
from fastapi.responses import JSONResponse
from starlette.requests import Request
from tinyllm.obs import hash_client
# The router sends the visitor's hash (obs.hash_client: 16 hex characters) to a private replica.
CLIENT_HEADER = "x-tinyllm-client"
_CLIENT_HASH = re.compile(r"[0-9a-f]{16}")
def router_client(value: str | None) -> str | None:
"""The visitor hash in a router header, or None if it is missing or malformed."""
return value if value and _CLIENT_HASH.fullmatch(value) else None
class RateLimiter:
"""Token bucket per client: `per_min` requests per minute, bursts up to `burst`.
Called from the event loop only. per_min <= 0 disables it."""
def __init__(self, per_min: float, burst: int):
self.rate = per_min / 60.0
self.burst = max(1, burst)
self._b: dict[str, tuple[float, float]] = {}
def check(self, client: str, now: float) -> float:
"""0 if allowed (and counted), else seconds until the next request is allowed."""
if self.rate <= 0:
return 0.0
tokens, last = self._b.get(client, (float(self.burst), now))
tokens = min(float(self.burst), tokens + (now - last) * self.rate)
if tokens >= 1.0:
self._b[client] = (tokens - 1.0, now)
if len(self._b) > 10_000: # forget clients whose bucket is full again
self._b = {k: v for k, v in self._b.items() if v[0] + (now - v[1]) * self.rate < self.burst}
return 0.0
self._b[client] = (tokens, now)
return (1.0 - tokens) / self.rate
_RID = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
class HeadersMiddleware:
"""Pure ASGI (keeps streaming and disconnects intact): every response gets X-Request-ID
(the client's, if it sent a sane one) and X-Model-Version."""
def __init__(self, app, default_version: str):
self.app = app
self.default_version = default_version
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
return await self.app(scope, receive, send)
incoming = dict(scope.get("headers") or []).get(b"x-request-id", b"").decode("latin-1")
rid = incoming if _RID.match(incoming) else uuid.uuid4().hex[:16]
state = scope.setdefault("state", {})
state["request_id"] = rid
async def send_with_headers(msg):
if msg["type"] == "http.response.start":
version = state.get("model_version", self.default_version)
msg = {**msg, "headers": [*msg.get("headers", []), (b"x-request-id", rid.encode()),
(b"x-model-version", version.encode())]}
await send(msg)
await self.app(scope, receive, send_with_headers)
WEB = Path(__file__).resolve().parent / "web"
def read_page(name: str) -> str:
"""The demo and status pages (6.5b): static HTML + JS in src/tinyllm/web, read per request
(small) so an edit shows on reload. They fetch the JSON endpoints with relative URLs."""
return (WEB / name).read_text(encoding="utf-8")
def forwarded_ip(xff: str, hops: int = 1) -> str | None:
"""The client's address in an X-Forwarded-For header behind `hops` trusted proxies.
Each proxy appends the address the request came from, so only the last `hops` entries
are trustworthy; anything left of them is whatever the client sent (spoofable, so never
the leftmost). With fewer entries than hops, all came from proxies: take the leftmost.
"""
parts = [p.strip() for p in xff.split(",") if p.strip()]
if not parts:
return None
return parts[-hops] if len(parts) >= hops else parts[0]
def client_hash(request: Request, trust_proxy: bool, hops: int, salt: str) -> str:
"""The salted visitor id for a request: from X-Forwarded-For behind a trusted proxy, else the peer."""
ip = request.client.host if request.client else "unknown"
if trust_proxy and (xff := request.headers.get("x-forwarded-for")):
ip = forwarded_ip(xff, hops) or ip
return hash_client(ip, salt)
def resolve_salt(salt: str | None = None) -> tuple[str, str]:
"""(salt, where it came from) for the client hash. The source is logged, never the salt.
Order: an explicit salt; $TINYLLM_LOG_SALT (a Space secret, the same value in every
replica Space, D-031); a hash of $SPACE_SIGNING_SECRET (HF's per-Space secret, stable
across restarts, but different in each Space); else random per start.
"""
if salt:
return salt, "config"
if s := os.environ.get("TINYLLM_LOG_SALT"):
return s, "TINYLLM_LOG_SALT"
if s := os.environ.get("SPACE_SIGNING_SECRET"):
return hashlib.sha256(f"tinyllm-log-salt:{s}".encode()).hexdigest(), "SPACE_SIGNING_SECRET"
return secrets.token_hex(16), "random"
def error_response(status: int, message: str, err_type: str, headers: dict | None = None) -> JSONResponse:
return JSONResponse({"error": {"message": message, "type": err_type, "code": status}},
status_code=status, headers=headers)