Create main.py
Browse files
main.py
ADDED
@@ -0,0 +1,291 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
import os
|
5 |
+
import random
|
6 |
+
import string
|
7 |
+
import time
|
8 |
+
import uuid
|
9 |
+
from http import HTTPStatus
|
10 |
+
from typing import AsyncGenerator, Dict, List, Any
|
11 |
+
|
12 |
+
import aiohttp
|
13 |
+
from fastapi import FastAPI, Request, HTTPException
|
14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
15 |
+
from fastapi.responses import StreamingResponse
|
16 |
+
from tenacity import (
|
17 |
+
retry,
|
18 |
+
stop_after_attempt,
|
19 |
+
wait_exponential,
|
20 |
+
retry_if_exception_type,
|
21 |
+
RetryError,
|
22 |
+
)
|
23 |
+
|
24 |
+
# βββ Logging βββ
|
25 |
+
logging.basicConfig(
|
26 |
+
level=logging.INFO,
|
27 |
+
format="%(asctime)s %(levelname)s %(message)s",
|
28 |
+
datefmt="%H:%M:%S",
|
29 |
+
)
|
30 |
+
logger = logging.getLogger("proxy")
|
31 |
+
|
32 |
+
# βββ Config βββ
|
33 |
+
BLACKBOX_URL = "https://www.blackbox.ai/api/chat"
|
34 |
+
CONNECTION_LIMIT = 200
|
35 |
+
CONNECTION_LIMIT_PER_HOST = 50
|
36 |
+
REQUEST_TIMEOUT = 300
|
37 |
+
WORKER_COUNT = 10
|
38 |
+
|
39 |
+
# βββ Static Headers βββ
|
40 |
+
HEADERS = {
|
41 |
+
"accept": "*/*",
|
42 |
+
"content-type": "application/json",
|
43 |
+
"origin": "https://www.blackbox.ai",
|
44 |
+
"referer": "https://www.blackbox.ai/",
|
45 |
+
"user-agent": (
|
46 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
47 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
48 |
+
"Chrome/136.0.0.0 Safari/537.36"
|
49 |
+
),
|
50 |
+
}
|
51 |
+
|
52 |
+
# βββ FastAPI Setup βββ
|
53 |
+
app = FastAPI()
|
54 |
+
app.add_middleware(
|
55 |
+
CORSMiddleware,
|
56 |
+
allow_origins=["*"],
|
57 |
+
allow_credentials=True,
|
58 |
+
allow_methods=["*"],
|
59 |
+
allow_headers=["*"],
|
60 |
+
)
|
61 |
+
|
62 |
+
HTTP_SESSION: aiohttp.ClientSession = None
|
63 |
+
REQUEST_QUEUE: asyncio.Queue = asyncio.Queue()
|
64 |
+
WORKER_TASKS: List[asyncio.Task] = []
|
65 |
+
|
66 |
+
# βββ Retryable Error βββ
|
67 |
+
class RetryableStatusError(Exception):
|
68 |
+
def __init__(self, status: int, text: str):
|
69 |
+
self.status = status
|
70 |
+
self.text = text
|
71 |
+
super().__init__(f"status={status} body={text[:100]}...")
|
72 |
+
|
73 |
+
RETRYABLE_STATUSES = {400, 429, 500, 502, 503, 504}
|
74 |
+
|
75 |
+
# βββ Random Data βββ
|
76 |
+
_ascii = string.ascii_letters + string.digits
|
77 |
+
def _rand(n, pool=_ascii): return "".join(random.choice(pool) for _ in range(n))
|
78 |
+
def random_email(): return _rand(12) + "@gmail.com"
|
79 |
+
def random_id(): return _rand(21, string.digits)
|
80 |
+
def random_customer_id(): return "cus_" + _rand(12)
|
81 |
+
|
82 |
+
# βββ Payload Generator βββ
|
83 |
+
def build_payload(messages: List[Dict[str, Any]]) -> Dict[str, Any]:
|
84 |
+
return {
|
85 |
+
"messages": messages,
|
86 |
+
"id": _rand(8),
|
87 |
+
"agentMode": {},
|
88 |
+
"codeModelMode": True,
|
89 |
+
"trendingAgentMode": {},
|
90 |
+
"isMicMode": False,
|
91 |
+
"userSystemPrompt": None,
|
92 |
+
"maxTokens": 1024,
|
93 |
+
"playgroundTopP": None,
|
94 |
+
"playgroundTemperature": None,
|
95 |
+
"isChromeExt": False,
|
96 |
+
"githubToken": "",
|
97 |
+
"clickedAnswer2": False,
|
98 |
+
"clickedAnswer3": False,
|
99 |
+
"clickedForceWebSearch": False,
|
100 |
+
"visitFromDelta": False,
|
101 |
+
"isMemoryEnabled": False,
|
102 |
+
"mobileClient": False,
|
103 |
+
"userSelectedModel": None,
|
104 |
+
"validated": str(uuid.uuid4()),
|
105 |
+
"imageGenerationMode": False,
|
106 |
+
"webSearchModePrompt": False,
|
107 |
+
"deepSearchMode": True,
|
108 |
+
"domains": None,
|
109 |
+
"vscodeClient": False,
|
110 |
+
"codeInterpreterMode": False,
|
111 |
+
"customProfile": {
|
112 |
+
"name": "",
|
113 |
+
"occupation": "",
|
114 |
+
"traits": [],
|
115 |
+
"additionalInfo": "",
|
116 |
+
"enableNewChats": False
|
117 |
+
},
|
118 |
+
"session": {
|
119 |
+
"user": {
|
120 |
+
"name": "S.C gaming",
|
121 |
+
"email": random_email(),
|
122 |
+
"image": "https://lh3.googleusercontent.com/a/default",
|
123 |
+
"id": random_id()
|
124 |
+
},
|
125 |
+
"expires": "2025-12-31T23:59:59Z",
|
126 |
+
"isNewUser": False
|
127 |
+
},
|
128 |
+
"isPremium": True,
|
129 |
+
"subscriptionCache": {
|
130 |
+
"status": "PREMIUM",
|
131 |
+
"customerId": random_customer_id(),
|
132 |
+
"expiryTimestamp": int(time.time()) + 30 * 86400,
|
133 |
+
"lastChecked": int(time.time()),
|
134 |
+
"isTrialSubscription": False
|
135 |
+
},
|
136 |
+
"beastMode": False,
|
137 |
+
"reasoningMode": False,
|
138 |
+
"designerMode": False
|
139 |
+
}
|
140 |
+
|
141 |
+
# βββ Retry Wrapper βββ
|
142 |
+
def log_retry(retry_state):
|
143 |
+
rid = retry_state.kwargs.get("request_id", "unknown")
|
144 |
+
attempt = retry_state.attempt_number
|
145 |
+
err = retry_state.outcome.exception()
|
146 |
+
logger.warning("[%s] retry %s/10 due to %s", rid, attempt, err)
|
147 |
+
|
148 |
+
@retry(
|
149 |
+
stop=stop_after_attempt(10),
|
150 |
+
wait=wait_exponential(min=1, max=10),
|
151 |
+
retry=retry_if_exception_type(
|
152 |
+
(aiohttp.ClientConnectionError, aiohttp.ClientResponseError, asyncio.TimeoutError, RetryableStatusError)
|
153 |
+
),
|
154 |
+
before_sleep=log_retry
|
155 |
+
)
|
156 |
+
async def get_blackbox_response(*, data, stream: bool, request_id: str) -> AsyncGenerator[str, None]:
|
157 |
+
assert HTTP_SESSION
|
158 |
+
async with HTTP_SESSION.post(BLACKBOX_URL, json=data, headers=HEADERS, timeout=REQUEST_TIMEOUT) as resp:
|
159 |
+
if resp.status != 200:
|
160 |
+
body = await resp.text()
|
161 |
+
logger.error("[%s] Upstream %s error: %s", request_id, BLACKBOX_URL, resp.status)
|
162 |
+
if resp.status in RETRYABLE_STATUSES:
|
163 |
+
raise RetryableStatusError(resp.status, body)
|
164 |
+
raise HTTPException(status_code=502, detail=f"Upstream error {resp.status}")
|
165 |
+
if stream:
|
166 |
+
async for chunk in resp.content.iter_any():
|
167 |
+
if chunk:
|
168 |
+
yield chunk.decode("utf-8", "ignore")
|
169 |
+
else:
|
170 |
+
yield await resp.text()
|
171 |
+
|
172 |
+
# βββ Worker Thread βββ
|
173 |
+
async def _worker():
|
174 |
+
while True:
|
175 |
+
try:
|
176 |
+
data, request_id, out_q = await REQUEST_QUEUE.get()
|
177 |
+
try:
|
178 |
+
async for piece in get_blackbox_response(data=data, stream=False, request_id=request_id):
|
179 |
+
await out_q.put(piece)
|
180 |
+
except Exception as e:
|
181 |
+
await out_q.put(f"Error:{e}")
|
182 |
+
finally:
|
183 |
+
await out_q.put(None)
|
184 |
+
REQUEST_QUEUE.task_done()
|
185 |
+
except asyncio.CancelledError:
|
186 |
+
break
|
187 |
+
|
188 |
+
# βββ Middleware βββ
|
189 |
+
@app.middleware("http")
|
190 |
+
async def add_request_id(request: Request, call_next):
|
191 |
+
request.state.request_id = rid = str(uuid.uuid4())
|
192 |
+
logger.info("[%s] %s %s", rid, request.method, request.url.path)
|
193 |
+
start = time.perf_counter()
|
194 |
+
resp = await call_next(request)
|
195 |
+
logger.info("[%s] finished in %.2fs", rid, time.perf_counter() - start)
|
196 |
+
return resp
|
197 |
+
|
198 |
+
# βββ Root & Health βββ
|
199 |
+
@app.get("/")
|
200 |
+
async def root():
|
201 |
+
return {"message": "API is running"}
|
202 |
+
|
203 |
+
@app.get("/health")
|
204 |
+
async def health():
|
205 |
+
return {"status": "ok"}
|
206 |
+
|
207 |
+
# βββ Chat Completion βββ
|
208 |
+
@app.post("/v1/chat/completions")
|
209 |
+
async def chat_completions(request: Request):
|
210 |
+
rid = request.state.request_id
|
211 |
+
try:
|
212 |
+
body = await request.json()
|
213 |
+
messages = body.get("messages", [])
|
214 |
+
if not messages:
|
215 |
+
raise HTTPException(status_code=400, detail="Missing 'messages'")
|
216 |
+
stream = body.get("stream", False)
|
217 |
+
|
218 |
+
payload = build_payload(messages)
|
219 |
+
|
220 |
+
if not stream:
|
221 |
+
q: asyncio.Queue = asyncio.Queue()
|
222 |
+
await REQUEST_QUEUE.put((payload, rid, q))
|
223 |
+
chunks: List[str] = []
|
224 |
+
while True:
|
225 |
+
part = await q.get()
|
226 |
+
if part is None:
|
227 |
+
break
|
228 |
+
if isinstance(part, str) and part.startswith("Error:"):
|
229 |
+
raise HTTPException(status_code=502, detail=part)
|
230 |
+
chunks.append(part)
|
231 |
+
answer = "".join(chunks) or "No response."
|
232 |
+
return {
|
233 |
+
"id": str(uuid.uuid4()),
|
234 |
+
"object": "chat.completion",
|
235 |
+
"created": int(time.time()),
|
236 |
+
"model": "DeepResearch",
|
237 |
+
"choices": [
|
238 |
+
{
|
239 |
+
"index": 0,
|
240 |
+
"message": {"role": "assistant", "content": answer},
|
241 |
+
"finish_reason": "stop",
|
242 |
+
}
|
243 |
+
],
|
244 |
+
}
|
245 |
+
|
246 |
+
async def event_stream():
|
247 |
+
try:
|
248 |
+
async for chunk in get_blackbox_response(data=payload, stream=True, request_id=rid):
|
249 |
+
msg = {
|
250 |
+
"id": str(uuid.uuid4()),
|
251 |
+
"object": "chat.completion.chunk",
|
252 |
+
"created": int(time.time()),
|
253 |
+
"model": "DeepResearch",
|
254 |
+
"choices": [{"index": 0, "delta": {"content": chunk}}],
|
255 |
+
}
|
256 |
+
yield f"data: {json.dumps(msg)}\n\n"
|
257 |
+
yield "data: [DONE]\n\n"
|
258 |
+
except Exception as e:
|
259 |
+
logger.error("[%s] stream error: %s", rid, e)
|
260 |
+
raise HTTPException(status_code=500, detail="streaming error")
|
261 |
+
|
262 |
+
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
263 |
+
|
264 |
+
except json.JSONDecodeError:
|
265 |
+
raise HTTPException(status_code=400, detail="Invalid JSON")
|
266 |
+
except RetryError as re:
|
267 |
+
logger.error("[%s] retries failed: %s", rid, re)
|
268 |
+
raise HTTPException(status_code=502, detail="Blackbox upstream failed")
|
269 |
+
except Exception as e:
|
270 |
+
logger.exception("[%s] error", rid)
|
271 |
+
raise HTTPException(status_code=500, detail="Internal proxy error")
|
272 |
+
|
273 |
+
# βββ Startup & Shutdown βββ
|
274 |
+
@app.on_event("startup")
|
275 |
+
async def startup():
|
276 |
+
global HTTP_SESSION, WORKER_TASKS
|
277 |
+
HTTP_SESSION = aiohttp.ClientSession(
|
278 |
+
connector=aiohttp.TCPConnector(limit=CONNECTION_LIMIT, limit_per_host=CONNECTION_LIMIT_PER_HOST),
|
279 |
+
timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT),
|
280 |
+
)
|
281 |
+
WORKER_TASKS = [asyncio.create_task(_worker()) for _ in range(WORKER_COUNT)]
|
282 |
+
logger.info("Started %d workers", WORKER_COUNT)
|
283 |
+
|
284 |
+
@app.on_event("shutdown")
|
285 |
+
async def shutdown():
|
286 |
+
for t in WORKER_TASKS:
|
287 |
+
t.cancel()
|
288 |
+
await asyncio.gather(*WORKER_TASKS, return_exceptions=True)
|
289 |
+
if HTTP_SESSION:
|
290 |
+
await HTTP_SESSION.close()
|
291 |
+
logger.info("Shutdown complete")
|