Norod78 commited on
Commit
38909c2
·
verified ·
1 Parent(s): 4aa1f1d

Space: session limit counts only non-cached answers (cache-only at limit)

Browse files
Files changed (1) hide show
  1. app.py +28 -11
app.py CHANGED
@@ -289,7 +289,14 @@ def _api_url() -> str:
289
  return str(_CFG["api_url"]).strip()
290
 
291
 
292
- def _session_allow(session_hash: str, timestamps: list[float]) -> tuple[bool, list[float]]:
 
 
 
 
 
 
 
293
  now = time.monotonic()
294
  cap = int(_CFG["asks_per_min"])
295
  window_s = float(_CFG["rate_window_s"])
@@ -306,13 +313,18 @@ def _session_allow(session_hash: str, timestamps: list[float]) -> tuple[bool, li
306
  for bucket in stale:
307
  _SESSION_HITS.pop(bucket, None)
308
  backup = [stamp for stamp in _SESSION_HITS.get(key, []) if now - stamp < window_s]
309
- if cap <= 0 or len(window) >= cap or len(backup) >= cap:
310
- _SESSION_HITS[key] = backup
311
- return False, window
312
- window.append(now)
313
- backup.append(now)
314
  _SESSION_HITS[key] = backup
315
- return True, window
 
 
 
 
 
 
 
 
 
 
316
 
317
 
318
  def _friendly_http_error(status: int) -> str:
@@ -348,23 +360,28 @@ def ask(
348
  return render_notice(_msg_too_long()), timestamps or []
349
 
350
  session_hash = getattr(request, "session_hash", None) or ""
351
- allowed, timestamps = _session_allow(session_hash, timestamps or [])
352
- if not allowed:
353
- return render_notice(_ui("msg_rate")), timestamps
354
 
355
  token = os.environ.get("THEANSWER_API_TOKEN") or ""
356
  if not token.strip():
357
  return render_notice(_ui("msg_unconfigured")), timestamps
358
 
 
 
 
359
  try:
360
  resp = httpx.post(
361
  _api_url(),
362
  json={"question": question},
363
- headers={"Authorization": f"Bearer {token}"},
364
  timeout=float(_CFG["http_timeout_s"]),
365
  )
366
  except (httpx.TimeoutException, httpx.NetworkError, httpx.HTTPError):
 
 
367
  return render_notice(_ui("msg_network")), timestamps
 
 
368
 
369
  if resp.status_code != 200:
370
  return render_notice(_friendly_http_error(resp.status_code)), timestamps
 
289
  return str(_CFG["api_url"]).strip()
290
 
291
 
292
+ # API protocol headers (v2/api/public.py): the response says whether it came from the question cache;
293
+ # at the session limit the Space asks for a cache-only answer, so cached repeats never hit the limit.
294
+ CACHE_HEADER = "X-TheAnswer-Cache"
295
+ CACHE_ONLY_HEADER = "X-TheAnswer-Cache-Only"
296
+
297
+
298
+ def _session_room(session_hash: str, timestamps: list[float]) -> tuple[bool, list[float]]:
299
+ """Whether this session may trigger a new (non-cached) answer now. Records nothing."""
300
  now = time.monotonic()
301
  cap = int(_CFG["asks_per_min"])
302
  window_s = float(_CFG["rate_window_s"])
 
313
  for bucket in stale:
314
  _SESSION_HITS.pop(bucket, None)
315
  backup = [stamp for stamp in _SESSION_HITS.get(key, []) if now - stamp < window_s]
 
 
 
 
 
316
  _SESSION_HITS[key] = backup
317
+ room = cap > 0 and len(window) < cap and len(backup) < cap
318
+ return room, window
319
+
320
+
321
+ def _session_record(session_hash: str, window: list[float]) -> list[float]:
322
+ """Count one non-cached answer against this session."""
323
+ now = time.monotonic()
324
+ key = session_hash or ""
325
+ with _SESSION_LOCK:
326
+ _SESSION_HITS.setdefault(key, []).append(now)
327
+ return list(window) + [now]
328
 
329
 
330
  def _friendly_http_error(status: int) -> str:
 
360
  return render_notice(_msg_too_long()), timestamps or []
361
 
362
  session_hash = getattr(request, "session_hash", None) or ""
363
+ room, timestamps = _session_room(session_hash, timestamps or [])
 
 
364
 
365
  token = os.environ.get("THEANSWER_API_TOKEN") or ""
366
  if not token.strip():
367
  return render_notice(_ui("msg_unconfigured")), timestamps
368
 
369
+ headers = {"Authorization": f"Bearer {token}"}
370
+ if not room: # at the session limit: only a cached answer may come back (no new Gemini call)
371
+ headers[CACHE_ONLY_HEADER] = "1"
372
  try:
373
  resp = httpx.post(
374
  _api_url(),
375
  json={"question": question},
376
+ headers=headers,
377
  timeout=float(_CFG["http_timeout_s"]),
378
  )
379
  except (httpx.TimeoutException, httpx.NetworkError, httpx.HTTPError):
380
+ if room:
381
+ timestamps = _session_record(session_hash, timestamps)
382
  return render_notice(_ui("msg_network")), timestamps
383
+ if room and resp.headers.get(CACHE_HEADER) != "hit": # only non-cached answers count
384
+ timestamps = _session_record(session_hash, timestamps)
385
 
386
  if resp.status_code != 200:
387
  return render_notice(_friendly_http_error(resp.status_code)), timestamps