From 63dd93dde9e9ed4f709cf58099c72e56a73f349d Mon Sep 17 00:00:00 2001 From: leora Date: Thu, 8 Oct 2026 19:31:04 +0800 Subject: [PATCH] Rate-limit only LLM-bound chat turns; clicks and payment events stay unlimited --- README.md | 2 +- server.py | 4 +++- tests/llm_proxy_check.sh | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7b7ca24..88b15b2 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ key stays on the server: it never goes to the browser, never into the repo, and | `AGENT_MARKET_LLM_MODELS` | comma-separated fallback chain, tried in order | | `AGENT_MARKET_LLM_TIMEOUT` / `_BUDGET` | hard cap per model attempt (12 s) and for the whole chain (28 s) | | `AGENT_MARKET_LLM_COOLDOWN` | how long a model that returned 429 gets skipped (60 s) | -| `AGENT_MARKET_CHAT_RPM` / `_BURST` | per-IP token bucket on `/api/chat` (12/min, burst 6). `CF-Connecting-IP` is trusted only from loopback, i.e. the tunnel. | +| `AGENT_MARKET_CHAT_RPM` / `_BURST` | per-IP token bucket on `/api/chat` turns that reach the LLM (12/min, burst 6). Deterministic UI events aren't limited. `CF-Connecting-IP` is trusted only from loopback, i.e. the tunnel. | `deploy.sh` loads `~/.config/agent-market/llm.env` and `~/.config/agent-market/.env` if they exist. Both sit outside the repo. The box runs OpenRouter free models: diff --git a/server.py b/server.py index 236af40..d9b8b40 100755 --- a/server.py +++ b/server.py @@ -312,7 +312,9 @@ class H(BaseHTTPRequestHandler): return self._json(200, self._webhook(body)) actor = self._actor(body) if path == "/api/chat": - ok, retry = rate_ok(self._client_ip()) + # Only free-text turns reach the LLM; deterministic UI events (clicks, payments) are not limited. + llm_bound = not isinstance(body.get("event"), dict) or A.LLM_EVENTS + ok, retry = rate_ok(self._client_ip()) if llm_bound else (True, 0) if not ok: return self._json(429, {"error": "rate_limited", "retry_after": retry, "agent": "rate-limit", "reply": f"You're sending messages quickly. Please wait about {retry}s and try again.", diff --git a/tests/llm_proxy_check.sh b/tests/llm_proxy_check.sh index c7e1ef5..c4a6d34 100755 --- a/tests/llm_proxy_check.sh +++ b/tests/llm_proxy_check.sh @@ -29,6 +29,8 @@ assert d["agent"]=="fallback" and d["llm_error"]=="all_models_failed" and d["act print("PASS all models rate-limited -> rule-based agent still renders", [a["type"] for a in d["actions"]])' curl -s localhost:18800/api/status | python3 -c ' import json,sys; a=json.load(sys.stdin)["agent"]; assert set(a["cooling_down"])=={"busy-model","busy-model-2"}, a; print("PASS 429ed models put on cooldown")' -CODES=$(for i in 1 2 3 4 5; do curl -s -o /dev/null -w '%{http_code} ' localhost:18801/api/chat -H 'Content-Type: application/json' -H 'CF-Connecting-IP: 203.0.113.9' -d '{"event":{"type":"landing"}}'; done) -OTHER=$(curl -s -o /dev/null -w '%{http_code}' localhost:18801/api/chat -H 'Content-Type: application/json' -H 'CF-Connecting-IP: 203.0.113.10' -d '{"event":{"type":"landing"}}') +CODES=$(for i in 1 2 3 4 5; do curl -s -o /dev/null -w '%{http_code} ' localhost:18801/api/chat -H 'Content-Type: application/json' -H 'CF-Connecting-IP: 203.0.113.9' -d '{"message":"show me bikes"}'; done) +OTHER=$(curl -s -o /dev/null -w '%{http_code}' localhost:18801/api/chat -H 'Content-Type: application/json' -H 'CF-Connecting-IP: 203.0.113.10' -d '{"message":"show me bikes"}') +EV=$(curl -s -o /dev/null -w '%{http_code}' localhost:18801/api/chat -H 'Content-Type: application/json' -H 'CF-Connecting-IP: 203.0.113.9' -d '{"event":{"type":"landing"}}') +[[ "$EV" == "200" ]] && echo "PASS UI events (no LLM call) not rate-limited" || { echo "FAIL event limited: $EV"; exit 1; } [[ "$CODES" == "200 200 200 429 429 " && "$OTHER" == "200" ]] && echo "PASS per-IP rate limit (burst 3: $CODES; other IP $OTHER)" || { echo "FAIL rate limit: $CODES / $OTHER"; exit 1; } -- 2.47.3