Update main.py
Browse files
main.py
CHANGED
@@ -9,11 +9,10 @@ import asyncio
|
|
9 |
import time
|
10 |
from collections import defaultdict
|
11 |
from typing import List, Dict, Any, Optional, Union, AsyncGenerator
|
12 |
-
from datetime import datetime
|
13 |
|
14 |
from aiohttp import ClientSession, ClientResponseError
|
15 |
from fastapi import FastAPI, HTTPException, Request, Depends, Header
|
16 |
-
from fastapi.responses import JSONResponse
|
17 |
from pydantic import BaseModel
|
18 |
|
19 |
# Configure logging
|
@@ -39,13 +38,6 @@ rate_limit_store = defaultdict(lambda: {"count": 0, "timestamp": time.time()})
|
|
39 |
CLEANUP_INTERVAL = 60 # seconds
|
40 |
RATE_LIMIT_WINDOW = 60 # seconds
|
41 |
|
42 |
-
class ImageResponse:
|
43 |
-
def __init__(self, images: Union[str, List[str]], alt: str = "Generated Image"):
|
44 |
-
if isinstance(images, str):
|
45 |
-
images = [images]
|
46 |
-
self.images = images
|
47 |
-
self.alt = alt
|
48 |
-
|
49 |
class Blackbox:
|
50 |
label = "Blackbox AI"
|
51 |
url = "https://www.blackbox.ai"
|
@@ -191,7 +183,7 @@ class Blackbox:
|
|
191 |
|
192 |
@staticmethod
|
193 |
def clean_response(text: str) -> str:
|
194 |
-
pattern = r'^\$\@\$v=undefined-rv1
|
195 |
cleaned_text = re.sub(pattern, '', text)
|
196 |
return cleaned_text
|
197 |
|
@@ -203,19 +195,9 @@ class Blackbox:
|
|
203 |
proxy: Optional[str] = None,
|
204 |
websearch: bool = False,
|
205 |
**kwargs
|
206 |
-
) -> AsyncGenerator[Union[str,
|
207 |
"""
|
208 |
Creates an asynchronous generator for streaming responses from Blackbox AI.
|
209 |
-
|
210 |
-
Parameters:
|
211 |
-
model (str): Model to use for generating responses.
|
212 |
-
messages (List[Dict[str, str]]): Message history.
|
213 |
-
proxy (Optional[str]): Proxy URL, if needed.
|
214 |
-
websearch (bool): Enables or disables web search mode.
|
215 |
-
**kwargs: Additional keyword arguments.
|
216 |
-
|
217 |
-
Yields:
|
218 |
-
Union[str, ImageResponse]: Segments of the generated response or ImageResponse objects.
|
219 |
"""
|
220 |
model = cls.get_model(model)
|
221 |
|
@@ -234,7 +216,7 @@ class Blackbox:
|
|
234 |
content = message.get('content', '')
|
235 |
if role and content:
|
236 |
formatted_prompt += f"{role}: {content}\n"
|
237 |
-
|
238 |
if prefix:
|
239 |
formatted_prompt = f"{prefix} {formatted_prompt}".strip()
|
240 |
|
@@ -295,18 +277,6 @@ class Blackbox:
|
|
295 |
"userSelectedModel": cls.userSelectedModel.get(model, model)
|
296 |
}
|
297 |
|
298 |
-
headers_chat = {
|
299 |
-
'Accept': 'text/x-component',
|
300 |
-
'Content-Type': 'text/plain;charset=UTF-8',
|
301 |
-
'Referer': f'{cls.url}/chat/{chat_id}?model={model}',
|
302 |
-
'next-action': next_action,
|
303 |
-
'next-router-state-tree': next_router_state_tree,
|
304 |
-
'next-url': '/'
|
305 |
-
}
|
306 |
-
headers_chat_combined = {**common_headers, **headers_chat}
|
307 |
-
|
308 |
-
data_chat = '[]'
|
309 |
-
|
310 |
async with ClientSession(headers=common_headers) as session:
|
311 |
try:
|
312 |
async with session.post(
|
@@ -316,14 +286,42 @@ class Blackbox:
|
|
316 |
proxy=proxy
|
317 |
) as response_api_chat:
|
318 |
response_api_chat.raise_for_status()
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
327 |
except ClientResponseError as e:
|
328 |
error_text = f"Error {e.status}: {e.message}"
|
329 |
try:
|
@@ -336,79 +334,6 @@ class Blackbox:
|
|
336 |
except Exception as e:
|
337 |
yield f"Unexpected error during /api/chat request: {str(e)}"
|
338 |
|
339 |
-
# Not clear what to do with this second request; keeping it for compatibility
|
340 |
-
chat_url = f'{cls.url}/chat/{chat_id}?model={model}'
|
341 |
-
|
342 |
-
try:
|
343 |
-
async with session.post(
|
344 |
-
chat_url,
|
345 |
-
headers=headers_chat_combined,
|
346 |
-
data=data_chat,
|
347 |
-
proxy=proxy
|
348 |
-
) as response_chat:
|
349 |
-
response_chat.raise_for_status()
|
350 |
-
pass
|
351 |
-
except ClientResponseError as e:
|
352 |
-
error_text = f"Error {e.status}: {e.message}"
|
353 |
-
try:
|
354 |
-
error_response = await e.response.text()
|
355 |
-
cleaned_error = cls.clean_response(error_response)
|
356 |
-
error_text += f" - {cleaned_error}"
|
357 |
-
except Exception:
|
358 |
-
pass
|
359 |
-
yield error_text
|
360 |
-
except Exception as e:
|
361 |
-
yield f"Unexpected error during /chat/{chat_id} request: {str(e)}"
|
362 |
-
|
363 |
-
# Custom exception for model not working
|
364 |
-
class ModelNotWorkingException(Exception):
|
365 |
-
def __init__(self, model: str):
|
366 |
-
self.model = model
|
367 |
-
self.message = f"The model '{model}' is currently not working. Please try another model or wait for it to be fixed."
|
368 |
-
super().__init__(self.message)
|
369 |
-
|
370 |
-
async def cleanup_rate_limit_stores():
|
371 |
-
"""
|
372 |
-
Periodically cleans up stale entries in the rate_limit_store to prevent memory bloat.
|
373 |
-
"""
|
374 |
-
while True:
|
375 |
-
current_time = time.time()
|
376 |
-
ips_to_delete = [ip for ip, value in rate_limit_store.items() if current_time - value["timestamp"] > RATE_LIMIT_WINDOW * 2]
|
377 |
-
for ip in ips_to_delete:
|
378 |
-
del rate_limit_store[ip]
|
379 |
-
logger.debug(f"Cleaned up rate_limit_store for IP: {ip}")
|
380 |
-
await asyncio.sleep(CLEANUP_INTERVAL)
|
381 |
-
|
382 |
-
async def rate_limiter_per_ip(request: Request):
|
383 |
-
"""
|
384 |
-
Rate limiter that enforces a limit based on the client's IP address.
|
385 |
-
"""
|
386 |
-
client_ip = request.client.host
|
387 |
-
current_time = time.time()
|
388 |
-
|
389 |
-
# Initialize or update the count and timestamp
|
390 |
-
if current_time - rate_limit_store[client_ip]["timest(由Dream 提供Free https://opus.gptuu.com)amp"] > RATE_LIMIT_WINDOW:
|
391 |
-
rate_limit_store[client_ip] = {"count": 1, "timestamp": current_time}
|
392 |
-
else:
|
393 |
-
if rate_limit_store[client_ip]["count"] >= RATE_LIMIT:
|
394 |
-
logger.warning(f"Rate limit exceeded for IP address: {client_ip}")
|
395 |
-
raise HTTPException(status_code=429, detail='Rate limit exceeded for IP address | NiansuhAI')
|
396 |
-
rate_limit_store[client_ip]["count"] += 1
|
397 |
-
|
398 |
-
async def get_api_key(request: Request, authorization: str = Header(None)) -> str:
|
399 |
-
"""
|
400 |
-
Dependency to extract and validate the API key from the Authorization header.
|
401 |
-
"""
|
402 |
-
client_ip = request.client.host
|
403 |
-
if authorization is None or not authorization.startswith('Bearer '):
|
404 |
-
logger.warning(f"Invalid or missing authorization header from IP: {client_ip}")
|
405 |
-
raise HTTPException(status_code=401, detail='Invalid authorization header format')
|
406 |
-
api_key = authorization[7:]
|
407 |
-
if api_key not in API_KEYS:
|
408 |
-
logger.warning(f"Invalid API key attempted: {api_key} from IP: {client_ip}")
|
409 |
-
raise HTTPException(status_code=401, detail='Invalid API key')
|
410 |
-
return api_key
|
411 |
-
|
412 |
# FastAPI app setup
|
413 |
app = FastAPI()
|
414 |
|
@@ -449,7 +374,6 @@ class Message(BaseModel):
|
|
449 |
class ChatRequest(BaseModel):
|
450 |
model: str
|
451 |
messages: List[Message]
|
452 |
-
stream: Optional[bool] = False # Added for streaming support
|
453 |
temperature: Optional[float] = 1.0
|
454 |
top_p: Optional[float] = 1.0
|
455 |
n: Optional[int] = 1
|
@@ -459,17 +383,6 @@ class ChatRequest(BaseModel):
|
|
459 |
logit_bias: Optional[Dict[str, float]] = None
|
460 |
user: Optional[str] = None
|
461 |
|
462 |
-
# Helper function to collect responses from async generator
|
463 |
-
async def collect_response_content(generator: AsyncGenerator[Union[str, ImageResponse], None]) -> str:
|
464 |
-
response_content = ''
|
465 |
-
async for chunk in generator:
|
466 |
-
if isinstance(chunk, str):
|
467 |
-
response_content += chunk
|
468 |
-
elif isinstance(chunk, ImageResponse):
|
469 |
-
# Handle image response if needed
|
470 |
-
response_content += f"[Image: {chunk.alt}] {', '.join(chunk.images)}\n"
|
471 |
-
return response_content
|
472 |
-
|
473 |
@app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
|
474 |
async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
|
475 |
client_ip = req.client.host
|
@@ -485,63 +398,35 @@ async def chat_completions(request: ChatRequest, req: Request, api_key: str = De
|
|
485 |
raise HTTPException(status_code=400, detail="Requested model is not available.")
|
486 |
|
487 |
# Process the request with actual message content, but don't log it
|
488 |
-
|
489 |
model=request.model,
|
490 |
messages=[{"role": msg.role, "content": msg.content} for msg in request.messages],
|
491 |
temperature=request.temperature,
|
492 |
max_tokens=request.max_tokens
|
493 |
)
|
494 |
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
logger.info(f"Streaming response enabled for API key: {api_key} | IP: {client_ip}")
|
519 |
-
|
520 |
-
return StreamingResponse(stream_response(), media_type="text/event-stream")
|
521 |
-
else:
|
522 |
-
response_content = await collect_response_content(generator)
|
523 |
-
logger.info(f"Completed response generation for API key: {api_key} | IP: {client_ip}")
|
524 |
-
return {
|
525 |
-
"id": f"chatcmpl-{uuid.uuid4()}",
|
526 |
-
"object": "chat.completion",
|
527 |
-
"created": int(datetime.now().timestamp()),
|
528 |
-
"model": request.model,
|
529 |
-
"choices": [
|
530 |
-
{
|
531 |
-
"index": 0,
|
532 |
-
"message": {
|
533 |
-
"role": "assistant",
|
534 |
-
"content": response_content
|
535 |
-
},
|
536 |
-
"finish_reason": "stop"
|
537 |
-
}
|
538 |
-
],
|
539 |
-
"usage": {
|
540 |
-
"prompt_tokens": sum(len(msg.content.split()) for msg in request.messages),
|
541 |
-
"completion_tokens": len(response_content.split()),
|
542 |
-
"total_tokens": sum(len(msg.content.split()) for msg in request.messages) + len(response_content.split())
|
543 |
-
},
|
544 |
-
}
|
545 |
except ModelNotWorkingException as e:
|
546 |
logger.warning(f"Model not working: {e} | IP: {client_ip}")
|
547 |
raise HTTPException(status_code=503, detail=str(e))
|
@@ -582,7 +467,3 @@ async def http_exception_handler(request: Request, exc: HTTPException):
|
|
582 |
}
|
583 |
},
|
584 |
)
|
585 |
-
|
586 |
-
if __name__ == "__main__":
|
587 |
-
import uvicorn
|
588 |
-
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
9 |
import time
|
10 |
from collections import defaultdict
|
11 |
from typing import List, Dict, Any, Optional, Union, AsyncGenerator
|
|
|
12 |
|
13 |
from aiohttp import ClientSession, ClientResponseError
|
14 |
from fastapi import FastAPI, HTTPException, Request, Depends, Header
|
15 |
+
from fastapi.responses import JSONResponse
|
16 |
from pydantic import BaseModel
|
17 |
|
18 |
# Configure logging
|
|
|
38 |
CLEANUP_INTERVAL = 60 # seconds
|
39 |
RATE_LIMIT_WINDOW = 60 # seconds
|
40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
class Blackbox:
|
42 |
label = "Blackbox AI"
|
43 |
url = "https://www.blackbox.ai"
|
|
|
183 |
|
184 |
@staticmethod
|
185 |
def clean_response(text: str) -> str:
|
186 |
+
pattern = r'^\$\@\$v=undefined-rv1\$\@\$'
|
187 |
cleaned_text = re.sub(pattern, '', text)
|
188 |
return cleaned_text
|
189 |
|
|
|
195 |
proxy: Optional[str] = None,
|
196 |
websearch: bool = False,
|
197 |
**kwargs
|
198 |
+
) -> AsyncGenerator[Union[str, Dict[str, Any]], None]:
|
199 |
"""
|
200 |
Creates an asynchronous generator for streaming responses from Blackbox AI.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
"""
|
202 |
model = cls.get_model(model)
|
203 |
|
|
|
216 |
content = message.get('content', '')
|
217 |
if role and content:
|
218 |
formatted_prompt += f"{role}: {content}\n"
|
219 |
+
|
220 |
if prefix:
|
221 |
formatted_prompt = f"{prefix} {formatted_prompt}".strip()
|
222 |
|
|
|
277 |
"userSelectedModel": cls.userSelectedModel.get(model, model)
|
278 |
}
|
279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
280 |
async with ClientSession(headers=common_headers) as session:
|
281 |
try:
|
282 |
async with session.post(
|
|
|
286 |
proxy=proxy
|
287 |
) as response_api_chat:
|
288 |
response_api_chat.raise_for_status()
|
289 |
+
text = await response_api_chat.text()
|
290 |
+
cleaned_response = cls.clean_response(text)
|
291 |
+
|
292 |
+
if model in cls.image_models:
|
293 |
+
match = re.search(r'!\[.*?\]\((https?://[^\)]+)\)', cleaned_response)
|
294 |
+
if match:
|
295 |
+
image_url = match.group(1)
|
296 |
+
yield {"type": "image", "url": image_url, "alt": "Generated Image"}
|
297 |
+
else:
|
298 |
+
yield cleaned_response
|
299 |
+
else:
|
300 |
+
if websearch:
|
301 |
+
match = re.search(r'\$~~~\$(.*?)\$~~~\$', cleaned_response, re.DOTALL)
|
302 |
+
if match:
|
303 |
+
source_part = match.group(1).strip()
|
304 |
+
answer_part = cleaned_response[match.end():].strip()
|
305 |
+
try:
|
306 |
+
sources = json.loads(source_part)
|
307 |
+
source_formatted = "**Source:**\n"
|
308 |
+
for item in sources:
|
309 |
+
title = item.get('title', 'No Title')
|
310 |
+
link = item.get('link', '#')
|
311 |
+
position = item.get('position', '')
|
312 |
+
source_formatted += f"{position}. [{title}]({link})\n"
|
313 |
+
final_response = f"{answer_part}\n\n{source_formatted}"
|
314 |
+
except json.JSONDecodeError:
|
315 |
+
final_response = f"{answer_part}\n\nSource information is unavailable."
|
316 |
+
else:
|
317 |
+
final_response = cleaned_response
|
318 |
+
else:
|
319 |
+
if '$~~~$' in cleaned_response:
|
320 |
+
final_response = cleaned_response.split('$~~~$')[0].strip()
|
321 |
+
else:
|
322 |
+
final_response = cleaned_response
|
323 |
+
|
324 |
+
yield final_response
|
325 |
except ClientResponseError as e:
|
326 |
error_text = f"Error {e.status}: {e.message}"
|
327 |
try:
|
|
|
334 |
except Exception as e:
|
335 |
yield f"Unexpected error during /api/chat request: {str(e)}"
|
336 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
337 |
# FastAPI app setup
|
338 |
app = FastAPI()
|
339 |
|
|
|
374 |
class ChatRequest(BaseModel):
|
375 |
model: str
|
376 |
messages: List[Message]
|
|
|
377 |
temperature: Optional[float] = 1.0
|
378 |
top_p: Optional[float] = 1.0
|
379 |
n: Optional[int] = 1
|
|
|
383 |
logit_bias: Optional[Dict[str, float]] = None
|
384 |
user: Optional[str] = None
|
385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
386 |
@app.post("/v1/chat/completions", dependencies=[Depends(rate_limiter_per_ip)])
|
387 |
async def chat_completions(request: ChatRequest, req: Request, api_key: str = Depends(get_api_key)):
|
388 |
client_ip = req.client.host
|
|
|
398 |
raise HTTPException(status_code=400, detail="Requested model is not available.")
|
399 |
|
400 |
# Process the request with actual message content, but don't log it
|
401 |
+
response_content = await Blackbox.create_async_generator(
|
402 |
model=request.model,
|
403 |
messages=[{"role": msg.role, "content": msg.content} for msg in request.messages],
|
404 |
temperature=request.temperature,
|
405 |
max_tokens=request.max_tokens
|
406 |
)
|
407 |
|
408 |
+
logger.info(f"Completed response generation for API key: {api_key} | IP: {client_ip}")
|
409 |
+
return {
|
410 |
+
"id": f"chatcmpl-{uuid.uuid4()}",
|
411 |
+
"object": "chat.completion",
|
412 |
+
"created": int(datetime.now().timestamp()),
|
413 |
+
"model": request.model,
|
414 |
+
"choices": [
|
415 |
+
{
|
416 |
+
"index": 0,
|
417 |
+
"message": {
|
418 |
+
"role": "assistant",
|
419 |
+
"content": response_content
|
420 |
+
},
|
421 |
+
"finish_reason": "stop"
|
422 |
+
}
|
423 |
+
],
|
424 |
+
"usage": {
|
425 |
+
"prompt_tokens": sum(len(msg.content.split()) for msg in request.messages),
|
426 |
+
"completion_tokens": len(response_content.split()),
|
427 |
+
"total_tokens": sum(len(msg.content.split()) for msg in request.messages) + len(response_content.split())
|
428 |
+
},
|
429 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
430 |
except ModelNotWorkingException as e:
|
431 |
logger.warning(f"Model not working: {e} | IP: {client_ip}")
|
432 |
raise HTTPException(status_code=503, detail=str(e))
|
|
|
467 |
}
|
468 |
},
|
469 |
)
|
|
|
|
|
|
|
|