adAstra144 commited on
Commit
9bdced2
·
verified ·
1 Parent(s): ea73075

Thread lock

Browse files
Files changed (1) hide show
  1. app.py +52 -10
app.py CHANGED
@@ -4,6 +4,8 @@ from pydantic import BaseModel
4
  import requests
5
  import os
6
  import logging
 
 
7
 
8
  # Configure logging
9
  logging.basicConfig(level=logging.INFO)
@@ -25,6 +27,44 @@ OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
25
  if not OPENROUTER_API_KEY:
26
  raise RuntimeError("OPENROUTER_API_KEY environment variable not set!")
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Request schema
29
  class ExplainRequest(BaseModel):
30
  message: str
@@ -74,7 +114,6 @@ def explain(req: ExplainRequest):
74
  "Respond using the same language as the message."
75
  )
76
 
77
- url = "https://openrouter.ai/api/v1/chat/completions"
78
  headers = {
79
  "Authorization": f"Bearer {OPENROUTER_API_KEY}",
80
  "Content-Type": "application/json"
@@ -89,17 +128,17 @@ def explain(req: ExplainRequest):
89
 
90
  try:
91
  logger.info(f"Calling OpenRouter /explain with model: {req.model_id}")
92
- response = requests.post(url, headers=headers, json=payload, timeout=20)
93
  response.raise_for_status()
94
  result = response.json()
95
-
96
  # Check for OpenRouter error in response body (even with 200 status)
97
  if "error" in result:
98
  error_detail = result.get("error", {}).get("message", str(result.get("error")))
99
  logger.error(f"OpenRouter returned error in /explain: {error_detail}")
100
  logger.error(f"Full response: {result}")
101
  raise HTTPException(status_code=500, detail=f"OpenRouter error: {error_detail}")
102
-
103
  reply = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
104
  if not reply:
105
  logger.error(f"OpenRouter returned empty response in /explain. Full result: {result}")
@@ -109,6 +148,8 @@ def explain(req: ExplainRequest):
109
  except requests.RequestException as e:
110
  logger.error(f"OpenRouter network error in /explain: {e}")
111
  raise HTTPException(status_code=500, detail=f"Error contacting OpenRouter: {e}")
 
 
112
  except Exception as e:
113
  logger.error(f"Unexpected error in /explain: {e}")
114
  raise HTTPException(status_code=500, detail=f"Unexpected error: {e}")
@@ -129,7 +170,6 @@ def classify(req: ExplainRequest):
129
  'No other text.'
130
  )
131
 
132
- url = "https://openrouter.ai/api/v1/chat/completions"
133
  headers = {
134
  "Authorization": f"Bearer {OPENROUTER_API_KEY}",
135
  "Content-Type": "application/json"
@@ -146,19 +186,19 @@ def classify(req: ExplainRequest):
146
 
147
  try:
148
  logger.info(f"Calling OpenRouter /classify with model: {model_id}")
149
- response = requests.post(url, headers=headers, json=payload, timeout=20)
150
  response.raise_for_status()
151
  result = response.json()
152
-
153
  # Check for OpenRouter error in response body (even with 200 status)
154
  if "error" in result:
155
  error_detail = result.get("error", {}).get("message", str(result.get("error")))
156
  logger.error(f"OpenRouter returned error in /classify: {error_detail}")
157
  logger.error(f"Full response: {result}")
158
  raise HTTPException(status_code=500, detail=f"OpenRouter error: {error_detail}")
159
-
160
  reply = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
161
-
162
  if not reply:
163
  logger.warning(f"Empty content in response. Finish reason: {result.get('choices', [{}])[0].get('finish_reason')}")
164
  # If content is empty, try to extract from reasoning or log for debugging
@@ -167,12 +207,14 @@ def classify(req: ExplainRequest):
167
  logger.warning(f"Model has reasoning but no content. This may indicate truncation.")
168
  logger.error(f"OpenRouter returned empty response in /classify. Full result: {result}")
169
  raise HTTPException(status_code=500, detail="No response from OpenRouter")
170
-
171
  logger.info(f"Classification successful with model {model_id}: {reply}")
172
  return {"reply": reply, "status": response.status_code, "model": model_id}
173
  except requests.RequestException as e:
174
  logger.error(f"OpenRouter network error in /classify: {e}")
175
  raise HTTPException(status_code=500, detail=f"Error contacting OpenRouter: {e}")
 
 
176
  except Exception as e:
177
  logger.error(f"Unexpected error in /classify: {e}")
178
  raise HTTPException(status_code=500, detail=f"Unexpected error: {e}")
 
4
  import requests
5
  import os
6
  import logging
7
+ import threading
8
+ import time
9
 
10
  # Configure logging
11
  logging.basicConfig(level=logging.INFO)
 
27
  if not OPENROUTER_API_KEY:
28
  raise RuntimeError("OPENROUTER_API_KEY environment variable not set!")
29
 
30
+ # ---------------------------------------------------------------------------
31
+ # Global rate limiter for OpenRouter calls
32
+ #
33
+ # Free-tier OpenRouter models throttle/reject requests that arrive too close
34
+ # together. Since /classify and /explain can be hit concurrently by the
35
+ # frontend (each running in its own thread pool worker), we serialize ALL
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", "2.0"))
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
+ OpenRouter call (across ALL endpoints) before firing this one.
49
+ """
50
+ global _last_call_time
51
+
52
+ url = "https://openrouter.ai/api/v1/chat/completions"
53
+
54
+ with _or_lock:
55
+ now = time.monotonic()
56
+ elapsed = now - _last_call_time
57
+ wait_for = MIN_INTERVAL_SECONDS - elapsed
58
+ if wait_for > 0:
59
+ logger.info(f"Rate limiting: waiting {wait_for:.2f}s before next OpenRouter call")
60
+ time.sleep(wait_for)
61
+
62
+ response = requests.post(url, headers=headers, json=payload, timeout=timeout)
63
+ _last_call_time = time.monotonic()
64
+
65
+ return response
66
+
67
+
68
  # Request schema
69
  class ExplainRequest(BaseModel):
70
  message: str
 
114
  "Respond using the same language as the message."
115
  )
116
 
 
117
  headers = {
118
  "Authorization": f"Bearer {OPENROUTER_API_KEY}",
119
  "Content-Type": "application/json"
 
128
 
129
  try:
130
  logger.info(f"Calling OpenRouter /explain with model: {req.model_id}")
131
+ response = call_openrouter(payload, headers)
132
  response.raise_for_status()
133
  result = response.json()
134
+
135
  # Check for OpenRouter error in response body (even with 200 status)
136
  if "error" in result:
137
  error_detail = result.get("error", {}).get("message", str(result.get("error")))
138
  logger.error(f"OpenRouter returned error in /explain: {error_detail}")
139
  logger.error(f"Full response: {result}")
140
  raise HTTPException(status_code=500, detail=f"OpenRouter error: {error_detail}")
141
+
142
  reply = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
143
  if not reply:
144
  logger.error(f"OpenRouter returned empty response in /explain. Full result: {result}")
 
148
  except requests.RequestException as e:
149
  logger.error(f"OpenRouter network error in /explain: {e}")
150
  raise HTTPException(status_code=500, detail=f"Error contacting OpenRouter: {e}")
151
+ except HTTPException:
152
+ raise
153
  except Exception as e:
154
  logger.error(f"Unexpected error in /explain: {e}")
155
  raise HTTPException(status_code=500, detail=f"Unexpected error: {e}")
 
170
  'No other text.'
171
  )
172
 
 
173
  headers = {
174
  "Authorization": f"Bearer {OPENROUTER_API_KEY}",
175
  "Content-Type": "application/json"
 
186
 
187
  try:
188
  logger.info(f"Calling OpenRouter /classify with model: {model_id}")
189
+ response = call_openrouter(payload, headers)
190
  response.raise_for_status()
191
  result = response.json()
192
+
193
  # Check for OpenRouter error in response body (even with 200 status)
194
  if "error" in result:
195
  error_detail = result.get("error", {}).get("message", str(result.get("error")))
196
  logger.error(f"OpenRouter returned error in /classify: {error_detail}")
197
  logger.error(f"Full response: {result}")
198
  raise HTTPException(status_code=500, detail=f"OpenRouter error: {error_detail}")
199
+
200
  reply = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
201
+
202
  if not reply:
203
  logger.warning(f"Empty content in response. Finish reason: {result.get('choices', [{}])[0].get('finish_reason')}")
204
  # If content is empty, try to extract from reasoning or log for debugging
 
207
  logger.warning(f"Model has reasoning but no content. This may indicate truncation.")
208
  logger.error(f"OpenRouter returned empty response in /classify. Full result: {result}")
209
  raise HTTPException(status_code=500, detail="No response from OpenRouter")
210
+
211
  logger.info(f"Classification successful with model {model_id}: {reply}")
212
  return {"reply": reply, "status": response.status_code, "model": model_id}
213
  except requests.RequestException as e:
214
  logger.error(f"OpenRouter network error in /classify: {e}")
215
  raise HTTPException(status_code=500, detail=f"Error contacting OpenRouter: {e}")
216
+ except HTTPException:
217
+ raise
218
  except Exception as e:
219
  logger.error(f"Unexpected error in /classify: {e}")
220
  raise HTTPException(status_code=500, detail=f"Unexpected error: {e}")