Thread lock 2
Browse files
app.py
CHANGED
|
@@ -36,16 +36,32 @@ if not OPENROUTER_API_KEY:
|
|
| 36 |
# outbound OpenRouter calls behind a single lock and enforce a minimum gap
|
| 37 |
# between them. Whichever request arrives second simply waits its turn.
|
| 38 |
# ---------------------------------------------------------------------------
|
| 39 |
-
MIN_INTERVAL_SECONDS = float(os.environ.get("OPENROUTER_MIN_INTERVAL", "
|
|
|
|
|
|
|
| 40 |
|
| 41 |
_or_lock = threading.Lock()
|
| 42 |
_last_call_time = 0.0
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
def call_openrouter(payload: dict, headers: dict, timeout: int = 20):
|
| 45 |
"""Thread-safe, rate-limited call to OpenRouter's chat completions endpoint.
|
| 46 |
|
| 47 |
-
Ensures at least MIN_INTERVAL_SECONDS has elapsed since the previous
|
| 48 |
-
|
|
|
|
|
|
|
| 49 |
"""
|
| 50 |
global _last_call_time
|
| 51 |
|
|
@@ -62,6 +78,16 @@ def call_openrouter(payload: dict, headers: dict, timeout: int = 20):
|
|
| 62 |
response = requests.post(url, headers=headers, json=payload, timeout=timeout)
|
| 63 |
_last_call_time = time.monotonic()
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
return response
|
| 66 |
|
| 67 |
|
|
|
|
| 36 |
# outbound OpenRouter calls behind a single lock and enforce a minimum gap
|
| 37 |
# between them. Whichever request arrives second simply waits its turn.
|
| 38 |
# ---------------------------------------------------------------------------
|
| 39 |
+
MIN_INTERVAL_SECONDS = float(os.environ.get("OPENROUTER_MIN_INTERVAL", "6.0"))
|
| 40 |
+
MAX_RETRIES = int(os.environ.get("OPENROUTER_MAX_RETRIES", "3"))
|
| 41 |
+
RETRY_BACKOFF_SECONDS = float(os.environ.get("OPENROUTER_RETRY_BACKOFF", "5.0"))
|
| 42 |
|
| 43 |
_or_lock = threading.Lock()
|
| 44 |
_last_call_time = 0.0
|
| 45 |
|
| 46 |
+
def _is_rate_limited(response: requests.Response) -> bool:
|
| 47 |
+
"""Detect a 429 whether it comes as an HTTP status or embedded in a 200 body
|
| 48 |
+
(OpenRouter sometimes returns 200 with {"error": {"code": 429, ...}})."""
|
| 49 |
+
if response.status_code == 429:
|
| 50 |
+
return True
|
| 51 |
+
try:
|
| 52 |
+
body = response.json()
|
| 53 |
+
except ValueError:
|
| 54 |
+
return False
|
| 55 |
+
return isinstance(body, dict) and body.get("error", {}).get("code") == 429
|
| 56 |
+
|
| 57 |
+
|
| 58 |
def call_openrouter(payload: dict, headers: dict, timeout: int = 20):
|
| 59 |
"""Thread-safe, rate-limited call to OpenRouter's chat completions endpoint.
|
| 60 |
|
| 61 |
+
- Ensures at least MIN_INTERVAL_SECONDS has elapsed since the previous
|
| 62 |
+
OpenRouter call (across ALL endpoints) before firing this one.
|
| 63 |
+
- If the provider still returns a 429 (as a status code or embedded in the
|
| 64 |
+
JSON body), retries with exponential backoff up to MAX_RETRIES times.
|
| 65 |
"""
|
| 66 |
global _last_call_time
|
| 67 |
|
|
|
|
| 78 |
response = requests.post(url, headers=headers, json=payload, timeout=timeout)
|
| 79 |
_last_call_time = time.monotonic()
|
| 80 |
|
| 81 |
+
attempt = 0
|
| 82 |
+
while _is_rate_limited(response) and attempt < MAX_RETRIES:
|
| 83 |
+
backoff = RETRY_BACKOFF_SECONDS * (2 ** attempt)
|
| 84 |
+
logger.warning(f"OpenRouter rate limited (attempt {attempt + 1}/{MAX_RETRIES}); "
|
| 85 |
+
f"backing off {backoff:.1f}s before retry")
|
| 86 |
+
time.sleep(backoff)
|
| 87 |
+
response = requests.post(url, headers=headers, json=payload, timeout=timeout)
|
| 88 |
+
_last_call_time = time.monotonic()
|
| 89 |
+
attempt += 1
|
| 90 |
+
|
| 91 |
return response
|
| 92 |
|
| 93 |
|